vendor: 70,541 bytes, lines 1-2382
1/*! 2 * AngularJS Material Design 3 * https://github.com/angular/material 4 * @license MIT 5 * v1.1.4-master-baa869a 6 */ 7(function( window, angular, undefined ){ 8"use strict"; 9 10(function(){ 11"use strict"; 12 13angular.module('ngMaterial', ["ng","ngAnimate","ngAria","material.core","material.core.gestures","material.core.interaction","material.core.layout","material.core.meta","material.core.theming.palette","material.core.theming","material.core.animate","material.components.autocomplete","material.components.backdrop","material.components.bottomSheet","material.components.button","material.components.card","material.components.checkbox","material.components.chips","material.components.colors","material.components.content","material.components.datepicker","material.components.dialog","material.components.divider","material.components.fabActions","material.components.fabShared","material.components.fabSpeedDial","material.components.fabToolbar","material.components.gridList","material.components.icon","material.components.input","material.components.list","material.components.menu","material.components.menuBar","material.components.navBar","material.components.panel","material.components.progressCircular","material.components.progressLinear","material.components.radioButton","material.components.select","material.components.showHide","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.truncate","material.components.virtualRepeat","material.components.whiteframe"]); 14})(); 15(function(){ 16"use strict"; 17 18/** 19 * Initialization function that validates environment 20 * requirements. 21 */ 22DetectNgTouch.$inject = ["$log", "$injector"]; 23MdCoreConfigure.$inject = ["$provide", "$mdThemingProvider"]; 24rAFDecorator.$inject = ["$delegate"]; 25qDecorator.$inject = ["$delegate"]; 26angular 27 .module('material.core', [ 28 'ngAnimate', 29 'material.core.animate', 30 'material.core.layout', 31 'material.core.interaction', 32 'material.core.gestures', 33 'material.core.theming' 34 ]) 35 .config(MdCoreConfigure) 36 .run(DetectNgTouch); 37 38 39/** 40 * Detect if the ng-Touch module is also being used. 41 * Warn if detected. 42 * @ngInject 43 */ 44function DetectNgTouch($log, $injector) { 45 if ( $injector.has('$swipe') ) { 46 var msg = "" + 47 "You are using the ngTouch module. \n" + 48 "AngularJS Material already has mobile click, tap, and swipe support... \n" + 49 "ngTouch is not supported with AngularJS Material!"; 50 $log.warn(msg); 51 } 52} 53 54/** 55 * @ngInject 56 */ 57function MdCoreConfigure($provide, $mdThemingProvider) { 58 59 $provide.decorator('$$rAF', ['$delegate', rAFDecorator]); 60 $provide.decorator('$q', ['$delegate', qDecorator]); 61 62 $mdThemingProvider.theme('default') 63 .primaryPalette('indigo') 64 .accentPalette('pink') 65 .warnPalette('deep-orange') 66 .backgroundPalette('grey'); 67} 68 69/** 70 * @ngInject 71 */ 72function rAFDecorator($delegate) { 73 /** 74 * Use this to throttle events that come in often. 75 * The throttled function will always use the *last* invocation before the 76 * coming frame. 77 * 78 * For example, window resize events that fire many times a second: 79 * If we set to use an raf-throttled callback on window resize, then 80 * our callback will only be fired once per frame, with the last resize 81 * event that happened before that frame. 82 * 83 * @param {function} callback function to debounce 84 */ 85 $delegate.throttle = function(cb) { 86 var queuedArgs, alreadyQueued, queueCb, context; 87 return function debounced() { 88 queuedArgs = arguments; 89 context = this; 90 queueCb = cb; 91 if (!alreadyQueued) { 92 alreadyQueued = true; 93 $delegate(function() { 94 queueCb.apply(context, Array.prototype.slice.call(queuedArgs)); 95 alreadyQueued = false; 96 }); 97 } 98 }; 99 }; 100 return $delegate; 101} 102 103/** 104 * @ngInject 105 */ 106function qDecorator($delegate) { 107 /** 108 * Adds a shim for $q.resolve for AngularJS version that don't have it, 109 * so we don't have to think about it. 110 * 111 * via https://github.com/angular/angular.js/pull/11987 112 */ 113 114 // TODO(crisbeto): this won't be necessary once we drop AngularJS 1.3 115 if (!$delegate.resolve) { 116 $delegate.resolve = $delegate.when; 117 } 118 return $delegate; 119} 120 121})(); 122(function(){ 123"use strict"; 124 125 126MdAutofocusDirective.$inject = ["$parse"];angular.module('material.core') 127 .directive('mdAutofocus', MdAutofocusDirective) 128 129 // Support the deprecated md-auto-focus and md-sidenav-focus as well 130 .directive('mdAutoFocus', MdAutofocusDirective) 131 .directive('mdSidenavFocus', MdAutofocusDirective); 132 133/** 134 * @ngdoc directive 135 * @name mdAutofocus 136 * @module material.core.util 137 * 138 * @description 139 * 140 * `[md-autofocus]` provides an optional way to identify the focused element when a `$mdDialog`, 141 * `$mdBottomSheet`, `$mdMenu` or `$mdSidenav` opens or upon page load for input-like elements. 142 * 143 * When one of these opens, it will find the first nested element with the `[md-autofocus]` 144 * attribute directive and optional expression. An expression may be specified as the directive 145 * value to enable conditional activation of the autofocus. 146 * 147 * @usage 148 * 149 * ### Dialog 150 * <hljs lang="html"> 151 * <md-dialog> 152 * <form> 153 * <md-input-container> 154 * <label for="testInput">Label</label> 155 * <input id="testInput" type="text" md-autofocus> 156 * </md-input-container> 157 * </form> 158 * </md-dialog> 159 * </hljs> 160 * 161 * ### Bottomsheet 162 * <hljs lang="html"> 163 * <md-bottom-sheet class="md-list md-has-header"> 164 * <md-subheader>Comment Actions</md-subheader> 165 * <md-list> 166 * <md-list-item ng-repeat="item in items"> 167 * 168 * <md-button md-autofocus="$index == 2"> 169 * <md-icon md-svg-src="{{item.icon}}"></md-icon> 170 * <span class="md-inline-list-icon-label">{{ item.name }}</span> 171 * </md-button> 172 * 173 * </md-list-item> 174 * </md-list> 175 * </md-bottom-sheet> 176 * </hljs> 177 * 178 * ### Autocomplete 179 * <hljs lang="html"> 180 * <md-autocomplete 181 * md-autofocus 182 * md-selected-item="selectedItem" 183 * md-search-text="searchText" 184 * md-items="item in getMatches(searchText)" 185 * md-item-text="item.display"> 186 * <span md-highlight-text="searchText">{{item.display}}</span> 187 * </md-autocomplete> 188 * </hljs> 189 * 190 * ### Sidenav 191 * <hljs lang="html"> 192 * <div layout="row" ng-controller="MyController"> 193 * <md-sidenav md-component-id="left" class="md-sidenav-left"> 194 * Left Nav! 195 * </md-sidenav> 196 * 197 * <md-content> 198 * Center Content 199 * <md-button ng-click="openLeftMenu()"> 200 * Open Left Menu 201 * </md-button> 202 * </md-content> 203 * 204 * <md-sidenav md-component-id="right" 205 * md-is-locked-open="$mdMedia('min-width: 333px')" 206 * class="md-sidenav-right"> 207 * <form> 208 * <md-input-container> 209 * <label for="testInput">Test input</label> 210 * <input id="testInput" type="text" 211 * ng-model="data" md-autofocus> 212 * </md-input-container> 213 * </form> 214 * </md-sidenav> 215 * </div> 216 * </hljs> 217 **/ 218function MdAutofocusDirective($parse) { 219 return { 220 restrict: 'A', 221 link: { 222 pre: preLink 223 } 224 }; 225 226 function preLink(scope, element, attr) { 227 var attrExp = attr.mdAutoFocus || attr.mdAutofocus || attr.mdSidenavFocus; 228 229 // Initially update the expression by manually parsing the expression as per $watch source. 230 updateExpression($parse(attrExp)(scope)); 231 232 // Only watch the expression if it is not empty. 233 if (attrExp) { 234 scope.$watch(attrExp, updateExpression); 235 } 236 237 /** 238 * Updates the autofocus class which is used to determine whether the attribute 239 * expression evaluates to true or false. 240 * @param {string|boolean} value Attribute Value 241 */ 242 function updateExpression(value) { 243 244 // Rather than passing undefined to the jqLite toggle class function we explicitly set the 245 // value to true. Otherwise the class will be just toggled instead of being forced. 246 if (angular.isUndefined(value)) { 247 value = true; 248 } 249 250 element.toggleClass('md-autofocus', !!value); 251 } 252 } 253 254} 255 256})(); 257(function(){ 258"use strict"; 259 260/** 261 * @ngdoc module 262 * @name material.core.colorUtil 263 * @description 264 * Color Util 265 */ 266angular 267 .module('material.core') 268 .factory('$mdColorUtil', ColorUtilFactory); 269 270function ColorUtilFactory() { 271 /** 272 * Converts hex value to RGBA string 273 * @param color {string} 274 * @returns {string} 275 */ 276 function hexToRgba (color) { 277 var hex = color[ 0 ] === '#' ? color.substr(1) : color, 278 dig = hex.length / 3, 279 red = hex.substr(0, dig), 280 green = hex.substr(dig, dig), 281 blue = hex.substr(dig * 2); 282 if (dig === 1) { 283 red += red; 284 green += green; 285 blue += blue; 286 } 287 return 'rgba(' + parseInt(red, 16) + ',' + parseInt(green, 16) + ',' + parseInt(blue, 16) + ',0.1)'; 288 } 289 290 /** 291 * Converts rgba value to hex string 292 * @param color {string} 293 * @returns {string} 294 */ 295 function rgbaToHex(color) { 296 color = color.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i); 297 298 var hex = (color && color.length === 4) ? "#" + 299 ("0" + parseInt(color[1],10).toString(16)).slice(-2) + 300 ("0" + parseInt(color[2],10).toString(16)).slice(-2) + 301 ("0" + parseInt(color[3],10).toString(16)).slice(-2) : ''; 302 303 return hex.toUpperCase(); 304 } 305 306 /** 307 * Converts an RGB color to RGBA 308 * @param color {string} 309 * @returns {string} 310 */ 311 function rgbToRgba (color) { 312 return color.replace(')', ', 0.1)').replace('(', 'a('); 313 } 314 315 /** 316 * Converts an RGBA color to RGB 317 * @param color {string} 318 * @returns {string} 319 */ 320 function rgbaToRgb (color) { 321 return color 322 ? color.replace('rgba', 'rgb').replace(/,[^\),]+\)/, ')') 323 : 'rgb(0,0,0)'; 324 } 325 326 return { 327 rgbaToHex: rgbaToHex, 328 hexToRgba: hexToRgba, 329 rgbToRgba: rgbToRgba, 330 rgbaToRgb: rgbaToRgb 331 }; 332} 333 334})(); 335(function(){ 336"use strict"; 337 338angular.module('material.core') 339.factory('$mdConstant', MdConstantFactory); 340 341/** 342 * Factory function that creates the grab-bag $mdConstant service. 343 * @ngInject 344 */ 345function MdConstantFactory() { 346 347 var prefixTestEl = document.createElement('div'); 348 var vendorPrefix = getVendorPrefix(prefixTestEl); 349 var isWebkit = /webkit/i.test(vendorPrefix); 350 var SPECIAL_CHARS_REGEXP = /([:\-_]+(.))/g; 351 352 function vendorProperty(name) { 353 // Add a dash between the prefix and name, to be able to transform the string into camelcase. 354 var prefixedName = vendorPrefix + '-' + name; 355 var ucPrefix = camelCase(prefixedName); 356 var lcPrefix = ucPrefix.charAt(0).toLowerCase() + ucPrefix.substring(1); 357 358 return hasStyleProperty(prefixTestEl, name) ? name : // The current browser supports the un-prefixed property 359 hasStyleProperty(prefixTestEl, ucPrefix) ? ucPrefix : // The current browser only supports the prefixed property. 360 hasStyleProperty(prefixTestEl, lcPrefix) ? lcPrefix : name; // Some browsers are only supporting the prefix in lowercase. 361 } 362 363 function hasStyleProperty(testElement, property) { 364 return angular.isDefined(testElement.style[property]); 365 } 366 367 function camelCase(input) { 368 return input.replace(SPECIAL_CHARS_REGEXP, function(matches, separator, letter, offset) { 369 return offset ? letter.toUpperCase() : letter; 370 }); 371 } 372 373 function getVendorPrefix(testElement) { 374 var prop, match; 375 var vendorRegex = /^(Moz|webkit|ms)(?=[A-Z])/; 376 377 for (prop in testElement.style) { 378 if (match = vendorRegex.exec(prop)) { 379 return match[0]; 380 } 381 } 382 } 383 384 var self = { 385 isInputKey : function(e) { return (e.keyCode >= 31 && e.keyCode <= 90); }, 386 isNumPadKey : function(e) { return (3 === e.location && e.keyCode >= 97 && e.keyCode <= 105); }, 387 isMetaKey: function(e) { return (e.keyCode >= 91 && e.keyCode <= 93); }, 388 isFnLockKey: function(e) { return (e.keyCode >= 112 && e.keyCode <= 145); }, 389 isNavigationKey : function(e) { 390 var kc = self.KEY_CODE, NAVIGATION_KEYS = [kc.SPACE, kc.ENTER, kc.UP_ARROW, kc.DOWN_ARROW]; 391 return (NAVIGATION_KEYS.indexOf(e.keyCode) != -1); 392 }, 393 hasModifierKey: function(e) { 394 return e.ctrlKey || e.metaKey || e.altKey; 395 }, 396 397 /** 398 * Maximum size, in pixels, that can be explicitly set to an element. The actual value varies 399 * between browsers, but IE11 has the very lowest size at a mere 1,533,917px. Ideally we could 400 * compute this value, but Firefox always reports an element to have a size of zero if it 401 * goes over the max, meaning that we'd have to binary search for the value. 402 */ 403 ELEMENT_MAX_PIXELS: 1533917, 404 405 /** 406 * Priority for a directive that should run before the directives from ngAria. 407 */ 408 BEFORE_NG_ARIA: 210, 409 410 /** 411 * Common Keyboard actions and their associated keycode. 412 */ 413 KEY_CODE: { 414 COMMA: 188, 415 SEMICOLON : 186, 416 ENTER: 13, 417 ESCAPE: 27, 418 SPACE: 32, 419 PAGE_UP: 33, 420 PAGE_DOWN: 34, 421 END: 35, 422 HOME: 36, 423 LEFT_ARROW : 37, 424 UP_ARROW : 38, 425 RIGHT_ARROW : 39, 426 DOWN_ARROW : 40, 427 TAB : 9, 428 BACKSPACE: 8, 429 DELETE: 46 430 }, 431 432 /** 433 * Vendor prefixed CSS properties to be used to support the given functionality in older browsers 434 * as well. 435 */ 436 CSS: { 437 /* Constants */ 438 TRANSITIONEND: 'transitionend' + (isWebkit ? ' webkitTransitionEnd' : ''), 439 ANIMATIONEND: 'animationend' + (isWebkit ? ' webkitAnimationEnd' : ''), 440 441 TRANSFORM: vendorProperty('transform'), 442 TRANSFORM_ORIGIN: vendorProperty('transformOrigin'), 443 TRANSITION: vendorProperty('transition'), 444 TRANSITION_DURATION: vendorProperty('transitionDuration'), 445 ANIMATION_PLAY_STATE: vendorProperty('animationPlayState'), 446 ANIMATION_DURATION: vendorProperty('animationDuration'), 447 ANIMATION_NAME: vendorProperty('animationName'), 448 ANIMATION_TIMING: vendorProperty('animationTimingFunction'), 449 ANIMATION_DIRECTION: vendorProperty('animationDirection') 450 }, 451 452 /** 453 * As defined in core/style/variables.scss 454 * 455 * $layout-breakpoint-xs: 600px !default; 456 * $layout-breakpoint-sm: 960px !default; 457 * $layout-breakpoint-md: 1280px !default; 458 * $layout-breakpoint-lg: 1920px !default; 459 * 460 */ 461 MEDIA: { 462 'xs' : '(max-width: 599px)' , 463 'gt-xs' : '(min-width: 600px)' , 464 'sm' : '(min-width: 600px) and (max-width: 959px)' , 465 'gt-sm' : '(min-width: 960px)' , 466 'md' : '(min-width: 960px) and (max-width: 1279px)' , 467 'gt-md' : '(min-width: 1280px)' , 468 'lg' : '(min-width: 1280px) and (max-width: 1919px)', 469 'gt-lg' : '(min-width: 1920px)' , 470 'xl' : '(min-width: 1920px)' , 471 'landscape' : '(orientation: landscape)' , 472 'portrait' : '(orientation: portrait)' , 473 'print' : 'print' 474 }, 475 476 MEDIA_PRIORITY: [ 477 'xl', 478 'gt-lg', 479 'lg', 480 'gt-md', 481 'md', 482 'gt-sm', 483 'sm', 484 'gt-xs', 485 'xs', 486 'landscape', 487 'portrait', 488 'print' 489 ] 490 }; 491 492 return self; 493} 494 495})(); 496(function(){ 497"use strict"; 498 499 angular 500 .module('material.core') 501 .config( ["$provide", function($provide){ 502 $provide.decorator('$mdUtil', ['$delegate', function ($delegate){ 503 /** 504 * Inject the iterator facade to easily support iteration and accessors 505 * @see iterator below 506 */ 507 $delegate.iterator = MdIterator; 508 509 return $delegate; 510 } 511 ]); 512 }]); 513 514 /** 515 * iterator is a list facade to easily support iteration and accessors 516 * 517 * @param items Array list which this iterator will enumerate 518 * @param reloop Boolean enables iterator to consider the list as an endless reloop 519 */ 520 function MdIterator(items, reloop) { 521 var trueFn = function() { return true; }; 522 523 if (items && !angular.isArray(items)) { 524 items = Array.prototype.slice.call(items); 525 } 526 527 reloop = !!reloop; 528 var _items = items || [ ]; 529 530 // Published API 531 return { 532 items: getItems, 533 count: count, 534 535 inRange: inRange, 536 contains: contains, 537 indexOf: indexOf, 538 itemAt: itemAt, 539 540 findBy: findBy, 541 542 add: add, 543 remove: remove, 544 545 first: first, 546 last: last, 547 next: angular.bind(null, findSubsequentItem, false), 548 previous: angular.bind(null, findSubsequentItem, true), 549 550 hasPrevious: hasPrevious, 551 hasNext: hasNext 552 553 }; 554 555 /** 556 * Publish copy of the enumerable set 557 * @returns {Array|*} 558 */ 559 function getItems() { 560 return [].concat(_items); 561 } 562 563 /** 564 * Determine length of the list 565 * @returns {Array.length|*|number} 566 */ 567 function count() { 568 return _items.length; 569 } 570 571 /** 572 * Is the index specified valid 573 * @param index 574 * @returns {Array.length|*|number|boolean} 575 */ 576 function inRange(index) { 577 return _items.length && ( index > -1 ) && (index < _items.length ); 578 } 579 580 /** 581 * Can the iterator proceed to the next item in the list; relative to 582 * the specified item. 583 * 584 * @param item 585 * @returns {Array.length|*|number|boolean} 586 */ 587 function hasNext(item) { 588 return item ? inRange(indexOf(item) + 1) : false; 589 } 590 591 /** 592 * Can the iterator proceed to the previous item in the list; relative to 593 * the specified item. 594 * 595 * @param item 596 * @returns {Array.length|*|number|boolean} 597 */ 598 function hasPrevious(item) { 599 return item ? inRange(indexOf(item) - 1) : false; 600 } 601 602 /** 603 * Get item at specified index/position 604 * @param index 605 * @returns {*} 606 */ 607 function itemAt(index) { 608 return inRange(index) ? _items[index] : null; 609 } 610 611 /** 612 * Find all elements matching the key/value pair 613 * otherwise return null 614 * 615 * @param val 616 * @param key 617 * 618 * @return array 619 */ 620 function findBy(key, val) { 621 return _items.filter(function(item) { 622 return item[key] === val; 623 }); 624 } 625 626 /** 627 * Add item to list 628 * @param item 629 * @param index 630 * @returns {*} 631 */ 632 function add(item, index) { 633 if ( !item ) return -1; 634 635 if (!angular.isNumber(index)) { 636 index = _items.length; 637 } 638 639 _items.splice(index, 0, item); 640 641 return indexOf(item); 642 } 643 644 /** 645 * Remove item from list... 646 * @param item 647 */ 648 function remove(item) { 649 if ( contains(item) ){ 650 _items.splice(indexOf(item), 1); 651 } 652 } 653 654 /** 655 * Get the zero-based index of the target item 656 * @param item 657 * @returns {*} 658 */ 659 function indexOf(item) { 660 return _items.indexOf(item); 661 } 662 663 /** 664 * Boolean existence check 665 * @param item 666 * @returns {boolean} 667 */ 668 function contains(item) { 669 return item && (indexOf(item) > -1); 670 } 671 672 /** 673 * Return first item in the list 674 * @returns {*} 675 */ 676 function first() { 677 return _items.length ? _items[0] : null; 678 } 679 680 /** 681 * Return last item in the list... 682 * @returns {*} 683 */ 684 function last() { 685 return _items.length ? _items[_items.length - 1] : null; 686 } 687 688 /** 689 * Find the next item. If reloop is true and at the end of the list, it will go back to the 690 * first item. If given, the `validate` callback will be used to determine whether the next item 691 * is valid. If not valid, it will try to find the next item again. 692 * 693 * @param {boolean} backwards Specifies the direction of searching (forwards/backwards) 694 * @param {*} item The item whose subsequent item we are looking for 695 * @param {Function=} validate The `validate` function 696 * @param {integer=} limit The recursion limit 697 * 698 * @returns {*} The subsequent item or null 699 */ 700 function findSubsequentItem(backwards, item, validate, limit) { 701 validate = validate || trueFn; 702 703 var curIndex = indexOf(item); 704 while (true) { 705 if (!inRange(curIndex)) return null; 706 707 var nextIndex = curIndex + (backwards ? -1 : 1); 708 var foundItem = null; 709 if (inRange(nextIndex)) { 710 foundItem = _items[nextIndex]; 711 } else if (reloop) { 712 foundItem = backwards ? last() : first(); 713 nextIndex = indexOf(foundItem); 714 } 715 716 if ((foundItem === null) || (nextIndex === limit)) return null; 717 if (validate(foundItem)) return foundItem; 718 719 if (angular.isUndefined(limit)) limit = nextIndex; 720 721 curIndex = nextIndex; 722 } 723 } 724 } 725 726 727})(); 728(function(){ 729"use strict"; 730 731 732mdMediaFactory.$inject = ["$mdConstant", "$rootScope", "$window"];angular.module('material.core') 733.factory('$mdMedia', mdMediaFactory); 734 735/** 736 * @ngdoc service 737 * @name $mdMedia 738 * @module material.core 739 * 740 * @description 741 * `$mdMedia` is used to evaluate whether a given media query is true or false given the 742 * current device's screen / window size. The media query will be re-evaluated on resize, allowing 743 * you to register a watch. 744 * 745 * `$mdMedia` also has pre-programmed support for media queries that match the layout breakpoints: 746 * 747 * <table class="md-api-table"> 748 * <thead> 749 * <tr> 750 * <th>Breakpoint</th> 751 * <th>mediaQuery</th> 752 * </tr> 753 * </thead> 754 * <tbody> 755 * <tr> 756 * <td>xs</td> 757 * <td>(max-width: 599px)</td> 758 * </tr> 759 * <tr> 760 * <td>gt-xs</td> 761 * <td>(min-width: 600px)</td> 762 * </tr> 763 * <tr> 764 * <td>sm</td> 765 * <td>(min-width: 600px) and (max-width: 959px)</td> 766 * </tr> 767 * <tr> 768 * <td>gt-sm</td> 769 * <td>(min-width: 960px)</td> 770 * </tr> 771 * <tr> 772 * <td>md</td> 773 * <td>(min-width: 960px) and (max-width: 1279px)</td> 774 * </tr> 775 * <tr> 776 * <td>gt-md</td> 777 * <td>(min-width: 1280px)</td> 778 * </tr> 779 * <tr> 780 * <td>lg</td> 781 * <td>(min-width: 1280px) and (max-width: 1919px)</td> 782 * </tr> 783 * <tr> 784 * <td>gt-lg</td> 785 * <td>(min-width: 1920px)</td> 786 * </tr> 787 * <tr> 788 * <td>xl</td> 789 * <td>(min-width: 1920px)</td> 790 * </tr> 791 * <tr> 792 * <td>landscape</td> 793 * <td>landscape</td> 794 * </tr> 795 * <tr> 796 * <td>portrait</td> 797 * <td>portrait</td> 798 * </tr> 799 * <tr> 800 * <td>print</td> 801 * <td>print</td> 802 * </tr> 803 * </tbody> 804 * </table> 805 * 806 * See Material Design's <a href="https://material.google.com/layout/responsive-ui.html">Layout - Adaptive UI</a> for more details. 807 * 808 * <a href="https://www.google.com/design/spec/layout/adaptive-ui.html"> 809 * <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> 810 * </a> 811 * 812 * @returns {boolean} a boolean representing whether or not the given media query is true or false. 813 * 814 * @usage 815 * <hljs lang="js"> 816 * app.controller('MyController', function($mdMedia, $scope) { 817 * $scope.$watch(function() { return $mdMedia('lg'); }, function(big) { 818 * $scope.bigScreen = big; 819 * }); 820 * 821 * $scope.screenIsSmall = $mdMedia('sm'); 822 * $scope.customQuery = $mdMedia('(min-width: 1234px)'); 823 * $scope.anotherCustom = $mdMedia('max-width: 300px'); 824 * }); 825 * </hljs> 826 */ 827 828/* @ngInject */ 829function mdMediaFactory($mdConstant, $rootScope, $window) { 830 var queries = {}; 831 var mqls = {}; 832 var results = {}; 833 var normalizeCache = {}; 834 835 $mdMedia.getResponsiveAttribute = getResponsiveAttribute; 836 $mdMedia.getQuery = getQuery; 837 $mdMedia.watchResponsiveAttributes = watchResponsiveAttributes; 838 839 return $mdMedia; 840 841 function $mdMedia(query) { 842 var validated = queries[query]; 843 if (angular.isUndefined(validated)) { 844 validated = queries[query] = validate(query); 845 } 846 847 var result = results[validated]; 848 if (angular.isUndefined(result)) { 849 result = add(validated); 850 } 851 852 return result; 853 } 854 855 function validate(query) { 856 return $mdConstant.MEDIA[query] || 857 ((query.charAt(0) !== '(') ? ('(' + query + ')') : query); 858 } 859 860 function add(query) { 861 var result = mqls[query]; 862 if ( !result ) { 863 result = mqls[query] = $window.matchMedia(query); 864 } 865 866 result.addListener(onQueryChange); 867 return (results[result.media] = !!result.matches); 868 } 869 870 function onQueryChange(query) { 871 $rootScope.$evalAsync(function() { 872 results[query.media] = !!query.matches; 873 }); 874 } 875 876 function getQuery(name) { 877 return mqls[name]; 878 } 879 880 function getResponsiveAttribute(attrs, attrName) { 881 for (var i = 0; i < $mdConstant.MEDIA_PRIORITY.length; i++) { 882 var mediaName = $mdConstant.MEDIA_PRIORITY[i]; 883 if (!mqls[queries[mediaName]].matches) { 884 continue; 885 } 886 887 var normalizedName = getNormalizedName(attrs, attrName + '-' + mediaName); 888 if (attrs[normalizedName]) { 889 return attrs[normalizedName]; 890 } 891 } 892 893 // fallback on unprefixed 894 return attrs[getNormalizedName(attrs, attrName)]; 895 } 896 897 function watchResponsiveAttributes(attrNames, attrs, watchFn) { 898 var unwatchFns = []; 899 attrNames.forEach(function(attrName) { 900 var normalizedName = getNormalizedName(attrs, attrName); 901 if (angular.isDefined(attrs[normalizedName])) { 902 unwatchFns.push( 903 attrs.$observe(normalizedName, angular.bind(void 0, watchFn, null))); 904 } 905 906 for (var mediaName in $mdConstant.MEDIA) { 907 normalizedName = getNormalizedName(attrs, attrName + '-' + mediaName); 908 if (angular.isDefined(attrs[normalizedName])) { 909 unwatchFns.push( 910 attrs.$observe(normalizedName, angular.bind(void 0, watchFn, mediaName))); 911 } 912 } 913 }); 914 915 return function unwatch() { 916 unwatchFns.forEach(function(fn) { fn(); }); 917 }; 918 } 919 920 // Improves performance dramatically 921 function getNormalizedName(attrs, attrName) { 922 return normalizeCache[attrName] || 923 (normalizeCache[attrName] = attrs.$normalize(attrName)); 924 } 925} 926 927})(); 928(function(){ 929"use strict"; 930 931angular 932 .module('material.core') 933 .config( ["$provide", function($provide) { 934 $provide.decorator('$mdUtil', ['$delegate', function ($delegate) { 935 936 // Inject the prefixer into our original $mdUtil service. 937 $delegate.prefixer = MdPrefixer; 938 939 return $delegate; 940 }]); 941 }]); 942 943function MdPrefixer(initialAttributes, buildSelector) { 944 var PREFIXES = ['data', 'x']; 945 946 if (initialAttributes) { 947 // The prefixer also accepts attributes as a parameter, and immediately builds a list or selector for 948 // the specified attributes. 949 return buildSelector ? _buildSelector(initialAttributes) : _buildList(initialAttributes); 950 } 951 952 return { 953 buildList: _buildList, 954 buildSelector: _buildSelector, 955 hasAttribute: _hasAttribute, 956 removeAttribute: _removeAttribute 957 }; 958 959 function _buildList(attributes) { 960 attributes = angular.isArray(attributes) ? attributes : [attributes]; 961 962 attributes.forEach(function(item) { 963 PREFIXES.forEach(function(prefix) { 964 attributes.push(prefix + '-' + item); 965 }); 966 }); 967 968 return attributes; 969 } 970 971 function _buildSelector(attributes) { 972 attributes = angular.isArray(attributes) ? attributes : [attributes]; 973 974 return _buildList(attributes) 975 .map(function(item) { 976 return '[' + item + ']'; 977 }) 978 .join(','); 979 } 980 981 function _hasAttribute(element, attribute) { 982 element = _getNativeElement(element); 983 984 if (!element) { 985 return false; 986 } 987 988 var prefixedAttrs = _buildList(attribute); 989 990 for (var i = 0; i < prefixedAttrs.length; i++) { 991 if (element.hasAttribute(prefixedAttrs[i])) { 992 return true; 993 } 994 } 995 996 return false; 997 } 998 999 function _removeAttribute(element, attribute) { 1000 element = _getNativeElement(element); 1001 1002 if (!element) { 1003 return; 1004 } 1005 1006 _buildList(attribute).forEach(function(prefixedAttribute) { 1007 element.removeAttribute(prefixedAttribute); 1008 }); 1009 } 1010 1011 /** 1012 * Transforms a jqLite or DOM element into a HTML element. 1013 * This is useful when supporting jqLite elements and DOM elements at 1014 * same time. 1015 * @param element {JQLite|Element} Element to be parsed 1016 * @returns {HTMLElement} Parsed HTMLElement 1017 */ 1018 function _getNativeElement(element) { 1019 element = element[0] || element; 1020 1021 if (element.nodeType) { 1022 return element; 1023 } 1024 } 1025 1026} 1027 1028})(); 1029(function(){ 1030"use strict"; 1031 1032/* 1033 * This var has to be outside the angular factory, otherwise when 1034 * there are multiple material apps on the same page, each app 1035 * will create its own instance of this array and the app's IDs 1036 * will not be unique. 1037 */ 1038UtilFactory.$inject = ["$document", "$timeout", "$compile", "$rootScope", "$$mdAnimate", "$interpolate", "$log", "$rootElement", "$window", "$$rAF"]; 1039var nextUniqueId = 0; 1040 1041/** 1042 * @ngdoc module 1043 * @name material.core.util 1044 * @description 1045 * Util 1046 */ 1047angular 1048 .module('material.core') 1049 .factory('$mdUtil', UtilFactory); 1050 1051/** 1052 * @ngInject 1053 */ 1054function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $interpolate, $log, $rootElement, $window, $$rAF) { 1055 // Setup some core variables for the processTemplate method 1056 var startSymbol = $interpolate.startSymbol(), 1057 endSymbol = $interpolate.endSymbol(), 1058 usesStandardSymbols = ((startSymbol === '{{') && (endSymbol === '}}')); 1059 1060 /** 1061 * Checks if the target element has the requested style by key 1062 * @param {DOMElement|JQLite} target Target element 1063 * @param {string} key Style key 1064 * @param {string=} expectedVal Optional expected value 1065 * @returns {boolean} Whether the target element has the style or not 1066 */ 1067 var hasComputedStyle = function (target, key, expectedVal) { 1068 var hasValue = false; 1069 1070 if ( target && target.length ) { 1071 var computedStyles = $window.getComputedStyle(target[0]); 1072 hasValue = angular.isDefined(computedStyles[key]) && (expectedVal ? computedStyles[key] == expectedVal : true); 1073 } 1074 1075 return hasValue; 1076 }; 1077 1078 function validateCssValue(value) { 1079 return !value ? '0' : 1080 hasPx(value) || hasPercent(value) ? value : value + 'px'; 1081 } 1082 1083 function hasPx(value) { 1084 return String(value).indexOf('px') > -1; 1085 } 1086 1087 function hasPercent(value) { 1088 return String(value).indexOf('%') > -1; 1089 1090 } 1091 1092 var $mdUtil = { 1093 dom: {}, 1094 now: window.performance && window.performance.now ? 1095 angular.bind(window.performance, window.performance.now) : Date.now || function() { 1096 return new Date().getTime(); 1097 }, 1098 1099 /** 1100 * Cross-version compatibility method to retrieve an option of a ngModel controller, 1101 * which supports the breaking changes in the AngularJS snapshot (SHA 87a2ff76af5d0a9268d8eb84db5755077d27c84c). 1102 * @param {!angular.ngModelCtrl} ngModelCtrl 1103 * @param {!string} optionName 1104 * @returns {Object|undefined} 1105 */ 1106 getModelOption: function (ngModelCtrl, optionName) { 1107 if (!ngModelCtrl.$options) { 1108 return; 1109 } 1110 1111 var $options = ngModelCtrl.$options; 1112 1113 // The newer versions of AngularJS introduced a `getOption function and made the option values no longer 1114 // visible on the $options object. 1115 return $options.getOption ? $options.getOption(optionName) : $options[optionName] 1116 }, 1117 1118 /** 1119 * Bi-directional accessor/mutator used to easily update an element's 1120 * property based on the current 'dir'ectional value. 1121 */ 1122 bidi : function(element, property, lValue, rValue) { 1123 var ltr = !($document[0].dir == 'rtl' || $document[0].body.dir == 'rtl'); 1124 1125 // If accessor 1126 if ( arguments.length == 0 ) return ltr ? 'ltr' : 'rtl'; 1127 1128 // If mutator 1129 var elem = angular.element(element); 1130 1131 if ( ltr && angular.isDefined(lValue)) { 1132 elem.css(property, validateCssValue(lValue)); 1133 } 1134 else if ( !ltr && angular.isDefined(rValue)) { 1135 elem.css(property, validateCssValue(rValue) ); 1136 } 1137 }, 1138 1139 bidiProperty: function (element, lProperty, rProperty, value) { 1140 var ltr = !($document[0].dir == 'rtl' || $document[0].body.dir == 'rtl'); 1141 1142 var elem = angular.element(element); 1143 1144 if ( ltr && angular.isDefined(lProperty)) { 1145 elem.css(lProperty, validateCssValue(value)); 1146 elem.css(rProperty, ''); 1147 } 1148 else if ( !ltr && angular.isDefined(rProperty)) { 1149 elem.css(rProperty, validateCssValue(value) ); 1150 elem.css(lProperty, ''); 1151 } 1152 }, 1153 1154 clientRect: function(element, offsetParent, isOffsetRect) { 1155 var node = getNode(element); 1156 offsetParent = getNode(offsetParent || node.offsetParent || document.body); 1157 var nodeRect = node.getBoundingClientRect(); 1158 1159 // The user can ask for an offsetRect: a rect relative to the offsetParent, 1160 // or a clientRect: a rect relative to the page 1161 var offsetRect = isOffsetRect ? 1162 offsetParent.getBoundingClientRect() : 1163 {left: 0, top: 0, width: 0, height: 0}; 1164 return { 1165 left: nodeRect.left - offsetRect.left, 1166 top: nodeRect.top - offsetRect.top, 1167 width: nodeRect.width, 1168 height: nodeRect.height 1169 }; 1170 }, 1171 offsetRect: function(element, offsetParent) { 1172 return $mdUtil.clientRect(element, offsetParent, true); 1173 }, 1174 1175 // Annoying method to copy nodes to an array, thanks to IE 1176 nodesToArray: function(nodes) { 1177 nodes = nodes || []; 1178 1179 var results = []; 1180 for (var i = 0; i < nodes.length; ++i) { 1181 results.push(nodes.item(i)); 1182 } 1183 return results; 1184 }, 1185 1186 /** 1187 * Determines the absolute position of the viewport. 1188 * Useful when making client rectangles absolute. 1189 * @returns {number} 1190 */ 1191 getViewportTop: function() { 1192 return window.scrollY || window.pageYOffset || 0; 1193 }, 1194 1195 /** 1196 * Finds the proper focus target by searching the DOM. 1197 * 1198 * @param containerEl 1199 * @param attributeVal 1200 * @returns {*} 1201 */ 1202 findFocusTarget: function(containerEl, attributeVal) { 1203 var AUTO_FOCUS = this.prefixer('md-autofocus', true); 1204 var elToFocus; 1205 1206 elToFocus = scanForFocusable(containerEl, attributeVal || AUTO_FOCUS); 1207 1208 if ( !elToFocus && attributeVal != AUTO_FOCUS) { 1209 // Scan for deprecated attribute 1210 elToFocus = scanForFocusable(containerEl, this.prefixer('md-auto-focus', true)); 1211 1212 if ( !elToFocus ) { 1213 // Scan for fallback to 'universal' API 1214 elToFocus = scanForFocusable(containerEl, AUTO_FOCUS); 1215 } 1216 } 1217 1218 return elToFocus; 1219 1220 /** 1221 * Can target and nested children for specified Selector (attribute) 1222 * whose value may be an expression that evaluates to True/False. 1223 */ 1224 function scanForFocusable(target, selector) { 1225 var elFound, items = target[0].querySelectorAll(selector); 1226 1227 // Find the last child element with the focus attribute 1228 if ( items && items.length ){ 1229 items.length && angular.forEach(items, function(it) { 1230 it = angular.element(it); 1231 1232 // Check the element for the md-autofocus class to ensure any associated expression 1233 // evaluated to true. 1234 var isFocusable = it.hasClass('md-autofocus'); 1235 if (isFocusable) elFound = it; 1236 }); 1237 } 1238 return elFound; 1239 } 1240 }, 1241 1242 /** 1243 * Disables scroll around the passed parent element. 1244 * @param element Unused 1245 * @param {!Element|!angular.JQLite} parent Element to disable scrolling within. 1246 * Defaults to body if none supplied. 1247 * @param options Object of options to modify functionality 1248 * - disableScrollMask Boolean of whether or not to create a scroll mask element or 1249 * use the passed parent element. 1250 */ 1251 disableScrollAround: function(element, parent, options) { 1252 options = options || {}; 1253 1254 $mdUtil.disableScrollAround._count = Math.max(0, $mdUtil.disableScrollAround._count || 0); 1255 $mdUtil.disableScrollAround._count++; 1256 1257 if ($mdUtil.disableScrollAround._restoreScroll) { 1258 return $mdUtil.disableScrollAround._restoreScroll; 1259 } 1260 1261 var body = $document[0].body; 1262 var restoreBody = disableBodyScroll(); 1263 var restoreElement = disableElementScroll(parent); 1264 1265 return $mdUtil.disableScrollAround._restoreScroll = function() { 1266 if (--$mdUtil.disableScrollAround._count <= 0) { 1267 restoreBody(); 1268 restoreElement(); 1269 delete $mdUtil.disableScrollAround._restoreScroll; 1270 } 1271 }; 1272 1273 /** 1274 * Creates a virtual scrolling mask to prevent touchmove, keyboard, scrollbar clicking, 1275 * and wheel events 1276 */ 1277 function disableElementScroll(element) { 1278 element = angular.element(element || body); 1279 1280 var scrollMask; 1281 1282 if (options.disableScrollMask) { 1283 scrollMask = element; 1284 } else { 1285 scrollMask = angular.element( 1286 '<div class="md-scroll-mask">' + 1287 ' <div class="md-scroll-mask-bar"></div>' + 1288 '</div>'); 1289 element.append(scrollMask); 1290 } 1291 1292 scrollMask.on('wheel', preventDefault); 1293 scrollMask.on('touchmove', preventDefault); 1294 1295 return function restoreElementScroll() { 1296 scrollMask.off('wheel'); 1297 scrollMask.off('touchmove'); 1298 1299 if (!options.disableScrollMask && scrollMask[0].parentNode ) { 1300 scrollMask[0].parentNode.removeChild(scrollMask[0]); 1301 } 1302 }; 1303 1304 function preventDefault(e) { 1305 e.preventDefault(); 1306 } 1307 } 1308 1309 // Converts the body to a position fixed block and translate it to the proper scroll position 1310 function disableBodyScroll() { 1311 var documentElement = $document[0].documentElement; 1312 1313 var prevDocumentStyle = documentElement.style.cssText || ''; 1314 var prevBodyStyle = body.style.cssText || ''; 1315 1316 var viewportTop = $mdUtil.getViewportTop(); 1317 var clientWidth = body.clientWidth; 1318 var hasVerticalScrollbar = body.scrollHeight > body.clientHeight + 1; 1319 1320 // Scroll may be set on <html> element (for example by overflow-y: scroll) 1321 // but Chrome is reporting the scrollTop position always on <body>. 1322 // scrollElement will allow to restore the scrollTop position to proper target. 1323 var scrollElement = documentElement.scrollTop > 0 ? documentElement : body; 1324 1325 if (hasVerticalScrollbar) { 1326 angular.element(body).css({ 1327 position: 'fixed', 1328 width: '100%', 1329 top: -viewportTop + 'px' 1330 }); 1331 } 1332 1333 if (body.clientWidth < clientWidth) { 1334 body.style.overflow = 'hidden'; 1335 } 1336 1337 // This should be applied after the manipulation to the body, because 1338 // adding a scrollbar can potentially resize it, causing the measurement 1339 // to change. 1340 if (hasVerticalScrollbar) { 1341 documentElement.style.overflowY = 'scroll'; 1342 } 1343 1344 return function restoreScroll() { 1345 // Reset the inline style CSS to the previous. 1346 body.style.cssText = prevBodyStyle; 1347 documentElement.style.cssText = prevDocumentStyle; 1348 1349 // The scroll position while being fixed 1350 scrollElement.scrollTop = viewportTop; 1351 }; 1352 } 1353 1354 }, 1355 1356 enableScrolling: function() { 1357 var restoreFn = this.disableScrollAround._restoreScroll; 1358 restoreFn && restoreFn(); 1359 }, 1360 1361 floatingScrollbars: function() { 1362 if (this.floatingScrollbars.cached === undefined) { 1363 var tempNode = angular.element('<div><div></div></div>').css({ 1364 width: '100%', 1365 'z-index': -1, 1366 position: 'absolute', 1367 height: '35px', 1368 'overflow-y': 'scroll' 1369 }); 1370 tempNode.children().css('height', '60px'); 1371 1372 $document[0].body.appendChild(tempNode[0]); 1373 this.floatingScrollbars.cached = (tempNode[0].offsetWidth == tempNode[0].childNodes[0].offsetWidth); 1374 tempNode.remove(); 1375 } 1376 return this.floatingScrollbars.cached; 1377 }, 1378 1379 // Mobile safari only allows you to set focus in click event listeners... 1380 forceFocus: function(element) { 1381 var node = element[0] || element; 1382 1383 document.addEventListener('click', function focusOnClick(ev) { 1384 if (ev.target === node && ev.$focus) { 1385 node.focus(); 1386 ev.stopImmediatePropagation(); 1387 ev.preventDefault(); 1388 node.removeEventListener('click', focusOnClick); 1389 } 1390 }, true); 1391 1392 var newEvent = document.createEvent('MouseEvents'); 1393 newEvent.initMouseEvent('click', false, true, window, {}, 0, 0, 0, 0, 1394 false, false, false, false, 0, null); 1395 newEvent.$material = true; 1396 newEvent.$focus = true; 1397 node.dispatchEvent(newEvent); 1398 }, 1399 1400 /** 1401 * facade to build md-backdrop element with desired styles 1402 * NOTE: Use $compile to trigger backdrop postLink function 1403 */ 1404 createBackdrop: function(scope, addClass) { 1405 return $compile($mdUtil.supplant('<md-backdrop class="{0}">', [addClass]))(scope); 1406 }, 1407 1408 /** 1409 * supplant() method from Crockford's `Remedial Javascript` 1410 * Equivalent to use of $interpolate; without dependency on 1411 * interpolation symbols and scope. Note: the '{<token>}' can 1412 * be property names, property chains, or array indices. 1413 */ 1414 supplant: function(template, values, pattern) { 1415 pattern = pattern || /\{([^\{\}]*)\}/g; 1416 return template.replace(pattern, function(a, b) { 1417 var p = b.split('.'), 1418 r = values; 1419 try { 1420 for (var s in p) { 1421 if (p.hasOwnProperty(s) ) { 1422 r = r[p[s]]; 1423 } 1424 } 1425 } catch (e) { 1426 r = a; 1427 } 1428 return (typeof r === 'string' || typeof r === 'number') ? r : a; 1429 }); 1430 }, 1431 1432 fakeNgModel: function() { 1433 return { 1434 $fake: true, 1435 $setTouched: angular.noop, 1436 $setViewValue: function(value) { 1437 this.$viewValue = value; 1438 this.$render(value); 1439 this.$viewChangeListeners.forEach(function(cb) { 1440 cb(); 1441 }); 1442 }, 1443 $isEmpty: function(value) { 1444 return ('' + value).length === 0; 1445 }, 1446 $parsers: [], 1447 $formatters: [], 1448 $viewChangeListeners: [], 1449 $render: angular.noop 1450 }; 1451 }, 1452 1453 // Returns a function, that, as long as it continues to be invoked, will not 1454 // be triggered. The function will be called after it stops being called for 1455 // N milliseconds. 1456 // @param wait Integer value of msecs to delay (since last debounce reset); default value 10 msecs 1457 // @param invokeApply should the $timeout trigger $digest() dirty checking 1458 debounce: function(func, wait, scope, invokeApply) { 1459 var timer; 1460 1461 return function debounced() { 1462 var context = scope, 1463 args = Array.prototype.slice.call(arguments); 1464 1465 $timeout.cancel(timer); 1466 timer = $timeout(function() { 1467 1468 timer = undefined; 1469 func.apply(context, args); 1470 1471 }, wait || 10, invokeApply); 1472 }; 1473 }, 1474 1475 // Returns a function that can only be triggered every `delay` milliseconds. 1476 // In other words, the function will not be called unless it has been more 1477 // than `delay` milliseconds since the last call. 1478 throttle: function throttle(func, delay) { 1479 var recent; 1480 return function throttled() { 1481 var context = this; 1482 var args = arguments; 1483 var now = $mdUtil.now(); 1484 1485 if (!recent || (now - recent > delay)) { 1486 func.apply(context, args); 1487 recent = now; 1488 } 1489 }; 1490 }, 1491 1492 /** 1493 * Measures the number of milliseconds taken to run the provided callback 1494 * function. Uses a high-precision timer if available. 1495 */ 1496 time: function time(cb) { 1497 var start = $mdUtil.now(); 1498 cb(); 1499 return $mdUtil.now() - start; 1500 }, 1501 1502 /** 1503 * Create an implicit getter that caches its `getter()` 1504 * lookup value 1505 */ 1506 valueOnUse : function (scope, key, getter) { 1507 var value = null, args = Array.prototype.slice.call(arguments); 1508 var params = (args.length > 3) ? args.slice(3) : [ ]; 1509 1510 Object.defineProperty(scope, key, { 1511 get: function () { 1512 if (value === null) value = getter.apply(scope, params); 1513 return value; 1514 } 1515 }); 1516 }, 1517 1518 /** 1519 * Get a unique ID. 1520 * 1521 * @returns {string} an unique numeric string 1522 */ 1523 nextUid: function() { 1524 return '' + nextUniqueId++; 1525 }, 1526 1527 // Stop watchers and events from firing on a scope without destroying it, 1528 // by disconnecting it from its parent and its siblings' linked lists. 1529 disconnectScope: function disconnectScope(scope) { 1530 if (!scope) return; 1531 1532 // we can't destroy the root scope or a scope that has been already destroyed 1533 if (scope.$root === scope) return; 1534 if (scope.$$destroyed) return; 1535 1536 var parent = scope.$parent; 1537 scope.$$disconnected = true; 1538 1539 // See Scope.$destroy 1540 if (parent.$$childHead === scope) parent.$$childHead = scope.$$nextSibling; 1541 if (parent.$$childTail === scope) parent.$$childTail = scope.$$prevSibling; 1542 if (scope.$$prevSibling) scope.$$prevSibling.$$nextSibling = scope.$$nextSibling; 1543 if (scope.$$nextSibling) scope.$$nextSibling.$$prevSibling = scope.$$prevSibling; 1544 1545 scope.$$nextSibling = scope.$$prevSibling = null; 1546 1547 }, 1548 1549 // Undo the effects of disconnectScope above. 1550 reconnectScope: function reconnectScope(scope) { 1551 if (!scope) return; 1552 1553 // we can't disconnect the root node or scope already disconnected 1554 if (scope.$root === scope) return; 1555 if (!scope.$$disconnected) return; 1556 1557 var child = scope; 1558 1559 var parent = child.$parent; 1560 child.$$disconnected = false; 1561 // See Scope.$new for this logic... 1562 child.$$prevSibling = parent.$$childTail; 1563 if (parent.$$childHead) { 1564 parent.$$childTail.$$nextSibling = child; 1565 parent.$$childTail = child; 1566 } else { 1567 parent.$$childHead = parent.$$childTail = child; 1568 } 1569 }, 1570 1571 /* 1572 * getClosest replicates jQuery.closest() to walk up the DOM tree until it finds a matching nodeName 1573 * 1574 * @param el Element to start walking the DOM from 1575 * @param check Either a string or a function. If a string is passed, it will be evaluated against 1576 * each of the parent nodes' tag name. If a function is passed, the loop will call it with each of 1577 * the parents and will use the return value to determine whether the node is a match. 1578 * @param onlyParent Only start checking from the parent element, not `el`. 1579 */ 1580 getClosest: function getClosest(el, validateWith, onlyParent) { 1581 if ( angular.isString(validateWith) ) { 1582 var tagName = validateWith.toUpperCase(); 1583 validateWith = function(el) { 1584 return el.nodeName.toUpperCase() === tagName; 1585 }; 1586 } 1587 1588 if (el instanceof angular.element) el = el[0]; 1589 if (onlyParent) el = el.parentNode; 1590 if (!el) return null; 1591 1592 do { 1593 if (validateWith(el)) { 1594 return el; 1595 } 1596 } while (el = el.parentNode); 1597 1598 return null; 1599 }, 1600 1601 /** 1602 * Build polyfill for the Node.contains feature (if needed) 1603 */ 1604 elementContains: function(node, child) { 1605 var hasContains = (window.Node && window.Node.prototype && Node.prototype.contains); 1606 var findFn = hasContains ? angular.bind(node, node.contains) : angular.bind(node, function(arg) { 1607 // compares the positions of two nodes and returns a bitmask 1608 return (node === child) || !!(this.compareDocumentPosition(arg) & 16) 1609 }); 1610 1611 return findFn(child); 1612 }, 1613 1614 /** 1615 * Functional equivalent for $element.filter(‘md-bottom-sheet’) 1616 * useful with interimElements where the element and its container are important... 1617 * 1618 * @param {[]} elements to scan 1619 * @param {string} name of node to find (e.g. 'md-dialog') 1620 * @param {boolean=} optional flag to allow deep scans; defaults to 'false'. 1621 * @param {boolean=} optional flag to enable log warnings; defaults to false 1622 */ 1623 extractElementByName: function(element, nodeName, scanDeep, warnNotFound) { 1624 var found = scanTree(element); 1625 if (!found && !!warnNotFound) { 1626 $log.warn( $mdUtil.supplant("Unable to find node '{0}' in element '{1}'.",[nodeName, element[0].outerHTML]) ); 1627 } 1628 1629 return angular.element(found || element); 1630 1631 /** 1632 * Breadth-First tree scan for element with matching `nodeName` 1633 */ 1634 function scanTree(element) { 1635 return scanLevel(element) || (!!scanDeep ? scanChildren(element) : null); 1636 } 1637 1638 /** 1639 * Case-insensitive scan of current elements only (do not descend). 1640 */ 1641 function scanLevel(element) { 1642 if ( element ) { 1643 for (var i = 0, len = element.length; i < len; i++) { 1644 if (element[i].nodeName.toLowerCase() === nodeName) { 1645 return element[i]; 1646 } 1647 } 1648 } 1649 return null; 1650 } 1651 1652 /** 1653 * Scan children of specified node 1654 */ 1655 function scanChildren(element) { 1656 var found; 1657 if ( element ) { 1658 for (var i = 0, len = element.length; i < len; i++) { 1659 var target = element[i]; 1660 if ( !found ) { 1661 for (var j = 0, numChild = target.childNodes.length; j < numChild; j++) { 1662 found = found || scanTree([target.childNodes[j]]); 1663 } 1664 } 1665 } 1666 } 1667 return found; 1668 } 1669 1670 }, 1671 1672 /** 1673 * Give optional properties with no value a boolean true if attr provided or false otherwise 1674 */ 1675 initOptionalProperties: function(scope, attr, defaults) { 1676 defaults = defaults || {}; 1677 angular.forEach(scope.$$isolateBindings, function(binding, key) { 1678 if (binding.optional && angular.isUndefined(scope[key])) { 1679 var attrIsDefined = angular.isDefined(attr[binding.attrName]); 1680 scope[key] = angular.isDefined(defaults[key]) ? defaults[key] : attrIsDefined; 1681 } 1682 }); 1683 }, 1684 1685 /** 1686 * Alternative to $timeout calls with 0 delay. 1687 * nextTick() coalesces all calls within a single frame 1688 * to minimize $digest thrashing 1689 * 1690 * @param callback 1691 * @param digest 1692 * @returns {*} 1693 */ 1694 nextTick: function(callback, digest, scope) { 1695 //-- grab function reference for storing state details 1696 var nextTick = $mdUtil.nextTick; 1697 var timeout = nextTick.timeout; 1698 var queue = nextTick.queue || []; 1699 1700 //-- add callback to the queue 1701 queue.push({scope: scope, callback: callback}); 1702 1703 //-- set default value for digest 1704 if (digest == null) digest = true; 1705 1706 //-- store updated digest/queue values 1707 nextTick.digest = nextTick.digest || digest; 1708 nextTick.queue = queue; 1709 1710 //-- either return existing timeout or create a new one 1711 return timeout || (nextTick.timeout = $timeout(processQueue, 0, false)); 1712 1713 /** 1714 * Grab a copy of the current queue 1715 * Clear the queue for future use 1716 * Process the existing queue 1717 * Trigger digest if necessary 1718 */ 1719 function processQueue() { 1720 var queue = nextTick.queue; 1721 var digest = nextTick.digest; 1722 1723 nextTick.queue = []; 1724 nextTick.timeout = null; 1725 nextTick.digest = false; 1726 1727 queue.forEach(function(queueItem) { 1728 var skip = queueItem.scope && queueItem.scope.$$destroyed; 1729 if (!skip) { 1730 queueItem.callback(); 1731 } 1732 }); 1733 1734 if (digest) $rootScope.$digest(); 1735 } 1736 }, 1737 1738 /** 1739 * Processes a template and replaces the start/end symbols if the application has 1740 * overriden them. 1741 * 1742 * @param template The template to process whose start/end tags may be replaced. 1743 * @returns {*} 1744 */ 1745 processTemplate: function(template) { 1746 if (usesStandardSymbols) { 1747 return template; 1748 } else { 1749 if (!template || !angular.isString(template)) return template; 1750 return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol); 1751 } 1752 }, 1753 1754 /** 1755 * Scan up dom hierarchy for enabled parent; 1756 */ 1757 getParentWithPointerEvents: function (element) { 1758 var parent = element.parent(); 1759 1760 // jqLite might return a non-null, but still empty, parent; so check for parent and length 1761 while (hasComputedStyle(parent, 'pointer-events', 'none')) { 1762 parent = parent.parent(); 1763 } 1764 1765 return parent; 1766 }, 1767 1768 getNearestContentElement: function (element) { 1769 var current = element.parent()[0]; 1770 // Look for the nearest parent md-content, stopping at the rootElement. 1771 while (current && current !== $rootElement[0] && current !== document.body && current.nodeName.toUpperCase() !== 'MD-CONTENT') { 1772 current = current.parentNode; 1773 } 1774 return current; 1775 }, 1776 1777 /** 1778 * Checks if the current browser is natively supporting the `sticky` position. 1779 * @returns {string} supported sticky property name 1780 */ 1781 checkStickySupport: function() { 1782 var stickyProp; 1783 var testEl = angular.element('<div>'); 1784 $document[0].body.appendChild(testEl[0]); 1785 1786 var stickyProps = ['sticky', '-webkit-sticky']; 1787 for (var i = 0; i < stickyProps.length; ++i) { 1788 testEl.css({ 1789 position: stickyProps[i], 1790 top: 0, 1791 'z-index': 2 1792 }); 1793 1794 if (testEl.css('position') == stickyProps[i]) { 1795 stickyProp = stickyProps[i]; 1796 break; 1797 } 1798 } 1799 1800 testEl.remove(); 1801 1802 return stickyProp; 1803 }, 1804 1805 /** 1806 * Parses an attribute value, mostly a string. 1807 * By default checks for negated values and returns `false´ if present. 1808 * Negated values are: (native falsy) and negative strings like: 1809 * `false` or `0`. 1810 * @param value Attribute value which should be parsed. 1811 * @param negatedCheck When set to false, won't check for negated values. 1812 * @returns {boolean} 1813 */ 1814 parseAttributeBoolean: function(value, negatedCheck) { 1815 return value === '' || !!value && (negatedCheck === false || value !== 'false' && value !== '0'); 1816 }, 1817 1818 hasComputedStyle: hasComputedStyle, 1819 1820 /** 1821 * Returns true if the parent form of the element has been submitted. 1822 * 1823 * @param element An AngularJS or HTML5 element. 1824 * 1825 * @returns {boolean} 1826 */ 1827 isParentFormSubmitted: function(element) { 1828 var parent = $mdUtil.getClosest(element, 'form'); 1829 var form = parent ? angular.element(parent).controller('form') : null; 1830 1831 return form ? form.$submitted : false; 1832 }, 1833 1834 /** 1835 * Animate the requested element's scrollTop to the requested scrollPosition with basic easing. 1836 * 1837 * @param {!HTMLElement} element The element to scroll. 1838 * @param {number} scrollEnd The new/final scroll position. 1839 * @param {number=} duration Duration of the scroll. Default is 1000ms. 1840 */ 1841 animateScrollTo: function(element, scrollEnd, duration) { 1842 var scrollStart = element.scrollTop; 1843 var scrollChange = scrollEnd - scrollStart; 1844 var scrollingDown = scrollStart < scrollEnd; 1845 var startTime = $mdUtil.now(); 1846 1847 $$rAF(scrollChunk); 1848 1849 function scrollChunk() { 1850 var newPosition = calculateNewPosition(); 1851 1852 element.scrollTop = newPosition; 1853 1854 if (scrollingDown ? newPosition < scrollEnd : newPosition > scrollEnd) { 1855 $$rAF(scrollChunk); 1856 } 1857 } 1858 1859 function calculateNewPosition() { 1860 var easeDuration = duration || 1000; 1861 var currentTime = $mdUtil.now() - startTime; 1862 1863 return ease(currentTime, scrollStart, scrollChange, easeDuration); 1864 } 1865 1866 function ease(currentTime, start, change, duration) { 1867 // If the duration has passed (which can occur if our app loses focus due to $$rAF), jump 1868 // straight to the proper position 1869 if (currentTime > duration) { 1870 return start + change; 1871 } 1872 1873 var ts = (currentTime /= duration) * currentTime; 1874 var tc = ts * currentTime; 1875 1876 return start + change * (-2 * tc + 3 * ts); 1877 } 1878 }, 1879 1880 /** 1881 * Provides an easy mechanism for removing duplicates from an array. 1882 * 1883 * var myArray = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4]; 1884 * 1885 * $mdUtil.uniq(myArray) => [1, 2, 3, 4] 1886 * 1887 * @param {array} array The array whose unique values should be returned. 1888 * 1889 * @returns {array} A copy of the array containing only unique values. 1890 */ 1891 uniq: function(array) { 1892 if (!array) { return; } 1893 1894 return array.filter(function(value, index, self) { 1895 return self.indexOf(value) === index; 1896 }); 1897 } 1898 }; 1899 1900 1901// Instantiate other namespace utility methods 1902 1903 $mdUtil.dom.animator = $$mdAnimate($mdUtil); 1904 1905 return $mdUtil; 1906 1907 function getNode(el) { 1908 return el[0] || el; 1909 } 1910 1911} 1912 1913/* 1914 * Since removing jQuery from the demos, some code that uses `element.focus()` is broken. 1915 * We need to add `element.focus()`, because it's testable unlike `element[0].focus`. 1916 */ 1917 1918angular.element.prototype.focus = angular.element.prototype.focus || function() { 1919 if (this.length) { 1920 this[0].focus(); 1921 } 1922 return this; 1923 }; 1924angular.element.prototype.blur = angular.element.prototype.blur || function() { 1925 if (this.length) { 1926 this[0].blur(); 1927 } 1928 return this; 1929 }; 1930 1931})(); 1932(function(){ 1933"use strict"; 1934 1935/** 1936 * @ngdoc module 1937 * @name material.core.aria 1938 * @description 1939 * Aria Expectations for AngularJS Material components. 1940 */ 1941MdAriaService.$inject = ["$$rAF", "$log", "$window", "$interpolate"]; 1942angular 1943 .module('material.core') 1944 .provider('$mdAria', MdAriaProvider); 1945 1946/** 1947 * @ngdoc service 1948 * @name $mdAriaProvider 1949 * @module material.core.aria 1950 * 1951 * @description 1952 * 1953 * Modify options of the `$mdAria` service, which will be used by most of the AngularJS Material 1954 * components. 1955 * 1956 * You are able to disable `$mdAria` warnings, by using the following markup. 1957 * 1958 * <hljs lang="js"> 1959 * app.config(function($mdAriaProvider) { 1960 * // Globally disables all ARIA warnings. 1961 * $mdAriaProvider.disableWarnings(); 1962 * }); 1963 * </hljs> 1964 * 1965 */ 1966function MdAriaProvider() { 1967 1968 var config = { 1969 /** Whether we should show ARIA warnings in the console if labels are missing on the element */ 1970 showWarnings: true 1971 }; 1972 1973 return { 1974 disableWarnings: disableWarnings, 1975 $get: ["$$rAF", "$log", "$window", "$interpolate", function($$rAF, $log, $window, $interpolate) { 1976 return MdAriaService.apply(config, arguments); 1977 }] 1978 }; 1979 1980 /** 1981 * @ngdoc method 1982 * @name $mdAriaProvider#disableWarnings 1983 * @description Disables all ARIA warnings generated by AngularJS Material. 1984 */ 1985 function disableWarnings() { 1986 config.showWarnings = false; 1987 } 1988} 1989 1990/* 1991 * @ngInject 1992 */ 1993function MdAriaService($$rAF, $log, $window, $interpolate) { 1994 1995 // Load the showWarnings option from the current context and store it inside of a scope variable, 1996 // because the context will be probably lost in some function calls. 1997 var showWarnings = this.showWarnings; 1998 1999 return { 2000 expect: expect, 2001 expectAsync: expectAsync, 2002 expectWithText: expectWithText, 2003 expectWithoutText: expectWithoutText, 2004 getText: getText, 2005 hasAriaLabel: hasAriaLabel, 2006 parentHasAriaLabel: parentHasAriaLabel 2007 }; 2008 2009 /** 2010 * Check if expected attribute has been specified on the target element or child 2011 * @param element 2012 * @param attrName 2013 * @param {optional} defaultValue What to set the attr to if no value is found 2014 */ 2015 function expect(element, attrName, defaultValue) { 2016 2017 var node = angular.element(element)[0] || element; 2018 2019 // if node exists and neither it nor its children have the attribute 2020 if (node && 2021 ((!node.hasAttribute(attrName) || 2022 node.getAttribute(attrName).length === 0) && 2023 !childHasAttribute(node, attrName))) { 2024 2025 defaultValue = angular.isString(defaultValue) ? defaultValue.trim() : ''; 2026 if (defaultValue.length) { 2027 element.attr(attrName, defaultValue); 2028 } else if (showWarnings) { 2029 $log.warn('ARIA: Attribute "', attrName, '", required for accessibility, is missing on node:', node); 2030 } 2031 2032 } 2033 } 2034 2035 function expectAsync(element, attrName, defaultValueGetter) { 2036 // Problem: when retrieving the element's contents synchronously to find the label, 2037 // the text may not be defined yet in the case of a binding. 2038 // There is a higher chance that a binding will be defined if we wait one frame. 2039 $$rAF(function() { 2040 expect(element, attrName, defaultValueGetter()); 2041 }); 2042 } 2043 2044 function expectWithText(element, attrName) { 2045 var content = getText(element) || ""; 2046 var hasBinding = content.indexOf($interpolate.startSymbol()) > -1; 2047 2048 if (hasBinding) { 2049 expectAsync(element, attrName, function() { 2050 return getText(element); 2051 }); 2052 } else { 2053 expect(element, attrName, content); 2054 } 2055 } 2056 2057 function expectWithoutText(element, attrName) { 2058 var content = getText(element); 2059 var hasBinding = content.indexOf($interpolate.startSymbol()) > -1; 2060 2061 if ( !hasBinding && !content) { 2062 expect(element, attrName, content); 2063 } 2064 } 2065 2066 function getText(element) { 2067 element = element[0] || element; 2068 var walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false); 2069 var text = ''; 2070 2071 var node; 2072 while (node = walker.nextNode()) { 2073 if (!isAriaHiddenNode(node)) { 2074 text += node.textContent; 2075 } 2076 } 2077 2078 return text.trim() || ''; 2079 2080 function isAriaHiddenNode(node) { 2081 while (node.parentNode && (node = node.parentNode) !== element) { 2082 if (node.getAttribute && node.getAttribute('aria-hidden') === 'true') { 2083 return true; 2084 } 2085 } 2086 } 2087 } 2088 2089 function childHasAttribute(node, attrName) { 2090 var hasChildren = node.hasChildNodes(), 2091 hasAttr = false; 2092 2093 function isHidden(el) { 2094 var style = el.currentStyle ? el.currentStyle : $window.getComputedStyle(el); 2095 return (style.display === 'none'); 2096 } 2097 2098 if (hasChildren) { 2099 var children = node.childNodes; 2100 for (var i=0; i < children.length; i++) { 2101 var child = children[i]; 2102 if (child.nodeType === 1 && child.hasAttribute(attrName)) { 2103 if (!isHidden(child)) { 2104 hasAttr = true; 2105 } 2106 } 2107 } 2108 } 2109 return hasAttr; 2110 } 2111 2112 /** 2113 * Check if expected element has aria label attribute 2114 * @param element 2115 */ 2116 function hasAriaLabel(element) { 2117 var node = angular.element(element)[0] || element; 2118 2119 /* Check if compatible node type (ie: not HTML Document node) */ 2120 if (!node.hasAttribute) { 2121 return false; 2122 } 2123 2124 /* Check label or description attributes */ 2125 return node.hasAttribute('aria-label') || node.hasAttribute('aria-labelledby') || node.hasAttribute('aria-describedby'); 2126 } 2127 2128 /** 2129 * Check if expected element's parent has aria label attribute and has valid role and tagName 2130 * @param element 2131 * @param {optional} level Number of levels deep search should be performed 2132 */ 2133 function parentHasAriaLabel(element, level) { 2134 level = level || 1; 2135 var node = angular.element(element)[0] || element; 2136 if (!node.parentNode) { 2137 return false; 2138 } 2139 if (performCheck(node.parentNode)) { 2140 return true; 2141 } 2142 level--; 2143 if (level) { 2144 return parentHasAriaLabel(node.parentNode, level); 2145 } 2146 return false; 2147 2148 function performCheck(parentNode) { 2149 if (!hasAriaLabel(parentNode)) { 2150 return false; 2151 } 2152 /* Perform role blacklist check */ 2153 if (parentNode.hasAttribute('role')) { 2154 switch(parentNode.getAttribute('role').toLowerCase()) { 2155 case 'command': 2156 case 'definition': 2157 case 'directory': 2158 case 'grid': 2159 case 'list': 2160 case 'listitem': 2161 case 'log': 2162 case 'marquee': 2163 case 'menu': 2164 case 'menubar': 2165 case 'note': 2166 case 'presentation': 2167 case 'separator': 2168 case 'scrollbar': 2169 case 'status': 2170 case 'tablist': 2171 return false; 2172 } 2173 } 2174 /* Perform tagName blacklist check */ 2175 switch(parentNode.tagName.toLowerCase()) { 2176 case 'abbr': 2177 case 'acronym': 2178 case 'address': 2179 case 'applet': 2180 case 'audio': 2181 case 'b': 2182 case 'bdi': 2183 case 'bdo': 2184 case 'big': 2185 case 'blockquote': 2186 case 'br': 2187 case 'canvas': 2188 case 'caption': 2189 case 'center': 2190 case 'cite': 2191 case 'code': 2192 case 'col': 2193 case 'data': 2194 case 'dd': 2195 case 'del': 2196 case 'dfn': 2197 case 'dir': 2198 case 'div': 2199 case 'dl': 2200 case 'em': 2201 case 'embed': 2202 case 'fieldset': 2203 case 'figcaption': 2204 case 'font': 2205 case 'h1': 2206 case 'h2': 2207 case 'h3': 2208 case 'h4': 2209 case 'h5': 2210 case 'h6': 2211 case 'hgroup': 2212 case 'html': 2213 case 'i': 2214 case 'ins': 2215 case 'isindex': 2216 case 'kbd': 2217 case 'keygen': 2218 case 'label': 2219 case 'legend': 2220 case 'li': 2221 case 'map': 2222 case 'mark': 2223 case 'menu': 2224 case 'object': 2225 case 'ol': 2226 case 'output': 2227 case 'pre': 2228 case 'presentation': 2229 case 'q': 2230 case 'rt': 2231 case 'ruby': 2232 case 'samp': 2233 case 'small': 2234 case 'source': 2235 case 'span': 2236 case 'status': 2237 case 'strike': 2238 case 'strong': 2239 case 'sub': 2240 case 'sup': 2241 case 'svg': 2242 case 'tbody': 2243 case 'td': 2244 case 'th': 2245 case 'thead': 2246 case 'time': 2247 case 'tr': 2248 case 'track': 2249 case 'tt': 2250 case 'ul': 2251 case 'var': 2252 return false; 2253 } 2254 return true; 2255 } 2256 } 2257} 2258 2259})(); 2260(function(){ 2261"use strict"; 2262 2263/** 2264 * @ngdoc module 2265 * @name material.core.compiler 2266 * @description 2267 * AngularJS Material template and element compiler. 2268 */ 2269angular 2270 .module('material.core') 2271 .provider('$mdCompiler', MdCompilerProvider); 2272 2273/** 2274 * @ngdoc provider 2275 * @name $mdCompilerProvider 2276 * 2277 * @description 2278 */ 2279/** 2280 * @ngdoc service 2281 * @name $mdCompiler 2282 * @module material.core.compiler 2283 * @description 2284 * The $mdCompiler service is an abstraction of AngularJS's compiler, that allows developers 2285 * to easily compile an element with options like in a Directive Definition Object. 2286 * 2287 * > The compiler powers a lot of components inside of AngularJS Material. 2288 * > Like the `$mdPanel` or `$mdDialog`. 2289 * 2290 * @usage 2291 * 2292 * Basic Usage with a template 2293 * 2294 * <hljs lang="js"> 2295 * $mdCompiler.compile({ 2296 * templateUrl: 'modal.html', 2297 * controller: 'ModalCtrl', 2298 * locals: { 2299 * modal: myModalInstance; 2300 * } 2301 * }).then(function (compileData) { 2302 * compileData.element; // Compiled DOM element 2303 * compileData.link(myScope); // Instantiate controller and link element to scope. 2304 * }); 2305 * </hljs> 2306 * 2307 * Example with a content element 2308 * 2309 * <hljs lang="js"> 2310 * 2311 * // Create a virtual element and link it manually. 2312 * // The compiler doesn't need to recompile the element each time. 2313 * var myElement = $compile('<span>Test</span>')(myScope); 2314 * 2315 * $mdCompiler.compile({ 2316 * contentElement: myElement 2317 * }).then(function (compileData) { 2318 * compileData.element // Content Element (same as above) 2319 * compileData.link // This does nothing when using a contentElement. 2320 * }); 2321 * </hljs> 2322 * 2323 * > Content Element is a significant performance improvement when the developer already knows that the 2324 * > compiled element will be always the same and the scope will not change either. 2325 * 2326 * The `contentElement` option also supports DOM elements which will be temporary removed and restored 2327 * at its old position. 2328 * 2329 * <hljs lang="js"> 2330 * var domElement = document.querySelector('#myElement'); 2331 * 2332 * $mdCompiler.compile({ 2333 * contentElement: myElement 2334 * }).then(function (compileData) { 2335 * compileData.element // Content Element (same as above) 2336 * compileData.link // This does nothing when using a contentElement. 2337 * }); 2338 * </hljs> 2339 * 2340 * The `$mdCompiler` can also query for the element in the DOM itself. 2341 * 2342 * <hljs lang="js"> 2343 * $mdCompiler.compile({ 2344 * contentElement: '#myElement' 2345 * }).then(function (compileData) { 2346 * compileData.element // Content Element (same as above) 2347 * compileData.link // This does nothing when using a contentElement. 2348 * }); 2349 * </hljs> 2350 * 2351 */ 2352MdCompilerProvider.$inject = ['$compileProvider']; 2353function MdCompilerProvider($compileProvider) { 2354 2355 var provider = this; 2356 2357 /** 2358 * @ngdoc method 2359 * @name $mdCompilerProvider#respectPreAssignBindingsEnabled 2360 * 2361 * @param {boolean=} respected update the respectPreAssignBindingsEnabled state if provided, otherwise just return 2362 * the current Material preAssignBindingsEnabled state 2363 * @returns {*} current value if used as getter or itself (chaining) if used as setter 2364 * 2365 * @kind function 2366 * 2367 * @description 2368 * Call this method to enable/disable whether Material-specific (dialogs/toasts) controllers respect the AngularJS 2369 * `$compile.preAssignBindingsEnabled` flag. Note that this doesn't affect directives/components created via 2370 * regular AngularJS methods which constitute most Material & user-created components. 2371 * 2372 * @see [AngularJS documentation for `$compile.preAssignBindingsEnabled` 2373 * ](https://code.angularjs.org/1.6.4/docs/api/ng/provider/$compileProvider#preAssignBindingsEnabled) 2374 * for more information. 2375 * 2376 * If disabled (false), the compiler assigns the value of each of the bindings to the 2377 * properties of the controller object before the constructor of this object is called. 2378 * 2379 * If enabled (true) the behavior depends on the AngularJS version used: 2380 * 2381 * - `<1.5.10` - bindings are pre-assigned 2382 * - `>=1.5.10 <1.7` - behaves like set to whatever `$compileProvider.preAssignBindingsEnabled()` reports; if
2383 * the `$compileProvider` flag wasn't set manually, it defaults to pre-assigning bindings 2384 * with AngularJS `1.5.x` & to calling the constructor first with AngularJS `1.6.x`. 2385 * - `>=1.7` - the compiler calls the constructor first before assigning bindings 2386 * 2387 * The default value is `false` but will change to `true` in AngularJS Material 1.2. 2388 * 2389 * It is recommended to set this flag to `true` in AngularJS Material 1.1.x; the only reason it's not set that way 2390 * by default is backwards compatibility. Not setting the flag to `true` when 2391 * `$compileProvider.preAssignBindingsEnabled()` is set to `false` (i.e. default behavior in AngularJS 1.6.0 or newer) 2392 * makes it hard to unit test Material Dialog/Toast controllers using the `$controller` helper as it always follows 2393 * the `$compileProvider.preAssignBindingsEnabled()` value. 2394 */ 2395 // TODO change it to `true` in Material 1.2. 2396 var respectPreAssignBindingsEnabled = false; 2397 this.respectPreAssignBindingsEnabled = function(respected) { 2398 if (angular.isDefined(respected)) { 2399 respectPreAssignBindingsEnabled = respected; 2400 return this; 2401 } 2402 2403 return respectPreAssignBindingsEnabled; 2404 }; 2405 2406 /** 2407 * @ngdoc function 2408 * @name getPreAssignBindingsEnabled 2409 * @private 2410 * 2411 * @returns {*} current preAssignBindingsEnabled state 2412 * 2413 * @kind function 2414 * 2415 * @description 2416 * This function returns `true` if Material-specific (dialogs/toasts) controllers have bindings pre-assigned in 2417 * controller constructors and `false` otherwise. 2418 * 2419 * Note that this doesn't affect directives/components created via regular AngularJS methods which constitute most 2420 * Material & user-created components; their behavior can be checked via `$compileProvider.preAssignBindingsEnabled()` 2421 * in AngularJS `>=1.5.10 <1.7.0`. 2422 */ 2423 function getPreAssignBindingsEnabled() { 2424 if (!respectPreAssignBindingsEnabled) { 2425 // respectPreAssignBindingsEnabled === false 2426 // We're ignoring the AngularJS `$compileProvider.preAssignBindingsEnabled()` value in this case. 2427 return true; 2428 } 2429 2430 // respectPreAssignBindingsEnabled === true 2431 2432 if (typeof $compileProvider.preAssignBindingsEnabled === 'function') { 2433 return $compileProvider.preAssignBindingsEnabled(); 2434 } 2435 2436 // Flag respected but not present => apply logic based on AngularJS version used. 2437 if (angular.version.major === 1 && angular.version.minor < 6) { 2438 // AngularJS <1.5.10 2439 return true; 2440 } 2441 2442 // AngularJS >=1.7.0 2443 return false; 2444 } 2445 2446 this.$get = ["$q", "$templateRequest", "$injector", "$compile", "$controller", 2447 function($q, $templateRequest, $injector, $compile, $controller) { 2448 return new MdCompilerService($q, $templateRequest, $injector, $compile, $controller); 2449 }]; 2450 2451 function MdCompilerService($q, $templateRequest, $injector, $compile, $controller) { 2452 2453 /** @private @const {!angular.$q} */ 2454 this.$q = $q; 2455 2456 /** @private @const {!angular.$templateRequest} */ 2457 this.$templateRequest = $templateRequest; 2458 2459 /** @private @const {!angular.$injector} */ 2460 this.$injector = $injector; 2461 2462 /** @private @const {!angular.$compile} */ 2463 this.$compile = $compile; 2464 2465 /** @private @const {!angular.$controller} */ 2466 this.$controller = $controller; 2467 } 2468 2469 /** 2470 * @ngdoc method 2471 * @name $mdCompiler#compile 2472 * @description 2473 * 2474 * A method to compile a HTML template with the AngularJS compiler. 2475 * The `$mdCompiler` is wrapper around the AngularJS compiler and provides extra functionality 2476 * like controller instantiation or async resolves. 2477 * 2478 * @param {!Object} options An options object, with the following properties: 2479 * 2480 * - `controller` - `{string|Function}` Controller fn that should be associated with 2481 * newly created scope or the name of a registered controller if passed as a string. 2482 * - `controllerAs` - `{string=}` A controller alias name. If present the controller will be 2483 * published to scope under the `controllerAs` name. 2484 * - `contentElement` - `{string|Element}`: Instead of using a template, which will be 2485 * compiled each time, you can also use a DOM element.<br/> 2486 * - `template` - `{string=}` An html template as a string. 2487 * - `templateUrl` - `{string=}` A path to an html template. 2488 * - `transformTemplate` - `{function(template)=}` A function which transforms the template after 2489 * it is loaded. It will be given the template string as a parameter, and should 2490 * return a a new string representing the transformed template. 2491 * - `resolve` - `{Object.<string, function>=}` - An optional map of dependencies which should 2492 * be injected into the controller. If any of these dependencies are promises, the compiler 2493 * will wait for them all to be resolved, or if one is rejected before the controller is 2494 * instantiated `compile()` will fail.. 2495 * * `key` - `{string}`: a name of a dependency to be injected into the controller. 2496 * * `factory` - `{string|function}`: If `string` then it is an alias for a service. 2497 * Otherwise if function, then it is injected and the return value is treated as the 2498 * dependency. If the result is a promise, it is resolved before its value is 2499 * injected into the controller. 2500 * 2501 * @returns {Object} promise A promise, which will be resolved with a `compileData` object. 2502 * `compileData` has the following properties: 2503 * 2504 * - `element` - `{element}`: an uncompiled element matching the provided template. 2505 * - `link` - `{function(scope)}`: A link function, which, when called, will compile 2506 * the element and instantiate the provided controller (if given). 2507 * - `locals` - `{object}`: The locals which will be passed into the controller once `link` is 2508 * called. If `bindToController` is true, they will be coppied to the ctrl instead 2509 * 2510 */ 2511 MdCompilerService.prototype.compile = function(options) { 2512 2513 if (options.contentElement) { 2514 return this._prepareContentElement(options); 2515 } else { 2516 return this._compileTemplate(options); 2517 } 2518 2519 }; 2520 2521 /** 2522 * Instead of compiling any template, the compiler just fetches an existing HTML element from the DOM and 2523 * provides a restore function to put the element back it old DOM position. 2524 * @param {!Object} options Options to be used for the compiler. 2525 * @private 2526 */ 2527 MdCompilerService.prototype._prepareContentElement = function(options) { 2528 2529 var contentElement = this._fetchContentElement(options); 2530 2531 return this.$q.resolve({ 2532 element: contentElement.element, 2533 cleanup: contentElement.restore, 2534 locals: {}, 2535 link: function() { 2536 return contentElement.element; 2537 } 2538 }); 2539 2540 }; 2541 2542 /** 2543 * Compiles a template by considering all options and waiting for all resolves to be ready. 2544 * @param {!Object} options Compile options 2545 * @returns {!Object} Compile data with link function. 2546 * @private 2547 */ 2548 MdCompilerService.prototype._compileTemplate = function(options) { 2549 2550 var self = this; 2551 var templateUrl = options.templateUrl; 2552 var template = options.template || ''; 2553 var resolve = angular.extend({}, options.resolve); 2554 var locals = angular.extend({}, options.locals); 2555 var transformTemplate = options.transformTemplate || angular.identity; 2556 2557 // Take resolve values and invoke them. 2558 // Resolves can either be a string (value: 'MyRegisteredAngularConst'), 2559 // or an invokable 'factory' of sorts: (value: function ValueGetter($dependency) {}) 2560 angular.forEach(resolve, function(value, key) { 2561 if (angular.isString(value)) { 2562 resolve[key] = self.$injector.get(value); 2563 } else { 2564 resolve[key] = self.$injector.invoke(value); 2565 } 2566 }); 2567 2568 // Add the locals, which are just straight values to inject 2569 // eg locals: { three: 3 }, will inject three into the controller 2570 angular.extend(resolve, locals); 2571 2572 if (templateUrl) { 2573 resolve.$$ngTemplate = this.$templateRequest(templateUrl); 2574 } else { 2575 resolve.$$ngTemplate = this.$q.when(template); 2576 } 2577 2578 2579 // Wait for all the resolves to finish if they are promises 2580 return this.$q.all(resolve).then(function(locals) { 2581 2582 var template = transformTemplate(locals.$$ngTemplate, options); 2583 var element = options.element || angular.element('<div>').html(template.trim()).contents(); 2584 2585 return self._compileElement(locals, element, options); 2586 }); 2587 2588 }; 2589 2590 /** 2591 * Method to compile an element with the given options. 2592 * @param {!Object} locals Locals to be injected to the controller if present 2593 * @param {!JQLite} element Element to be compiled and linked 2594 * @param {!Object} options Options to be used for linking. 2595 * @returns {!Object} Compile data with link function. 2596 * @private 2597 */ 2598 MdCompilerService.prototype._compileElement = function(locals, element, options) { 2599 var self = this; 2600 var ngLinkFn = this.$compile(element); 2601 2602 var compileData = { 2603 element: element, 2604 cleanup: element.remove.bind(element), 2605 locals: locals, 2606 link: linkFn 2607 }; 2608 2609 function linkFn(scope) { 2610 locals.$scope = scope; 2611 2612 // Instantiate controller if the developer provided one. 2613 if (options.controller) { 2614 2615 var injectLocals = angular.extend({}, locals, { 2616 $element: element 2617 }); 2618 2619 // Create the specified controller instance. 2620 var ctrl = self._createController(options, injectLocals, locals); 2621 2622 // Unique identifier for AngularJS Route ngView controllers. 2623 element.data('$ngControllerController', ctrl); 2624 element.children().data('$ngControllerController', ctrl); 2625 2626 // Expose the instantiated controller to the compile data 2627 compileData.controller = ctrl; 2628 } 2629 2630 // Invoke the AngularJS $compile link function. 2631 return ngLinkFn(scope); 2632 } 2633 2634 return compileData; 2635 2636 }; 2637 2638 /** 2639 * Creates and instantiates a new controller with the specified options. 2640 * @param {!Object} options Options that include the controller 2641 * @param {!Object} injectLocals Locals to to be provided in the controller DI. 2642 * @param {!Object} locals Locals to be injected to the controller. 2643 * @returns {!Object} Created controller instance. 2644 * @private 2645 */ 2646 MdCompilerService.prototype._createController = function(options, injectLocals, locals) { 2647 var invokeCtrl = this.$controller(options.controller, injectLocals, true, options.controllerAs); 2648 2649 if (getPreAssignBindingsEnabled() && options.bindToController) { 2650 angular.extend(invokeCtrl.instance, locals); 2651 } 2652 2653 // Instantiate and initialize the specified controller. 2654 var ctrl = invokeCtrl(); 2655 2656 if (!getPreAssignBindingsEnabled() && options.bindToController) { 2657 angular.extend(invokeCtrl.instance, locals); 2658 } 2659 2660 // Call the $onInit hook if it's present on the controller. 2661 angular.isFunction(ctrl.$onInit) && ctrl.$onInit(); 2662 2663 return ctrl; 2664 }; 2665 2666 /** 2667 * Fetches an element removing it from the DOM and using it temporary for the compiler. 2668 * Elements which were fetched will be restored after use. 2669 * @param {!Object} options Options to be used for the compilation. 2670 * @returns {{element: !JQLite, restore: !Function}} 2671 * @private 2672 */ 2673 MdCompilerService.prototype._fetchContentElement = function(options) { 2674 2675 var contentEl = options.contentElement; 2676 var restoreFn = null; 2677 2678 if (angular.isString(contentEl)) { 2679 contentEl = document.querySelector(contentEl); 2680 restoreFn = createRestoreFn(contentEl); 2681 } else { 2682 contentEl = contentEl[0] || contentEl; 2683 2684 // When the element is visible in the DOM, then we restore it at close of the dialog. 2685 // Otherwise it will be removed from the DOM after close. 2686 if (document.contains(contentEl)) { 2687 restoreFn = createRestoreFn(contentEl); 2688 } else { 2689 restoreFn = function() { 2690 if (contentEl.parentNode) { 2691 contentEl.parentNode.removeChild(contentEl); 2692 } 2693 } 2694 } 2695 } 2696 2697 return { 2698 element: angular.element(contentEl), 2699 restore: restoreFn 2700 }; 2701 2702 function createRestoreFn(element) { 2703 var parent = element.parentNode; 2704 var nextSibling = element.nextElementSibling; 2705 2706 return function() { 2707 if (!nextSibling) { 2708 // When the element didn't had any sibling, then it can be simply appended to the 2709 // parent, because it plays no role, which index it had before. 2710 parent.appendChild(element); 2711 } else { 2712 // When the element had a sibling, which marks the previous position of the element 2713 // in the DOM, we insert it correctly before the sibling, to have the same index as 2714 // before. 2715 parent.insertBefore(element, nextSibling); 2716 } 2717 } 2718 } 2719 }; 2720} 2721 2722 2723})(); 2724(function(){ 2725"use strict"; 2726 2727 2728MdGesture.$inject = ["$$MdGestureHandler", "$$rAF", "$timeout"]; 2729attachToDocument.$inject = ["$mdGesture", "$$MdGestureHandler"];var HANDLERS = {}; 2730 2731/* The state of the current 'pointer' 2732 * The pointer represents the state of the current touch.
2733 * It contains normalized x and y coordinates from DOM events, 2734 * as well as other information abstracted from the DOM. 2735 */ 2736 2737var pointer, lastPointer, forceSkipClickHijack = false, maxClickDistance = 6; 2738 2739/** 2740 * The position of the most recent click if that click was on a label element. 2741 * @type {{x: number, y: number}?} 2742 */ 2743var lastLabelClickPos = null; 2744 2745// Used to attach event listeners once when multiple ng-apps are running. 2746var isInitialized = false; 2747 2748angular 2749 .module('material.core.gestures', [ ]) 2750 .provider('$mdGesture', MdGestureProvider) 2751 .factory('$$MdGestureHandler', MdGestureHandler) 2752 .run(attachToDocument ); 2753 2754/** 2755 * @ngdoc service 2756 * @name $mdGestureProvider 2757 * @module material.core.gestures 2758 * 2759 * @description 2760 * In some scenarios on Mobile devices (without jQuery), the click events should NOT be hijacked. 2761 * `$mdGestureProvider` is used to configure the Gesture module to ignore or skip click hijacking on mobile 2762 * devices. 2763 * You can also change max click distance (6px by default) if you have issues on some touch screens. 2764 * 2765 * <hljs lang="js"> 2766 * app.config(function($mdGestureProvider) { 2767 * 2768 * // For mobile devices without jQuery loaded, do not 2769 * // intercept click events during the capture phase. 2770 * $mdGestureProvider.skipClickHijack(); 2771 * 2772 * // If hijcacking clicks, change default 6px click distance 2773 * $mdGestureProvider.setMaxClickDistance(12); 2774 * 2775 * }); 2776 * </hljs> 2777 * 2778 */ 2779function MdGestureProvider() { } 2780 2781MdGestureProvider.prototype = { 2782 2783 // Publish access to setter to configure a variable BEFORE the 2784 // $mdGesture service is instantiated... 2785 skipClickHijack: function() { 2786 return forceSkipClickHijack = true; 2787 }, 2788 2789 setMaxClickDistance: function(clickDistance) { 2790 maxClickDistance = parseInt(clickDistance); 2791 }, 2792 2793 /** 2794 * $get is used to build an instance of $mdGesture 2795 * @ngInject 2796 */ 2797 $get : ["$$MdGestureHandler", "$$rAF", "$timeout", function($$MdGestureHandler, $$rAF, $timeout) { 2798 return new MdGesture($$MdGestureHandler, $$rAF, $timeout); 2799 }] 2800}; 2801 2802 2803 2804/** 2805 * MdGesture factory construction function 2806 * @ngInject 2807 */ 2808function MdGesture($$MdGestureHandler, $$rAF, $timeout) { 2809 var userAgent = navigator.userAgent || navigator.vendor || window.opera; 2810 var isIos = userAgent.match(/ipad|iphone|ipod/i); 2811 var isAndroid = userAgent.match(/android/i); 2812 var touchActionProperty = getTouchAction(); 2813 var hasJQuery = (typeof window.jQuery !== 'undefined') && (angular.element === window.jQuery); 2814 2815 var self = { 2816 handler: addHandler, 2817 register: register, 2818 // On mobile w/out jQuery, we normally intercept clicks. Should we skip that? 2819 isHijackingClicks: (isIos || isAndroid) && !hasJQuery && !forceSkipClickHijack 2820 }; 2821 2822 if (self.isHijackingClicks) { 2823 self.handler('click', { 2824 options: { 2825 maxDistance: maxClickDistance 2826 }, 2827 onEnd: checkDistanceAndEmit('click') 2828 }); 2829 2830 self.handler('focus', { 2831 options: { 2832 maxDistance: maxClickDistance 2833 }, 2834 onEnd: function(ev, pointer) { 2835 if (pointer.distance < this.state.options.maxDistance && canFocus(ev.target)) { 2836 this.dispatchEvent(ev, 'focus', pointer); 2837 ev.target.focus(); 2838 } 2839 } 2840 }); 2841 2842 self.handler('mouseup', { 2843 options: { 2844 maxDistance: maxClickDistance 2845 }, 2846 onEnd: checkDistanceAndEmit('mouseup') 2847 }); 2848 2849 self.handler('mousedown', { 2850 onStart: function(ev) { 2851 this.dispatchEvent(ev, 'mousedown'); 2852 } 2853 }); 2854 } 2855 2856 function checkDistanceAndEmit(eventName) { 2857 return function(ev, pointer) { 2858 if (pointer.distance < this.state.options.maxDistance) { 2859 this.dispatchEvent(ev, eventName, pointer); 2860 } 2861 }; 2862 } 2863 2864 /* 2865 * Register an element to listen for a handler. 2866 * This allows an element to override the default options for a handler. 2867 * Additionally, some handlers like drag and hold only dispatch events if 2868 * the domEvent happens inside an element that's registered to listen for these events. 2869 * 2870 * @see GestureHandler for how overriding of default options works. 2871 * @example $mdGesture.register(myElement, 'drag', { minDistance: 20, horziontal: false }) 2872 */ 2873 function register(element, handlerName, options) { 2874 var handler = HANDLERS[handlerName.replace(/^\$md./, '')]; 2875 if (!handler) { 2876 throw new Error('Failed to register element with handler ' + handlerName + '. ' + 2877 'Available handlers: ' + Object.keys(HANDLERS).join(', ')); 2878 } 2879 return handler.registerElement(element, options); 2880 } 2881 2882 /* 2883 * add a handler to $mdGesture. see below.
vendor: 21,626 bytes, lines 2884-3502
2884 */ 2885 function addHandler(name, definition) { 2886 var handler = new $$MdGestureHandler(name); 2887 angular.extend(handler, definition); 2888 HANDLERS[name] = handler; 2889 2890 return self; 2891 } 2892 2893 /* 2894 * Register handlers. These listen to touch/start/move events, interpret them, 2895 * and dispatch gesture events depending on options & conditions. These are all 2896 * instances of GestureHandler. 2897 * @see GestureHandler 2898 */ 2899 return self 2900 /* 2901 * The press handler dispatches an event on touchdown/touchend. 2902 * It's a simple abstraction of touch/mouse/pointer start and end. 2903 */ 2904 .handler('press', { 2905 onStart: function (ev, pointer) { 2906 this.dispatchEvent(ev, '$md.pressdown'); 2907 }, 2908 onEnd: function (ev, pointer) { 2909 this.dispatchEvent(ev, '$md.pressup'); 2910 } 2911 }) 2912 2913 /* 2914 * The hold handler dispatches an event if the user keeps their finger within 2915 * the same <maxDistance> area for <delay> ms. 2916 * The hold handler will only run if a parent of the touch target is registered 2917 * to listen for hold events through $mdGesture.register() 2918 */ 2919 .handler('hold', { 2920 options: { 2921 maxDistance: 6, 2922 delay: 500 2923 }, 2924 onCancel: function () { 2925 $timeout.cancel(this.state.timeout); 2926 }, 2927 onStart: function (ev, pointer) { 2928 // For hold, require a parent to be registered with $mdGesture.register() 2929 // Because we prevent scroll events, this is necessary. 2930 if (!this.state.registeredParent) return this.cancel(); 2931 2932 this.state.pos = {x: pointer.x, y: pointer.y}; 2933 this.state.timeout = $timeout(angular.bind(this, function holdDelayFn() { 2934 this.dispatchEvent(ev, '$md.hold'); 2935 this.cancel(); //we're done! 2936 }), this.state.options.delay, false); 2937 }, 2938 onMove: function (ev, pointer) { 2939 // Don't scroll while waiting for hold. 2940 // If we don't preventDefault touchmove events here, Android will assume we don't 2941 // want to listen to anymore touch events. It will start scrolling and stop sending 2942 // touchmove events. 2943 if (!touchActionProperty && ev.type === 'touchmove') ev.preventDefault(); 2944 2945 // If the user moves greater than <maxDistance> pixels, stop the hold timer 2946 // set in onStart 2947 var dx = this.state.pos.x - pointer.x; 2948 var dy = this.state.pos.y - pointer.y; 2949 if (Math.sqrt(dx * dx + dy * dy) > this.options.maxDistance) { 2950 this.cancel(); 2951 } 2952 }, 2953 onEnd: function () { 2954 this.onCancel(); 2955 } 2956 }) 2957 2958 /* 2959 * The drag handler dispatches a drag event if the user holds and moves his finger greater than 2960 * <minDistance> px in the x or y direction, depending on options.horizontal. 2961 * The drag will be cancelled if the user moves his finger greater than <minDistance>*<cancelMultiplier> in 2962 * the perpendicular direction. Eg if the drag is horizontal and the user moves his finger <minDistance>*<cancelMultiplier> 2963 * pixels vertically, this handler won't consider the move part of a drag. 2964 */ 2965 .handler('drag', { 2966 options: { 2967 minDistance: 6, 2968 horizontal: true, 2969 cancelMultiplier: 1.5 2970 }, 2971 onSetup: function(element, options) { 2972 if (touchActionProperty) { 2973 // We check for horizontal to be false, because otherwise we would overwrite the default opts. 2974 this.oldTouchAction = element[0].style[touchActionProperty]; 2975 element[0].style[touchActionProperty] = options.horizontal ? 'pan-y' : 'pan-x'; 2976 } 2977 }, 2978 onCleanup: function(element) { 2979 if (this.oldTouchAction) { 2980 element[0].style[touchActionProperty] = this.oldTouchAction; 2981 } 2982 }, 2983 onStart: function (ev) { 2984 // For drag, require a parent to be registered with $mdGesture.register() 2985 if (!this.state.registeredParent) this.cancel(); 2986 }, 2987 onMove: function (ev, pointer) { 2988 var shouldStartDrag, shouldCancel; 2989 // Don't scroll while deciding if this touchmove qualifies as a drag event. 2990 // If we don't preventDefault touchmove events here, Android will assume we don't 2991 // want to listen to anymore touch events. It will start scrolling and stop sending 2992 // touchmove events. 2993 if (!touchActionProperty && ev.type === 'touchmove') ev.preventDefault(); 2994 2995 if (!this.state.dragPointer) { 2996 if (this.state.options.horizontal) { 2997 shouldStartDrag = Math.abs(pointer.distanceX) > this.state.options.minDistance; 2998 shouldCancel = Math.abs(pointer.distanceY) > this.state.options.minDistance * this.state.options.cancelMultiplier; 2999 } else { 3000 shouldStartDrag = Math.abs(pointer.distanceY) > this.state.options.minDistance; 3001 shouldCancel = Math.abs(pointer.distanceX) > this.state.options.minDistance * this.state.options.cancelMultiplier; 3002 } 3003 3004 if (shouldStartDrag) { 3005 // Create a new pointer representing this drag, starting at this point where the drag started. 3006 this.state.dragPointer = makeStartPointer(ev); 3007 updatePointerState(ev, this.state.dragPointer); 3008 this.dispatchEvent(ev, '$md.dragstart', this.state.dragPointer); 3009 3010 } else if (shouldCancel) { 3011 this.cancel(); 3012 } 3013 } else { 3014 this.dispatchDragMove(ev); 3015 } 3016 }, 3017 // Only dispatch dragmove events every frame; any more is unnecessary 3018 dispatchDragMove: $$rAF.throttle(function (ev) { 3019 // Make sure the drag didn't stop while waiting for the next frame 3020 if (this.state.isRunning) { 3021 updatePointerState(ev, this.state.dragPointer); 3022 this.dispatchEvent(ev, '$md.drag', this.state.dragPointer); 3023 } 3024 }), 3025 onEnd: function (ev, pointer) { 3026 if (this.state.dragPointer) { 3027 updatePointerState(ev, this.state.dragPointer); 3028 this.dispatchEvent(ev, '$md.dragend', this.state.dragPointer); 3029 } 3030 } 3031 }) 3032 3033 /* 3034 * The swipe handler will dispatch a swipe event if, on the end of a touch, 3035 * the velocity and distance were high enough. 3036 */ 3037 .handler('swipe', { 3038 options: { 3039 minVelocity: 0.65, 3040 minDistance: 10 3041 }, 3042 onEnd: function (ev, pointer) { 3043 var eventType; 3044 3045 if (Math.abs(pointer.velocityX) > this.state.options.minVelocity && 3046 Math.abs(pointer.distanceX) > this.state.options.minDistance) { 3047 eventType = pointer.directionX == 'left' ? '$md.swipeleft' : '$md.swiperight'; 3048 this.dispatchEvent(ev, eventType); 3049 } 3050 else if (Math.abs(pointer.velocityY) > this.state.options.minVelocity && 3051 Math.abs(pointer.distanceY) > this.state.options.minDistance) { 3052 eventType = pointer.directionY == 'up' ? '$md.swipeup' : '$md.swipedown'; 3053 this.dispatchEvent(ev, eventType); 3054 } 3055 } 3056 }); 3057 3058 function getTouchAction() { 3059 var testEl = document.createElement('div'); 3060 var vendorPrefixes = ['', 'webkit', 'Moz', 'MS', 'ms', 'o']; 3061 3062 for (var i = 0; i < vendorPrefixes.length; i++) { 3063 var prefix = vendorPrefixes[i]; 3064 var property = prefix ? prefix + 'TouchAction' : 'touchAction'; 3065 if (angular.isDefined(testEl.style[property])) { 3066 return property; 3067 } 3068 } 3069 } 3070 3071} 3072 3073/** 3074 * MdGestureHandler 3075 * A GestureHandler is an object which is able to dispatch custom dom events 3076 * based on native dom {touch,pointer,mouse}{start,move,end} events. 3077 * 3078 * A gesture will manage its lifecycle through the start,move,end, and cancel 3079 * functions, which are called by native dom events. 3080 * 3081 * A gesture has the concept of 'options' (eg a swipe's required velocity), which can be 3082 * overridden by elements registering through $mdGesture.register() 3083 */ 3084function GestureHandler (name) { 3085 this.name = name; 3086 this.state = {}; 3087} 3088 3089function MdGestureHandler() { 3090 var hasJQuery = (typeof window.jQuery !== 'undefined') && (angular.element === window.jQuery); 3091 3092 GestureHandler.prototype = { 3093 options: {}, 3094 // jQuery listeners don't work with custom DOMEvents, so we have to dispatch events 3095 // differently when jQuery is loaded 3096 dispatchEvent: hasJQuery ? jQueryDispatchEvent : nativeDispatchEvent, 3097 3098 // These are overridden by the registered handler 3099 onSetup: angular.noop, 3100 onCleanup: angular.noop, 3101 onStart: angular.noop, 3102 onMove: angular.noop, 3103 onEnd: angular.noop, 3104 onCancel: angular.noop, 3105 3106 // onStart sets up a new state for the handler, which includes options from the 3107 // nearest registered parent element of ev.target. 3108 start: function (ev, pointer) { 3109 if (this.state.isRunning) return; 3110 var parentTarget = this.getNearestParent(ev.target); 3111 // Get the options from the nearest registered parent 3112 var parentTargetOptions = parentTarget && parentTarget.$mdGesture[this.name] || {}; 3113 3114 this.state = { 3115 isRunning: true, 3116 // Override the default options with the nearest registered parent's options 3117 options: angular.extend({}, this.options, parentTargetOptions), 3118 // Pass in the registered parent node to the state so the onStart listener can use 3119 registeredParent: parentTarget 3120 }; 3121 this.onStart(ev, pointer); 3122 }, 3123 move: function (ev, pointer) { 3124 if (!this.state.isRunning) return; 3125 this.onMove(ev, pointer); 3126 }, 3127 end: function (ev, pointer) { 3128 if (!this.state.isRunning) return; 3129 this.onEnd(ev, pointer); 3130 this.state.isRunning = false; 3131 }, 3132 cancel: function (ev, pointer) { 3133 this.onCancel(ev, pointer); 3134 this.state = {}; 3135 }, 3136 3137 // Find and return the nearest parent element that has been registered to 3138 // listen for this handler via $mdGesture.register(element, 'handlerName'). 3139 getNearestParent: function (node) { 3140 var current = node; 3141 while (current) { 3142 if ((current.$mdGesture || {})[this.name]) { 3143 return current; 3144 } 3145 current = current.parentNode; 3146 } 3147 return null; 3148 }, 3149 3150 // Called from $mdGesture.register when an element registers itself with a handler. 3151 // Store the options the user gave on the DOMElement itself. These options will 3152 // be retrieved with getNearestParent when the handler starts. 3153 registerElement: function (element, options) { 3154 var self = this; 3155 element[0].$mdGesture = element[0].$mdGesture || {}; 3156 element[0].$mdGesture[this.name] = options || {}; 3157 element.on('$destroy', onDestroy); 3158 3159 self.onSetup(element, options || {}); 3160 3161 return onDestroy; 3162 3163 function onDestroy() { 3164 delete element[0].$mdGesture[self.name]; 3165 element.off('$destroy', onDestroy); 3166 3167 self.onCleanup(element, options || {}); 3168 } 3169 } 3170 }; 3171 3172 return GestureHandler; 3173 3174 /* 3175 * Dispatch an event with jQuery 3176 * TODO: Make sure this sends bubbling events 3177 * 3178 * @param srcEvent the original DOM touch event that started this. 3179 * @param eventType the name of the custom event to send (eg 'click' or '$md.drag') 3180 * @param eventPointer the pointer object that matches this event. 3181 */ 3182 function jQueryDispatchEvent(srcEvent, eventType, eventPointer) { 3183 eventPointer = eventPointer || pointer; 3184 var eventObj = new angular.element.Event(eventType); 3185 3186 eventObj.$material = true; 3187 eventObj.pointer = eventPointer; 3188 eventObj.srcEvent = srcEvent; 3189 3190 angular.extend(eventObj, { 3191 clientX: eventPointer.x, 3192 clientY: eventPointer.y, 3193 screenX: eventPointer.x, 3194 screenY: eventPointer.y, 3195 pageX: eventPointer.x, 3196 pageY: eventPointer.y, 3197 ctrlKey: srcEvent.ctrlKey, 3198 altKey: srcEvent.altKey, 3199 shiftKey: srcEvent.shiftKey, 3200 metaKey: srcEvent.metaKey 3201 }); 3202 angular.element(eventPointer.target).trigger(eventObj); 3203 } 3204 3205 /* 3206 * NOTE: nativeDispatchEvent is very performance sensitive. 3207 * @param srcEvent the original DOM touch event that started this. 3208 * @param eventType the name of the custom event to send (eg 'click' or '$md.drag') 3209 * @param eventPointer the pointer object that matches this event. 3210 */ 3211 function nativeDispatchEvent(srcEvent, eventType, eventPointer) { 3212 eventPointer = eventPointer || pointer; 3213 var eventObj; 3214 3215 if (eventType === 'click' || eventType == 'mouseup' || eventType == 'mousedown' ) { 3216 eventObj = document.createEvent('MouseEvents'); 3217 eventObj.initMouseEvent( 3218 eventType, true, true, window, srcEvent.detail, 3219 eventPointer.x, eventPointer.y, eventPointer.x, eventPointer.y, 3220 srcEvent.ctrlKey, srcEvent.altKey, srcEvent.shiftKey, srcEvent.metaKey, 3221 srcEvent.button, srcEvent.relatedTarget || null 3222 ); 3223 3224 } else { 3225 eventObj = document.createEvent('CustomEvent'); 3226 eventObj.initCustomEvent(eventType, true, true, {}); 3227 } 3228 eventObj.$material = true; 3229 eventObj.pointer = eventPointer; 3230 eventObj.srcEvent = srcEvent; 3231 eventPointer.target.dispatchEvent(eventObj); 3232 } 3233 3234} 3235 3236/** 3237 * Attach Gestures: hook document and check shouldHijack clicks 3238 * @ngInject 3239 */ 3240function attachToDocument( $mdGesture, $$MdGestureHandler ) { 3241 3242 // Polyfill document.contains for IE11. 3243 // TODO: move to util 3244 document.contains || (document.contains = function (node) { 3245 return document.body.contains(node); 3246 }); 3247 3248 if (!isInitialized && $mdGesture.isHijackingClicks ) { 3249 /* 3250 * If hijack clicks is true, we preventDefault any click that wasn't 3251 * sent by AngularJS Material. This is because on older Android & iOS, a false, or 'ghost', 3252 * click event will be sent ~400ms after a touchend event happens. 3253 * The only way to know if this click is real is to prevent any normal 3254 * click events, and add a flag to events sent by material so we know not to prevent those. 3255 * 3256 * Two exceptions to click events that should be prevented are: 3257 * - click events sent by the keyboard (eg form submit) 3258 * - events that originate from an Ionic app 3259 */ 3260 document.addEventListener('click' , clickHijacker , true); 3261 document.addEventListener('mouseup' , mouseInputHijacker, true); 3262 document.addEventListener('mousedown', mouseInputHijacker, true); 3263 document.addEventListener('focus' , mouseInputHijacker, true); 3264 3265 isInitialized = true; 3266 } 3267 3268 function mouseInputHijacker(ev) { 3269 var isKeyClick = !ev.clientX && !ev.clientY; 3270 3271 if ( 3272 !isKeyClick && 3273 !ev.$material && 3274 !ev.isIonicTap && 3275 !isInputEventFromLabelClick(ev) && 3276 (ev.type !== 'mousedown' || (!canFocus(ev.target) && !canFocus(document.activeElement))) 3277 ) { 3278 ev.preventDefault(); 3279 ev.stopPropagation(); 3280 } 3281 } 3282 3283 function clickHijacker(ev) { 3284 var isKeyClick = ev.clientX === 0 && ev.clientY === 0; 3285 var isSubmitEvent = ev.target && ev.target.type === 'submit'; 3286 if (!isKeyClick && !ev.$material && !ev.isIonicTap 3287 && !isInputEventFromLabelClick(ev) 3288 && !isSubmitEvent) { 3289 ev.preventDefault(); 3290 ev.stopPropagation(); 3291 lastLabelClickPos = null; 3292 } else { 3293 lastLabelClickPos = null; 3294 if (ev.target.tagName.toLowerCase() == 'label') { 3295 lastLabelClickPos = {x: ev.x, y: ev.y}; 3296 } 3297 } 3298 } 3299 3300 3301 // Listen to all events to cover all platforms. 3302 var START_EVENTS = 'mousedown touchstart pointerdown'; 3303 var MOVE_EVENTS = 'mousemove touchmove pointermove'; 3304 var END_EVENTS = 'mouseup mouseleave touchend touchcancel pointerup pointercancel'; 3305 3306 angular.element(document) 3307 .on(START_EVENTS, gestureStart) 3308 .on(MOVE_EVENTS, gestureMove) 3309 .on(END_EVENTS, gestureEnd) 3310 // For testing 3311 .on('$$mdGestureReset', function gestureClearCache () { 3312 lastPointer = pointer = null; 3313 }); 3314 3315 /* 3316 * When a DOM event happens, run all registered gesture handlers' lifecycle 3317 * methods which match the DOM event. 3318 * Eg when a 'touchstart' event happens, runHandlers('start') will call and 3319 * run `handler.cancel()` and `handler.start()` on all registered handlers. 3320 */ 3321 function runHandlers(handlerEvent, event) { 3322 var handler; 3323 for (var name in HANDLERS) { 3324 handler = HANDLERS[name]; 3325 if( handler instanceof $$MdGestureHandler ) { 3326 3327 if (handlerEvent === 'start') { 3328 // Run cancel to reset any handlers' state 3329 handler.cancel(); 3330 } 3331 handler[handlerEvent](event, pointer); 3332 3333 } 3334 } 3335 } 3336 3337 /* 3338 * gestureStart vets if a start event is legitimate (and not part of a 'ghost click' from iOS/Android) 3339 * If it is legitimate, we initiate the pointer state and mark the current pointer's type 3340 * For example, for a touchstart event, mark the current pointer as a 'touch' pointer, so mouse events 3341 * won't effect it. 3342 */ 3343 function gestureStart(ev) { 3344 // If we're already touched down, abort 3345 if (pointer) return; 3346 3347 var now = +Date.now(); 3348 3349 // iOS & old android bug: after a touch event, a click event is sent 350 ms later. 3350 // If <400ms have passed, don't allow an event of a different type than the previous event 3351 if (lastPointer && !typesMatch(ev, lastPointer) && (now - lastPointer.endTime < 1500)) { 3352 return; 3353 } 3354 3355 pointer = makeStartPointer(ev); 3356 3357 runHandlers('start', ev); 3358 } 3359 /* 3360 * If a move event happens of the right type, update the pointer and run all the move handlers. 3361 * "of the right type": if a mousemove happens but our pointer started with a touch event, do nothing. 3362 */ 3363 function gestureMove(ev) { 3364 if (!pointer || !typesMatch(ev, pointer)) return; 3365 3366 updatePointerState(ev, pointer); 3367 runHandlers('move', ev); 3368 } 3369 /* 3370 * If an end event happens of the right type, update the pointer, run endHandlers, and save the pointer as 'lastPointer' 3371 */ 3372 function gestureEnd(ev) { 3373 if (!pointer || !typesMatch(ev, pointer)) return; 3374 3375 updatePointerState(ev, pointer); 3376 pointer.endTime = +Date.now(); 3377 3378 if (ev.type !== 'pointercancel') { 3379 runHandlers('end', ev); 3380 } 3381 3382 lastPointer = pointer; 3383 pointer = null; 3384 } 3385 3386} 3387 3388// ******************** 3389// Module Functions 3390// ******************** 3391 3392/* 3393 * Initiate the pointer. x, y, and the pointer's type. 3394 */ 3395function makeStartPointer(ev) { 3396 var point = getEventPoint(ev); 3397 var startPointer = { 3398 startTime: +Date.now(), 3399 target: ev.target, 3400 // 'p' for pointer events, 'm' for mouse, 't' for touch 3401 type: ev.type.charAt(0) 3402 }; 3403 startPointer.startX = startPointer.x = point.pageX; 3404 startPointer.startY = startPointer.y = point.pageY; 3405 return startPointer; 3406} 3407 3408/* 3409 * return whether the pointer's type matches the event's type. 3410 * Eg if a touch event happens but the pointer has a mouse type, return false. 3411 */ 3412function typesMatch(ev, pointer) { 3413 return ev && pointer && ev.type.charAt(0) === pointer.type; 3414} 3415 3416/** 3417 * Gets whether the given event is an input event that was caused by clicking on an 3418 * associated label element. 3419 * 3420 * This is necessary because the browser will, upon clicking on a label element, fire an 3421 * *extra* click event on its associated input (if any). mdGesture is able to flag the label 3422 * click as with `$material` correctly, but not the second input click. 3423 * 3424 * In order to determine whether an input event is from a label click, we compare the (x, y) for 3425 * the event to the (x, y) for the most recent label click (which is cleared whenever a non-label 3426 * click occurs). Unfortunately, there are no event properties that tie the input and the label 3427 * together (such as relatedTarget). 3428 * 3429 * @param {MouseEvent} event 3430 * @returns {boolean} 3431 */ 3432function isInputEventFromLabelClick(event) { 3433 return lastLabelClickPos 3434 && lastLabelClickPos.x == event.x 3435 && lastLabelClickPos.y == event.y; 3436} 3437 3438/* 3439 * Update the given pointer based upon the given DOMEvent. 3440 * Distance, velocity, direction, duration, etc 3441 */ 3442function updatePointerState(ev, pointer) { 3443 var point = getEventPoint(ev); 3444 var x = pointer.x = point.pageX; 3445 var y = pointer.y = point.pageY; 3446 3447 pointer.distanceX = x - pointer.startX; 3448 pointer.distanceY = y - pointer.startY; 3449 pointer.distance = Math.sqrt( 3450 pointer.distanceX * pointer.distanceX + pointer.distanceY * pointer.distanceY 3451 ); 3452 3453 pointer.directionX = pointer.distanceX > 0 ? 'right' : pointer.distanceX < 0 ? 'left' : ''; 3454 pointer.directionY = pointer.distanceY > 0 ? 'down' : pointer.distanceY < 0 ? 'up' : ''; 3455 3456 pointer.duration = +Date.now() - pointer.startTime; 3457 pointer.velocityX = pointer.distanceX / pointer.duration; 3458 pointer.velocityY = pointer.distanceY / pointer.duration; 3459} 3460 3461/* 3462 * Normalize the point where the DOM event happened whether it's touch or mouse. 3463 * @returns point event obj with pageX and pageY on it. 3464 */ 3465function getEventPoint(ev) { 3466 ev = ev.originalEvent || ev; // support jQuery events 3467 return (ev.touches && ev.touches[0]) || 3468 (ev.changedTouches && ev.changedTouches[0]) || 3469 ev; 3470} 3471 3472/** Checks whether an element can be focused. */ 3473function canFocus(element) { 3474 return ( 3475 !!element && 3476 element.getAttribute('tabindex') != '-1' && 3477 !element.hasAttribute('disabled') && 3478 ( 3479 element.hasAttribute('tabindex') || 3480 element.hasAttribute('href') || 3481 element.isContentEditable || 3482 ['INPUT', 'SELECT', 'BUTTON', 'TEXTAREA', 'VIDEO', 'AUDIO'].indexOf(element.nodeName) != -1 3483 ) 3484 ); 3485} 3486 3487})(); 3488(function(){ 3489"use strict"; 3490 3491/** 3492 * @ngdoc module 3493 * @name material.core.interaction 3494 * @description 3495 * User interaction detection to provide proper accessibility. 3496 */ 3497MdInteractionService.$inject = ["$timeout", "$mdUtil"]; 3498angular 3499 .module('material.core.interaction', []) 3500 .service('$mdInteraction', MdInteractionService); 3501 3502
3503/** 3504 * @ngdoc service 3505 * @name $mdInteraction 3506 * @module material.core.interaction 3507 * 3508 * @description 3509 * 3510 * Service which keeps track of the last interaction type and validates them for several browsers. 3511 * The service hooks into the document's body and listens for touch, mouse and keyboard events. 3512 * 3513 * The most recent interaction type can be retrieved by calling the `getLastInteractionType` method. 3514 * 3515 * Here is an example markup for using the interaction service. 3516 * 3517 * <hljs lang="js"> 3518 * var lastType = $mdInteraction.getLastInteractionType(); 3519 * 3520 * if (lastType === 'keyboard') { 3521 * // We only restore the focus for keyboard users. 3522 * restoreFocus(); 3523 * } 3524 * </hljs> 3525 * 3526 */ 3527function MdInteractionService($timeout, $mdUtil) { 3528 this.$timeout = $timeout; 3529 this.$mdUtil = $mdUtil; 3530 3531 this.bodyElement = angular.element(document.body); 3532 this.isBuffering = false; 3533 this.bufferTimeout = null; 3534 this.lastInteractionType = null; 3535 this.lastInteractionTime = null; 3536 3537 // Type Mappings for the different events 3538 // There will be three three interaction types 3539 // `keyboard`, `mouse` and `touch` 3540 // type `pointer` will be evaluated in `pointerMap` for IE Browser events 3541 this.inputEventMap = { 3542 'keydown': 'keyboard', 3543 'mousedown': 'mouse', 3544 'mouseenter': 'mouse', 3545 'touchstart': 'touch', 3546 'pointerdown': 'pointer', 3547 'MSPointerDown': 'pointer' 3548 }; 3549 3550 // IE PointerDown events will be validated in `touch` or `mouse` 3551 // Index numbers referenced here: https://msdn.microsoft.com/library/windows/apps/hh466130.aspx 3552 this.iePointerMap = { 3553 2: 'touch', 3554 3: 'touch', 3555 4: 'mouse' 3556 }; 3557 3558 this.initializeEvents(); 3559} 3560 3561/** 3562 * Initializes the interaction service, by registering all interaction events to the 3563 * body element. 3564 */ 3565MdInteractionService.prototype.initializeEvents = function() { 3566 // IE browsers can also trigger pointer events, which also leads to an interaction. 3567 var pointerEvent = 'MSPointerEvent' in window ? 'MSPointerDown' : 'PointerEvent' in window ? 'pointerdown' : null; 3568 3569 this.bodyElement.on('keydown mousedown', this.onInputEvent.bind(this)); 3570 3571 if ('ontouchstart' in document.documentElement) { 3572 this.bodyElement.on('touchstart', this.onBufferInputEvent.bind(this)); 3573 } 3574 3575 if (pointerEvent) { 3576 this.bodyElement.on(pointerEvent, this.onInputEvent.bind(this)); 3577 } 3578 3579}; 3580 3581/** 3582 * Event listener for normal interaction events, which should be tracked. 3583 * @param event {MouseEvent|KeyboardEvent|PointerEvent|TouchEvent} 3584 */ 3585MdInteractionService.prototype.onInputEvent = function(event) { 3586 if (this.isBuffering) { 3587 return; 3588 } 3589 3590 var type = this.inputEventMap[event.type]; 3591 3592 if (type === 'pointer') { 3593 type = this.iePointerMap[event.pointerType] || event.pointerType; 3594 } 3595 3596 this.lastInteractionType = type; 3597 this.lastInteractionTime = this.$mdUtil.now(); 3598}; 3599 3600/** 3601 * Event listener for interaction events which should be buffered (touch events). 3602 * @param event {TouchEvent} 3603 */ 3604MdInteractionService.prototype.onBufferInputEvent = function(event) { 3605 this.$timeout.cancel(this.bufferTimeout); 3606 3607 this.onInputEvent(event); 3608 this.isBuffering = true; 3609 3610 // The timeout of 650ms is needed to delay the touchstart, because otherwise the touch will call 3611 // the `onInput` function multiple times. 3612 this.bufferTimeout = this.$timeout(function() { 3613 this.isBuffering = false; 3614 }.bind(this), 650, false); 3615 3616}; 3617 3618/** 3619 * @ngdoc method 3620 * @name $mdInteraction#getLastInteractionType 3621 * @description Retrieves the last interaction type triggered in body. 3622 * @returns {string|null} Last interaction type. 3623 */ 3624MdInteractionService.prototype.getLastInteractionType = function() { 3625 return this.lastInteractionType; 3626}; 3627 3628/** 3629 * @ngdoc method 3630 * @name $mdInteraction#isUserInvoked 3631 * @description Method to detect whether any interaction happened recently or not. 3632 * @param {number=} checkDelay Time to check for any interaction to have been triggered. 3633 * @returns {boolean} Whether there was any interaction or not. 3634 */ 3635MdInteractionService.prototype.isUserInvoked = function(checkDelay) {
vendor: 38,151 bytes, lines 3636-4752
3636 var delay = angular.isNumber(checkDelay) ? checkDelay : 15; 3637 3638 // Check for any interaction to be within the specified check time. 3639 return this.lastInteractionTime >= this.$mdUtil.now() - delay; 3640}; 3641 3642})(); 3643(function(){ 3644"use strict"; 3645 3646angular.module('material.core') 3647 .provider('$$interimElement', InterimElementProvider); 3648 3649/* 3650 * @ngdoc service 3651 * @name $$interimElement 3652 * @module material.core 3653 * 3654 * @description 3655 * 3656 * Factory that contructs `$$interimElement.$service` services. 3657 * Used internally in material design for elements that appear on screen temporarily. 3658 * The service provides a promise-like API for interacting with the temporary 3659 * elements. 3660 * 3661 * ```js 3662 * app.service('$mdToast', function($$interimElement) { 3663 * var $mdToast = $$interimElement(toastDefaultOptions); 3664 * return $mdToast; 3665 * }); 3666 * ``` 3667 * @param {object=} defaultOptions Options used by default for the `show` method on the service. 3668 * 3669 * @returns {$$interimElement.$service} 3670 * 3671 */ 3672 3673function InterimElementProvider() { 3674 InterimElementFactory.$inject = ["$document", "$q", "$rootScope", "$timeout", "$rootElement", "$animate", "$mdUtil", "$mdCompiler", "$mdTheming", "$injector", "$exceptionHandler"]; 3675 createInterimElementProvider.$get = InterimElementFactory; 3676 return createInterimElementProvider; 3677 3678 /** 3679 * Returns a new provider which allows configuration of a new interimElement 3680 * service. Allows configuration of default options & methods for options, 3681 * as well as configuration of 'preset' methods (eg dialog.basic(): basic is a preset method) 3682 */ 3683 function createInterimElementProvider(interimFactoryName) { 3684 factory.$inject = ["$$interimElement", "$injector"]; 3685 var EXPOSED_METHODS = ['onHide', 'onShow', 'onRemove']; 3686 3687 var customMethods = {}; 3688 var providerConfig = { 3689 presets: {} 3690 }; 3691 3692 var provider = { 3693 setDefaults: setDefaults, 3694 addPreset: addPreset, 3695 addMethod: addMethod, 3696 $get: factory 3697 }; 3698 3699 /** 3700 * all interim elements will come with the 'build' preset 3701 */ 3702 provider.addPreset('build', { 3703 methods: ['controller', 'controllerAs', 'resolve', 'multiple', 3704 'template', 'templateUrl', 'themable', 'transformTemplate', 'parent', 'contentElement'] 3705 }); 3706 3707 return provider; 3708 3709 /** 3710 * Save the configured defaults to be used when the factory is instantiated 3711 */ 3712 function setDefaults(definition) { 3713 providerConfig.optionsFactory = definition.options; 3714 providerConfig.methods = (definition.methods || []).concat(EXPOSED_METHODS); 3715 return provider; 3716 } 3717 3718 /** 3719 * Add a method to the factory that isn't specific to any interim element operations 3720 */ 3721 3722 function addMethod(name, fn) { 3723 customMethods[name] = fn; 3724 return provider; 3725 } 3726 3727 /** 3728 * Save the configured preset to be used when the factory is instantiated 3729 */ 3730 function addPreset(name, definition) { 3731 definition = definition || {}; 3732 definition.methods = definition.methods || []; 3733 definition.options = definition.options || function() { return {}; }; 3734 3735 if (/^cancel|hide|show$/.test(name)) { 3736 throw new Error("Preset '" + name + "' in " + interimFactoryName + " is reserved!"); 3737 } 3738 if (definition.methods.indexOf('_options') > -1) { 3739 throw new Error("Method '_options' in " + interimFactoryName + " is reserved!"); 3740 } 3741 providerConfig.presets[name] = { 3742 methods: definition.methods.concat(EXPOSED_METHODS), 3743 optionsFactory: definition.options, 3744 argOption: definition.argOption 3745 }; 3746 return provider; 3747 } 3748 3749 function addPresetMethod(presetName, methodName, method) { 3750 providerConfig.presets[presetName][methodName] = method; 3751 } 3752 3753 /** 3754 * Create a factory that has the given methods & defaults implementing interimElement 3755 */ 3756 /* @ngInject */ 3757 function factory($$interimElement, $injector) { 3758 var defaultMethods; 3759 var defaultOptions; 3760 var interimElementService = $$interimElement(); 3761 3762 /* 3763 * publicService is what the developer will be using. 3764 * It has methods hide(), cancel(), show(), build(), and any other 3765 * presets which were set during the config phase. 3766 */ 3767 var publicService = { 3768 hide: interimElementService.hide, 3769 cancel: interimElementService.cancel, 3770 show: showInterimElement, 3771 3772 // Special internal method to destroy an interim element without animations 3773 // used when navigation changes causes a $scope.$destroy() action 3774 destroy : destroyInterimElement 3775 }; 3776 3777 3778 defaultMethods = providerConfig.methods || []; 3779 // This must be invoked after the publicService is initialized 3780 defaultOptions = invokeFactory(providerConfig.optionsFactory, {}); 3781 3782 // Copy over the simple custom methods 3783 angular.forEach(customMethods, function(fn, name) { 3784 publicService[name] = fn; 3785 }); 3786 3787 angular.forEach(providerConfig.presets, function(definition, name) { 3788 var presetDefaults = invokeFactory(definition.optionsFactory, {}); 3789 var presetMethods = (definition.methods || []).concat(defaultMethods); 3790 3791 // Every interimElement built with a preset has a field called `$type`, 3792 // which matches the name of the preset. 3793 // Eg in preset 'confirm', options.$type === 'confirm' 3794 angular.extend(presetDefaults, { $type: name }); 3795 3796 // This creates a preset class which has setter methods for every 3797 // method given in the `.addPreset()` function, as well as every 3798 // method given in the `.setDefaults()` function. 3799 // 3800 // @example 3801 // .setDefaults({ 3802 // methods: ['hasBackdrop', 'clickOutsideToClose', 'escapeToClose', 'targetEvent'], 3803 // options: dialogDefaultOptions 3804 // }) 3805 // .addPreset('alert', { 3806 // methods: ['title', 'ok'], 3807 // options: alertDialogOptions 3808 // }) 3809 // 3810 // Set values will be passed to the options when interimElement.show() is called. 3811 function Preset(opts) { 3812 this._options = angular.extend({}, presetDefaults, opts); 3813 } 3814 angular.forEach(presetMethods, function(name) { 3815 Preset.prototype[name] = function(value) { 3816 this._options[name] = value; 3817 return this; 3818 }; 3819 }); 3820 3821 // Create shortcut method for one-linear methods 3822 if (definition.argOption) { 3823 var methodName = 'show' + name.charAt(0).toUpperCase() + name.slice(1); 3824 publicService[methodName] = function(arg) { 3825 var config = publicService[name](arg); 3826 return publicService.show(config); 3827 }; 3828 } 3829 3830 // eg $mdDialog.alert() will return a new alert preset 3831 publicService[name] = function(arg) { 3832 // If argOption is supplied, eg `argOption: 'content'`, then we assume 3833 // if the argument is not an options object then it is the `argOption` option. 3834 // 3835 // @example `$mdToast.simple('hello')` // sets options.content to hello 3836 // // because argOption === 'content' 3837 if (arguments.length && definition.argOption && 3838 !angular.isObject(arg) && !angular.isArray(arg)) { 3839 3840 return (new Preset())[definition.argOption](arg); 3841 3842 } else { 3843 return new Preset(arg); 3844 } 3845 3846 }; 3847 }); 3848 3849 return publicService; 3850 3851 /** 3852 * 3853 */ 3854 function showInterimElement(opts) { 3855 // opts is either a preset which stores its options on an _options field, 3856 // or just an object made up of options 3857 opts = opts || { }; 3858 if (opts._options) opts = opts._options; 3859 3860 return interimElementService.show( 3861 angular.extend({}, defaultOptions, opts) 3862 ); 3863 } 3864 3865 /** 3866 * Special method to hide and destroy an interimElement WITHOUT 3867 * any 'leave` or hide animations ( an immediate force hide/remove ) 3868 * 3869 * NOTE: This calls the onRemove() subclass method for each component... 3870 * which must have code to respond to `options.$destroy == true` 3871 */ 3872 function destroyInterimElement(opts) { 3873 return interimElementService.destroy(opts); 3874 } 3875 3876 /** 3877 * Helper to call $injector.invoke with a local of the factory name for 3878 * this provider. 3879 * If an $mdDialog is providing options for a dialog and tries to inject 3880 * $mdDialog, a circular dependency error will happen. 3881 * We get around that by manually injecting $mdDialog as a local. 3882 */ 3883 function invokeFactory(factory, defaultVal) { 3884 var locals = {}; 3885 locals[interimFactoryName] = publicService; 3886 return $injector.invoke(factory || function() { return defaultVal; }, {}, locals); 3887 } 3888 3889 } 3890 3891 } 3892 3893 /* @ngInject */ 3894 function InterimElementFactory($document, $q, $rootScope, $timeout, $rootElement, $animate, 3895 $mdUtil, $mdCompiler, $mdTheming, $injector, $exceptionHandler) { 3896 return function createInterimElementService() { 3897 var SHOW_CANCELLED = false; 3898 3899 /* 3900 * @ngdoc service 3901 * @name $$interimElement.$service 3902 * 3903 * @description 3904 * A service used to control inserting and removing an element into the DOM. 3905 * 3906 */ 3907 3908 var service; 3909 3910 var showPromises = []; // Promises for the interim's which are currently opening. 3911 var hidePromises = []; // Promises for the interim's which are currently hiding. 3912 var showingInterims = []; // Interim elements which are currently showing up. 3913 3914 // Publish instance $$interimElement service; 3915 // ... used as $mdDialog, $mdToast, $mdMenu, and $mdSelect 3916 3917 return service = { 3918 show: show, 3919 hide: waitForInterim(hide), 3920 cancel: waitForInterim(cancel), 3921 destroy : destroy, 3922 $injector_: $injector 3923 }; 3924 3925 /* 3926 * @ngdoc method 3927 * @name $$interimElement.$service#show 3928 * @kind function 3929 * 3930 * @description 3931 * Adds the `$interimElement` to the DOM and returns a special promise that will be resolved or rejected 3932 * with hide or cancel, respectively. To external cancel/hide, developers should use the 3933 * 3934 * @param {*} options is hashMap of settings 3935 * @returns a Promise 3936 * 3937 */ 3938 function show(options) { 3939 options = options || {}; 3940 var interimElement = new InterimElement(options || {}); 3941 3942 // When an interim element is currently showing, we have to cancel it. 3943 // Just hiding it, will resolve the InterimElement's promise, the promise should be 3944 // rejected instead. 3945 var hideAction = options.multiple ? $q.resolve() : $q.all(showPromises); 3946 3947 if (!options.multiple) { 3948 // Wait for all opening interim's to finish their transition. 3949 hideAction = hideAction.then(function() { 3950 // Wait for all closing and showing interim's to be completely closed. 3951 var promiseArray = hidePromises.concat(showingInterims.map(service.cancel)); 3952 return $q.all(promiseArray); 3953 }); 3954 } 3955 3956 var showAction = hideAction.then(function() { 3957 3958 return interimElement 3959 .show() 3960 .catch(function(reason) { return reason; }) 3961 .finally(function() { 3962 showPromises.splice(showPromises.indexOf(showAction), 1); 3963 showingInterims.push(interimElement); 3964 }); 3965 3966 }); 3967 3968 showPromises.push(showAction); 3969 3970 // In AngularJS 1.6+, exceptions inside promises will cause a rejection. We need to handle 3971 // the rejection and only log it if it's an error. 3972 interimElement.deferred.promise.catch(function(fault) { 3973 if (fault instanceof Error) { 3974 $exceptionHandler(fault); 3975 } 3976 3977 return fault; 3978 }); 3979 3980 // Return a promise that will be resolved when the interim 3981 // element is hidden or cancelled... 3982 return interimElement.deferred.promise; 3983 } 3984 3985 /* 3986 * @ngdoc method 3987 * @name $$interimElement.$service#hide 3988 * @kind function 3989 * 3990 * @description 3991 * Removes the `$interimElement` from the DOM and resolves the promise returned from `show` 3992 * 3993 * @param {*} resolveParam Data to resolve the promise with 3994 * @returns a Promise that will be resolved after the element has been removed. 3995 * 3996 */ 3997 function hide(reason, options) { 3998 options = options || {}; 3999 4000 if (options.closeAll) { 4001 // We have to make a shallow copy of the array, because otherwise the map will break. 4002 return $q.all(showingInterims.slice().reverse().map(closeElement)); 4003 } else if (options.closeTo !== undefined) { 4004 return $q.all(showingInterims.slice(options.closeTo).map(closeElement)); 4005 } 4006 4007 // Hide the latest showing interim element. 4008 return closeElement(showingInterims[showingInterims.length - 1]); 4009 4010 function closeElement(interim) { 4011 4012 var hideAction = interim 4013 .remove(reason, false, options || { }) 4014 .catch(function(reason) { return reason; }) 4015 .finally(function() { 4016 hidePromises.splice(hidePromises.indexOf(hideAction), 1); 4017 }); 4018 4019 showingInterims.splice(showingInterims.indexOf(interim), 1); 4020 hidePromises.push(hideAction); 4021 4022 return interim.deferred.promise; 4023 } 4024 } 4025 4026 /* 4027 * @ngdoc method 4028 * @name $$interimElement.$service#cancel 4029 * @kind function 4030 * 4031 * @description 4032 * Removes the `$interimElement` from the DOM and rejects the promise returned from `show` 4033 * 4034 * @param {*} reason Data to reject the promise with 4035 * @returns Promise that will be resolved after the element has been removed. 4036 * 4037 */ 4038 function cancel(reason, options) { 4039 var interim = showingInterims.pop(); 4040 if (!interim) { 4041 return $q.when(reason); 4042 } 4043 4044 var cancelAction = interim 4045 .remove(reason, true, options || {}) 4046 .catch(function(reason) { return reason; }) 4047 .finally(function() { 4048 hidePromises.splice(hidePromises.indexOf(cancelAction), 1); 4049 }); 4050 4051 hidePromises.push(cancelAction); 4052 4053 // Since AngularJS 1.6.7, promises will be logged to $exceptionHandler when the promise 4054 // is not handling the rejection. We create a pseudo catch handler, which will prevent the 4055 // promise from being logged to the $exceptionHandler. 4056 return interim.deferred.promise.catch(angular.noop); 4057 } 4058 4059 /** 4060 * Creates a function to wait for at least one interim element to be available. 4061 * @param callbackFn Function to be used as callback 4062 * @returns {Function} 4063 */ 4064 function waitForInterim(callbackFn) { 4065 return function() { 4066 var fnArguments = arguments; 4067 4068 if (!showingInterims.length) { 4069 // When there are still interim's opening, then wait for the first interim element to 4070 // finish its open animation. 4071 if (showPromises.length) { 4072 return showPromises[0].finally(function () { 4073 return callbackFn.apply(service, fnArguments); 4074 }); 4075 } 4076 4077 return $q.when("No interim elements currently showing up."); 4078 } 4079 4080 return callbackFn.apply(service, fnArguments); 4081 }; 4082 } 4083 4084 /* 4085 * Special method to quick-remove the interim element without animations 4086 * Note: interim elements are in "interim containers" 4087 */ 4088 function destroy(targetEl) { 4089 var interim = !targetEl ? showingInterims.shift() : null; 4090 4091 var parentEl = angular.element(targetEl).length && angular.element(targetEl)[0].parentNode; 4092 4093 if (parentEl) { 4094 // Try to find the interim in the stack which corresponds to the supplied DOM element. 4095 var filtered = showingInterims.filter(function(entry) { 4096 return entry.options.element[0] === parentEl; 4097 }); 4098 4099 // Note: This function might be called when the element already has been removed, 4100 // in which case we won't find any matches. 4101 if (filtered.length) { 4102 interim = filtered[0]; 4103 showingInterims.splice(showingInterims.indexOf(interim), 1); 4104 } 4105 } 4106 4107 return interim ? interim.remove(SHOW_CANCELLED, false, { '$destroy': true }) : 4108 $q.when(SHOW_CANCELLED); 4109 } 4110 4111 /* 4112 * Internal Interim Element Object 4113 * Used internally to manage the DOM element and related data 4114 */ 4115 function InterimElement(options) { 4116 var self, element, showAction = $q.when(true); 4117 4118 options = configureScopeAndTransitions(options); 4119 4120 return self = { 4121 options : options, 4122 deferred: $q.defer(), 4123 show : createAndTransitionIn, 4124 remove : transitionOutAndRemove 4125 }; 4126 4127 /** 4128 * Compile, link, and show this interim element 4129 * Use optional autoHided and transition-in effects 4130 */ 4131 function createAndTransitionIn() { 4132 return $q(function(resolve, reject) { 4133 4134 // Trigger onCompiling callback before the compilation starts. 4135 // This is useful, when modifying options, which can be influenced by developers. 4136 options.onCompiling && options.onCompiling(options); 4137 4138 compileElement(options) 4139 .then(function( compiledData ) { 4140 element = linkElement( compiledData, options ); 4141 4142 // Expose the cleanup function from the compiler. 4143 options.cleanupElement = compiledData.cleanup; 4144 4145 showAction = showElement(element, options, compiledData.controller) 4146 .then(resolve, rejectAll); 4147 }).catch(rejectAll); 4148 4149 function rejectAll(fault) { 4150 // Force the '$md<xxx>.show()' promise to reject 4151 self.deferred.reject(fault); 4152 4153 // Continue rejection propagation 4154 reject(fault); 4155 } 4156 }); 4157 } 4158 4159 /** 4160 * After the show process has finished/rejected: 4161 * - announce 'removing', 4162 * - perform the transition-out, and 4163 * - perform optional clean up scope. 4164 */ 4165 function transitionOutAndRemove(response, isCancelled, opts) { 4166 4167 // abort if the show() and compile failed 4168 if ( !element ) return $q.when(false); 4169 4170 options = angular.extend(options || {}, opts || {}); 4171 options.cancelAutoHide && options.cancelAutoHide(); 4172 options.element.triggerHandler('$mdInterimElementRemove'); 4173 4174 if ( options.$destroy === true ) { 4175 4176 return hideElement(options.element, options).then(function(){ 4177 (isCancelled && rejectAll(response)) || resolveAll(response); 4178 }); 4179 4180 } else { 4181 $q.when(showAction).finally(function() { 4182 hideElement(options.element, options).then(function() { 4183 isCancelled ? rejectAll(response) : resolveAll(response); 4184 }, rejectAll); 4185 }); 4186 4187 return self.deferred.promise; 4188 } 4189 4190 4191 /** 4192 * The `show()` returns a promise that will be resolved when the interim 4193 * element is hidden or cancelled... 4194 */ 4195 function resolveAll(response) { 4196 self.deferred.resolve(response); 4197 } 4198 4199 /** 4200 * Force the '$md<xxx>.show()' promise to reject 4201 */ 4202 function rejectAll(fault) { 4203 self.deferred.reject(fault); 4204 } 4205 } 4206 4207 /** 4208 * Prepare optional isolated scope and prepare $animate with default enter and leave 4209 * transitions for the new element instance. 4210 */ 4211 function configureScopeAndTransitions(options) { 4212 options = options || { }; 4213 if ( options.template ) { 4214 options.template = $mdUtil.processTemplate(options.template); 4215 } 4216 4217 return angular.extend({ 4218 preserveScope: false, 4219 cancelAutoHide : angular.noop, 4220 scope: options.scope || $rootScope.$new(options.isolateScope), 4221 4222 /** 4223 * Default usage to enable $animate to transition-in; can be easily overridden via 'options' 4224 */ 4225 onShow: function transitionIn(scope, element, options) { 4226 return $animate.enter(element, options.parent); 4227 }, 4228 4229 /** 4230 * Default usage to enable $animate to transition-out; can be easily overridden via 'options' 4231 */ 4232 onRemove: function transitionOut(scope, element) { 4233 // Element could be undefined if a new element is shown before 4234 // the old one finishes compiling. 4235 return element && $animate.leave(element) || $q.when(); 4236 } 4237 }, options ); 4238 4239 } 4240 4241 /** 4242 * Compile an element with a templateUrl, controller, and locals 4243 */ 4244 function compileElement(options) { 4245 4246 var compiled = !options.skipCompile ? $mdCompiler.compile(options) : null; 4247 4248 return compiled || $q(function (resolve) { 4249 resolve({ 4250 locals: {}, 4251 link: function () { 4252 return options.element; 4253 } 4254 }); 4255 }); 4256 } 4257 4258 /** 4259 * Link an element with compiled configuration 4260 */ 4261 function linkElement(compileData, options){ 4262 angular.extend(compileData.locals, options); 4263 4264 var element = compileData.link(options.scope); 4265 4266 // Search for parent at insertion time, if not specified 4267 options.element = element; 4268 options.parent = findParent(element, options); 4269 if (options.themable) $mdTheming(element); 4270 4271 return element; 4272 } 4273 4274 /** 4275 * Search for parent at insertion time, if not specified 4276 */ 4277 function findParent(element, options) { 4278 var parent = options.parent; 4279 4280 // Search for parent at insertion time, if not specified 4281 if (angular.isFunction(parent)) { 4282 parent = parent(options.scope, element, options); 4283 } else if (angular.isString(parent)) { 4284 parent = angular.element($document[0].querySelector(parent)); 4285 } else { 4286 parent = angular.element(parent); 4287 } 4288 4289 // If parent querySelector/getter function fails, or it's just null, 4290 // find a default. 4291 if (!(parent || {}).length) { 4292 var el; 4293 if ($rootElement[0] && $rootElement[0].querySelector) { 4294 el = $rootElement[0].querySelector(':not(svg) > body'); 4295 } 4296 if (!el) el = $rootElement[0]; 4297 if (el.nodeName == '#comment') { 4298 el = $document[0].body; 4299 } 4300 return angular.element(el); 4301 } 4302 4303 return parent; 4304 } 4305 4306 /** 4307 * If auto-hide is enabled, start timer and prepare cancel function 4308 */ 4309 function startAutoHide() { 4310 var autoHideTimer, cancelAutoHide = angular.noop; 4311 4312 if (options.hideDelay) { 4313 autoHideTimer = $timeout(service.hide, options.hideDelay) ; 4314 cancelAutoHide = function() { 4315 $timeout.cancel(autoHideTimer); 4316 }; 4317 } 4318 4319 // Cache for subsequent use 4320 options.cancelAutoHide = function() { 4321 cancelAutoHide(); 4322 options.cancelAutoHide = undefined; 4323 }; 4324 } 4325 4326 /** 4327 * Show the element ( with transitions), notify complete and start 4328 * optional auto-Hide 4329 */ 4330 function showElement(element, options, controller) { 4331 // Trigger onShowing callback before the `show()` starts 4332 var notifyShowing = options.onShowing || angular.noop; 4333 // Trigger onComplete callback when the `show()` finishes 4334 var notifyComplete = options.onComplete || angular.noop; 4335 4336 // Necessary for consistency between AngularJS 1.5 and 1.6. 4337 try { 4338 notifyShowing(options.scope, element, options, controller); 4339 } catch (e) { 4340 return $q.reject(e); 4341 } 4342 4343 return $q(function (resolve, reject) { 4344 try { 4345 // Start transitionIn 4346 $q.when(options.onShow(options.scope, element, options, controller)) 4347 .then(function () { 4348 notifyComplete(options.scope, element, options); 4349 startAutoHide(); 4350 4351 resolve(element); 4352 }, reject); 4353 4354 } catch (e) { 4355 reject(e.message); 4356 } 4357 }); 4358 } 4359 4360 function hideElement(element, options) { 4361 var announceRemoving = options.onRemoving || angular.noop; 4362 4363 return $q(function (resolve, reject) { 4364 try { 4365 // Start transitionIn 4366 var action = $q.when( options.onRemove(options.scope, element, options) || true ); 4367 4368 // Trigger callback *before* the remove operation starts 4369 announceRemoving(element, action); 4370 4371 if (options.$destroy) { 4372 // For $destroy, onRemove should be synchronous 4373 resolve(element); 4374 4375 if (!options.preserveScope && options.scope ) { 4376 // scope destroy should still be be done after the current digest is done 4377 action.then( function() { options.scope.$destroy(); }); 4378 } 4379 } else { 4380 // Wait until transition-out is done 4381 action.then(function () { 4382 if (!options.preserveScope && options.scope ) { 4383 options.scope.$destroy(); 4384 } 4385 4386 resolve(element); 4387 }, reject); 4388 } 4389 } catch (e) { 4390 reject(e.message); 4391 } 4392 }); 4393 } 4394 4395 } 4396 }; 4397 4398 } 4399 4400} 4401 4402})(); 4403(function(){ 4404"use strict"; 4405 4406(function() { 4407 'use strict'; 4408 4409 var $mdUtil, $interpolate, $log; 4410 4411 var SUFFIXES = /(-gt)?-(sm|md|lg|print)/g; 4412 var WHITESPACE = /\s+/g; 4413 4414 var FLEX_OPTIONS = ['grow', 'initial', 'auto', 'none', 'noshrink', 'nogrow' ]; 4415 var LAYOUT_OPTIONS = ['row', 'column']; 4416 var ALIGNMENT_MAIN_AXIS= [ "", "start", "center", "end", "stretch", "space-around", "space-between" ]; 4417 var ALIGNMENT_CROSS_AXIS= [ "", "start", "center", "end", "stretch" ]; 4418 4419 var config = { 4420 /** 4421 * Enable directive attribute-to-class conversions 4422 * Developers can use `<body md-layout-css />` to quickly 4423 * disable the Layout directives and prohibit the injection of Layout classNames 4424 */ 4425 enabled: true, 4426 4427 /** 4428 * List of mediaQuery breakpoints and associated suffixes 4429 * 4430 * [ 4431 * { suffix: "sm", mediaQuery: "screen and (max-width: 599px)" }, 4432 * { suffix: "md", mediaQuery: "screen and (min-width: 600px) and (max-width: 959px)" } 4433 * ] 4434 */ 4435 breakpoints: [] 4436 }; 4437 4438 registerLayoutAPI( angular.module('material.core.layout', ['ng']) ); 4439 4440 /** 4441 * registerLayoutAPI() 4442 * 4443 * The original AngularJS Material Layout solution used attribute selectors and CSS. 4444 * 4445 * ```html 4446 * <div layout="column"> My Content </div> 4447 * ``` 4448 * 4449 * ```css 4450 * [layout] { 4451 * box-sizing: border-box; 4452 * display:flex; 4453 * } 4454 * [layout=column] { 4455 * flex-direction : column 4456 * } 4457 * ``` 4458 * 4459 * Use of attribute selectors creates significant performance impacts in some 4460 * browsers... mainly IE. 4461 * 4462 * This module registers directives that allow the same layout attributes to be 4463 * interpreted and converted to class selectors. The directive will add equivalent classes to each element that 4464 * contains a Layout directive. 4465 * 4466 * ```html 4467 * <div layout="column" class="layout layout-column"> My Content </div> 4468 *``` 4469 * 4470 * ```css 4471 * .layout { 4472 * box-sizing: border-box; 4473 * display:flex; 4474 * } 4475 * .layout-column { 4476 * flex-direction : column 4477 * } 4478 * ``` 4479 */ 4480 function registerLayoutAPI(module){ 4481 var PREFIX_REGEXP = /^((?:x|data)[\:\-_])/i; 4482 var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g; 4483 4484 // NOTE: these are also defined in constants::MEDIA_PRIORITY and constants::MEDIA 4485 var BREAKPOINTS = [ "", "xs", "gt-xs", "sm", "gt-sm", "md", "gt-md", "lg", "gt-lg", "xl", "print" ]; 4486 var API_WITH_VALUES = [ "layout", "flex", "flex-order", "flex-offset", "layout-align" ]; 4487 var API_NO_VALUES = [ "show", "hide", "layout-padding", "layout-margin" ]; 4488 4489 4490 // Build directive registration functions for the standard Layout API... for all breakpoints. 4491 angular.forEach(BREAKPOINTS, function(mqb) { 4492 4493 // Attribute directives with expected, observable value(s) 4494 angular.forEach( API_WITH_VALUES, function(name){ 4495 var fullName = mqb ? name + "-" + mqb : name; 4496 module.directive( directiveNormalize(fullName), attributeWithObserve(fullName)); 4497 }); 4498 4499 // Attribute directives with no expected value(s) 4500 angular.forEach( API_NO_VALUES, function(name){ 4501 var fullName = mqb ? name + "-" + mqb : name; 4502 module.directive( directiveNormalize(fullName), attributeWithoutValue(fullName)); 4503 }); 4504 4505 }); 4506 4507 // Register other, special directive functions for the Layout features: 4508 module 4509 4510 .provider('$$mdLayout' , function() { 4511 // Publish internal service for Layouts 4512 return { 4513 $get : angular.noop, 4514 validateAttributeValue : validateAttributeValue, 4515 validateAttributeUsage : validateAttributeUsage, 4516 /** 4517 * Easy way to disable/enable the Layout API. 4518 * When disabled, this stops all attribute-to-classname generations 4519 */ 4520 disableLayouts : function(isDisabled) { 4521 config.enabled = (isDisabled !== true); 4522 } 4523 }; 4524 }) 4525 4526 .directive('mdLayoutCss' , disableLayoutDirective ) 4527 .directive('ngCloak' , buildCloakInterceptor('ng-cloak')) 4528 4529 .directive('layoutWrap' , attributeWithoutValue('layout-wrap')) 4530 .directive('layoutNowrap' , attributeWithoutValue('layout-nowrap')) 4531 .directive('layoutNoWrap' , attributeWithoutValue('layout-no-wrap')) 4532 .directive('layoutFill' , attributeWithoutValue('layout-fill')) 4533 4534 // !! Deprecated attributes: use the `-lt` (aka less-than) notations 4535 4536 .directive('layoutLtMd' , warnAttrNotSupported('layout-lt-md', true)) 4537 .directive('layoutLtLg' , warnAttrNotSupported('layout-lt-lg', true)) 4538 .directive('flexLtMd' , warnAttrNotSupported('flex-lt-md', true)) 4539 .directive('flexLtLg' , warnAttrNotSupported('flex-lt-lg', true)) 4540 4541 .directive('layoutAlignLtMd', warnAttrNotSupported('layout-align-lt-md')) 4542 .directive('layoutAlignLtLg', warnAttrNotSupported('layout-align-lt-lg')) 4543 .directive('flexOrderLtMd' , warnAttrNotSupported('flex-order-lt-md')) 4544 .directive('flexOrderLtLg' , warnAttrNotSupported('flex-order-lt-lg')) 4545 .directive('offsetLtMd' , warnAttrNotSupported('flex-offset-lt-md')) 4546 .directive('offsetLtLg' , warnAttrNotSupported('flex-offset-lt-lg')) 4547 4548 .directive('hideLtMd' , warnAttrNotSupported('hide-lt-md')) 4549 .directive('hideLtLg' , warnAttrNotSupported('hide-lt-lg')) 4550 .directive('showLtMd' , warnAttrNotSupported('show-lt-md')) 4551 .directive('showLtLg' , warnAttrNotSupported('show-lt-lg')) 4552 4553 // Determine if 4554 .config( detectDisabledLayouts ); 4555 4556 /** 4557 * Converts snake_case to camelCase. 4558 * Also there is special case for Moz prefix starting with upper case letter. 4559 * @param name Name to normalize 4560 */ 4561 function directiveNormalize(name) { 4562 return name 4563 .replace(PREFIX_REGEXP, '') 4564 .replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) { 4565 return offset ? letter.toUpperCase() : letter; 4566 }); 4567 } 4568 4569 } 4570 4571 4572 /** 4573 * Detect if any of the HTML tags has a [md-layouts-disabled] attribute; 4574 * If yes, then immediately disable all layout API features 4575 * 4576 * Note: this attribute should be specified on either the HTML or BODY tags 4577 */ 4578 /** 4579 * @ngInject 4580 */ 4581 function detectDisabledLayouts() { 4582 var isDisabled = !!document.querySelector('[md-layouts-disabled]'); 4583 config.enabled = !isDisabled; 4584 } 4585 4586 /** 4587 * Special directive that will disable ALL Layout conversions of layout 4588 * attribute(s) to classname(s). 4589 * 4590 * <link rel="stylesheet" href="angular-material.min.css"> 4591 * <link rel="stylesheet" href="angular-material.layout.css"> 4592 * 4593 * <body md-layout-css> 4594 * ... 4595 * </body> 4596 * 4597 * Note: Using md-layout-css directive requires the developer to load the Material 4598 * Layout Attribute stylesheet (which only uses attribute selectors): 4599 * 4600 * `angular-material.layout.css` 4601 * 4602 * Another option is to use the LayoutProvider to configure and disable the attribute 4603 * conversions; this would obviate the use of the `md-layout-css` directive 4604 * 4605 */ 4606 function disableLayoutDirective() { 4607 // Return a 1x-only, first-match attribute directive 4608 config.enabled = false; 4609 4610 return { 4611 restrict : 'A', 4612 priority : '900' 4613 }; 4614 } 4615 4616 /** 4617 * Tail-hook ngCloak to delay the uncloaking while Layout transformers 4618 * finish processing. Eliminates flicker with Material.Layouts 4619 */ 4620 function buildCloakInterceptor(className) { 4621 return [ '$timeout', function($timeout){ 4622 return { 4623 restrict : 'A', 4624 priority : -10, // run after normal ng-cloak 4625 compile : function( element ) { 4626 if (!config.enabled) return angular.noop; 4627 4628 // Re-add the cloak 4629 element.addClass(className); 4630 4631 return function( scope, element ) { 4632 // Wait while layout injectors configure, then uncloak 4633 // NOTE: $rAF does not delay enough... and this is a 1x-only event, 4634 // $timeout is acceptable. 4635 $timeout( function(){ 4636 element.removeClass(className); 4637 }, 10, false); 4638 }; 4639 } 4640 }; 4641 }]; 4642 } 4643 4644 4645 // ********************************************************************************* 4646 // 4647 // These functions create registration functions for AngularJS Material Layout attribute directives 4648 // This provides easy translation to switch AngularJS Material attribute selectors to 4649 // CLASS selectors and directives; which has huge performance implications 4650 // for IE Browsers 4651 // 4652 // ********************************************************************************* 4653 4654 /** 4655 * Creates a directive registration function where a possible dynamic attribute 4656 * value will be observed/watched. 4657 * @param {string} className attribute name; eg `layout-gt-md` with value ="row" 4658 */ 4659 function attributeWithObserve(className) { 4660 4661 return ['$mdUtil', '$interpolate', "$log", function(_$mdUtil_, _$interpolate_, _$log_) { 4662 $mdUtil = _$mdUtil_; 4663 $interpolate = _$interpolate_; 4664 $log = _$log_; 4665 4666 return { 4667 restrict: 'A', 4668 compile: function(element, attr) { 4669 var linkFn; 4670 if (config.enabled) { 4671 // immediately replace static (non-interpolated) invalid values... 4672 4673 validateAttributeUsage(className, attr, element, $log); 4674 4675 validateAttributeValue( className, 4676 getNormalizedAttrValue(className, attr, ""), 4677 buildUpdateFn(element, className, attr) 4678 ); 4679 4680 linkFn = translateWithValueToCssClass; 4681 } 4682 4683 // Use for postLink to account for transforms after ng-transclude. 4684 return linkFn || angular.noop; 4685 } 4686 }; 4687 }]; 4688 4689 /** 4690 * Add as transformed class selector(s), then 4691 * remove the deprecated attribute selector 4692 */ 4693 function translateWithValueToCssClass(scope, element, attrs) { 4694 var updateFn = updateClassWithValue(element, className, attrs); 4695 var unwatch = attrs.$observe(attrs.$normalize(className), updateFn); 4696 4697 updateFn(getNormalizedAttrValue(className, attrs, "")); 4698 scope.$on("$destroy", function() { unwatch(); }); 4699 } 4700 } 4701 4702 /** 4703 * Creates a registration function for AngularJS Material Layout attribute directive. 4704 * This is a `simple` transpose of attribute usage to class usage; where we ignore 4705 * any attribute value 4706 */ 4707 function attributeWithoutValue(className) { 4708 return ['$mdUtil', '$interpolate', "$log", function(_$mdUtil_, _$interpolate_, _$log_) { 4709 $mdUtil = _$mdUtil_; 4710 $interpolate = _$interpolate_; 4711 $log = _$log_; 4712 4713 return { 4714 restrict: 'A', 4715 compile: function(element, attr) { 4716 var linkFn; 4717 if (config.enabled) { 4718 // immediately replace static (non-interpolated) invalid values... 4719 4720 validateAttributeValue( className, 4721 getNormalizedAttrValue(className, attr, ""), 4722 buildUpdateFn(element, className, attr) 4723 ); 4724 4725 translateToCssClass(null, element); 4726 4727 // Use for postLink to account for transforms after ng-transclude. 4728 linkFn = translateToCssClass; 4729 } 4730 4731 return linkFn || angular.noop; 4732 } 4733 }; 4734 }]; 4735 4736 /** 4737 * Add as transformed class selector, then 4738 * remove the deprecated attribute selector 4739 */ 4740 function translateToCssClass(scope, element) { 4741 element.addClass(className); 4742 } 4743 } 4744 4745 4746 4747 /** 4748 * After link-phase, do NOT remove deprecated layout attribute selector. 4749 * Instead watch the attribute so interpolated data-bindings to layout 4750 * selectors will continue to be supported. 4751 * 4752 * $observe() the className and update with new class (after remov
4752ing the last one) 4753 * 4754 * e.g. `layout="{{layoutDemo.direction}}"` will update... 4755 * 4756 * NOTE: The value must match one of the specified styles in the CSS. 4757 * For example `flex-gt-md="{{size}}` where `scope.size == 47` will NOT work since 4758 * only breakpoints for 0, 5, 10, 15... 100, 33, 34, 66, 67 are defined. 4759 * 4760 */ 4761 function updateClassWithValue(element, className) { 4762 var lastClass; 4763 4764 return function updateClassFn(newValue) { 4765 var value = validateAttributeValue(className, newValue || ""); 4766 if ( angular.isDefined(value) ) { 4767 if (lastClass) element.removeClass(lastClass); 4768 lastClass = !value ? className : className + "-" + value.trim().replace(WHITESPACE, "-"); 4769 element.addClass(lastClass); 4770 } 4771 }; 4772 } 4773 4774 /** 4775 * Provide console warning that this layout attribute has been deprecated 4776 * 4777 */ 4778 function warnAttrNotSupported(className) { 4779 var parts = className.split("-"); 4780 return ["$log", function($log) { 4781 $log.warn(className + "has been deprecated. Please use a `" + parts[0] + "-gt-<xxx>` variant."); 4782 return angular.noop; 4783 }]; 4784 } 4785 4786 /** 4787 * Centralize warnings for known flexbox issues (especially IE-related issues) 4788 */ 4789 function validateAttributeUsage(className, attr, element, $log){ 4790 var message, usage, url; 4791 var nodeName = element[0].nodeName.toLowerCase(); 4792 4793 switch(className.replace(SUFFIXES,"")) { 4794 case "flex": 4795 if ((nodeName == "md-button") || (nodeName == "fieldset")){ 4796 // @see https://github.com/philipwalton/flexbugs#9-some-html-elements-cant-be-flex-containers 4797 // Use <div flex> wrapper inside (preferred) or outside 4798 4799 usage = "<" + nodeName + " " + className + "></" + nodeName + ">"; 4800 url = "https://github.com/philipwalton/flexbugs#9-some-html-elements-cant-be-flex-containers"; 4801 message = "Markup '{0}' may not work as expected in IE Browsers. Consult '{1}' for details."; 4802 4803 $log.warn( $mdUtil.supplant(message, [usage, url]) ); 4804 } 4805 } 4806 4807 } 4808 4809 4810 /** 4811 * For the Layout attribute value, validate or replace with default 4812 * fallback value 4813 */ 4814 function validateAttributeValue(className, value, updateFn) { 4815 var origValue; 4816 4817 if (!needsInterpolation(value)) { 4818 switch (className.replace(SUFFIXES,"")) { 4819 case 'layout' : 4820 if ( !findIn(value, LAYOUT_OPTIONS) ) { 4821 value = LAYOUT_OPTIONS[0]; // 'row'; 4822 } 4823 break; 4824 4825 case 'flex' : 4826 if (!findIn(value, FLEX_OPTIONS)) { 4827 if (isNaN(value)) { 4828 value = ''; 4829 } 4830 } 4831 break; 4832 4833 case 'flex-offset' : 4834 case 'flex-order' : 4835 if (!value || isNaN(+value)) { 4836 value = '0'; 4837 } 4838 break; 4839 4840 case 'layout-align' : 4841 var axis = extractAlignAxis(value); 4842 value = $mdUtil.supplant("{main}-{cross}",axis); 4843 break; 4844 4845 case 'layout-padding' : 4846 case 'layout-margin' : 4847 case 'layout-fill' : 4848 case 'layout-wrap' : 4849 case 'layout-nowrap' : 4850 case 'layout-nowrap' : 4851 value = ''; 4852 break; 4853 } 4854 4855 if (value != origValue) { 4856 (updateFn || angular.noop)(value); 4857 } 4858 } 4859 4860 return value ? value.trim() : ""; 4861 } 4862 4863 /** 4864 * Replace current attribute value with fallback value 4865 */ 4866 function buildUpdateFn(element, className, attrs) { 4867 return function updateAttrValue(fallback) { 4868 if (!needsInterpolation(fallback)) { 4869 // Do not modify the element's attribute value; so 4870 // uses '<ui-layout layout="/api/sidebar.html" />' will not 4871 // be affected. Just update the attrs value. 4872 attrs[attrs.$normalize(className)] = fallback; 4873 } 4874 }; 4875 } 4876 4877 /** 4878 * See if the original value has interpolation symbols: 4879 * e.g. flex-gt-md="{{triggerPoint}}" 4880 */ 4881 function needsInterpolation(value) { 4882 return (value || "").indexOf($interpolate.startSymbol()) > -1; 4883 } 4884 4885 function getNormalizedAttrValue(className, attrs, defaultVal) { 4886 var normalizedAttr = attrs.$normalize(className); 4887 return attrs[normalizedAttr] ? attrs[normalizedAttr].trim().replace(WHITESPACE, "-") : defaultVal || null; 4888 } 4889 4890 function findIn(item, list, replaceWith) { 4891 item = replaceWith && item ? item.replace(WHITESPACE, replaceWith) : item; 4892 4893 var found = false;
vendor: 22,610 bytes, lines 4894-5699
4894 if (item) { 4895 list.forEach(function(it) { 4896 it = replaceWith ? it.replace(WHITESPACE, replaceWith) : it; 4897 found = found || (it === item); 4898 }); 4899 } 4900 return found; 4901 } 4902 4903 function extractAlignAxis(attrValue) { 4904 var axis = { 4905 main : "start", 4906 cross: "stretch" 4907 }, values; 4908 4909 attrValue = (attrValue || ""); 4910 4911 if ( attrValue.indexOf("-") === 0 || attrValue.indexOf(" ") === 0) { 4912 // For missing main-axis values 4913 attrValue = "none" + attrValue; 4914 } 4915 4916 values = attrValue.toLowerCase().trim().replace(WHITESPACE, "-").split("-"); 4917 if ( values.length && (values[0] === "space") ) { 4918 // for main-axis values of "space-around" or "space-between" 4919 values = [ values[0]+"-"+values[1],values[2] ]; 4920 } 4921 4922 if ( values.length > 0 ) axis.main = values[0] || axis.main; 4923 if ( values.length > 1 ) axis.cross = values[1] || axis.cross; 4924 4925 if ( ALIGNMENT_MAIN_AXIS.indexOf(axis.main) < 0 ) axis.main = "start"; 4926 if ( ALIGNMENT_CROSS_AXIS.indexOf(axis.cross) < 0 ) axis.cross = "stretch"; 4927 4928 return axis; 4929 } 4930 4931 4932})(); 4933 4934})(); 4935(function(){ 4936"use strict"; 4937 4938/** 4939 * @ngdoc module 4940 * @name material.core.liveannouncer 4941 * @description 4942 * AngularJS Material Live Announcer to provide accessibility for Voice Readers. 4943 */ 4944MdLiveAnnouncer.$inject = ["$timeout"]; 4945angular 4946 .module('material.core') 4947 .service('$mdLiveAnnouncer', MdLiveAnnouncer); 4948 4949/** 4950 * @ngdoc service 4951 * @name $mdLiveAnnouncer 4952 * @module material.core.liveannouncer 4953 * 4954 * @description 4955 * 4956 * Service to announce messages to supported screenreaders. 4957 * 4958 * > The `$mdLiveAnnouncer` service is internally used for components to provide proper accessibility. 4959 * 4960 * <hljs lang="js"> 4961 * module.controller('AppCtrl', function($mdLiveAnnouncer) { 4962 * // Basic announcement (Polite Mode) 4963 * $mdLiveAnnouncer.announce('Hey Google'); 4964 * 4965 * // Custom announcement (Assertive Mode) 4966 * $mdLiveAnnouncer.announce('Hey Google', 'assertive'); 4967 * }); 4968 * </hljs> 4969 * 4970 */ 4971function MdLiveAnnouncer($timeout) { 4972 /** @private @const @type {!angular.$timeout} */ 4973 this._$timeout = $timeout; 4974 4975 /** @private @const @type {!HTMLElement} */ 4976 this._liveElement = this._createLiveElement(); 4977 4978 /** @private @const @type {!number} */ 4979 this._announceTimeout = 100; 4980} 4981 4982/** 4983 * @ngdoc method 4984 * @name $mdLiveAnnouncer#announce 4985 * @description Announces messages to supported screenreaders. 4986 * @param {string} message Message to be announced to the screenreader 4987 * @param {'off'|'polite'|'assertive'} politeness The politeness of the announcer element. 4988 */ 4989MdLiveAnnouncer.prototype.announce = function(message, politeness) { 4990 if (!politeness) { 4991 politeness = 'polite'; 4992 } 4993 4994 var self = this; 4995 4996 self._liveElement.textContent = ''; 4997 self._liveElement.setAttribute('aria-live', politeness); 4998 4999 // This 100ms timeout is necessary for some browser + screen-reader combinations: 5000 // - Both JAWS and NVDA over IE11 will not announce anything without a non-zero timeout. 5001 // - With Chrome and IE11 with NVDA or JAWS, a repeated (identical) message won't be read a 5002 // second time without clearing and then using a non-zero delay. 5003 // (using JAWS 17 at time of this writing). 5004 self._$timeout(function() { 5005 self._liveElement.textContent = message; 5006 }, self._announceTimeout, false); 5007}; 5008 5009/** 5010 * Creates a live announcer element, which listens for DOM changes and announces them 5011 * to the screenreaders. 5012 * @returns {!HTMLElement} 5013 * @private 5014 */ 5015MdLiveAnnouncer.prototype._createLiveElement = function() { 5016 var liveEl = document.createElement('div'); 5017 5018 liveEl.classList.add('md-visually-hidden'); 5019 liveEl.setAttribute('role', 'status'); 5020 liveEl.setAttribute('aria-atomic', 'true'); 5021 liveEl.setAttribute('aria-live', 'polite'); 5022 5023 document.body.appendChild(liveEl); 5024 5025 return liveEl; 5026}; 5027 5028})(); 5029(function(){ 5030"use strict"; 5031 5032/** 5033 * @ngdoc service 5034 * @name $$mdMeta 5035 * @module material.core.meta 5036 * 5037 * @description 5038 * 5039 * A provider and a service that simplifies meta tags access 5040 * 5041 * Note: This is intended only for use with dynamic meta tags such as browser color and title. 5042 * Tags that are only processed when the page is rendered (such as `charset`, and `http-equiv`) 5043 * will not work since `$$mdMeta` adds the tags after the page has already been loaded. 5044 * 5045 * ```js 5046 * app.config(function($$mdMetaProvider) { 5047 * var removeMeta = $$mdMetaProvider.setMeta('meta-name', 'content'); 5048 * var metaValue = $$mdMetaProvider.getMeta('meta-name'); // -> 'content' 5049 * 5050 * removeMeta(); 5051 * }); 5052 * 5053 * app.controller('myController', function($$mdMeta) { 5054 * var removeMeta = $$mdMeta.setMeta('meta-name', 'content'); 5055 * var metaValue = $$mdMeta.getMeta('meta-name'); // -> 'content' 5056 * 5057 * removeMeta(); 5058 * }); 5059 * ``` 5060 * 5061 * @returns {$$mdMeta.$service} 5062 * 5063 */ 5064angular.module('material.core.meta', []) 5065 .provider('$$mdMeta', function () { 5066 var head = angular.element(document.head); 5067 var metaElements = {}; 5068 5069 /** 5070 * Checks if the requested element was written manually and maps it 5071 * 5072 * @param {string} name meta tag 'name' attribute value 5073 * @returns {boolean} returns true if there is an element with the requested name 5074 */ 5075 function mapExistingElement(name) { 5076 if (metaElements[name]) { 5077 return true; 5078 } 5079 5080 var element = document.getElementsByName(name)[0]; 5081 5082 if (!element) { 5083 return false; 5084 } 5085 5086 metaElements[name] = angular.element(element); 5087 5088 return true; 5089 } 5090 5091 /** 5092 * @ngdoc method 5093 * @name $$mdMeta#setMeta 5094 * 5095 * @description 5096 * Creates meta element with the 'name' and 'content' attributes, 5097 * if the meta tag is already created than we replace the 'content' value 5098 * 5099 * @param {string} name meta tag 'name' attribute value 5100 * @param {string} content meta tag 'content' attribute value 5101 * @returns {function} remove function 5102 * 5103 */ 5104 function setMeta(name, content) { 5105 mapExistingElement(name); 5106 5107 if (!metaElements[name]) { 5108 var newMeta = angular.element('<meta name="' + name + '" content="' + content + '"/>'); 5109 head.append(newMeta); 5110 metaElements[name] = newMeta; 5111 } 5112 else { 5113 metaElements[name].attr('content', content); 5114 } 5115 5116 return function () { 5117 metaElements[name].attr('content', ''); 5118 metaElements[name].remove(); 5119 delete metaElements[name]; 5120 }; 5121 } 5122 5123 /** 5124 * @ngdoc method 5125 * @name $$mdMeta#getMeta 5126 * 5127 * @description 5128 * Gets the 'content' attribute value of the wanted meta element 5129 * 5130 * @param {string} name meta tag 'name' attribute value 5131 * @returns {string} content attribute value 5132 */ 5133 function getMeta(name) { 5134 if (!mapExistingElement(name)) { 5135 throw Error('$$mdMeta: could not find a meta tag with the name \'' + name + '\''); 5136 } 5137 5138 return metaElements[name].attr('content'); 5139 } 5140 5141 var module = { 5142 setMeta: setMeta, 5143 getMeta: getMeta 5144 }; 5145 5146 return angular.extend({}, module, { 5147 $get: function () { 5148 return module; 5149 } 5150 }); 5151 }); 5152})(); 5153(function(){ 5154"use strict"; 5155 5156 /** 5157 * @ngdoc module 5158 * @name material.core.componentRegistry 5159 * 5160 * @description 5161 * A component instance registration service. 5162 * Note: currently this as a private service in the SideNav component. 5163 */ 5164 ComponentRegistry.$inject = ["$log", "$q"]; 5165 angular.module('material.core') 5166 .factory('$mdComponentRegistry', ComponentRegistry); 5167 5168 /* 5169 * @private 5170 * @ngdoc factory 5171 * @name ComponentRegistry 5172 * @module material.core.componentRegistry 5173 * 5174 */ 5175 function ComponentRegistry($log, $q) { 5176 5177 var self; 5178 var instances = [ ]; 5179 var pendings = { }; 5180 5181 return self = { 5182 /** 5183 * Used to print an error when an instance for a handle isn't found. 5184 */ 5185 notFoundError: function(handle, msgContext) { 5186 $log.error( (msgContext || "") + 'No instance found for handle', handle); 5187 }, 5188 /** 5189 * Return all registered instances as an array. 5190 */ 5191 getInstances: function() { 5192 return instances; 5193 }, 5194 5195 /** 5196 * Get a registered instance. 5197 * @param handle the String handle to look up for a registered instance. 5198 */ 5199 get: function(handle) { 5200 if ( !isValidID(handle) ) return null; 5201 5202 var i, j, instance; 5203 for(i = 0, j = instances.length; i < j; i++) { 5204 instance = instances[i]; 5205 if(instance.$$mdHandle === handle) { 5206 return instance; 5207 } 5208 } 5209 return null; 5210 }, 5211 5212 /** 5213 * Register an instance. 5214 * @param instance the instance to register 5215 * @param handle the handle to identify the instance under. 5216 */ 5217 register: function(instance, handle) { 5218 if ( !handle ) return angular.noop; 5219 5220 instance.$$mdHandle = handle; 5221 instances.push(instance); 5222 resolveWhen(); 5223 5224 return deregister; 5225 5226 /** 5227 * Remove registration for an instance 5228 */ 5229 function deregister() { 5230 var index = instances.indexOf(instance); 5231 if (index !== -1) { 5232 instances.splice(index, 1); 5233 } 5234 } 5235 5236 /** 5237 * Resolve any pending promises for this instance 5238 */ 5239 function resolveWhen() { 5240 var dfd = pendings[handle]; 5241 if ( dfd ) { 5242 dfd.forEach(function (promise) { 5243 promise.resolve(instance); 5244 }); 5245 delete pendings[handle]; 5246 } 5247 } 5248 }, 5249 5250 /** 5251 * Async accessor to registered component instance 5252 * If not available then a promise is created to notify 5253 * all listeners when the instance is registered. 5254 */ 5255 when : function(handle) { 5256 if ( isValidID(handle) ) { 5257 var deferred = $q.defer(); 5258 var instance = self.get(handle); 5259 5260 if ( instance ) { 5261 deferred.resolve( instance ); 5262 } else { 5263 if (pendings[handle] === undefined) { 5264 pendings[handle] = []; 5265 } 5266 pendings[handle].push(deferred); 5267 } 5268 5269 return deferred.promise; 5270 } 5271 return $q.reject("Invalid `md-component-id` value."); 5272 } 5273 5274 }; 5275 5276 function isValidID(handle){ 5277 return handle && (handle !== ""); 5278 } 5279 5280 } 5281 5282})(); 5283(function(){ 5284"use strict"; 5285 5286(function() { 5287 'use strict'; 5288 5289 /** 5290 * @ngdoc service 5291 * @name $mdButtonInkRipple 5292 * @module material.core 5293 * 5294 * @description 5295 * Provides ripple effects for md-button. See $mdInkRipple service for all possible configuration options. 5296 * 5297 * @param {object=} scope Scope within the current context 5298 * @param {object=} element The element the ripple effect should be applied to 5299 * @param {object=} options (Optional) Configuration options to override the default ripple configuration 5300 */ 5301 5302 MdButtonInkRipple.$inject = ["$mdInkRipple"]; 5303 angular.module('material.core') 5304 .factory('$mdButtonInkRipple', MdButtonInkRipple); 5305 5306 function MdButtonInkRipple($mdInkRipple) { 5307 return { 5308 attach: function attachRipple(scope, element, options) { 5309 options = angular.extend(optionsForElement(element), options); 5310 5311 return $mdInkRipple.attach(scope, element, options); 5312 } 5313 }; 5314 5315 function optionsForElement(element) { 5316 if (element.hasClass('md-icon-button')) { 5317 return { 5318 isMenuItem: element.hasClass('md-menu-item'), 5319 fitRipple: true, 5320 center: true 5321 }; 5322 } else { 5323 return { 5324 isMenuItem: element.hasClass('md-menu-item'), 5325 dimBackground: true 5326 }; 5327 } 5328 } 5329 } 5330})(); 5331 5332})(); 5333(function(){ 5334"use strict"; 5335 5336(function() { 5337 'use strict'; 5338 5339 /** 5340 * @ngdoc service 5341 * @name $mdCheckboxInkRipple 5342 * @module material.core 5343 * 5344 * @description 5345 * Provides ripple effects for md-checkbox. See $mdInkRipple service for all possible configuration options. 5346 * 5347 * @param {object=} scope Scope within the current context 5348 * @param {object=} element The element the ripple effect should be applied to 5349 * @param {object=} options (Optional) Configuration options to override the defaultripple configuration 5350 */ 5351 5352 MdCheckboxInkRipple.$inject = ["$mdInkRipple"]; 5353 angular.module('material.core') 5354 .factory('$mdCheckboxInkRipple', MdCheckboxInkRipple); 5355 5356 function MdCheckboxInkRipple($mdInkRipple) { 5357 return { 5358 attach: attach 5359 }; 5360 5361 function attach(scope, element, options) { 5362 return $mdInkRipple.attach(scope, element, angular.extend({ 5363 center: true, 5364 dimBackground: false, 5365 fitRipple: true 5366 }, options)); 5367 } 5368 } 5369})(); 5370 5371})(); 5372(function(){ 5373"use strict"; 5374 5375(function() { 5376 'use strict'; 5377 5378 /** 5379 * @ngdoc service 5380 * @name $mdListInkRipple 5381 * @module material.core 5382 * 5383 * @description 5384 * Provides ripple effects for md-list. See $mdInkRipple service for all possible configuration options. 5385 * 5386 * @param {object=} scope Scope within the current context 5387 * @param {object=} element The element the ripple effect should be applied to 5388 * @param {object=} options (Optional) Configuration options to override the defaultripple configuration 5389 */ 5390 5391 MdListInkRipple.$inject = ["$mdInkRipple"]; 5392 angular.module('material.core') 5393 .factory('$mdListInkRipple', MdListInkRipple); 5394 5395 function MdListInkRipple($mdInkRipple) { 5396 return { 5397 attach: attach 5398 }; 5399 5400 function attach(scope, element, options) { 5401 return $mdInkRipple.attach(scope, element, angular.extend({ 5402 center: false, 5403 dimBackground: true, 5404 outline: false, 5405 rippleSize: 'full' 5406 }, options)); 5407 } 5408 } 5409})(); 5410 5411})(); 5412(function(){ 5413"use strict"; 5414 5415/** 5416 * @ngdoc module 5417 * @name material.core.ripple 5418 * @description 5419 * Ripple 5420 */ 5421InkRippleCtrl.$inject = ["$scope", "$element", "rippleOptions", "$window", "$timeout", "$mdUtil", "$mdColorUtil"]; 5422InkRippleDirective.$inject = ["$mdButtonInkRipple", "$mdCheckboxInkRipple"]; 5423angular.module('material.core') 5424 .provider('$mdInkRipple', InkRippleProvider) 5425 .directive('mdInkRipple', InkRippleDirective) 5426 .directive('mdNoInk', attrNoDirective) 5427 .directive('mdNoBar', attrNoDirective) 5428 .directive('mdNoStretch', attrNoDirective); 5429 5430var DURATION = 450; 5431 5432/** 5433 * @ngdoc directive 5434 * @name mdInkRipple 5435 * @module material.core.ripple 5436 * 5437 * @description 5438 * The `md-ink-ripple` directive allows you to specify the ripple color or id a ripple is allowed. 5439 * 5440 * @param {string|boolean} md-ink-ripple A color string `#FF0000` or boolean (`false` or `0`) for preventing ripple 5441 * 5442 * @usage 5443 * ### String values 5444 * <hljs lang="html"> 5445 * <ANY md-ink-ripple="#FF0000"> 5446 * Ripples in red 5447 * </ANY> 5448 * 5449 * <ANY md-ink-ripple="false"> 5450 * Not rippling 5451 * </ANY> 5452 * </hljs> 5453 * 5454 * ### Interpolated values 5455 * <hljs lang="html"> 5456 * <ANY md-ink-ripple="{{ randomColor() }}"> 5457 * Ripples with the return value of 'randomColor' function 5458 * </ANY> 5459 * 5460 * <ANY md-ink-ripple="{{ canRipple() }}"> 5461 * Ripples if 'canRipple' function return value is not 'false' or '0' 5462 * </ANY> 5463 * </hljs> 5464 */ 5465function InkRippleDirective ($mdButtonInkRipple, $mdCheckboxInkRipple) { 5466 return { 5467 controller: angular.noop, 5468 link: function (scope, element, attr) { 5469 attr.hasOwnProperty('mdInkRippleCheckbox') 5470 ? $mdCheckboxInkRipple.attach(scope, element) 5471 : $mdButtonInkRipple.attach(scope, element); 5472 } 5473 }; 5474} 5475 5476/** 5477 * @ngdoc service 5478 * @name $mdInkRipple 5479 * @module material.core.ripple 5480 * 5481 * @description 5482 * `$mdInkRipple` is a service for adding ripples to any element 5483 * 5484 * @usage 5485 * <hljs lang="js"> 5486 * app.factory('$myElementInkRipple', function($mdInkRipple) { 5487 * return { 5488 * attach: function (scope, element, options) { 5489 * return $mdInkRipple.attach(scope, element, angular.extend({ 5490 * center: false, 5491 * dimBackground: true 5492 * }, options)); 5493 * } 5494 * }; 5495 * }); 5496 * 5497 * app.controller('myController', function ($scope, $element, $myElementInkRipple) { 5498 * $scope.onClick = function (ev) { 5499 * $myElementInkRipple.attach($scope, angular.element(ev.target), { center: true }); 5500 * } 5501 * }); 5502 * </hljs> 5503 * 5504 * ### Disabling ripples globally 5505 * If you want to disable ink ripples globally, for all components, you can call the 5506 * `disableInkRipple` method in your app's config. 5507 * 5508 * <hljs lang="js"> 5509 * app.config(function ($mdInkRippleProvider) { 5510 * $mdInkRippleProvider.disableInkRipple(); 5511 * }); 5512 */ 5513 5514function InkRippleProvider () { 5515 var isDisabledGlobally = false; 5516 5517 return { 5518 disableInkRipple: disableInkRipple, 5519 $get: ["$injector", function($injector) { 5520 return { attach: attach }; 5521 5522 /** 5523 * @ngdoc method 5524 * @name $mdInkRipple#attach 5525 * 5526 * @description 5527 * Attaching given scope, element and options to inkRipple controller 5528 * 5529 * @param {object=} scope Scope within the current context 5530 * @param {object=} element The element the ripple effect should be applied to 5531 * @param {object=} options (Optional) Configuration options to override the defaultRipple configuration 5532 * * `center` - Whether the ripple should start from the center of the container element 5533 * * `dimBackground` - Whether the background should be dimmed with the ripple color 5534 * * `colorElement` - The element the ripple should take its color from, defined by css property `color` 5535 * * `fitRipple` - Whether the ripple should fill the element 5536 */ 5537 function attach (scope, element, options) { 5538 if (isDisabledGlobally || element.controller('mdNoInk')) return angular.noop; 5539 return $injector.instantiate(InkRippleCtrl, { 5540 $scope: scope, 5541 $element: element, 5542 rippleOptions: options 5543 }); 5544 } 5545 }] 5546 }; 5547 5548 /** 5549 * @ngdoc method 5550 * @name $mdInkRipple#disableInkRipple 5551 * 5552 * @description 5553 * A config-time method that, when called, disables ripples globally. 5554 */ 5555 function disableInkRipple () { 5556 isDisabledGlobally = true; 5557 } 5558} 5559 5560/** 5561 * Controller used by the ripple service in order to apply ripples 5562 * @ngInject 5563 */ 5564function InkRippleCtrl ($scope, $element, rippleOptions, $window, $timeout, $mdUtil, $mdColorUtil) { 5565 this.$window = $window; 5566 this.$timeout = $timeout; 5567 this.$mdUtil = $mdUtil; 5568 this.$mdColorUtil = $mdColorUtil; 5569 this.$scope = $scope; 5570 this.$element = $element; 5571 this.options = rippleOptions; 5572 this.mousedown = false; 5573 this.ripples = []; 5574 this.timeout = null; // Stores a reference to the most-recent ripple timeout 5575 this.lastRipple = null; 5576 5577 $mdUtil.valueOnUse(this, 'container', this.createContainer); 5578 5579 this.$element.addClass('md-ink-ripple'); 5580 5581 // attach method for unit tests 5582 ($element.controller('mdInkRipple') || {}).createRipple = angular.bind(this, this.createRipple); 5583 ($element.controller('mdInkRipple') || {}).setColor = angular.bind(this, this.color); 5584 5585 this.bindEvents(); 5586} 5587 5588 5589/** 5590 * Either remove or unlock any remaining ripples when the user mouses off of the element (either by 5591 * mouseup or mouseleave event) 5592 */ 5593function autoCleanup (self, cleanupFn) { 5594 5595 if ( self.mousedown || self.lastRipple ) { 5596 self.mousedown = false; 5597 self.$mdUtil.nextTick( angular.bind(self, cleanupFn), false); 5598 } 5599 5600} 5601 5602 5603/** 5604 * Returns the color that the ripple should be (either based on CSS or hard-coded) 5605 * @returns {string} 5606 */ 5607InkRippleCtrl.prototype.color = function (value) { 5608 var self = this; 5609 5610 // If assigning a color value, apply it to background and the ripple color 5611 if (angular.isDefined(value)) { 5612 self._color = self._parseColor(value); 5613 } 5614 5615 // If color lookup, use assigned, defined, or inherited 5616 return self._color || self._parseColor( self.inkRipple() ) || self._parseColor( getElementColor() ); 5617 5618 /** 5619 * Finds the color element and returns its text color for use as default ripple color 5620 * @returns {string} 5621 */ 5622 function getElementColor () { 5623 var items = self.options && self.options.colorElement ? self.options.colorElement : []; 5624 var elem = items.length ? items[ 0 ] : self.$element[ 0 ]; 5625 5626 return elem ? self.$window.getComputedStyle(elem).color : 'rgb(0,0,0)'; 5627 } 5628}; 5629 5630/** 5631 * Updating the ripple colors based on the current inkRipple value 5632 * or the element's computed style color 5633 */ 5634InkRippleCtrl.prototype.calculateColor = function () { 5635 return this.color(); 5636}; 5637 5638 5639/** 5640 * Takes a string color and converts it to RGBA format 5641 * @param color {string} 5642 * @param [multiplier] {int} 5643 * @returns {string} 5644 */ 5645 5646InkRippleCtrl.prototype._parseColor = function parseColor (color, multiplier) { 5647 multiplier = multiplier || 1; 5648 var colorUtil = this.$mdColorUtil; 5649 5650 if (!color) return; 5651 if (color.indexOf('rgba') === 0) return color.replace(/\d?\.?\d*\s*\)\s*$/, (0.1 * multiplier).toString() + ')'); 5652 if (color.indexOf('rgb') === 0) return colorUtil.rgbToRgba(color); 5653 if (color.indexOf('#') === 0) return colorUtil.hexToRgba(color); 5654 5655}; 5656 5657/** 5658 * Binds events to the root element for 5659 */ 5660InkRippleCtrl.prototype.bindEvents = function () { 5661 this.$element.on('mousedown', angular.bind(this, this.handleMousedown)); 5662 this.$element.on('mouseup touchend', angular.bind(this, this.handleMouseup)); 5663 this.$element.on('mouseleave', angular.bind(this, this.handleMouseup)); 5664 this.$element.on('touchmove', angular.bind(this, this.handleTouchmove)); 5665}; 5666 5667/** 5668 * Create a new ripple on every mousedown event from the root element 5669 * @param event {MouseEvent} 5670 */ 5671InkRippleCtrl.prototype.handleMousedown = function (event) { 5672 if ( this.mousedown ) return; 5673 5674 // When jQuery is loaded, we have to get the original event 5675 if (event.hasOwnProperty('originalEvent')) event = event.originalEvent; 5676 this.mousedown = true; 5677 if (this.options.center) { 5678 this.createRipple(this.container.prop('clientWidth') / 2, this.container.prop('clientWidth') / 2); 5679 } else { 5680 5681 // We need to calculate the relative coordinates if the target is a sublayer of the ripple element 5682 if (event.srcElement !== this.$element[0]) { 5683 var layerRect = this.$element[0].getBoundingClientRect(); 5684 var layerX = event.clientX - layerRect.left; 5685 var layerY = event.clientY - layerRect.top; 5686 5687 this.createRipple(layerX, layerY); 5688 } else { 5689 this.createRipple(event.offsetX, event.offsetY); 5690 } 5691 } 5692}; 5693 5694/** 5695 * Either remove or unlock any remaining ripples when the user mouses off of the element (either by 5696 * mouseup, touchend or mouseleave event) 5697 */ 5698InkRippleCtrl.prototype.handleMouseup = function () { 5699 autoCleanup(this, this.clearRipples
vendor: 6,620 bytes, lines 5699-5941
5699); 5700}; 5701 5702/** 5703 * Either remove or unlock any remaining ripples when the user mouses off of the element (by 5704 * touchmove) 5705 */ 5706InkRippleCtrl.prototype.handleTouchmove = function () { 5707 autoCleanup(this, this.deleteRipples); 5708}; 5709 5710/** 5711 * Cycles through all ripples and attempts to remove them. 5712 */ 5713InkRippleCtrl.prototype.deleteRipples = function () { 5714 for (var i = 0; i < this.ripples.length; i++) { 5715 this.ripples[ i ].remove(); 5716 } 5717}; 5718 5719/** 5720 * Cycles through all ripples and attempts to remove them with fade. 5721 * Depending on logic within `fadeInComplete`, some removals will be postponed. 5722 */ 5723InkRippleCtrl.prototype.clearRipples = function () { 5724 for (var i = 0; i < this.ripples.length; i++) { 5725 this.fadeInComplete(this.ripples[ i ]); 5726 } 5727}; 5728 5729/** 5730 * Creates the ripple container element 5731 * @returns {*} 5732 */ 5733InkRippleCtrl.prototype.createContainer = function () { 5734 var container = angular.element('<div class="md-ripple-container"></div>'); 5735 this.$element.append(container); 5736 return container; 5737}; 5738 5739InkRippleCtrl.prototype.clearTimeout = function () { 5740 if (this.timeout) { 5741 this.$timeout.cancel(this.timeout); 5742 this.timeout = null; 5743 } 5744}; 5745 5746InkRippleCtrl.prototype.isRippleAllowed = function () { 5747 var element = this.$element[0]; 5748 do { 5749 if (!element.tagName || element.tagName === 'BODY') break; 5750 5751 if (element && angular.isFunction(element.hasAttribute)) { 5752 if (element.hasAttribute('disabled')) return false; 5753 if (this.inkRipple() === 'false' || this.inkRipple() === '0') return false; 5754 } 5755 5756 } while (element = element.parentNode); 5757 return true; 5758}; 5759 5760/** 5761 * The attribute `md-ink-ripple` may be a static or interpolated 5762 * color value OR a boolean indicator (used to disable ripples) 5763 */ 5764InkRippleCtrl.prototype.inkRipple = function () { 5765 return this.$element.attr('md-ink-ripple'); 5766}; 5767 5768/** 5769 * Creates a new ripple and adds it to the container. Also tracks ripple in `this.ripples`. 5770 * @param left 5771 * @param top 5772 */ 5773InkRippleCtrl.prototype.createRipple = function (left, top) { 5774 if (!this.isRippleAllowed()) return; 5775 5776 var ctrl = this; 5777 var colorUtil = ctrl.$mdColorUtil; 5778 var ripple = angular.element('<div class="md-ripple"></div>'); 5779 var width = this.$element.prop('clientWidth'); 5780 var height = this.$element.prop('clientHeight'); 5781 var x = Math.max(Math.abs(width - left), left) * 2; 5782 var y = Math.max(Math.abs(height - top), top) * 2; 5783 var size = getSize(this.options.fitRipple, x, y); 5784 var color = this.calculateColor(); 5785 5786 ripple.css({ 5787 left: left + 'px', 5788 top: top + 'px', 5789 background: 'black', 5790 width: size + 'px', 5791 height: size + 'px', 5792 backgroundColor: colorUtil.rgbaToRgb(color), 5793 borderColor: colorUtil.rgbaToRgb(color) 5794 }); 5795 this.lastRipple = ripple; 5796 5797 // we only want one timeout to be running at a time 5798 this.clearTimeout(); 5799 this.timeout = this.$timeout(function () { 5800 ctrl.clearTimeout(); 5801 if (!ctrl.mousedown) ctrl.fadeInComplete(ripple); 5802 }, DURATION * 0.35, false); 5803 5804 if (this.options.dimBackground) this.container.css({ backgroundColor: color }); 5805 this.container.append(ripple); 5806 this.ripples.push(ripple); 5807 ripple.addClass('md-ripple-placed'); 5808 5809 this.$mdUtil.nextTick(function () { 5810 5811 ripple.addClass('md-ripple-scaled md-ripple-active'); 5812 ctrl.$timeout(function () { 5813 ctrl.clearRipples(); 5814 }, DURATION, false); 5815 5816 }, false); 5817 5818 function getSize (fit, x, y) { 5819 return fit 5820 ? Math.max(x, y) 5821 : Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)); 5822 } 5823}; 5824 5825 5826 5827/** 5828 * After fadeIn finishes, either kicks off the fade-out animation or queues the element for removal on mouseup 5829 * @param ripple 5830 */ 5831InkRippleCtrl.prototype.fadeInComplete = function (ripple) { 5832 if (this.lastRipple === ripple) { 5833 if (!this.timeout && !this.mousedown) { 5834 this.removeRipple(ripple); 5835 } 5836 } else { 5837 this.removeRipple(ripple); 5838 } 5839}; 5840 5841/** 5842 * Kicks off the animation for removing a ripple 5843 * @param ripple {Element} 5844 */ 5845InkRippleCtrl.prototype.removeRipple = function (ripple) { 5846 var ctrl = this; 5847 var index = this.ripples.indexOf(ripple); 5848 if (index < 0) return; 5849 this.ripples.splice(this.ripples.indexOf(ripple), 1); 5850 ripple.removeClass('md-ripple-active'); 5851 ripple.addClass('md-ripple-remove'); 5852 if (this.ripples.length === 0) this.container.css({ backgroundColor: '' }); 5853 // use a 2-second timeout in order to allow for the animation to finish 5854 // we don't actually care how long the animation takes 5855 this.$timeout(function () { 5856 ctrl.fadeOutComplete(ripple); 5857 }, DURATION, false); 5858}; 5859 5860/** 5861 * Removes the provided ripple from the DOM 5862 * @param ripple 5863 */ 5864InkRippleCtrl.prototype.fadeOutComplete = function (ripple) { 5865 ripple.remove(); 5866 this.lastRipple = null; 5867}; 5868 5869/** 5870 * Used to create an empty directive. This is used to track flag-directives whose children may have 5871 * functionality based on them. 5872 * 5873 * Example: `md-no-ink` will potentially be used by all child directives. 5874 */ 5875function attrNoDirective () { 5876 return { controller: angular.noop }; 5877} 5878 5879})(); 5880(function(){ 5881"use strict"; 5882 5883(function() { 5884 'use strict'; 5885 5886 /** 5887 * @ngdoc service 5888 * @name $mdTabInkRipple 5889 * @module material.core 5890 * 5891 * @description 5892 * Provides ripple effects for md-tabs. See $mdInkRipple service for all possible configuration options. 5893 * 5894 * @param {object=} scope Scope within the current context 5895 * @param {object=} element The element the ripple effect should be applied to 5896 * @param {object=} options (Optional) Configuration options to override the defaultripple configuration 5897 */ 5898 5899 MdTabInkRipple.$inject = ["$mdInkRipple"]; 5900 angular.module('material.core') 5901 .factory('$mdTabInkRipple', MdTabInkRipple); 5902 5903 function MdTabInkRipple($mdInkRipple) { 5904 return { 5905 attach: attach 5906 }; 5907 5908 function attach(scope, element, options) { 5909 return $mdInkRipple.attach(scope, element, angular.extend({ 5910 center: false, 5911 dimBackground: true, 5912 outline: false, 5913 rippleSize: 'full' 5914 }, options)); 5915 } 5916 } 5917})(); 5918 5919})(); 5920(function(){ 5921"use strict"; 5922 5923angular.module('material.core.theming.palette', []) 5924.constant('$mdColorPalette', { 5925 'red': { 5926 '50': '#ffebee', 5927 '100': '#ffcdd2', 5928 '200': '#ef9a9a', 5929 '300': '#e57373', 5930 '400': '#ef5350', 5931 '500': '#f44336', 5932 '600': '#e53935', 5933 '700': '#d32f2f', 5934 '800': '#c62828', 5935 '900': '#b71c1c', 5936 'A100': '#ff8a80', 5937 'A200': '#ff5252', 5938 'A400': '#ff1744', 5939 'A700': '#d50000', 5940 'contrastDefaultColor': 'light', 5941 'contrastDarkColors': '50 100 200 300
5941A100', 5942 'contrastStrongLightColors': '400 500 600 700 A200 A400 A700' 5943 }, 5944 'pink': { 5945 '50': '#fce4ec', 5946 '100': '#f8bbd0', 5947 '200': '#f48fb1', 5948 '300': '#f06292', 5949 '400': '#ec407a', 5950 '500': '#e91e63', 5951 '600': '#d81b60', 5952 '700': '#c2185b', 5953 '800': '#ad1457', 5954 '900': '#880e4f', 5955 'A100': '#ff80ab', 5956 'A200': '#ff4081', 5957 'A400': '#f50057', 5958 'A700': '#c51162', 5959 'contrastDefaultColor': 'light', 5960 'contrastDarkColors': '50 100 200 A100', 5961 'contrastStrongLightColors': '500 600 A200 A400 A700' 5962 }, 5963 'purple': { 5964 '50': '#f3e5f5', 5965 '100': '#e1bee7', 5966 '200': '#ce93d8', 5967 '300': '#ba68c8', 5968 '400': '#ab47bc', 5969 '500': '#9c27b0', 5970 '600': '#8e24aa', 5971 '700': '#7b1fa2', 5972 '800': '#6a1b9a', 5973 '900': '#4a148c', 5974 'A100': '#ea80fc', 5975 'A200': '#e040fb', 5976 'A400': '#d500f9', 5977 'A700': '#aa00ff', 5978 'contrastDefaultColor': 'light', 5979 'contrastDarkColors': '50 100 200 A100', 5980 'contrastStrongLightColors': '300 400 A200 A400 A700' 5981 }, 5982 'deep-purple': { 5983 '50': '#ede7f6', 5984 '100': '#d1c4e9', 5985 '200': '#b39ddb', 5986 '300': '#9575cd', 5987 '400': '#7e57c2', 5988 '500': '#673ab7', 5989 '600': '#5e35b1', 5990 '700': '#512da8', 5991 '800': '#4527a0', 5992 '900': '#311b92', 5993 'A100': '#b388ff', 5994 'A200': '#7c4dff', 5995 'A400': '#651fff', 5996 'A700': '#6200ea', 5997 'contrastDefaultColor': 'light', 5998 'contrastDarkColors': '50 100 200 A100', 5999 'contrastStrongLightColors': '300 400 A200' 6000 }, 6001 'indigo': { 6002 '50': '#e8eaf6', 6003 '100': '#c5cae9', 6004 '200': '#9fa8da', 6005 '300': '#7986cb', 6006 '400': '#5c6bc0', 6007 '500': '#3f51b5', 6008 '600': '#3949ab', 6009 '700': '#303f9f', 6010 '800': '#283593', 6011 '900': '#1a237e', 6012 'A100': '#8c9eff', 6013 'A200': '#536dfe', 6014 'A400': '#3d5afe', 6015 'A700': '#304ffe', 6016 'contrastDefaultColor': 'light', 6017 'contrastDarkColors': '50 100 200 A100', 6018 'contrastStrongLightColors': '300 400 A200 A400' 6019 }, 6020 'blue': { 6021 '50': '#e3f2fd', 6022 '100': '#bbdefb', 6023 '200': '#90caf9', 6024 '300': '#64b5f6', 6025 '400': '#42a5f5', 6026 '500': '#2196f3', 6027 '600': '#1e88e5', 6028 '700': '#1976d2', 6029 '800': '#1565c0', 6030 '900': '#0d47a1', 6031 'A100': '#82b1ff', 6032 'A200': '#448aff', 6033 'A400': '#2979ff', 6034 'A700': '#2962ff', 6035 'contrastDefaultColor': 'light', 6036 'contrastDarkColors': '50 100 200 300 400 A100', 6037 'contrastStrongLightColors': '500 600 700 A200 A400 A700' 6038 }, 6039 'light-blue': { 6040 '50': '#e1f5fe', 6041 '100': '#b3e5fc', 6042 '200': '#81d4fa', 6043 '300': '#4fc3f7', 6044 '400': '#29b6f6', 6045 '500': '#03a9f4', 6046 '600': '#039be5', 6047 '700': '#0288d1', 6048 '800': '#0277bd', 6049 '900': '#01579b', 6050 'A100': '#80d8ff', 6051 'A200': '#40c4ff', 6052 'A400': '#00b0ff', 6053 'A700': '#0091ea', 6054 'contrastDefaultColor': 'dark', 6055 'contrastLightColors': '600 700 800 900 A700', 6056 'contrastStrongLightColors': '600 700 800 A700' 6057 }, 6058 'cyan': { 6059 '50': '#e0f7fa', 6060 '100': '#b2ebf2', 6061 '200': '#80deea', 6062 '300': '#4dd0e1', 6063 '400': '#26c6da', 6064 '500': '#00bcd4', 6065 '600': '#00acc1', 6066 '700': '#0097a7', 6067 '800': '#00838f', 6068 '900': '#006064', 6069 'A100': '#84ffff', 6070 'A200': '#18ffff', 6071 'A400': '#00e5ff', 6072 'A700': '#00b8d4', 6073 'contrastDefaultColor': 'dark', 6074 'contrastLightColors': '700 800 900', 6075 'contrastStrongLightColors': '700 800 900' 6076 }, 6077 'teal': { 6078 '50': '#e0f2f1', 6079 '100': '#b2dfdb', 6080 '200': '#80cbc4', 6081 '300': '#4db6ac', 6082 '400': '#26a69a', 6083 '500': '#009688', 6084 '600': '#00897b', 6085 '700': '#00796b', 6086 '800': '#00695c', 6087 '900': '#004d40', 6088 'A100': '#a7ffeb', 6089 'A200': '#64ffda', 6090 'A400': '#1de9b6', 6091 'A700': '#00bfa5', 6092 'contrastDefaultColor': 'dark', 6093 'contrastLightColors': '500 600 700 800 900', 6094 'contrastStrongLightColors': '500 600 700' 6095 }, 6096 'green': { 6097 '50': '#e8f5e9', 6098 '100': '#c8e6c9', 6099 '200': '#a5d6a7', 6100 '300': '#81c784', 6101 '400': '#66bb6a', 6102 '500': '#4caf50', 6103 '600': '#43a047', 6104 '700': '#388e3c', 6105 '800': '#2e7d32', 6106 '900': '#1b5e20', 6107 'A100': '#b9f6ca', 6108 'A200': '#69f0ae', 6109 'A400': '#00e676', 6110 'A700': '#00c853', 6111 'contrastDefaultColor': 'dark', 6112 'contrastLightColors': '500 600 700 800 900', 6113 'contrastStrongLightColors': '500 600 700' 6114 }, 6115 'light-green': { 6116 '50': '#f1f8e9', 6117 '100': '#dcedc8', 6118 '200': '#c5e1a5', 6119 '300': '#aed581', 6120 '400': '#9ccc65', 6121 '500': '#8bc34a', 6122 '600': '#7cb342', 6123 '700': '#689f38', 6124 '800': '#558b2f', 6125 '900': '#33691e', 6126 'A100': '#ccff90', 6127 'A200': '#b2ff59', 6128 'A400': '#76ff03', 6129 'A700': '#64dd17', 6130 'contrastDefaultColor': 'dark', 6131 'contrastLightColors': '700 800 900', 6132 'contrastStrongLightColors': '700 800 900' 6133 }, 6134 'lime': { 6135 '50': '#f9fbe7', 6136 '100': '#f0f4c3', 6137 '200': '#e6ee9c', 6138 '300': '#dce775', 6139 '400': '#d4e157', 6140 '500': '#cddc39', 6141 '600': '#c0ca33', 6142 '700': '#afb42b', 6143 '800': '#9e9d24', 6144 '900': '#827717', 6145 'A100': '#f4ff81', 6146 'A200': '#eeff41', 6147 'A400': '#c6ff00', 6148 'A700': '#aeea00', 6149 'contrastDefaultColor': 'dark', 6150 'contrastLightColors': '900', 6151 'contrastStrongLightColors': '900' 6152 }, 6153 'yellow': { 6154 '50': '#fffde7', 6155 '100': '#fff9c4', 6156 '200': '#fff59d', 6157 '300': '#fff176', 6158 '400': '#ffee58', 6159 '500': '#ffeb3b', 6160 '600': '#fdd835', 6161 '700': '#fbc02d', 6162 '800': '#f9a825', 6163 '900': '#f57f17', 6164 'A100': '#ffff8d', 6165 'A200': '#ffff00', 6166 'A400': '#ffea00', 6167 'A700': '#ffd600', 6168 'contrastDefaultColor': 'dark' 6169 }, 6170 'amber': { 6171 '50': '#fff8e1', 6172 '100': '#ffecb3', 6173 '200': '#ffe082', 6174 '300': '#ffd54f', 6175 '400': '#ffca28', 6176 '500': '#ffc107', 6177 '600': '#ffb300', 6178 '700': '#ffa000', 6179 '800': '#ff8f00', 6180 '900': '#ff6f00', 6181 'A100': '#ffe57f', 6182 'A200': '#ffd740', 6183 'A400': '#ffc400', 6184 'A700': '#ffab00', 6185 'contrastDefaultColor': 'dark' 6186 }, 6187 'orange': { 6188 '50': '#fff3e0', 6189 '100': '#ffe0b2', 6190 '200': '#ffcc80', 6191 '300': '#ffb74d', 6192 '400': '#ffa726', 6193 '500': '#ff9800', 6194 '600': '#fb8c00', 6195 '700': '#f57c00', 6196 '800': '#ef6c00', 6197 '900': '#e65100', 6198 'A100': '#ffd180', 6199 'A200': '#ffab40', 6200 'A400': '#ff9100', 6201 'A700': '#ff6d00', 6202 'contrastDefaultColor': 'dark', 6203 'contrastLightColors': '800 900', 6204 'contrastStrongLightColors': '800 900' 6205 }, 6206 'deep-orange': { 6207 '50': '#fbe9e7', 6208 '100': '#ffccbc', 6209 '200': '#ffab91', 6210 '300': '#ff8a65', 6211 '400': '#ff7043', 6212 '500': '#ff5722', 6213 '600': '#f4511e', 6214 '700': '#e64a19', 6215 '800': '#d84315', 6216 '900': '#bf360c', 6217 'A100': '#ff9e80', 6218 'A200': '#ff6e40', 6219 'A400': '#ff3d00', 6220 'A700': '#dd2c00', 6221 'contrastDefaultColor': 'light', 6222 'contrastDarkColors': '50 100 200 300 400 A100 A200', 6223 'contrastStrongLightColors': '500 600 700 800 900 A400 A700' 6224 }, 6225 'brown': { 6226 '50': '#efebe9', 6227 '100': '#d7ccc8', 6228 '200': '#bcaaa4', 6229 '300': '#a1887f', 6230 '400': '#8d6e63', 6231 '500': '#795548', 6232 '600': '#6d4c41', 6233 '700': '#5d4037', 6234 '800': '#4e342e', 6235 '900': '#3e2723', 6236 'A100': '#d7ccc8', 6237 'A200': '#bcaaa4', 6238 'A400': '#8d6e63', 6239 'A700': '#5d4037', 6240 'contrastDefaultColor': 'light', 6241 'contrastDarkColors': '50 100 200 A100 A200', 6242 'contrastStrongLightColors': '300 400' 6243 }, 6244 'grey': { 6245 '50': '#fafafa', 6246 '100': '#f5f5f5', 6247 '200': '#eeeeee', 6248 '300': '#e0e0e0', 6249 '400': '#bdbdbd', 6250 '500': '#9e9e9e', 6251 '600': '#757575', 6252 '700': '#616161', 6253 '800': '#424242', 6254 '900': '#212121', 6255 'A100': '#ffffff', 6256 'A200': '#000000', 6257 'A400': '#303030', 6258 'A700': '#616161', 6259 'contrastDefaultColor': 'dark', 6260 'contrastLightColors': '600 700 800 900 A200 A400 A700' 6261 }, 6262 'blue-grey': { 6263 '50': '#eceff1', 6264 '100': '#cfd8dc', 6265 '200': '#b0bec5', 6266 '300': '#90a4ae', 6267 '400': '#78909c', 6268 '500': '#607d8b', 6269 '600': '#546e7a', 6270 '700': '#455a64', 6271 '800': '#37474f', 6272 '900': '#263238', 6273 'A100': '#cfd8dc', 6274 'A200': '#b0bec5', 6275 'A400': '#78909c', 6276 'A700': '#455a64', 6277 'contrastDefaultColor': 'light', 6278 'contrastDarkColors': '50 100 200 300 A100 A200', 6279 'contrastStrongLightColors': '400 500 700' 6280 } 6281}); 6282 6283})(); 6284(function(){ 6285"use strict"; 6286 6287(function(angular) { 6288 'use strict'; 6289/** 6290 * @ngdoc module 6291 * @name material.core.theming 6292 * @description 6293 * Theming 6294 */ 6295detectDisabledThemes.$inject = ["$mdThemingProvider"]; 6296ThemingDirective.$inject = ["$mdTheming", "$interpolate", "$parse", "$mdUtil", "$q", "$log"]; 6297ThemableDirective.$inject = ["$mdTheming"]; 6298ThemingProvider.$inject = ["$mdColorPalette", "$$mdMetaProvider"]; 6299generateAllThemes.$inject = ["$injector", "$mdTheming"]; 6300angular.module('material.core.theming', ['material.core.theming.palette', 'material.core.meta']) 6301 .directive('mdTheme', ThemingDirective) 6302 .directive('mdThemable', ThemableDirective) 6303 .directive('mdThemesDisabled', disableThemesDirective ) 6304 .provider('$mdTheming', ThemingProvider) 6305 .config( detectDisabledThemes ) 6306 .run(generateAllThemes); 6307 6308/** 6309 * Detect if the HTML or the BODY tags has a [md-themes-disabled] attribute 6310 * If yes, then immediately disable all theme stylesheet generation and DOM injection 6311 */ 6312/** 6313 * @ngInject 6314 */ 6315function detectDisabledThemes($mdThemingProvider) { 6316 var isDisabled = !!document.querySelector('[md-themes-disabled]'); 6317 $mdThemingProvider.disableTheming(isDisabled); 6318} 6319 6320/** 6321 * @ngdoc service 6322 * @name $mdThemingProvider 6323 * @module material.core.theming 6324 * 6325 * @description Provider to configure the `$mdTheming` service. 6326 * 6327 * ### Default Theme 6328 * The `$mdThemingProvider` uses by default the following theme configuration: 6329 * 6330 * - Primary Palette: `Blue` 6331 * - Accent Palette: `Pink` 6332 * - Warn Palette: `Deep-Orange`
6333 * - Background Palette: `Grey` 6334 * 6335 * If you don't want to use the `md-theme` directive on the elements itself, you may want to overwrite 6336 * the default theme.<br/> 6337 * This can be done by using the following markup. 6338 * 6339 * <hljs lang="js"> 6340 * myAppModule.config(function($mdThemingProvider) { 6341 * $mdThemingProvider 6342 * .theme('default') 6343 * .primaryPalette('blue') 6344 * .accentPalette('teal') 6345 * .warnPalette('red') 6346 * .backgroundPalette('grey'); 6347 * }); 6348 * </hljs> 6349 * 6350 6351 * ### Dynamic Themes 6352 * 6353 * By default, if you change a theme at runtime, the `$mdTheming` service will not detect those changes.<br/> 6354 * If you have an application, which changes its theme on runtime, you have to enable theme watching. 6355 * 6356 * <hljs lang="js"> 6357 * myAppModule.config(function($mdThemingProvider) { 6358 * // Enable theme watching. 6359 * $mdThemingProvider.alwaysWatchTheme(true); 6360 * }); 6361 * </hljs> 6362 * 6363 * ### Custom Theme Styles 6364 * 6365 * Sometimes you may want to use your own theme styles for some custom components.<br/> 6366 * You are able to register your own styles by using the following markup. 6367 * 6368 * <hljs lang="js"> 6369 * myAppModule.config(function($mdThemingProvider) { 6370 * // Register our custom stylesheet into the theming provider. 6371 * $mdThemingProvider.registerStyles(STYLESHEET); 6372 * }); 6373 * </hljs> 6374 * 6375 * The `registerStyles` method only accepts strings as value, so you're actually not able to load an external 6376 * stylesheet file into the `$mdThemingProvider`. 6377 * 6378 * If it's necessary to load an external stylesheet, we suggest using a bundler, which supports including raw content, 6379 * like [raw-loader](https://github.com/webpack/raw-loader) for `webpack`. 6380 * 6381 * <hljs lang="js"> 6382 * myAppModule.config(function($mdThemingProvider) { 6383 * // Register your custom stylesheet into the theming provider. 6384 * $mdThemingProvider.registerStyles(require('../styles/my-component.theme.css')); 6385 * }); 6386 * </hljs> 6387 * 6388 * ### Browser color 6389 * 6390 * Enables browser header coloring 6391 * for more info please visit: 6392 * https://developers.google.com/web/fundamentals/design-and-ui/browser-customization/theme-color 6393 * 6394 * Options parameter: <br/> 6395 * `theme` - A defined theme via `$mdThemeProvider` to use the palettes from. Default is `default` theme. <br/> 6396 * `palette` - Can be any one of the basic material design palettes, extended defined palettes and 'primary', 6397 * 'accent', 'background' and 'warn'. Default is `primary`. <br/> 6398 * `hue` - The hue from the selected palette. Default is `800`<br/> 6399 * 6400 * <hljs lang="js"> 6401 * myAppModule.config(function($mdThemingProvider) { 6402 * // Enable browser color 6403 * $mdThemingProvider.enableBrowserColor({ 6404 * theme: 'myTheme', // Default is 'default' 6405 * palette: 'accent', // Default is 'primary', any basic material palette and extended palettes are available 6406 * hue: '200' // Default is '800' 6407 * }); 6408 * }); 6409 * </hljs> 6410 */ 6411 6412/** 6413 * @ngdoc method 6414 * @name $mdThemingProvider#registerStyles 6415 * @param {string} styles The styles to be appended to AngularJS Material's built in theme css. 6416 */ 6417/** 6418 * @ngdoc method 6419 * @name $mdThemingProvider#setNonce 6420 * @param {string} nonceValue The nonce to be added as an attribute to the theme style tags. 6421 * Setting a value allows the use of CSP policy without using the unsafe-inline directive. 6422 */ 6423 6424/** 6425 * @ngdoc method 6426 * @name $mdThemingProvider#setDefaultTheme 6427 * @param {string} themeName Default theme name to be applied to elements. Default value is `default`. 6428 */ 6429 6430/** 6431 * @ngdoc method 6432 * @name $mdThemingProvider#alwaysWatchTheme 6433 * @param {boolean} watch Whether or not to always watch themes for changes and re-apply 6434 * classes when they change. Default is `false`. Enabling can reduce performance. 6435 */ 6436 6437/** 6438 * @ngdoc method 6439 * @name $mdThemingProvider#enableBrowserColor 6440 * @param {Object=} options Options object for the browser color<br/> 6441 * `theme` - A defined theme via `$mdThemeProvider` to use the palettes from. Default is `default` theme. <br/> 6442 * `palette` - Can be any one of the basic material design palettes, extended defined palettes and 'primary', 6443 * 'accent', 'background' and 'warn'. Default is `primary`. <br/> 6444 * `hue` - The hue from the selected palette. Default is `800`<br/> 6445 * @returns {Function} remove function of the browser color 6446 */ 6447 6448/* Some Example Valid Theming Expressions 6449 * ======================================= 6450 * 6451 * Intention group expansion: (valid for primary, accent, warn, background) 6452 * 6453 * {{primary-100}} - grab shade 100 from the primary palette 6454 * {{primary-100-0.7}} - grab shade 100, apply opacity of 0.7 6455 * {{primary-100-contrast}} - grab shade 100's contrast color 6456 * {{primary-hue-1}} - grab the shade assigned to hue-1 from the primary palette 6457 * {{primary-hue-1-0.7}} - apply 0.7 opacity to primary-hue-1 6458 * {{primary-color}} - Generates .md-hue-1, .md-hue-2, .md-hue-3 with configured shades set for each hue 6459 * {{primary-color-0.7}} - Apply 0.7 opacity to each of the above rules 6460 * {{primary-contrast}} - Generates .md-hue-1, .md-hue-2, .md-hue-3 with configured contrast (ie. text) color shades set for each hue 6461 * {{primary-contrast-0.7}} - Apply 0.7 opacity to each of the above rules 6462 * 6463 * Foreground expansion: Applies rgba to black/white foreground text 6464 * 6465 * {{foreground-1}} - used for primary text 6466 * {{foreground-2}} - used for secondary text/divider 6467 * {{foreground-3}} - used for disabled text 6468 * {{foreground-4}}
vendor: 9,814 bytes, lines 6468-6776
6468 - used for dividers 6469 * 6470 */ 6471 6472// In memory generated CSS rules; registered by theme.name 6473var GENERATED = { }; 6474 6475// In memory storage of defined themes and color palettes (both loaded by CSS, and user specified) 6476var PALETTES; 6477 6478// Text Colors on light and dark backgrounds 6479// @see https://www.google.com/design/spec/style/color.html#color-text-background-colors 6480var DARK_FOREGROUND = { 6481 name: 'dark', 6482 '1': 'rgba(0,0,0,0.87)', 6483 '2': 'rgba(0,0,0,0.54)', 6484 '3': 'rgba(0,0,0,0.38)', 6485 '4': 'rgba(0,0,0,0.12)' 6486}; 6487var LIGHT_FOREGROUND = { 6488 name: 'light', 6489 '1': 'rgba(255,255,255,1.0)', 6490 '2': 'rgba(255,255,255,0.7)', 6491 '3': 'rgba(255,255,255,0.5)', 6492 '4': 'rgba(255,255,255,0.12)' 6493}; 6494 6495var DARK_SHADOW = '1px 1px 0px rgba(0,0,0,0.4), -1px -1px 0px rgba(0,0,0,0.4)'; 6496var LIGHT_SHADOW = ''; 6497 6498var DARK_CONTRAST_COLOR = colorToRgbaArray('rgba(0,0,0,0.87)'); 6499var LIGHT_CONTRAST_COLOR = colorToRgbaArray('rgba(255,255,255,0.87)'); 6500var STRONG_LIGHT_CONTRAST_COLOR = colorToRgbaArray('rgb(255,255,255)'); 6501 6502var THEME_COLOR_TYPES = ['primary', 'accent', 'warn', 'background']; 6503var DEFAULT_COLOR_TYPE = 'primary'; 6504 6505// A color in a theme will use these hues by default, if not specified by user. 6506var LIGHT_DEFAULT_HUES = { 6507 'accent': { 6508 'default': 'A200', 6509 'hue-1': 'A100', 6510 'hue-2': 'A400', 6511 'hue-3': 'A700' 6512 }, 6513 'background': { 6514 'default': '50', 6515 'hue-1': 'A100', 6516 'hue-2': '100', 6517 'hue-3': '300' 6518 } 6519}; 6520 6521var DARK_DEFAULT_HUES = { 6522 'background': { 6523 'default': 'A400', 6524 'hue-1': '800', 6525 'hue-2': '900', 6526 'hue-3': 'A200' 6527 } 6528}; 6529THEME_COLOR_TYPES.forEach(function(colorType) { 6530 // Color types with unspecified default hues will use these default hue values 6531 var defaultDefaultHues = { 6532 'default': '500', 6533 'hue-1': '300', 6534 'hue-2': '800', 6535 'hue-3': 'A100' 6536 }; 6537 if (!LIGHT_DEFAULT_HUES[colorType]) LIGHT_DEFAULT_HUES[colorType] = defaultDefaultHues; 6538 if (!DARK_DEFAULT_HUES[colorType]) DARK_DEFAULT_HUES[colorType] = defaultDefaultHues; 6539}); 6540 6541var VALID_HUE_VALUES = [ 6542 '50', '100', '200', '300', '400', '500', '600', 6543 '700', '800', '900', 'A100', 'A200', 'A400', 'A700' 6544]; 6545 6546var themeConfig = { 6547 disableTheming : false, // Generate our themes at run time; also disable stylesheet DOM injection 6548 generateOnDemand : false, // Whether or not themes are to be generated on-demand (vs. eagerly). 6549 registeredStyles : [], // Custom styles registered to be used in the theming of custom components. 6550 nonce : null // Nonce to be added as an attribute to the generated themes style tags. 6551}; 6552 6553/** 6554 * 6555 */ 6556function ThemingProvider($mdColorPalette, $$mdMetaProvider) { 6557 ThemingService.$inject = ["$rootScope", "$mdUtil", "$q", "$log"]; 6558 PALETTES = { }; 6559 var THEMES = { }; 6560 6561 var themingProvider; 6562 6563 var alwaysWatchTheme = false; 6564 var defaultTheme = 'default'; 6565 6566 // Load JS Defined Palettes 6567 angular.extend(PALETTES, $mdColorPalette); 6568 6569 // Default theme defined in core.js 6570 6571 /** 6572 * Adds `theme-color` and `msapplication-navbutton-color` meta tags with the color parameter 6573 * @param {string} color Hex value of the wanted browser color 6574 * @returns {Function} Remove function of the meta tags 6575 */ 6576 var setBrowserColor = function (color) { 6577 // Chrome, Firefox OS and Opera 6578 var removeChrome = $$mdMetaProvider.setMeta('theme-color', color); 6579 // Windows Phone 6580 var removeWindows = $$mdMetaProvider.setMeta('msapplication-navbutton-color', color); 6581 6582 return function () { 6583 removeChrome(); 6584 removeWindows(); 6585 }; 6586 }; 6587 6588 /** 6589 * Enables browser header coloring 6590 * for more info please visit: 6591 * https://developers.google.com/web/fundamentals/design-and-ui/browser-customization/theme-color 6592 * 6593 * The default color is `800` from `primary` palette of the `default` theme 6594 * 6595 * options are: 6596 * `theme` - A defined theme via `$mdThemeProvider` to use the palettes from. Default is `default` theme 6597 * `palette` - Can be any one of the basic material design palettes, extended defined palettes and 'primary', 6598 * 'accent', 'background' and 'warn'. Default is `primary` 6599 * `hue` - The hue from the selected palette. Default is `800` 6600 * 6601 * @param {Object=} options Options object for the browser color 6602 * @returns {Function} remove function of the browser color 6603 */ 6604 var enableBrowserColor = function (options) { 6605 options = angular.isObject(options) ? options : {}; 6606 6607 var theme = options.theme || 'default'; 6608 var hue = options.hue || '800'; 6609 6610 var palette = PALETTES[options.palette] || 6611 PALETTES[THEMES[theme].colors[options.palette || 'primary'].name]; 6612 6613 var color = angular.isObject(palette[hue]) ? palette[hue].hex : palette[hue]; 6614 6615 return setBrowserColor(color); 6616 }; 6617 6618 return themingProvider = { 6619 definePalette: definePalette, 6620 extendPalette: extendPalette, 6621 theme: registerTheme, 6622 6623 /** 6624 * return a read-only clone of the current theme configuration 6625 */ 6626 configuration : function() { 6627 return angular.extend( { }, themeConfig, { 6628 defaultTheme : defaultTheme, 6629 alwaysWatchTheme : alwaysWatchTheme, 6630 registeredStyles : [].concat(themeConfig.registeredStyles) 6631 }); 6632 }, 6633 6634 /** 6635 * Easy way to disable theming without having to use 6636 * `.constant("$MD_THEME_CSS","");` This disables 6637 * all dynamic theme style sheet generations and injections... 6638 */ 6639 disableTheming: function(isDisabled) { 6640 themeConfig.disableTheming = angular.isUndefined(isDisabled) || !!isDisabled; 6641 }, 6642 6643 registerStyles: function(styles) { 6644 themeConfig.registeredStyles.push(styles); 6645 }, 6646 6647 setNonce: function(nonceValue) { 6648 themeConfig.nonce = nonceValue; 6649 }, 6650 6651 generateThemesOnDemand: function(onDemand) { 6652 themeConfig.generateOnDemand = onDemand; 6653 }, 6654 6655 setDefaultTheme: function(theme) { 6656 defaultTheme = theme; 6657 }, 6658 6659 alwaysWatchTheme: function(alwaysWatch) { 6660 alwaysWatchTheme = alwaysWatch; 6661 }, 6662 6663 enableBrowserColor: enableBrowserColor, 6664 6665 $get: ThemingService, 6666 _LIGHT_DEFAULT_HUES: LIGHT_DEFAULT_HUES, 6667 _DARK_DEFAULT_HUES: DARK_DEFAULT_HUES, 6668 _PALETTES: PALETTES, 6669 _THEMES: THEMES, 6670 _parseRules: parseRules, 6671 _rgba: rgba 6672 }; 6673 6674 // Example: $mdThemingProvider.definePalette('neonRed', { 50: '#f5fafa', ... }); 6675 function definePalette(name, map) { 6676 map = map || {}; 6677 PALETTES[name] = checkPaletteValid(name, map); 6678 return themingProvider; 6679 } 6680 6681 // Returns an new object which is a copy of a given palette `name` with variables from 6682 // `map` overwritten 6683 // Example: var neonRedMap = $mdThemingProvider.extendPalette('red', { 50: '#f5fafafa' }); 6684 function extendPalette(name, map) { 6685 return checkPaletteValid(name, angular.extend({}, PALETTES[name] || {}, map) ); 6686 } 6687 6688 // Make sure that palette has all required hues 6689 function checkPaletteValid(name, map) { 6690 var missingColors = VALID_HUE_VALUES.filter(function(field) { 6691 return !map[field]; 6692 }); 6693 if (missingColors.length) { 6694 throw new Error("Missing colors %1 in palette %2!" 6695 .replace('%1', missingColors.join(', ')) 6696 .replace('%2', name)); 6697 } 6698 6699 return map; 6700 } 6701 6702 // Register a theme (which is a collection of color palettes to use with various states 6703 // ie. warn, accent, primary ) 6704 // Optionally inherit from an existing theme 6705 // $mdThemingProvider.theme('custom-theme').primaryPalette('red'); 6706 function registerTheme(name, inheritFrom) { 6707 if (THEMES[name]) return THEMES[name]; 6708 6709 inheritFrom = inheritFrom || 'default'; 6710 6711 var parentTheme = typeof inheritFrom === 'string' ? THEMES[inheritFrom] : inheritFrom; 6712 var theme = new Theme(name); 6713 6714 if (parentTheme) { 6715 angular.forEach(parentTheme.colors, function(color, colorType) { 6716 theme.colors[colorType] = { 6717 name: color.name, 6718 // Make sure a COPY of the hues is given to the child color, 6719 // not the same reference. 6720 hues: angular.extend({}, color.hues) 6721 }; 6722 }); 6723 } 6724 THEMES[name] = theme; 6725 6726 return theme; 6727 } 6728 6729 function Theme(name) { 6730 var self = this; 6731 self.name = name; 6732 self.colors = {}; 6733 6734 self.dark = setDark; 6735 setDark(false); 6736 6737 function setDark(isDark) { 6738 isDark = arguments.length === 0 ? true : !!isDark; 6739 6740 // If no change, abort 6741 if (isDark === self.isDark) return; 6742 6743 self.isDark = isDark; 6744 6745 self.foregroundPalette = self.isDark ? LIGHT_FOREGROUND : DARK_FOREGROUND; 6746 self.foregroundShadow = self.isDark ? DARK_SHADOW : LIGHT_SHADOW; 6747 6748 // Light and dark themes have different default hues. 6749 // Go through each existing color type for this theme, and for every 6750 // hue value that is still the default hue value from the previous light/dark setting, 6751 // set it to the default hue value from the new light/dark setting. 6752 var newDefaultHues = self.isDark ? DARK_DEFAULT_HUES : LIGHT_DEFAULT_HUES; 6753 var oldDefaultHues = self.isDark ? LIGHT_DEFAULT_HUES : DARK_DEFAULT_HUES; 6754 angular.forEach(newDefaultHues, function(newDefaults, colorType) { 6755 var color = self.colors[colorType]; 6756 var oldDefaults = oldDefaultHues[colorType]; 6757 if (color) { 6758 for (var hueName in color.hues) { 6759 if (color.hues[hueName] === oldDefaults[hueName]) { 6760 color.hues[hueName] = newDefaults[hueName]; 6761 } 6762 } 6763 } 6764 }); 6765 6766 return self; 6767 } 6768 6769 THEME_COLOR_TYPES.forEach(function(colorType) { 6770 var defaultHues = (self.isDark ? DARK_DEFAULT_HUES : LIGHT_DEFAULT_HUES)[colorType]; 6771 self[colorType + 'Palette'] = function setPaletteType(paletteName, hues) { 6772 var color = self.colors[colorType] = { 6773 name: paletteName, 6774 hues: angular.extend({}, defaultHues, hues) 6775 }; 6776
6777 Object.keys(color.hues).forEach(function(name) { 6778 if (!defaultHues[name]) { 6779 throw new Error("Invalid hue name '%1' in theme %2's %3 color %4. Available hue names: %4" 6780 .replace('%1', name) 6781 .replace('%2', self.name) 6782 .replace('%3', paletteName) 6783 .replace('%4', Object.keys(defaultHues).join(', ')) 6784 ); 6785 } 6786 }); 6787 Object.keys(color.hues).map(function(key) { 6788 return color.hues[key]; 6789 }).forEach(function(hueValue) { 6790 if (VALID_HUE_VALUES.indexOf(hueValue) == -1) { 6791 throw new Error("Invalid hue value '%1' in theme %2's %3 color %4. Available hue values: %5" 6792 .replace('%1', hueValue) 6793 .replace('%2', self.name) 6794 .replace('%3', colorType) 6795 .replace('%4', paletteName) 6796 .replace('%5', VALID_HUE_VALUES.join(', ')) 6797 ); 6798 } 6799 }); 6800 return self; 6801 }; 6802 6803 self[colorType + 'Color'] = function() { 6804 var args = Array.prototype.slice.call(arguments); 6805 console.warn('$mdThemingProviderTheme.' + colorType + 'Color() has been deprecated. ' + 6806 'Use $mdThemingProviderTheme.' + colorType + 'Palette() instead.'); 6807 return self[colorType + 'Palette'].apply(self, args); 6808 }; 6809 }); 6810 } 6811 6812 /** 6813 * @ngdoc service 6814 * @name $mdTheming 6815 * @module material.core.theming 6816 * 6817 * @description 6818 * 6819 * Service that makes an element apply theming related <b>classes</b> to itself. 6820 * 6821 * <hljs lang="js"> 6822 * app.directive('myFancyDirective', function($mdTheming) { 6823 * return { 6824 * restrict: 'e', 6825 * link: function(scope, el, attrs) { 6826 * $mdTheming(el); 6827 * 6828 * $mdTheming.defineTheme('myTheme', { 6829 * primary: 'blue', 6830 * accent: 'pink', 6831 * dark: true 6832 * }) 6833 * } 6834 * }; 6835 * }); 6836 * </hljs> 6837 * @param {element=} element to apply theming to 6838 */ 6839 6840 /** 6841 * @ngdoc property 6842 * @name $mdTheming#THEMES 6843 * @description 6844 * Property to get all the themes defined 6845 * @returns {Object} All the themes defined with their properties 6846 */ 6847 6848 /** 6849 * @ngdoc property 6850 * @name $mdTheming#PALETTES 6851 * @description 6852 * Property to get all the palettes defined 6853 * @returns {Object} All the palettes defined with their colors 6854 */ 6855 6856 /** 6857 * @ngdoc method 6858 * @name $mdTheming#registered 6859 * @description 6860 * Determine is specified theme name is a valid, registered theme 6861 * @param {string} themeName the theme to check if registered 6862 * @returns {boolean} whether the theme is registered or not 6863 */ 6864 6865 /** 6866 * @ngdoc method 6867 * @name $mdTheming#defaultTheme 6868 * @description 6869 * Returns the default theme 6870 * @returns {string} The default theme 6871 */ 6872 6873 /** 6874 * @ngdoc method 6875 * @name $mdTheming#generateTheme 6876 * @description 6877 * Lazy generate themes - by default, every theme is generated
6877when defined. 6878 * You can disable this in the configuration section using the 6879 * `$mdThemingProvider.generateThemesOnDemand(true);` 6880 * 6881 * The theme name that is passed in must match the name of the theme that was defined as part of the configuration block. 6882 * 6883 * @param name {string} theme name to generate 6884 */ 6885 6886 /** 6887 * @ngdoc method 6888 * @name $mdTheming#setBrowserColor 6889 * @description 6890 * Sets browser header coloring 6891 * for more info please visit: 6892 * https://developers.google.com/web/fundamentals/design-and-ui/browser-customization/theme-color 6893 * 6894 * The default color is `800` from `primary` palette of the `default` theme 6895 * 6896 * options are:<br/> 6897 * `theme` - A defined theme via `$mdThemeProvider` to use the palettes from. Default is `default` theme.<br/> 6898 * `palette` - Can be any one of the basic material design palettes, extended defined palettes and 'primary', 6899 * 'accent', 'background' and 'warn'. Default is `primary`<br/> 6900 * `hue` - The hue from the selected palette. Default is `800` 6901 * 6902 * @param {Object} options Options object for the browser color 6903 * @returns {Function} remove function of the browser color 6904 */ 6905 6906 /** 6907 * @ngdoc method 6908 * @name $mdTheming#defineTheme 6909 * @description 6910 * Dynamically define a theme by an options object 6911 * 6912 * options are:<br/> 6913 * `primary` - The primary palette of the theme.<br/> 6914 * `accent` - The accent palette of the theme.<br/> 6915 * `warn` - The warn palette of the theme.<br/> 6916 * `background` - The background palette of the theme.<br/> 6917 * `dark` - Indicates if it's a dark theme.<br/> 6918 * 6919 * @param {String} name Theme name to define 6920 * @param {Object} options Theme definition options 6921 * @returns {Promise<string>} A resolved promise with the theme name 6922 */ 6923 6924 /* @ngInject */ 6925 function ThemingService($rootScope, $mdUtil, $q, $log) { 6926 // Allow us to be invoked via a linking function signature. 6927 var applyTheme = function (scope, el) { 6928 if (el === undefined) { el = scope; scope = undefined; } 6929 if (scope === undefined) { scope = $rootScope; } 6930 applyTheme.inherit(el, el); 6931 }; 6932 6933 Object.defineProperty(applyTheme, 'THEMES', { 6934 get: function () { 6935 return angular.extend({}, THEMES); 6936 } 6937 }); 6938 Object.defineProperty(applyTheme, 'PALETTES', { 6939 get: function () { 6940 return angular.extend({}, PALETTES); 6941 } 6942 }); 6943 Object.defineProperty(applyTheme, 'ALWAYS_WATCH', { 6944 get: function () { 6945 return alwaysWatchTheme; 6946 } 6947 }); 6948 applyTheme.inherit = inheritTheme; 6949 applyTheme.registered = registered; 6950 applyTheme.defaultTheme = function() { return defaultTheme; }; 6951 applyTheme.generateTheme = function(name) { generateTheme(THEMES[name], name, themeConfig.nonce); }; 6952 applyTheme.defineTheme = function(name, options) { 6953 options = options || {}; 6954 6955 var theme = registerTheme(name); 6956 6957 if (options.primary) { 6958 theme.primaryPalette(options.primary); 6959 } 6960 if (options.accent) { 6961 theme.accentPalette(options.accent); 6962 } 6963 if (options.warn) { 6964 theme.warnPalette(options.warn); 6965 } 6966 if (options.background) { 6967 theme.backgroundPalette(options.background); 6968 } 6969 if (options.dark){ 6970 theme.dark(); 6971 } 6972 6973 this.generateTheme(name); 6974 6975 return $q.resolve(name); 6976 }; 6977 applyTheme.setBrowserColor = enableBrowserColor; 6978 6979 return applyTheme; 6980 6981 /** 6982 * Determine is specified theme name is a valid, registered theme 6983 */ 6984 function registered(themeName) { 6985 if (themeName === undefined || themeName === '') return true; 6986 return applyTheme.THEMES[themeName] !== undefined; 6987 } 6988 6989 /** 6990 * Get theme name for the element, then update with Theme CSS class 6991 */ 6992 function inheritTheme (el, parent) { 6993 var ctrl = parent.controller('mdTheme') || el.data('$mdThemeController'); 6994 6995 updateThemeClass(lookupThemeName()); 6996 6997 if (ctrl) { 6998 var watchTheme = alwaysWatchTheme || 6999 ctrl.$shouldWatch || 7000 $mdUtil.parseAttributeBoolean(el.attr('md-theme-watch')); 7001 7002 var unwatch = ctrl.registerChanges(function (name) { 7003 updateThemeClass(name); 7004 7005 if (!watchTheme) { 7006 unwatch(); 7007 } 7008 else { 7009 el.on('$destroy', unwatch); 7010 } 7011 }); 7012 } 7013 7014 /** 7015 * Find the theme name from the parent controller or element data 7016 */ 7017 function lookupThemeName() { 7018 // As a few components (dialog) add their controllers later, we should also watch for a controller init. 7019 return ctrl && ctrl.$mdTheme || (defaultTheme == 'default' ? '' : defaultTheme); 7020 } 7021 7022 /** 7023 * Remove old theme class and apply a new one 7024 * NOTE: if not a valid theme name, then the current name is not changed 7025 */ 7026 function updateThemeClass(theme) { 7027 if (!theme) return; 7028 if (!registered(theme)) { 7029 $log.warn('Attempted to use unregistered theme \'' + theme + '\'. ' + 7030 'Register it with $mdThemingProvider.theme().'); 7031 } 7032 7033 var oldTheme = el.data('$mdThemeName'); 7034 if (oldTheme) el.removeClass('md-' + oldTheme +'-theme'); 7035 el.addClass('md-' + theme + '-theme'); 7036 el.data('$mdThemeName', theme); 7037 if (ctrl) { 7038 el.data('$mdThemeController', ctrl); 7039 } 7040 } 7041 } 7042 7043 } 7044} 7045 7046function ThemingDirective($mdTheming, $interpolate, $parse, $mdUtil, $q, $log) { 7047 return { 7048 priority: 101, // has to be more than 100 to be before interpolation (issue on IE) 7049 link: { 7050 pre: function(scope, el, attrs) { 7051 var registeredCallbacks = []; 7052 7053 var startSymbol = $interpolate.startSymbol(); 7054 var endSymbol = $interpolate.endSymbol(); 7055 7056 var theme = attrs.mdTheme.trim(); 7057 7058 var hasInterpolation = 7059 theme.substr(0, startSymbol.length) === startSymbol &&
7060 theme.lastIndexOf(endSymbol) === theme.length - endSymbol.length; 7061 7062 var oneTimeOperator = '::'; 7063 var oneTimeBind = attrs.mdTheme 7064 .split(startSymbol).join('') 7065 .split(endSymbol).join('') 7066 .trim() 7067 .substr(0, oneTimeOperator.length) === oneTimeOperator; 7068 7069 var ctrl = { 7070 registerChanges: function (cb, context) { 7071 if (context) { 7072 cb = angular.bind(context, cb); 7073 } 7074 7075 registeredCallbacks.push(cb); 7076 7077 return function () { 7078 var index = registeredCallbacks.indexOf(cb); 7079 7080 if (index > -1) { 7081 registeredCallbacks.splice(index, 1); 7082 } 7083 }; 7084 }, 7085 $setTheme: function (theme) { 7086 if (!$mdTheming.registered(theme)) { 7087 $log.warn('attempted to use unregistered theme \'' + theme + '\''); 7088 } 7089 7090 ctrl.$mdTheme = theme; 7091 7092 // Iterating backwards to support unregistering during iteration 7093 // http://stackoverflow.com/a/9882349/890293 7094 // we don't use `reverse()` of array because it mutates the array and we don't want it to get re-indexed 7095 for (var i = registeredCallbacks.length; i--;) { 7096 registeredCallbacks[i](theme); 7097 } 7098 }, 7099 $shouldWatch: $mdUtil.parseAttributeBoolean(el.attr('md-theme-watch')) || 7100 $mdTheming.ALWAYS_WATCH || 7101 (hasInterpolation && !oneTimeBind) 7102 }; 7103 7104 el.data('$mdThemeController', ctrl); 7105 7106 var getTheme = function () { 7107 var interpolation = $interpolate(attrs.mdTheme)(scope); 7108 return $parse(interpolation)(scope) || interpolation; 7109 }; 7110 7111 var setParsedTheme = function (theme) { 7112 if (typeof theme === 'string') { 7113 return ctrl.$setTheme(theme); 7114 } 7115 7116 $q.when( angular.isFunction(theme) ? theme() : theme ) 7117 .then(function(name){ 7118 ctrl.$setTheme(name); 7119 }); 7120 }; 7121 7122 setParsedTheme(getTheme()); 7123 7124 var unwatch = scope.$watch(getTheme, function(theme) { 7125 if (theme) { 7126 setParsedTheme(theme); 7127 7128 if (!ctrl.$shouldWatch) { 7129 unwatch(); 7130 } 7131 } 7132 }); 7133 } 7134 } 7135 }; 7136} 7137 7138/** 7139 * Special directive that will disable ALL runtime Theme style generation and DOM injection 7140 * 7141 * <link rel="stylesheet" href="angular-material.min.css"> 7142 * <link rel="stylesheet" href="angular-material.themes.css"> 7143 * 7144 * <body md-themes-disabled> 7145 * ... 7146 * </body> 7147 * 7148 * Note: Using md-themes-css directive requires the developer to load external 7149 * theme stylesheets; e.g. custom themes from Material-Tools: 7150 * 7151 * `angular-material.themes.css` 7152 * 7153 * Another option is to use the ThemingProvider to configure and disable the attribute 7154 * conversions; this would obviate the use of the `md-themes-css` directive 7155 * 7156 */ 7157function disableThemesDirective() { 7158 themeConfig.disableTheming = true; 7159 7160 // Return a 1x-only, first-match attribute directive 7161 return { 7162 restrict : 'A', 7163 priority : '900' 7164 }; 7165} 7166 7167function ThemableDirective($mdTheming) { 7168 return $mdTheming; 7169} 7170 7171function parseRules(theme, colorType, rules) { 7172 checkValidPalette(theme, colorType); 7173 7174 rules = rules.replace(/THEME_NAME/g, theme.name); 7175 var generatedRules = []; 7176 var color = theme.colors[colorType]; 7177 7178 var themeNameRegex = new RegExp('\\.md-' + theme.name + '-theme', 'g'); 7179 // Matches '{{ primary-color }}', etc 7180 var hueRegex = new RegExp('(\'|")?{{\\s*(' + colorType + ')-(color|contrast)-?(\\d\\.?\\d*)?\\s*}}(\"|\')?','g'); 7181 var simpleVariableRegex = /'?"?\{\{\s*([a-zA-Z]+)-(A?\d+|hue\-[0-3]|shadow|default)-?(\d\.?\d*)?(contrast)?\s*\}\}'?"?/g; 7182 var palette = PALETTES[color.name]; 7183 7184 // find and replace simple variables where we use a specific hue, not an entire palette 7185 // eg. "{{primary-100}}" 7186 //\(' + THEME_COLOR_TYPES.join('\|') + '\)' 7187 rules = rules.replace(simpleVariableRegex, function(match, colorType, hue, opacity, contrast) { 7188 if (colorType === 'foreground') { 7189 if (hue == 'shadow') { 7190 return theme.foregroundShadow; 7191 } else { 7192 return theme.foregroundPalette[hue] || theme.foregroundPalette['1']; 7193 } 7194 } 7195 7196 // `default` is also accepted as a hue-value, because the background palettes are 7197 // using it as a name for the default hue. 7198 if (hue.indexOf('hue') === 0 || hue === 'default') { 7199 hue = theme.colors[colorType].hues[hue]; 7200 } 7201 7202 return rgba( (PALETTES[ theme.colors[colorType].name ][hue] || '')[contrast ? 'contrast' : 'value'], opacity ); 7203 }); 7204 7205 // For each type, generate rules for each hue (ie. default, md-hue-1, md-hue-2, md-hue-3) 7206 angular.forEach(color.hues, function(hueValue, hueName) { 7207 var newRule = rules 7208 .replace(hueRegex, function(match, _, colorType, hueType, opacity) { 7209 return rgba(palette[hueValue][hueType === 'color' ? 'value' : 'contrast'], opacity); 7210 }); 7211 if (hueName !== 'default') { 7212 newRule = newRule.replace(themeNameRegex, '.md-' + theme.name + '-theme.md-' + hueName); 7213 } 7214 7215 // Don't apply a selector rule to the default theme, making it easier to override 7216 // styles of the base-component 7217 if (theme.name == 'default') { 7218 var themeRuleRegex = /((?:\s|>|\.|\w|-|:|\(|\)|\[|\]|"|'|=)*)\.md-default-theme((?:\s|>|\.|\w|-|:|\(|\)|\[|\]|"|'|=)*)/g; 7219 7220 newRule = newRule.replace(themeRuleRegex, function(match, start, end) { 7221 return match + ', ' + start + end; 7222 }); 7223 }
7224 generatedRules.push(newRule); 7225 }); 7226 7227 return generatedRules; 7228} 7229 7230var rulesByType = {}; 7231 7232// Generate our themes at run time given the state of THEMES and PALETTES 7233function generateAllThemes($injector, $mdTheming) { 7234 var head = document.head; 7235 var firstChild = head ? head.firstElementChild : null; 7236 var themeCss = !themeConfig.disableTheming && $injector.has('$MD_THEME_CSS') ? $injector.get('$MD_THEME_CSS') : ''; 7237 7238 // Append our custom registered styles to the theme stylesheet. 7239 themeCss += themeConfig.registeredStyles.join(''); 7240 7241 if ( !firstChild ) return; 7242 if (themeCss.length === 0) return; // no rules, so no point in running this expensive task 7243 7244 // Expose contrast colors for palettes to ensure that text is always readable 7245 angular.forEach(PALETTES, sanitizePalette); 7246 7247 // MD_THEME_CSS is a string generated by the build process that includes all the themable 7248 // components as templates 7249 7250 // Break the CSS into individual rules 7251 var rules = themeCss 7252 .split(/\}(?!(\}|'|"|;))/) 7253 .filter(function(rule) { return rule && rule.trim().length; }) 7254 .map(function(rule) { return rule.trim() + '}'; }); 7255 7256 7257 var ruleMatchRegex = new RegExp('md-(' + THEME_COLOR_TYPES.join('|') + ')', 'g'); 7258 7259 THEME_COLOR_TYPES.forEach(function(type) { 7260 rulesByType[type] = ''; 7261 }); 7262 7263 7264 // Sort the rules based on type, allowing us to do color substitution on a per-type basis
7265 rules.forEach(function(rule) { 7266 var match = rule.match(ruleMatchRegex); 7267 // First: test that if the rule has '.md-accent', it goes into the accent set of rules 7268 for (var i = 0, type; type = THEME_COLOR_TYPES[i]; i++) { 7269 if (rule.indexOf('.md-' + type) > -1) { 7270 return rulesByType[type] += rule; 7271 } 7272 } 7273 7274 // If no eg 'md-accent' class is found, try to just find 'accent' in the rule and guess from 7275 // there 7276 for (i = 0; type = THEME_COLOR_TYPES[i]; i++) { 7277 if (rule.indexOf(type) > -1) { 7278 return rulesByType[type] += rule; 7279 } 7280 } 7281 7282 // Default to the primary array 7283 return rulesByType[DEFAULT_COLOR_TYPE] += rule; 7284 }); 7285 7286 // If themes are being generated on-demand, quit here. The user will later manually 7287 // call generateTheme to do this on a theme-by-theme basis. 7288 if (themeConfig.generateOnDemand) return; 7289 7290 angular.forEach($mdTheming.THEMES, function(theme) { 7291 if (!GENERATED[theme.name] && !($mdTheming.defaultTheme() !== 'default' && theme.name === 'default')) { 7292 generateTheme(theme, theme.name, themeConfig.nonce); 7293 } 7294 }); 7295 7296 7297 // ************************* 7298 // Internal functions 7299 // ************************* 7300 7301 // The user specifies a 'default' contrast color as either light or dark, 7302 // then explicitly lists which hues are the opposite contrast (eg. A100 has dark, A200 has light) 7303 function sanitizePalette(palette, name) { 7304 var defaultContrast = palette.contrastDefaultColor; 7305 var lightColors = palette.contrastLightColors || []; 7306 var strongLightColors = palette.contrastStrongLightColors || []; 7307 var darkColors = palette.contrastDarkColors || []; 7308 7309 // These colors are provided as space-separated lists 7310 if (typeof lightColors === 'string') lightColors = lightColors.split(' '); 7311 if (typeof strongLightColors === 'string') strongLightColors = strongLightColors.split(' '); 7312 if (typeof darkColors === 'string') darkColors = darkColors.split(' '); 7313 7314 // Cleanup after ourselves 7315 delete palette.contrastDefaultColor; 7316 delete palette.contrastLightColors; 7317 delete palette.contrastStrongLightColors; 7318 delete palette.contrastDarkColors; 7319 7320 // Change { 'A100': '#fffeee' } to { 'A100': { value: '#fffeee', contrast:DARK_CONTRAST_COLOR } 7321 angular.forEach(palette, function(hueValue, hueName) { 7322 if (angular.isObject(hueValue)) return; // Already converted 7323 // Map everything to rgb colors 7324 var rgbValue = colorToRgbaArray(hueValue); 7325 if (!rgbValue) { 7326 throw new Error("Color %1, in palette %2's hue %3, is invalid. Hex or rgb(a) color expected." 7327 .replace('%1', hueValue) 7328 .replace('%2', palette.name) 7329 .replace('%3', hueName)); 7330 } 7331 7332 palette[hueName] = { 7333 hex: palette[hueName], 7334 value: rgbValue, 7335 contrast: getContrastColor() 7336 }; 7337 function getContrastColor() { 7338 if (defaultContrast === 'light') { 7339 if (darkColors.indexOf(hueName) > -1) { 7340 return DARK_CONTRAST_COLOR; 7341 } else { 7342 return strongLightColors.indexOf(hueName) > -1 ? STRONG_LIGHT_CONTRAST_COLOR 7343 : LIGHT_CONTRAST_COLOR; 7344 } 7345 } else { 7346 if (lightColors.indexOf(hueName) > -1) { 7347 return strongLightColors.indexOf(hueName) > -1 ? STRONG_LIGHT_CONTRAST_COLOR 7348 : LIGHT_CONTRAST_COLOR; 7349 } else { 7350 return DARK_CONTRAST_COLOR; 7351 } 7352 } 7353 } 7354 }); 7355 } 7356} 7357 7358function generateTheme(theme, name, nonce) { 7359 var head = document.head; 7360 var firstChild = head ? head.firstElementChild : null; 7361 7362 if (!GENERATED[name]) { 7363 // For each theme, use the color palettes specified for 7364 // `primary`, `warn` and `accent` to generate CSS rules. 7365 THEME_COLOR_TYPES.forEach(function(colorType) { 7366 var styleStrings = parseRules(theme, colorType, rulesByType[colorType]); 7367 while (styleStrings.length) { 7368 var styleContent = styleStrings.shift(); 7369 if (styleContent) { 7370 var style = document.createElement('style'); 7371 style.setAttribute('md-theme-style', ''); 7372 if (nonce) { 7373 style.setAttribute('nonce', nonce); 7374 } 7375 style.appendChild(document.createTextNode(styleContent)); 7376 head.insertBefore(style, firstChild); 7377 } 7378 } 7379 }); 7380 7381 GENERATED[theme.name] = true; 7382 } 7383 7384} 7385 7386 7387function checkValidPalette(theme, colorType) { 7388 // If theme attempts to use a palette that doesnt exist, throw error 7389 if (!PALETTES[ (theme.colors[colorType] || {}).name ]) { 7390 throw new Error(
vendor: 27,409 bytes, lines 7391-8289
7391 "You supplied an invalid color palette for theme %1's %2 palette. Available palettes: %3" 7392 .replace('%1', theme.name) 7393 .replace('%2', colorType) 7394 .replace('%3', Object.keys(PALETTES).join(', ')) 7395 ); 7396 } 7397} 7398 7399function colorToRgbaArray(clr) { 7400 if (angular.isArray(clr) && clr.length == 3) return clr; 7401 if (/^rgb/.test(clr)) { 7402 return clr.replace(/(^\s*rgba?\(|\)\s*$)/g, '').split(',').map(function(value, i) { 7403 return i == 3 ? parseFloat(value, 10) : parseInt(value, 10); 7404 }); 7405 } 7406 if (clr.charAt(0) == '#') clr = clr.substring(1); 7407 if (!/^([a-fA-F0-9]{3}){1,2}$/g.test(clr)) return; 7408 7409 var dig = clr.length / 3; 7410 var red = clr.substr(0, dig); 7411 var grn = clr.substr(dig, dig); 7412 var blu = clr.substr(dig * 2); 7413 if (dig === 1) { 7414 red += red; 7415 grn += grn; 7416 blu += blu; 7417 } 7418 return [parseInt(red, 16), parseInt(grn, 16), parseInt(blu, 16)]; 7419} 7420 7421function rgba(rgbArray, opacity) { 7422 if ( !rgbArray ) return "rgb('0,0,0')"; 7423 7424 if (rgbArray.length == 4) { 7425 rgbArray = angular.copy(rgbArray); 7426 opacity ? rgbArray.pop() : opacity = rgbArray.pop(); 7427 } 7428 return opacity && (typeof opacity == 'number' || (typeof opacity == 'string' && opacity.length)) ? 7429 'rgba(' + rgbArray.join(',') + ',' + opacity + ')' : 7430 'rgb(' + rgbArray.join(',') + ')'; 7431} 7432 7433 7434})(window.angular); 7435 7436})(); 7437(function(){ 7438"use strict"; 7439 7440// Polyfill angular < 1.4 (provide $animateCss) 7441angular 7442 .module('material.core') 7443 .factory('$$mdAnimate', ["$q", "$timeout", "$mdConstant", "$animateCss", function($q, $timeout, $mdConstant, $animateCss){ 7444 7445 // Since $$mdAnimate is injected into $mdUtil... use a wrapper function 7446 // to subsequently inject $mdUtil as an argument to the AnimateDomUtils 7447 7448 return function($mdUtil) { 7449 return AnimateDomUtils( $mdUtil, $q, $timeout, $mdConstant, $animateCss); 7450 }; 7451 }]); 7452 7453/** 7454 * Factory function that requires special injections 7455 */ 7456function AnimateDomUtils($mdUtil, $q, $timeout, $mdConstant, $animateCss) { 7457 var self; 7458 return self = { 7459 /** 7460 * 7461 */ 7462 translate3d : function( target, from, to, options ) { 7463 return $animateCss(target, { 7464 from: from, 7465 to: to, 7466 addClass: options.transitionInClass, 7467 removeClass: options.transitionOutClass, 7468 duration: options.duration 7469 }) 7470 .start() 7471 .then(function(){ 7472 // Resolve with reverser function... 7473 return reverseTranslate; 7474 }); 7475 7476 /** 7477 * Specific reversal of the request translate animation above... 7478 */ 7479 function reverseTranslate (newFrom) { 7480 return $animateCss(target, { 7481 to: newFrom || from, 7482 addClass: options.transitionOutClass, 7483 removeClass: options.transitionInClass, 7484 duration: options.duration 7485 }).start(); 7486 7487 } 7488 }, 7489 7490 /** 7491 * Listen for transitionEnd event (with optional timeout) 7492 * Announce completion or failure via promise handlers 7493 */ 7494 waitTransitionEnd: function (element, opts) { 7495 var TIMEOUT = 3000; // fallback is 3 secs 7496 7497 return $q(function(resolve, reject){ 7498 opts = opts || { }; 7499 7500 // If there is no transition is found, resolve immediately 7501 // 7502 // NOTE: using $mdUtil.nextTick() causes delays/issues 7503 if (noTransitionFound(opts.cachedTransitionStyles)) { 7504 TIMEOUT = 0; 7505 } 7506 7507 var timer = $timeout(finished, opts.timeout || TIMEOUT); 7508 element.on($mdConstant.CSS.TRANSITIONEND, finished); 7509 7510 /** 7511 * Upon timeout or transitionEnd, reject or resolve (respectively) this promise. 7512 * NOTE: Make sure this transitionEnd didn't bubble up from a child 7513 */ 7514 function finished(ev) { 7515 if ( ev && ev.target !== element[0]) return; 7516 7517 if ( ev ) $timeout.cancel(timer); 7518 element.off($mdConstant.CSS.TRANSITIONEND, finished); 7519 7520 // Never reject since ngAnimate may cause timeouts due missed transitionEnd events 7521 resolve(); 7522 7523 } 7524 7525 /** 7526 * Checks whether or not there is a transition. 7527 * 7528 * @param styles The cached styles to use for the calculation. If null, getComputedStyle() 7529 * will be used. 7530 * 7531 * @returns {boolean} True if there is no transition/duration; false otherwise. 7532 */ 7533 function noTransitionFound(styles) { 7534 styles = styles || window.getComputedStyle(element[0]); 7535 7536 return styles.transitionDuration == '0s' || (!styles.transition && !styles.transitionProperty); 7537 } 7538 7539 }); 7540 }, 7541 7542 calculateTransformValues: function (element, originator) { 7543 var origin = originator.element; 7544 var bounds = originator.bounds; 7545 7546 if (origin || bounds) { 7547 var originBnds = origin ? self.clientRect(origin) || currentBounds() : self.copyRect(bounds); 7548 var dialogRect = self.copyRect(element[0].getBoundingClientRect()); 7549 var dialogCenterPt = self.centerPointFor(dialogRect); 7550 var originCenterPt = self.centerPointFor(originBnds); 7551 7552 return { 7553 centerX: originCenterPt.x - dialogCenterPt.x, 7554 centerY: originCenterPt.y - dialogCenterPt.y, 7555 scaleX: Math.round(100 * Math.min(0.5, originBnds.width / dialogRect.width)) / 100, 7556 scaleY: Math.round(100 * Math.min(0.5, originBnds.height / dialogRect.height)) / 100 7557 }; 7558 } 7559 return {centerX: 0, centerY: 0, scaleX: 0.5, scaleY: 0.5}; 7560 7561 /** 7562 * This is a fallback if the origin information is no longer valid, then the 7563 * origin bounds simply becomes the current bounds for the dialogContainer's parent 7564 */ 7565 function currentBounds() { 7566 var cntr = element ? element.parent() : null; 7567 var parent = cntr ? cntr.parent() : null; 7568 7569 return parent ? self.clientRect(parent) : null; 7570 } 7571 }, 7572 7573 /** 7574 * Calculate the zoom transform from dialog to origin. 7575 * 7576 * We use this to set the dialog position immediately; 7577 * then the md-transition-in actually translates back to 7578 * `translate3d(0,0,0) scale(1.0)`... 7579 * 7580 * NOTE: all values are rounded to the nearest integer 7581 */ 7582 calculateZoomToOrigin: function (element, originator) { 7583 var zoomTemplate = "translate3d( {centerX}px, {centerY}px, 0 ) scale( {scaleX}, {scaleY} )"; 7584 var buildZoom = angular.bind(null, $mdUtil.supplant, zoomTemplate); 7585 7586 return buildZoom(self.calculateTransformValues(element, originator)); 7587 }, 7588 7589 /** 7590 * Calculate the slide transform from panel to origin. 7591 * NOTE: all values are rounded to the nearest integer 7592 */ 7593 calculateSlideToOrigin: function (element, originator) { 7594 var slideTemplate = "translate3d( {centerX}px, {centerY}px, 0 )"; 7595 var buildSlide = angular.bind(null, $mdUtil.supplant, slideTemplate); 7596 7597 return buildSlide(self.calculateTransformValues(element, originator)); 7598 }, 7599 7600 /** 7601 * Enhance raw values to represent valid css stylings... 7602 */ 7603 toCss : function( raw ) { 7604 var css = { }; 7605 var lookups = 'left top right bottom width height x y min-width min-height max-width max-height'; 7606 7607 angular.forEach(raw, function(value,key) { 7608 if ( angular.isUndefined(value) ) return; 7609 7610 if ( lookups.indexOf(key) >= 0 ) { 7611 css[key] = value + 'px'; 7612 } else { 7613 switch (key) { 7614 case 'transition': 7615 convertToVendor(key, $mdConstant.CSS.TRANSITION, value); 7616 break; 7617 case 'transform': 7618 convertToVendor(key, $mdConstant.CSS.TRANSFORM, value); 7619 break; 7620 case 'transformOrigin': 7621 convertToVendor(key, $mdConstant.CSS.TRANSFORM_ORIGIN, value); 7622 break; 7623 case 'font-size': 7624 css['font-size'] = value; // font sizes aren't always in px 7625 break; 7626 } 7627 } 7628 }); 7629 7630 return css; 7631 7632 function convertToVendor(key, vendor, value) { 7633 angular.forEach(vendor.split(' '), function (key) { 7634 css[key] = value; 7635 }); 7636 } 7637 }, 7638 7639 /** 7640 * Convert the translate CSS value to key/value pair(s). 7641 */ 7642 toTransformCss: function (transform, addTransition, transition) { 7643 var css = {}; 7644 angular.forEach($mdConstant.CSS.TRANSFORM.split(' '), function (key) { 7645 css[key] = transform; 7646 }); 7647 7648 if (addTransition) { 7649 transition = transition || "all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) !important"; 7650 css.transition = transition; 7651 } 7652 7653 return css; 7654 }, 7655 7656 /** 7657 * Clone the Rect and calculate the height/width if needed 7658 */ 7659 copyRect: function (source, destination) { 7660 if (!source) return null; 7661 7662 destination = destination || {}; 7663 7664 angular.forEach('left top right bottom width height'.split(' '), function (key) { 7665 destination[key] = Math.round(source[key]); 7666 }); 7667 7668 destination.width = destination.width || (destination.right - destination.left); 7669 destination.height = destination.height || (destination.bottom - destination.top); 7670 7671 return destination; 7672 }, 7673 7674 /** 7675 * Calculate ClientRect of element; return null if hidden or zero size 7676 */ 7677 clientRect: function (element) { 7678 var bounds = angular.element(element)[0].getBoundingClientRect(); 7679 var isPositiveSizeClientRect = function (rect) { 7680 return rect && (rect.width > 0) && (rect.height > 0); 7681 }; 7682 7683 // If the event origin element has zero size, it has probably been hidden. 7684 return isPositiveSizeClientRect(bounds) ? self.copyRect(bounds) : null; 7685 }, 7686 7687 /** 7688 * Calculate 'rounded' center point of Rect 7689 */ 7690 centerPointFor: function (targetRect) { 7691 return targetRect ? { 7692 x: Math.round(targetRect.left + (targetRect.width / 2)), 7693 y: Math.round(targetRect.top + (targetRect.height / 2)) 7694 } : { x : 0, y : 0 }; 7695 } 7696 7697 }; 7698} 7699 7700 7701})(); 7702(function(){ 7703"use strict"; 7704 7705if (angular.version.minor >= 4) { 7706 angular.module('material.core.animate', []); 7707} else { 7708(function() { 7709 "use strict"; 7710 7711 var forEach = angular.forEach; 7712 7713 var WEBKIT = angular.isDefined(document.documentElement.style.WebkitAppearance); 7714 var TRANSITION_PROP = WEBKIT ? 'WebkitTransition' : 'transition'; 7715 var ANIMATION_PROP = WEBKIT ? 'WebkitAnimation' : 'animation'; 7716 var PREFIX = WEBKIT ? '-webkit-' : ''; 7717 7718 var TRANSITION_EVENTS = (WEBKIT ? 'webkitTransitionEnd ' : '') + 'transitionend'; 7719 var ANIMATION_EVENTS = (WEBKIT ? 'webkitAnimationEnd ' : '') + 'animationend'; 7720 7721 var $$ForceReflowFactory = ['$document', function($document) { 7722 return function() { 7723 return $document[0].body.clientWidth + 1; 7724 }; 7725 }]; 7726 7727 var $$rAFMutexFactory = ['$$rAF', function($$rAF) { 7728 return function() { 7729 var passed = false; 7730 $$rAF(function() { 7731 passed = true; 7732 }); 7733 return function(fn) { 7734 passed ? fn() : $$rAF(fn); 7735 }; 7736 }; 7737 }]; 7738 7739 var $$AnimateRunnerFactory = ['$q', '$$rAFMutex', function($q, $$rAFMutex) { 7740 var INITIAL_STATE = 0; 7741 var DONE_PENDING_STATE = 1; 7742 var DONE_COMPLETE_STATE = 2; 7743 7744 function AnimateRunner(host) { 7745 this.setHost(host); 7746 7747 this._doneCallbacks = []; 7748 this._runInAnimationFrame = $$rAFMutex(); 7749 this._state = 0; 7750 } 7751 7752 AnimateRunner.prototype = { 7753 setHost: function(host) { 7754 this.host = host || {}; 7755 }, 7756 7757 done: function(fn) { 7758 if (this._state === DONE_COMPLETE_STATE) { 7759 fn(); 7760 } else { 7761 this._doneCallbacks.push(fn); 7762 } 7763 }, 7764 7765 progress: angular.noop, 7766 7767 getPromise: function() { 7768 if (!this.promise) { 7769 var self = this; 7770 this.promise = $q(function(resolve, reject) { 7771 self.done(function(status) { 7772 status === false ? reject() : resolve(); 7773 }); 7774 }); 7775 } 7776 return this.promise; 7777 }, 7778 7779 then: function(resolveHandler, rejectHandler) { 7780 return this.getPromise().then(resolveHandler, rejectHandler); 7781 }, 7782 7783 'catch': function(handler) { 7784 return this.getPromise()['catch'](handler); 7785 }, 7786 7787 'finally': function(handler) { 7788 return this.getPromise()['finally'](handler); 7789 }, 7790 7791 pause: function() { 7792 if (this.host.pause) { 7793 this.host.pause(); 7794 } 7795 }, 7796 7797 resume: function() { 7798 if (this.host.resume) { 7799 this.host.resume(); 7800 } 7801 }, 7802 7803 end: function() { 7804 if (this.host.end) { 7805 this.host.end(); 7806 } 7807 this._resolve(true); 7808 }, 7809 7810 cancel: function() { 7811 if (this.host.cancel) { 7812 this.host.cancel(); 7813 } 7814 this._resolve(false); 7815 }, 7816 7817 complete: function(response) { 7818 var self = this; 7819 if (self._state === INITIAL_STATE) { 7820 self._state = DONE_PENDING_STATE; 7821 self._runInAnimationFrame(function() { 7822 self._resolve(response); 7823 }); 7824 } 7825 }, 7826 7827 _resolve: function(response) { 7828 if (this._state !== DONE_COMPLETE_STATE) { 7829 forEach(this._doneCallbacks, function(fn) { 7830 fn(response); 7831 }); 7832 this._doneCallbacks.length = 0; 7833 this._state = DONE_COMPLETE_STATE; 7834 } 7835 } 7836 }; 7837 7838 // Polyfill AnimateRunner.all which is used by input animations 7839 AnimateRunner.all = function(runners, callback) { 7840 var count = 0; 7841 var status = true; 7842 forEach(runners, function(runner) { 7843 runner.done(onProgress); 7844 }); 7845 7846 function onProgress(response) { 7847 status = status && response; 7848 if (++count === runners.length) { 7849 callback(status); 7850 } 7851 } 7852 }; 7853 7854 return AnimateRunner; 7855 }]; 7856 7857 angular 7858 .module('material.core.animate', []) 7859 .factory('$$forceReflow', $$ForceReflowFactory) 7860 .factory('$$AnimateRunner', $$AnimateRunnerFactory) 7861 .factory('$$rAFMutex', $$rAFMutexFactory) 7862 .factory('$animateCss', ['$window', '$$rAF', '$$AnimateRunner', '$$forceReflow', '$$jqLite', '$timeout', '$animate', 7863 function($window, $$rAF, $$AnimateRunner, $$forceReflow, $$jqLite, $timeout, $animate) { 7864 7865 function init(element, options) { 7866 7867 var temporaryStyles = []; 7868 var node = getDomNode(element); 7869 var areAnimationsAllowed = node && $animate.enabled(); 7870 7871 var hasCompleteStyles = false; 7872 var hasCompleteClasses = false; 7873 7874 if (areAnimationsAllowed) { 7875 if (options.transitionStyle) { 7876 temporaryStyles.push([PREFIX + 'transition', options.transitionStyle]); 7877 } 7878 7879 if (options.keyframeStyle) { 7880 temporaryStyles.push([PREFIX + 'animation', options.keyframeStyle]); 7881 } 7882 7883 if (options.delay) { 7884 temporaryStyles.push([PREFIX + 'transition-delay', options.delay + 's']); 7885 } 7886 7887 if (options.duration) { 7888 temporaryStyles.push([PREFIX + 'transition-duration', options.duration + 's']); 7889 } 7890 7891 hasCompleteStyles = options.keyframeStyle || 7892 (options.to && (options.duration > 0 || options.transitionStyle)); 7893 hasCompleteClasses = !!options.addClass || !!options.removeClass; 7894 7895 blockTransition(element, true); 7896 } 7897 7898 var hasCompleteAnimation = areAnimationsAllowed && (hasCompleteStyles || hasCompleteClasses); 7899 7900 applyAnimationFromStyles(element, options); 7901 7902 var animationClosed = false; 7903 var events, eventFn; 7904 7905 return { 7906 close: $window.close, 7907 start: function() { 7908 var runner = new $$AnimateRunner(); 7909 waitUntilQuiet(function() { 7910 blockTransition(element, false); 7911 if (!hasCompleteAnimation) { 7912 return close(); 7913 } 7914 7915 forEach(temporaryStyles, function(entry) { 7916 var key = entry[0]; 7917 var value = entry[1]; 7918 node.style[camelCase(key)] = value; 7919 }); 7920 7921 applyClasses(element, options); 7922 7923 var timings = computeTimings(element); 7924 if (timings.duration === 0) { 7925 return close(); 7926 } 7927 7928 var moreStyles = []; 7929 7930 if (options.easing) { 7931 if (timings.transitionDuration) { 7932 moreStyles.push([PREFIX + 'transition-timing-function', options.easing]); 7933 } 7934 if (timings.animationDuration) { 7935 moreStyles.push([PREFIX + 'animation-timing-function', options.easing]); 7936 } 7937 } 7938 7939 if (options.delay && timings.animationDelay) { 7940 moreStyles.push([PREFIX + 'animation-delay', options.delay + 's']); 7941 } 7942 7943 if (options.duration && timings.animationDuration) { 7944 moreStyles.push([PREFIX + 'animation-duration', options.duration + 's']); 7945 } 7946 7947 forEach(moreStyles, function(entry) { 7948 var key = entry[0]; 7949 var value = entry[1]; 7950 node.style[camelCase(key)] = value; 7951 temporaryStyles.push(entry); 7952 }); 7953 7954 var maxDelay = timings.delay; 7955 var maxDelayTime = maxDelay * 1000; 7956 var maxDuration = timings.duration; 7957 var maxDurationTime = maxDuration * 1000; 7958 var startTime = Date.now(); 7959 7960 events = []; 7961 if (timings.transitionDuration) { 7962 events.push(TRANSITION_EVENTS); 7963 } 7964 if (timings.animationDuration) { 7965 events.push(ANIMATION_EVENTS); 7966 } 7967 events = events.join(' '); 7968 eventFn = function(event) { 7969 event.stopPropagation(); 7970 var ev = event.originalEvent || event; 7971 var timeStamp = ev.timeStamp || Date.now(); 7972 var elapsedTime = parseFloat(ev.elapsedTime.toFixed(3)); 7973 if (Math.max(timeStamp - startTime, 0) >= maxDelayTime && elapsedTime >= maxDuration) { 7974 close(); 7975 } 7976 }; 7977 element.on(events, eventFn); 7978 7979 applyAnimationToStyles(element, options); 7980 7981 $timeout(close, maxDelayTime + maxDurationTime * 1.5, false); 7982 }); 7983 7984 return runner; 7985 7986 function close() { 7987 if (animationClosed) return; 7988 animationClosed = true; 7989 7990 if (events && eventFn) { 7991 element.off(events, eventFn); 7992 } 7993 applyClasses(element, options); 7994 applyAnimationStyles(element, options); 7995 forEach(temporaryStyles, function(entry) { 7996 node.style[camelCase(entry[0])] = ''; 7997 }); 7998 runner.complete(true); 7999 return runner; 8000 } 8001 } 8002 }; 8003 } 8004 8005 function applyClasses(element, options) { 8006 if (options.addClass) { 8007 $$jqLite.addClass(element, options.addClass); 8008 options.addClass = null; 8009 } 8010 if (options.removeClass) { 8011 $$jqLite.removeClass(element, options.removeClass); 8012 options.removeClass = null; 8013 } 8014 } 8015 8016 function computeTimings(element) { 8017 var node = getDomNode(element); 8018 var cs = $window.getComputedStyle(node); 8019 var tdr = parseMaxTime(cs[prop('transitionDuration')]); 8020 var adr = parseMaxTime(cs[prop('animationDuration')]); 8021 var tdy = parseMaxTime(cs[prop('transitionDelay')]); 8022 var ady = parseMaxTime(cs[prop('animationDelay')]); 8023 8024 adr *= (parseInt(cs[prop('animationIterationCount')], 10) || 1); 8025 var duration = Math.max(adr, tdr); 8026 var delay = Math.max(ady, tdy); 8027 8028 return { 8029 duration: duration, 8030 delay: delay, 8031 animationDuration: adr, 8032 transitionDuration: tdr, 8033 animationDelay: ady, 8034 transitionDelay: tdy 8035 }; 8036 8037 function prop(key) { 8038 return WEBKIT ? 'Webkit' + key.charAt(0).toUpperCase() + key.substr(1) 8039 : key; 8040 } 8041 } 8042 8043 function parseMaxTime(str) { 8044 var maxValue = 0; 8045 var values = (str || "").split(/\s*,\s*/); 8046 forEach(values, function(value) { 8047 // it's always safe to consider only second values and omit `ms` values since 8048 // getComputedStyle will always handle the conversion for us 8049 if (value.charAt(value.length - 1) == 's') { 8050 value = value.substring(0, value.length - 1); 8051 } 8052 value = parseFloat(value) || 0; 8053 maxValue = maxValue ? Math.max(value, maxValue) : value; 8054 }); 8055 return maxValue; 8056 } 8057 8058 var cancelLastRAFRequest; 8059 var rafWaitQueue = []; 8060 function waitUntilQuiet(callback) { 8061 if (cancelLastRAFRequest) { 8062 cancelLastRAFRequest(); //cancels the request 8063 } 8064 rafWaitQueue.push(callback); 8065 cancelLastRAFRequest = $$rAF(function() { 8066 cancelLastRAFRequest = null; 8067 8068 // DO NOT REMOVE THIS LINE OR REFACTOR OUT THE `pageWidth` variable. 8069 // PLEASE EXAMINE THE `$$forceReflow` service to understand why. 8070 var pageWidth = $$forceReflow(); 8071 8072 // we use a for loop to ensure that if the queue is changed 8073 // during this looping then it will consider new requests 8074 for (var i = 0; i < rafWaitQueue.length; i++) { 8075 rafWaitQueue[i](pageWidth); 8076 } 8077 rafWaitQueue.length = 0; 8078 }); 8079 } 8080 8081 function applyAnimationStyles(element, options) { 8082 applyAnimationFromStyles(element, options); 8083 applyAnimationToStyles(element, options); 8084 } 8085 8086 function applyAnimationFromStyles(element, options) { 8087 if (options.from) { 8088 element.css(options.from); 8089 options.from = null; 8090 } 8091 } 8092 8093 function applyAnimationToStyles(element, options) { 8094 if (options.to) { 8095 element.css(options.to); 8096 options.to = null; 8097 } 8098 } 8099 8100 function getDomNode(element) { 8101 for (var i = 0; i < element.length; i++) { 8102 if (element[i].nodeType === 1) return element[i]; 8103 } 8104 } 8105 8106 function blockTransition(element, bool) { 8107 var node = getDomNode(element); 8108 var key = camelCase(PREFIX + 'transition-delay'); 8109 node.style[key] = bool ? '-9999s' : ''; 8110 } 8111 8112 return init; 8113 }]); 8114 8115 /** 8116 * Older browsers [FF31] expect camelCase 8117 * property keys. 8118 * e.g. 8119 * animation-duration --> animationDuration 8120 */ 8121 function camelCase(str) { 8122 return str.replace(/-[a-z]/g, function(str) { 8123 return str.charAt(1).toUpperCase(); 8124 }); 8125 } 8126 8127})(); 8128 8129} 8130 8131})(); 8132(function(){ 8133"use strict"; 8134 8135/** 8136 * @ngdoc module 8137 * @name material.components.autocomplete 8138 */ 8139/* 8140 * @see js folder for autocomplete implementation 8141 */ 8142angular.module('material.components.autocomplete', [ 8143 'material.core', 8144 'material.components.icon', 8145 'material.components.virtualRepeat' 8146]); 8147 8148})(); 8149(function(){ 8150"use strict"; 8151 8152/* 8153 * @ngdoc module 8154 * @name material.components.backdrop 8155 * @description Backdrop 8156 */ 8157 8158/** 8159 * @ngdoc directive 8160 * @name mdBackdrop 8161 * @module material.components.backdrop 8162 * 8163 * @restrict E 8164 * 8165 * @description 8166 * `<md-backdrop>` is a backdrop element used by other components, such as dialog and bottom sheet. 8167 * Apply class `opaque` to make the backdrop use the theme backdrop color. 8168 * 8169 */ 8170 8171angular 8172 .module('material.components.backdrop', ['material.core']) 8173 .directive('mdBackdrop', ["$mdTheming", "$mdUtil", "$animate", "$rootElement", "$window", "$log", "$$rAF", "$document", function BackdropDirective($mdTheming, $mdUtil, $animate, $rootElement, $window, $log, $$rAF, $document) { 8174 var ERROR_CSS_POSITION = '<md-backdrop> may not work properly in a scrolled, static-positioned parent container.'; 8175 8176 return { 8177 restrict: 'E', 8178 link: postLink 8179 }; 8180 8181 function postLink(scope, element, attrs) { 8182 // backdrop may be outside the $rootElement, tell ngAnimate to animate regardless 8183 if ($animate.pin) $animate.pin(element, $rootElement); 8184 8185 var bodyStyles; 8186 8187 $$rAF(function() { 8188 // If body scrolling has been disabled using mdUtil.disableBodyScroll(), 8189 // adjust the 'backdrop' height to account for the fixed 'body' top offset. 8190 // Note that this can be pretty expensive and is better done inside the $$rAF. 8191 bodyStyles = $window.getComputedStyle($document[0].body); 8192 8193 if (bodyStyles.position === 'fixed') { 8194 var resizeHandler = $mdUtil.debounce(function(){ 8195 bodyStyles = $window.getComputedStyle($document[0].body); 8196 resize(); 8197 }, 60, null, false); 8198 8199 resize(); 8200 angular.element($window).on('resize', resizeHandler); 8201 8202 scope.$on('$destroy', function() { 8203 angular.element($window).off('resize', resizeHandler); 8204 }); 8205 } 8206 8207 // Often $animate.enter() is used to append the backDrop element 8208 // so let's wait until $animate is done... 8209 var parent = element.parent(); 8210 8211 if (parent.length) { 8212 if (parent[0].nodeName === 'BODY') { 8213 element.css('position', 'fixed'); 8214 } 8215 8216 var styles = $window.getComputedStyle(parent[0]); 8217 8218 if (styles.position === 'static') { 8219 // backdrop uses position:absolute and will not work properly with parent position:static (default) 8220 $log.warn(ERROR_CSS_POSITION); 8221 } 8222 8223 // Only inherit the parent if the backdrop has a parent. 8224 $mdTheming.inherit(element, parent); 8225 } 8226 }); 8227 8228 function resize() { 8229 var viewportHeight = parseInt(bodyStyles.height, 10) + Math.abs(parseInt(bodyStyles.top, 10)); 8230 element.css('height', viewportHeight + 'px'); 8231 } 8232 } 8233 8234 }]); 8235 8236})(); 8237(function(){ 8238"use strict"; 8239 8240/** 8241 * @ngdoc module 8242 * @name material.components.bottomSheet 8243 * @description 8244 * BottomSheet 8245 */ 8246MdBottomSheetDirective.$inject = ["$mdBottomSheet"]; 8247MdBottomSheetProvider.$inject = ["$$interimElementProvider"]; 8248angular 8249 .module('material.components.bottomSheet', [ 8250 'material.core', 8251 'material.components.backdrop' 8252 ]) 8253 .directive('mdBottomSheet', MdBottomSheetDirective) 8254 .provider('$mdBottomSheet', MdBottomSheetProvider); 8255 8256/* @ngInject */ 8257function MdBottomSheetDirective($mdBottomSheet) { 8258 return { 8259 restrict: 'E', 8260 link : function postLink(scope, element) { 8261 element.addClass('_md'); // private md component indicator for styling 8262 8263 // When navigation force destroys an interimElement, then 8264 // listen and $destroy() that interim instance... 8265 scope.$on('$destroy', function() { 8266 $mdBottomSheet.destroy(); 8267 }); 8268 } 8269 }; 8270} 8271 8272 8273/** 8274 * @ngdoc service 8275 * @name $mdBottomSheet 8276 * @module material.components.bottomSheet 8277 * 8278 * @description 8279 * `$mdBottomSheet` opens a bottom sheet over the app and provides a simple promise API. 8280 * 8281 * ## Restrictions 8282 * 8283 * - The bottom sheet's template must have an outer `<md-bottom-sheet>` element. 8284 * - Add the `md-grid` class to the bottom sheet for a grid layout. 8285 * - Add the `md-list` class to the bottom sheet for a list layout. 8286 * 8287 * @usage 8288 * <hljs lang="html"> 8289 * <div ng-controller="MyController">
8290 * <md-button ng-click="openBottomSheet()"> 8291 * Open a Bottom Sheet! 8292 * </md-button> 8293 * </div> 8294 * </hljs> 8295 * <hljs lang="js"> 8296 * var app = angular.module('app', ['ngMaterial']); 8297 * app.controller('MyController', function($scope, $mdBottomSheet) { 8298 * $scope.openBottomSheet = function() { 8299 * $mdBottomSheet.show({ 8300 * template: '<md-bottom-sheet>' + 8301 * 'Hello! <md-button ng-click="closeBottomSheet()">Close</md-button>' + 8302 * '</md-bottom-sheet>' 8303 * }) 8304 * 8305 * // Fires when the hide() method is used 8306 * .then(function() { 8307 * console.log('You clicked the button to close the bottom sheet!'); 8308 * }) 8309 * 8310 * // Fires when the cancel() method is used 8311 * .catch(function() { 8312 * console.log('You hit escape or clicked the backdrop to close.'); 8313 * }); 8314 * }; 8315 * 8316 * $scope.closeBottomSheet = function($scope, $mdBottomSheet) { 8317 * $mdBottomSheet.hide(); 8318 * } 8319 * 8320 * }); 8321 * </hljs> 8322 */ 8323 8324 /** 8325 * @ngdoc method 8326 * @name $mdBottomSheet#show 8327 * 8328 * @description 8329 * Show a bottom sheet with the specified options. 8330 * 8331 * <em><b>Note:</b> You should <b>always</b> provide a `.catch()` method in case the user hits the 8332 * `esc` key or clicks the background to close. In this case, the `cancel()` method will 8333 * automatically be called on the bottom sheet which will `reject()` the promise. See the @usage 8334 * section above for an example. 8335 * 8336 * Newer versions of Angular will throw a `Possibly unhandled rejection` exception if you forget 8337 * this.</em> 8338 * 8339 * @param {object} options An options object, with the following properties: 8340 * 8341 * - `templateUrl` - `{string=}`: The url of an html template file that will 8342 * be used as the content of the bottom sheet. Restrictions: the template must 8343 * have an outer `md-bottom-sheet` element. 8344 * - `template` - `{string=}`: Same as templateUrl, except this is an actual 8345 * template string. 8346 * - `scope` - `{object=}`: the scope to link the template / controller to. If none is specified, it will create a new child scope. 8347 * This scope will be destroyed when the bottom sheet is removed unless `preserveScope` is set to true. 8348 * - `preserveScope` - `{boolean=}`: whether to preserve the scope when the element is removed. Default is false 8349 * - `controller` - `{string=}`: The controller to associate with this bottom sheet. 8350 * - `locals` - `{string=}`: An object containing key/value pairs. The keys will 8351 * be used as names of values to inject into the controller. For example, 8352 * `locals: {three: 3}` would inject `three` into the controller with the value 8353 * of 3. 8354 * - `clickOutsideToClose` - `{boolean=}`: Whether the user can click outside the bottom sheet to 8355 * close it. Default true. 8356 * - `bindToController` - `{boolean=}`: When set to true, the locals will be bound to the controller instance. 8357 * - `disableBackdrop` - `{boolean=}`: When set to true, the bottomsheet will not show a backdrop. 8358 * - `escapeToClose` - `{boolean=}`: Whether the user can press escape to close the bottom sheet. 8359 * Default true. 8360 * - `resolve` - `{object=}`: Similar to locals, except it takes promises as values 8361 * and the bottom sheet will not open until the promises resolve. 8362 * - `controllerAs` - `{string=}`: An alias to assign the controller to on the scope. 8363 * - `parent` - `{element=}`: The element to append the bottom sheet to. The `parent` may be a `function`, `string`, 8364 * `object`, or null. Defaults to appending to the body of the root element (or the root element) of the application. 8365 * e.g. angular.element(document.getElementById('content')) or "#content" 8366 * - `disableParentScroll` - `{boolean=}`: Whether to disable scrolling while the bottom sheet is open. 8367 * Default true. 8368 * 8369 * @returns {promise} A promise that can be resolved with `$mdBottomSheet.hide()` or 8370 * rejected with `$mdBottomSheet.cancel()`. 8371 */ 8372 8373/** 8374 * @ngdoc method 8375 * @name $mdBottomSheet#hide 8376 * 8377 * @description 8378 * Hide the existing bottom sheet and resolve the promise returned from 8379 * `$mdBottomSheet.show()`. This call will close the most recently opened/current bottomsheet (if 8380 * any). 8381 * 8382 * <em><b>Note:</b> Use a `.then()` on your `.show()` to handle this callback.</em> 8383 * 8384 * @param {*=} response An argument for the resolved promise. 8385 * 8386 */ 8387 8388/** 8389 * @ngdoc method 8390 * @name $mdBottomSheet#cancel 8391 * 8392 * @description 8393 * Hide the existing bottom sheet and reject the promise returned from 8394 * `$mdBottomSheet.show()`. 8395 * 8396 * <em><b>Note:</b> Use a `.catch()` on your `.show()` to handle this callback.</em> 8397 * 8398 * @param {*=} response An argument for the rejected promise. 8399 * 8400 */ 8401 8402function MdBottomSheetProvider($$interimElementProvider) { 8403 // how fast we need to flick down to close the sheet, pixels/ms 8404 bottomSheetDefaults.$inject = ["$animate", "$mdConstant", "$mdUtil", "$mdTheming", "$mdBottomSheet", "$rootElement", "$mdGesture", "$log"]; 8405 var CLOSING_VELOCITY = 0.5; 8406 var PADDING = 80; // same as css 8407 8408 return $$interimElementProvider('$mdBottomSheet') 8409 .setDefaults({ 8410 methods: ['disableParentScroll', 'escapeToClose', 'clickOutsideToClose'], 8411 options: bottomSheetDefaults 8412 }); 8413 8414 /* @ngInject */ 8415 function bottomSheetDefaults($animate, $mdConstant, $mdUtil, $mdTheming, $mdBottomSheet, $rootElement, 8416 $mdGesture, $log) { 8417 var backdrop; 8418 8419 return { 8420 themable: true, 8421 onShow: onShow, 8422 onRemove: onRemove, 8423 disableBackdrop: false, 8424 escapeToClose: true, 8425 clickOutsideToClose: true, 8426 disableParentScroll: true 8427 }; 8428 8429 8430 function onShow(scope, element, options, controller) { 8431 8432 element = $mdUtil.extractElementByName(element, 'md-bottom-sheet'); 8433 8434 // prevent tab focus or click focus on the bottom-sheet container 8435 element.attr('tabindex',"-1"); 8436 8437 // Once the md-bottom-sheet has `ng-cloak` applied on his template the opening animation will not work properly. 8438 // This is a very common problem, so we have to notify the developer about this. 8439 if (element.hasClass('ng-cloak')) { 8440 var message = '$mdBottomSheet: using `<md-bottom-sheet ng-cloak >` will affect the bottom-sheet opening animations.'; 8441 $log.warn( message, element[0] ); 8442 } 8443 8444 if (!options.disableBackdrop) { 8445 // Add a backdrop that will close on click 8446 backdrop = $mdUtil.createBackdrop(scope, "md-bottom-sheet-backdrop md-opaque"); 8447 8448 // Prevent mouse focus on backdrop; ONLY programatic focus allowed. 8449 // This allows clicks on backdrop to propogate to the $rootElement and 8450 // ESC key events to be detected properly. 8451 8452 backdrop[0].tabIndex = -1; 8453 8454 if (options.clickOutsideToClose) { 8455 backdrop.on('click', function() { 8456 $mdUtil.nextTick($mdBottomSheet.cancel,true); 8457 }); 8458 } 8459 8460 $mdTheming.inherit(backdrop, options.parent); 8461 8462 $animate.enter(backdrop, options.parent, null); 8463 } 8464 8465 var bottomSheet = new BottomSheet(element, options.parent); 8466 options.bottomSheet = bottomSheet; 8467 8468 $mdTheming.inherit(bottomSheet.element, options.parent); 8469 8470 if (options.disableParentScroll) { 8471 options.restoreScroll = $mdUtil.disableScrollAround(bottomSheet.element, options.parent); 8472 } 8473 8474 return $animate.enter(bottomSheet.element, options.parent, backdrop) 8475 .then(function() { 8476 var focusable = $mdUtil.findFocusTarget(element) || angular.element( 8477 element[0].querySelector('button') || 8478 element[0].querySelector('a') || 8479 element[0].querySelector($mdUtil.prefixer('ng-click', true)) 8480 ) || backdrop; 8481 8482 if (options.escapeToClose) { 8483 options.rootElementKeyupCallback = function(e) { 8484 if (e.keyCode === $mdConstant.KEY_CODE.ESCAPE) { 8485 $mdUtil.nextTick($mdBottomSheet.cancel,true); 8486 } 8487 }; 8488 8489 $rootElement.on('keyup', options.rootElementKeyupCallback); 8490 focusable && focusable.focus(); 8491 } 8492 }); 8493 8494 } 8495 8496 function onRemove(scope, element, options) { 8497 8498 var bottomSheet = options.bottomSheet; 8499 8500 if (!options.disableBackdrop) $animate.leave(backdrop); 8501 return $animate.leave(bottomSheet.element).then(function() { 8502 if (options.disableParentScroll) { 8503 options.restoreScroll(); 8504 delete options.restoreScroll; 8505 } 8506 8507 bottomSheet.cleanup(); 8508 }); 8509 } 8510 8511 /** 8512 * BottomSheet class to apply bottom-sheet behavior to an element 8513 */ 8514 function BottomSheet(element, parent) { 8515 var deregister = $mdGesture.register(parent, 'drag', { horizontal: false }); 8516 parent.on('$md.dragstart', onDragStart) 8517 .on('$md.drag', onDrag) 8518 .on('$md.dragend', onDragEnd); 8519 8520 return { 8521 element: element, 8522 cleanup: function cleanup() { 8523 deregister(); 8524 parent.off('$md.dragstart', onDragStart); 8525 parent.off('$md.drag', onDrag); 8526 parent.off('$md.dragend', onDragEnd); 8527 } 8528 }; 8529 8530 function onDragStart(ev) { 8531 // Disable transitions on transform so that it feels fast 8532 element.css($mdConstant.CSS.TRANSITION_DURATION, '0ms'); 8533 } 8534 8535 function onDrag(ev) { 8536 var transform = ev.pointer.distanceY; 8537 if (transform < 5) { 8538 // Slow down drag when trying to drag up, and stop after PADDING 8539 transform = Math.max(-PADDING, transform / 2); 8540 } 8541 element.css($mdConstant.CSS.TRANSFORM, 'translate3d(0,' + (PADDING + transform) + 'px,0)'); 8542 } 8543 8544 function onDragEnd(ev) { 8545 if (ev.pointer.distanceY > 0 && 8546 (ev.pointer.distanceY > 20 || Math.abs(ev.pointer.velocityY) > CLOSING_VELOCITY)) { 8547 var distanceRemaining = element.prop('offsetHeight') - ev.pointer.distanceY; 8548 var transitionDuration = Math.min(distanceRemaining / ev.pointer.velocityY * 0.75, 500); 8549 element.css($mdConstant.CSS.TRANSITION_DURATION, transitionDuration + 'ms'); 8550 $mdUtil.nextTick($mdBottomSheet.cancel,true); 8551 } else { 8552 element.css($mdConstant.CSS.TRANSITION_DURATION, ''); 8553 element.css($mdConstant.CSS.TRANSFORM, ''); 8554 } 8555 } 8556 } 8557 8558 } 8559 8560} 8561 8562})(); 8563(function(){ 8564"use strict"; 8565 8566/** 8567 * @ngdoc module 8568 * @name material.components.button 8569 * @description 8570 * 8571 * Button 8572 */ 8573MdButtonDirective.$inject = ["$mdButtonInkRipple", "$mdTheming", "$mdAria", "$mdInteraction"]; 8574MdAnchorDirective.$inject = ["$mdTheming"]; 8575angular 8576 .module('material.components.button', [ 'material.core' ]) 8577 .directive('mdButton', MdButtonDirective) 8578 .directive('a', MdAnchorDirective); 8579 8580 8581/** 8582 * @private 8583 * @restrict E 8584 * 8585 * @description 8586 * `a` is an anchor directive used to inherit theme colors for md-primary, md-accent, etc. 8587 * 8588 * @usage 8589 * 8590 * <hljs lang="html"> 8591 * <md-content md-theme="myTheme"> 8592 * <a href="#chapter1" class="md-accent"></a> 8593 * </md-content> 8594 * </hljs> 8595 */ 8596function MdAnchorDirective($mdTheming) { 8597 return { 8598 restrict : 'E', 8599 link : function postLink(scope, element) { 8600 // Make sure to inherit theme so stand-alone anchors 8601 // support theme colors for md-primary, md-accent, etc. 8602 $mdTheming(element); 8603 } 8604 }; 8605} 8606 8607 8608/** 8609 * @ngdoc directive 8610 * @name mdButton 8611 * @module material.components.button 8612 * 8613 * @restrict E 8614 * 8615 * @description 8616 * `<md-button>` is a button directive with optional ink ripples (default enabled). 8617 * 8618 * If you supply a `href` or `ng-href` attribute, it will become an `<a>` element. Otherwise, it 8619 * will become a `<button>` element. As per the 8620 * [Material Design specifications](https://material.google.com/style/color.html#color-color-palette) 8621 * the FAB button background is filled with the accent color [by default]. The primary color palette 8622 * may be used with the `md-primary` class. 8623 * 8624 * Developers can also change the color palette of the button, by using the following classes 8625 * - `md-primary` 8626 * - `md-accent` 8627 * - `md-warn` 8628 * 8629 * See for example 8630 * 8631 * <hljs lang="html"> 8632 * <md-button class="md-primary">Primary Button</md-button> 8633 * </hljs> 8634 * 8635 * Button can be also raised, which means that they will use the current color palette to fill the button. 8636 * 8637 * <hljs lang="html"> 8638 * <md-button class="md-accent md-raised">Raised and Accent Button</md-button> 8639 * </hljs> 8640 * 8641 * It is also possible to disable the focus effect on the button, by using the following markup. 8642 * 8643 * <hljs lang="html"> 8644 * <md-button class="md-no-focus">No Focus Style</md-button> 8645 * </hljs> 8646 * 8647 * @param {boolean=} md-no-ink If present, disable ripple ink effects. 8648 * @param {expression=} ng-disabled En/Disable based on the expression 8649 * @param {string=} md-ripple-size Overrides the default ripple size logic. Options: `full`, `partial`, `auto` 8650 * @param {string=} aria-label Adds alternative text to button for accessibility, useful for icon buttons. 8651 * If no default text is found, a warning will be logged. 8652 * 8653 * @usage 8654 * 8655 * Regular buttons: 8656 * 8657 * <hljs lang="html"> 8658 * <md-button> Flat Button </md-button> 8659 * <md-button href="http://google.com"> Flat link </md-button> 8660 * <md-button class="md-raised"> Raised Button </md-button>
8661 * <md-button ng-disabled="true"> Disabled Button </md-button> 8662 * <md-button> 8663 * <md-icon md-svg-src="your/icon.svg"></md-icon> 8664 * Register Now 8665 * </md-button> 8666 * </hljs> 8667 * 8668 * FAB buttons: 8669 * 8670 * <hljs lang="html"> 8671 * <md-button class="md-fab" aria-label="FAB"> 8672 * <md-icon md-svg-src="your/icon.svg"></md-icon> 8673 * </md-button> 8674 * <!-- mini-FAB --> 8675 * <md-button class="md-fab md-mini" aria-label="Mini FAB"> 8676 * <md-icon md-svg-src="your/icon.svg"></md-icon> 8677 * </md-button> 8678 * <!-- Button with SVG Icon --> 8679 * <md-button class="md-icon-button" aria-label="Custom Icon Button"> 8680 * <md-icon md-svg-icon="path/to/your.svg"></md-icon> 8681 * </md-button> 8682 * </hljs> 8683 */ 8684function MdButtonDirective($mdButtonInkRipple, $mdTheming, $mdAria, $mdInteraction) { 8685 8686 return { 8687 restrict: 'EA', 8688 replace: true, 8689 transclude: true, 8690 template: getTemplate, 8691 link: postLink 8692 }; 8693 8694 function isAnchor(attr) { 8695 return angular.isDefined(attr.href) || angular.isDefined(attr.ngHref) || angular.isDefined(attr.ngLink) || angular.isDefined(attr.uiSref); 8696 } 8697 8698 function getTemplate(element, attr) { 8699 if (isAnchor(attr)) { 8700 return '<a class="md-button" ng-transclude></a>'; 8701 } else { 8702 //If buttons don't have type="button", they will submit forms automatically. 8703 var btnType = (typeof attr.type === 'undefined') ? 'button' : attr.type; 8704 return '<button class="md-button" type="' + btnType + '" ng-transclude></button>'; 8705 } 8706 } 8707 8708 function postLink(scope, element, attr) { 8709 $mdTheming(element); 8710 $mdButtonInkRipple.attach(scope, element); 8711 8712 // Use async expect to support possible bindings in the button label 8713 $mdAria.expectWithoutText(element, 'aria-label'); 8714 8715 // For anchor elements, we have to set tabindex manually when the 8716 // element is disabled 8717 if (isAnchor(attr) && angular.isDefined(attr.ngDisabled) ) { 8718 scope.$watch(attr.ngDisabled, function(isDisabled) { 8719 element.attr('tabindex', isDisabled ? -1 : 0); 8720 }); 8721 } 8722 8723 // disabling click event when disabled is true 8724 element.on('click', function(e){ 8725 if (attr.disabled === true) { 8726 e.preventDefault(); 8727 e.stopImmediatePropagation(); 8728 } 8729 }); 8730 8731 if (!element.hasClass('md-no-focus')) { 8732 8733 element.on('focus', function() { 8734 8735 // Only show the focus effect when being focused through keyboard interaction or programmatically 8736 if (!$mdInteraction.isUserInvoked() || $mdInteraction.getLastInteractionType() === 'keyboard') { 8737 element.addClass('md-focused'); 8738 } 8739 8740 }); 8741 8742 element.on('blur', function() { 8743 element.removeClass('md-focused'); 8744 }); 8745 } 8746 8747 } 8748 8749} 8750 8751})(); 8752(function(){ 8753"use strict"; 8754 8755/** 8756 * @ngdoc module 8757 * @name material.components.card 8758 * 8759 * @description 8760 * Card components. 8761 */ 8762mdCardDirective.$inject = ["$mdTheming"]; 8763angular.module('material.components.card', [ 8764 'material.core' 8765 ]) 8766 .directive('mdCard', mdCardDirective); 8767 8768 8769/** 8770 * @ngdoc directive 8771 * @name mdCard 8772 * @module material.components.card 8773 * 8774 * @restrict E 8775 * 8776 * @description 8777 * The `<md-card>` directive is a container element used within `<md-content>` containers. 8778 * 8779 * An image included as a direct descendant will fill the card's width. If you want to avoid this, 8780 * you can add the `md-image-no-fill` class to the parent element. The `<md-card-content>` 8781 * container will wrap text content and provide padding. An `<md-card-footer>` element can be 8782 * optionally included to put content flush against the bottom edge of the card. 8783 * 8784 * Action buttons can be included in an `<md-card-actions>` element, similar to `<md-dialog-actions>`. 8785 * You can then position buttons using layout attributes. 8786 * 8787 * Card is built with: 8788 * * `<md-card-header>` - Header for the card, holds avatar, text and squared image 8789 * - `<md-card-avatar>` - Card avatar 8790 * - `md-user-avatar` - Class for user image 8791 * - `<md-icon>` 8792 * - `<md-card-header-text>` - Contains elements for the card description 8793 * - `md-title` - Class for the card title 8794 * - `md-subhead` - Class for the card sub header 8795 * * `<img>` - Image for the card 8796 * * `<md-card-title>` - Card content title 8797 * - `<md-card-title-text>` 8798 * - `md-headline` - Class for the card content title 8799 * - `md-subhead` - Class for the card content sub header 8800 * - `<md-card-title-media>` - Squared image within the title 8801 * - `md-media-sm` - Class for small image 8802 * - `md-media-md` - Class for medium image 8803 * - `md-media-lg` - Class for large image 8804 * - `md-media-xl` - Class for extra large image 8805 * * `<md-card-content>` - Card content 8806 * * `<md-card-actions>` - Card actions 8807 * - `<md-card-icon-actions>` - Icon actions 8808 * 8809 * Cards have constant width and variable heights; where the maximum height is limited to what can 8810 * fit within a single view on a platform, but it can temporarily expand as needed. 8811 * 8812 * @usage 8813 * ### Card with optional footer 8814 * <hljs lang="html"> 8815 * <md-card> 8816 * <img src="card-image.png" class="md-card-image" alt="image caption"> 8817 * <md-card-content> 8818 * <h2>Card headline</h2> 8819 * <p>Card content</p> 8820 * </md-card-content> 8821 * <md-card-footer> 8822 * Card footer 8823 * </md-card-footer> 8824 * </md-card> 8825 * </hljs> 8826 * 8827 * ### Card with actions 8828 * <hljs lang="html"> 8829 * <md-card> 8830 * <img src="card-image.png" class="md-card-image" alt="image caption"> 8831 * <md-card-content> 8832 * <h2>Card headline</h2> 8833 * <p>Card content</p> 8834 * </md-card-content> 8835 * <md-card-actions layout="row" layout-align="end center"> 8836 * <md-button>Action 1</md-button> 8837 * <md-button>Action 2</md-button> 8838 * </md-card-actions> 8839 * </md-card> 8840 * </hljs> 8841 * 8842 * ### Card with header, image, title actions and content 8843 * <hljs lang="html"> 8844 * <md-card> 8845 * <md-card-header> 8846 * <md-card-avatar> 8847 * <img class="md-user-avatar" src="avatar.png"/> 8848 * </md-card-avatar> 8849 * <md-card-header-text>
8850 * <span class="md-title">Title</span> 8851 * <span class="md-subhead">Sub header</span> 8852 * </md-card-header-text> 8853 * </md-card-header> 8854 * <img ng-src="card-image.png" class="md-card-image" alt="image caption"> 8855 * <md-card-title> 8856 * <md-card-title-text> 8857 * <span class="md-headline">Card headline</span> 8858 * <span class="md-subhead">Card subheader</span> 8859 * </md-card-title-text> 8860 * </md-card-title> 8861 * <md-card-actions layout="row" layout-align="start center"> 8862 * <md-button>Action 1</md-button> 8863 * <md-button>Action 2</md-button> 8864 * <md-card-icon-actions> 8865 * <md-button class="md-icon-button" aria-label="icon"> 8866 * <md-icon md-svg-icon="icon"></md-icon> 8867 * </md-button> 8868 * </md-card-icon-actions> 8869 * </md-card-actions> 8870 * <md-card-content> 8871 * <p> 8872 * Card content 8873 * </p> 8874 * </md-card-content> 8875 * </md-card> 8876 * </hljs> 8877 */ 8878function mdCardDirective($mdTheming) { 8879 return { 8880 restrict: 'E', 8881 link: function ($scope, $element, attr) { 8882 $element.addClass('_md'); // private md component indicator for styling 8883 $mdTheming($element); 8884 } 8885 }; 8886} 8887 8888})(); 8889(function(){ 8890"use strict"; 8891 8892/** 8893 * @ngdoc module 8894 * @name material.components.checkbox 8895 * @description Checkbox module! 8896 */ 8897MdCheckboxDirective.$inject = ["inputDirective", "$mdAria", "$mdConstant", "$mdTheming", "$mdUtil", "$mdInteraction"]; 8898angular 8899 .module('material.components.checkbox', ['material.core']) 8900 .directive('mdCheckbox', MdCheckboxDirective); 8901 8902/** 8903 * @ngdoc directive 8904 * @name mdCheckbox 8905 * @module material.components.checkbox 8906 * @restrict E 8907 * 8908 * @description 8909 * The checkbox directive is used like the normal [angular checkbox](https://docs.angularjs.org/api/ng/input/input%5Bcheckbox%5D). 8910 * 8911 * As per the [material design spec](http://www.google.com/design/spec/style/color.html#color-color-schemes) 8912 * the checkbox is in the accent color by default. The primary color palette may be used with 8913 * the `md-primary` class. 8914 * 8915 * @param {string} ng-model Assignable angular expression to data-bind to. 8916 * @param {string=} name Property name of the form under which the control is published. 8917 * @param {expression=} ng-true-value The value to which the expression should be set when selected. 8918 * @param {expression=} ng-false-value The value to which the expression should be set when not selected. 8919 * @param {string=} ng-change AngularJS expression to be executed when input changes due to user interaction with the input element. 8920 * @param {boolean=} md-no-ink Use of attribute indicates use of ripple ink effects 8921 * @param {string=} aria-label Adds label to checkbox for accessibility. 8922 * Defaults to checkbox's text. If no default text is found, a warning will be logged. 8923 * @param {expression=} md-indeterminate This determines when the checkbox should be rendered as 'indeterminate'. 8924 * If a truthy expression or no value is passed in the checkbox renders in the md-indeterminate state. 8925 * If falsy expression is passed in it just looks like a normal unchecked checkbox. 8926 * The indeterminate, checked, and unchecked states are mutually exclusive. A box cannot be in any two states at the same time. 8927 * Adding the 'md-indeterminate' attribute overrides any checked/unchecked rendering logic. 8928 * When using the 'md-indeterminate' attribute use 'ng-checked' to define rendering logic instead of using 'ng-model'. 8929 * @param {expression=} ng-checked If this expression evaluates as truthy, the 'md-checked' css class is added to the checkbox and it 8930 * will appear checked. 8931 * 8932 * @usage 8933 * <hljs lang="html"> 8934 * <md-checkbox ng-model="isChecked" aria-label="Finished?"> 8935 * Finished ? 8936 * </md-checkbox> 8937 * 8938 * <md-checkbox md-no-ink ng-model="hasInk" aria-label="No Ink Effects"> 8939 * No Ink Effects 8940 * </md-checkbox> 8941 * 8942 * <md-checkbox ng-disabled="true" ng-model="isDisabled" aria-label="Disabled"> 8943 * Disabled 8944 * </md-checkbox> 8945 * 8946 * </hljs> 8947 * 8948 */ 8949function MdCheckboxDirective(inputDirective, $mdAria, $mdConstant, $mdTheming, $mdUtil, $mdInteraction) { 8950 inputDirective = inputDirective[0]; 8951 8952 return { 8953 restrict: 'E', 8954 transclude: true, 8955 require: ['^?mdInputContainer', '?ngModel', '?^form'], 8956 priority: $mdConstant.BEFORE_NG_ARIA, 8957 template: 8958 '<div class="md-container" md-ink-ripple md-ink-ripple-checkbox>' + 8959 '<div class="md-icon"></div>' + 8960 '</div>' + 8961 '<div ng-transclude class="md-label"></div>', 8962 compile: compile 8963 }; 8964 8965 // ********************************************************** 8966 // Private Methods 8967 // ********************************************************** 8968
vendor: 17,759 bytes, lines 8969-9542
8969 function compile (tElement, tAttrs) { 8970 tAttrs.$set('tabindex', tAttrs.tabindex || '0'); 8971 tAttrs.$set('type', 'checkbox'); 8972 tAttrs.$set('role', tAttrs.type); 8973 8974 return { 8975 pre: function(scope, element) { 8976 // Attach a click handler during preLink, in order to immediately stop propagation 8977 // (especially for ng-click) when the checkbox is disabled. 8978 element.on('click', function(e) { 8979 if (this.hasAttribute('disabled')) { 8980 e.stopImmediatePropagation(); 8981 } 8982 }); 8983 }, 8984 post: postLink 8985 }; 8986 8987 function postLink(scope, element, attr, ctrls) { 8988 var isIndeterminate; 8989 var containerCtrl = ctrls[0]; 8990 var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel(); 8991 var formCtrl = ctrls[2]; 8992 8993 if (containerCtrl) { 8994 var isErrorGetter = containerCtrl.isErrorGetter || function() { 8995 return ngModelCtrl.$invalid && (ngModelCtrl.$touched || (formCtrl && formCtrl.$submitted)); 8996 }; 8997 8998 containerCtrl.input = element; 8999 9000 scope.$watch(isErrorGetter, containerCtrl.setInvalid); 9001 } 9002 9003 $mdTheming(element); 9004 9005 // Redirect focus events to the root element, because IE11 is always focusing the container element instead 9006 // of the md-checkbox element. This causes issues when using ngModelOptions: `updateOnBlur` 9007 element.children().on('focus', function() { 9008 element.focus(); 9009 }); 9010 9011 if ($mdUtil.parseAttributeBoolean(attr.mdIndeterminate)) { 9012 setIndeterminateState(); 9013 scope.$watch(attr.mdIndeterminate, setIndeterminateState); 9014 } 9015 9016 if (attr.ngChecked) { 9017 scope.$watch(scope.$eval.bind(scope, attr.ngChecked), function(value) { 9018 ngModelCtrl.$setViewValue(value); 9019 ngModelCtrl.$render(); 9020 }); 9021 } 9022 9023 $$watchExpr('ngDisabled', 'tabindex', { 9024 true: '-1', 9025 false: attr.tabindex 9026 }); 9027 9028 $mdAria.expectWithText(element, 'aria-label'); 9029 9030 // Reuse the original input[type=checkbox] directive from AngularJS core. 9031 // This is a bit hacky as we need our own event listener and own render 9032 // function. 9033 inputDirective.link.pre(scope, { 9034 on: angular.noop, 9035 0: {} 9036 }, attr, [ngModelCtrl]); 9037 9038 element.on('click', listener) 9039 .on('keypress', keypressHandler) 9040 .on('focus', function() { 9041 if ($mdInteraction.getLastInteractionType() === 'keyboard') { 9042 element.addClass('md-focused'); 9043 } 9044 }) 9045 .on('blur', function() { 9046 element.removeClass('md-focused'); 9047 }); 9048 9049 ngModelCtrl.$render = render; 9050 9051 function $$watchExpr(expr, htmlAttr, valueOpts) { 9052 if (attr[expr]) { 9053 scope.$watch(attr[expr], function(val) { 9054 if (valueOpts[val]) { 9055 element.attr(htmlAttr, valueOpts[val]); 9056 } 9057 }); 9058 } 9059 } 9060 9061 function keypressHandler(ev) { 9062 var keyCode = ev.which || ev.keyCode; 9063 if (keyCode === $mdConstant.KEY_CODE.SPACE || keyCode === $mdConstant.KEY_CODE.ENTER) { 9064 ev.preventDefault(); 9065 element.addClass('md-focused'); 9066 listener(ev); 9067 } 9068 } 9069 9070 function listener(ev) { 9071 // skipToggle boolean is used by the switch directive to prevent the click event 9072 // when releasing the drag. There will be always a click if releasing the drag over the checkbox 9073 if (element[0].hasAttribute('disabled') || scope.skipToggle) { 9074 return; 9075 } 9076 9077 scope.$apply(function() { 9078 // Toggle the checkbox value... 9079 var viewValue = attr.ngChecked && attr.ngClick ? attr.checked : !ngModelCtrl.$viewValue; 9080 9081 ngModelCtrl.$setViewValue(viewValue, ev && ev.type); 9082 ngModelCtrl.$render(); 9083 }); 9084 } 9085 9086 function render() { 9087 // Cast the $viewValue to a boolean since it could be undefined 9088 element.toggleClass('md-checked', !!ngModelCtrl.$viewValue && !isIndeterminate); 9089 } 9090 9091 function setIndeterminateState(newValue) { 9092 isIndeterminate = newValue !== false; 9093 if (isIndeterminate) { 9094 element.attr('aria-checked', 'mixed'); 9095 } 9096 element.toggleClass('md-indeterminate', isIndeterminate); 9097 } 9098 } 9099 } 9100} 9101 9102})(); 9103(function(){ 9104"use strict"; 9105 9106/** 9107 * @ngdoc module 9108 * @name material.components.chips 9109 */ 9110/* 9111 * @see js folder for chips implementation 9112 */ 9113angular.module('material.components.chips', [ 9114 'material.core', 9115 'material.components.autocomplete' 9116]); 9117 9118})(); 9119(function(){ 9120"use strict"; 9121 9122(function () { 9123 "use strict"; 9124 9125 /** 9126 * Use a RegExp to check if the `md-colors="<expression>"` is static string 9127 * or one that should be observed and dynamically interpolated. 9128 */ 9129 MdColorsDirective.$inject = ["$mdColors", "$mdUtil", "$log", "$parse"]; 9130 MdColorsService.$inject = ["$mdTheming", "$mdUtil", "$log"]; 9131 var STATIC_COLOR_EXPRESSION = /^{((\s|,)*?["'a-zA-Z-]+?\s*?:\s*?('|")[a-zA-Z0-9-.]*('|"))+\s*}$/; 9132 var colorPalettes = null; 9133 9134 /** 9135 * @ngdoc module 9136 * @name material.components.colors 9137 * 9138 * @description 9139 * Define $mdColors service and a `md-colors=""` attribute directive 9140 */ 9141 angular 9142 .module('material.components.colors', ['material.core']) 9143 .directive('mdColors', MdColorsDirective) 9144 .service('$mdColors', MdColorsService); 9145 9146 /** 9147 * @ngdoc service 9148 * @name $mdColors 9149 * @module material.components.colors 9150 * 9151 * @description 9152 * With only defining themes, one couldn't get non AngularJS Material elements colored with Material colors, 9153 * `$mdColors` service is used by the md-color directive to convert the 1..n color expressions to RGBA values and will apply 9154 * those values to element as CSS property values. 9155 * 9156 * @usage 9157 * <hljs lang="js"> 9158 * angular.controller('myCtrl', function ($mdColors) { 9159 * var color = $mdColors.getThemeColor('myTheme-red-200-0.5'); 9160 * ... 9161 * }); 9162 * </hljs> 9163 * 9164 */ 9165 function MdColorsService($mdTheming, $mdUtil, $log) { 9166 colorPalettes = colorPalettes || Object.keys($mdTheming.PALETTES); 9167 9168 // Publish service instance 9169 return { 9170 applyThemeColors: applyThemeColors, 9171 getThemeColor: getThemeColor, 9172 hasTheme: hasTheme 9173 }; 9174 9175 // ******************************************** 9176 // Internal Methods 9177 // ******************************************** 9178 9179 /** 9180 * @ngdoc method 9181 * @name $mdColors#applyThemeColors 9182 * 9183 * @description 9184 * Gets a color json object, keys are css properties and values are string of the wanted color 9185 * Then calculate the rgba() values based on the theme color parts 9186 * 9187 * @param {DOMElement} element the element to apply the styles on. 9188 * @param {object} colorExpression json object, keys are css properties and values are string of the wanted color, 9189 * for example: `{color: 'red-A200-0.3'}`. 9190 * 9191 * @usage 9192 * <hljs lang="js"> 9193 * app.directive('myDirective', function($mdColors) { 9194 * return { 9195 * ... 9196 * link: function (scope, elem) { 9197 * $mdColors.applyThemeColors(elem, {color: 'red'}); 9198 * } 9199 * } 9200 * }); 9201 * </hljs> 9202 */ 9203 function applyThemeColors(element, colorExpression) { 9204 try { 9205 if (colorExpression) { 9206 // Assign the calculate RGBA color values directly as inline CSS 9207 element.css(interpolateColors(colorExpression)); 9208 } 9209 } catch (e) { 9210 $log.error(e.message); 9211 } 9212 9213 } 9214 9215 /** 9216 * @ngdoc method 9217 * @name $mdColors#getThemeColor 9218 * 9219 * @description 9220 * Get parsed color from expression 9221 * 9222 * @param {string} expression string of a color expression (for instance `'red-700-0.8'`) 9223 * 9224 * @returns {string} a css color expression (for instance `rgba(211, 47, 47, 0.8)`) 9225 * 9226 * @usage 9227 * <hljs lang="js"> 9228 * angular.controller('myCtrl', function ($mdColors) { 9229 * var color = $mdColors.getThemeColor('myTheme-red-200-0.5'); 9230 * ... 9231 * }); 9232 * </hljs> 9233 */ 9234 function getThemeColor(expression) { 9235 var color = extractColorOptions(expression); 9236 9237 return parseColor(color); 9238 } 9239 9240 /** 9241 * Return the parsed color 9242 * @param color hashmap of color definitions 9243 * @param contrast whether use contrast color for foreground 9244 * @returns rgba color string 9245 */ 9246 function parseColor(color, contrast) { 9247 contrast = contrast || false; 9248 var rgbValues = $mdTheming.PALETTES[color.palette][color.hue]; 9249 9250 rgbValues = contrast ? rgbValues.contrast : rgbValues.value; 9251 9252 return $mdUtil.supplant('rgba({0}, {1}, {2}, {3})', 9253 [rgbValues[0], rgbValues[1], rgbValues[2], rgbValues[3] || color.opacity] 9254 ); 9255 } 9256 9257 /** 9258 * Convert the color expression into an object with scope-interpolated values 9259 * Then calculate the rgba() values based on the theme color parts 9260 * 9261 * @results Hashmap of CSS properties with associated `rgba( )` string vales 9262 * 9263 * 9264 */ 9265 function interpolateColors(themeColors) { 9266 var rgbColors = {}; 9267 9268 var hasColorProperty = themeColors.hasOwnProperty('color'); 9269 9270 angular.forEach(themeColors, function (value, key) { 9271 var color = extractColorOptions(value); 9272 var hasBackground = key.indexOf('background') > -1; 9273 9274 rgbColors[key] = parseColor(color); 9275 if (hasBackground && !hasColorProperty) { 9276 rgbColors.color = parseColor(color, true); 9277 } 9278 }); 9279 9280 return rgbColors; 9281 } 9282 9283 /** 9284 * Check if expression has defined theme 9285 * e.g. 9286 * 'myTheme-primary' => true 9287 * 'red-800' => false 9288 */ 9289 function hasTheme(expression) { 9290 return angular.isDefined($mdTheming.THEMES[expression.split('-')[0]]); 9291 } 9292 9293 /** 9294 * For the evaluated expression, extract the color parts into a hash map 9295 */ 9296 function extractColorOptions(expression) { 9297 var parts = expression.split('-'); 9298 var hasTheme = angular.isDefined($mdTheming.THEMES[parts[0]]); 9299 var theme = hasTheme ? parts.splice(0, 1)[0] : $mdTheming.defaultTheme(); 9300 9301 return { 9302 theme: theme, 9303 palette: extractPalette(parts, theme), 9304 hue: extractHue(parts, theme), 9305 opacity: parts[2] || 1 9306 }; 9307 } 9308 9309 /** 9310 * Calculate the theme palette name 9311 */ 9312 function extractPalette(parts, theme) { 9313 // If the next section is one of the palettes we assume it's a two word palette 9314 // Two word palette can be also written in camelCase, forming camelCase to dash-case 9315 9316 var isTwoWord = parts.length > 1 && colorPalettes.indexOf(parts[1]) !== -1; 9317 var palette = parts[0].replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); 9318 9319 if (isTwoWord) palette = parts[0] + '-' + parts.splice(1, 1); 9320 9321 if (colorPalettes.indexOf(palette) === -1) { 9322 // If the palette is not in the palette list it's one of primary/accent/warn/background 9323 var scheme = $mdTheming.THEMES[theme].colors[palette]; 9324 if (!scheme) { 9325 throw new Error($mdUtil.supplant('mdColors: couldn\'t find \'{palette}\' in the palettes.', {palette: palette})); 9326 } 9327 palette = scheme.name; 9328 } 9329 9330 return palette; 9331 } 9332 9333 function extractHue(parts, theme) { 9334 var themeColors = $mdTheming.THEMES[theme].colors; 9335 9336 if (parts[1] === 'hue') { 9337 var hueNumber = parseInt(parts.splice(2, 1)[0], 10); 9338 9339 if (hueNumber < 1 || hueNumber > 3) { 9340 throw new Error($mdUtil.supplant('mdColors: \'hue-{hueNumber}\' is not a valid hue, can be only \'hue-1\', \'hue-2\' and \'hue-3\'', {hueNumber: hueNumber})); 9341 } 9342 parts[1] = 'hue-' + hueNumber; 9343 9344 if (!(parts[0] in themeColors)) { 9345 throw new Error($mdUtil.supplant('mdColors: \'hue-x\' can only be used with [{availableThemes}], but was used with \'{usedTheme}\'', { 9346 availableThemes: Object.keys(themeColors).join(', '), 9347 usedTheme: parts[0] 9348 })); 9349 } 9350 9351 return themeColors[parts[0]].hues[parts[1]]; 9352 } 9353 9354 return parts[1] || themeColors[parts[0] in themeColors ? parts[0] : 'primary'].hues['default']; 9355 } 9356 } 9357 9358 /** 9359 * @ngdoc directive 9360 * @name mdColors 9361 * @module material.components.colors 9362 * 9363 * @restrict A 9364 * 9365 * @description 9366 * `mdColors` directive will apply the theme-based color expression as RGBA CSS style values. 9367 * 9368 * The format will be similar to our color defining in the scss files: 9369 * 9370 * ## `[?theme]-[palette]-[?hue]-[?opacity]` 9371 * - [theme] - default value is the default theme 9372 * - [palette] - can be either palette name or primary/accent/warn/background 9373 * - [hue] - default is 500 (hue-x can be used with primary/accent/warn/background) 9374 * - [opacity] - default is 1 9375 * 9376 * > `?` indicates optional parameter 9377 * 9378 * @usage 9379 * <hljs lang="html"> 9380 * <div md-colors="{background: 'myTheme-accent-900-0.43'}"> 9381 * <div md-colors="{color: 'red-A100', 'border-color': 'primary-600'}"> 9382 * <span>Color demo</span> 9383 * </div> 9384 * </div> 9385 * </hljs> 9386 * 9387 * `mdColors` directive will automatically watch for changes in the expression if it recognizes an interpolation 9388 * expression or a function. For performance options, you can use `::` prefix to the `md-colors` expression 9389 * to indicate a one-time data binding. 9390 * <hljs lang="html"> 9391 * <md-card md-colors="::{background: '{{theme}}-primary-700'}"> 9392 * </md-card> 9393 * </hljs> 9394 * 9395 */ 9396 function MdColorsDirective($mdColors, $mdUtil, $log, $parse) { 9397 return { 9398 restrict: 'A', 9399 require: ['^?mdTheme'], 9400 compile: function (tElem, tAttrs) { 9401 var shouldWatch = shouldColorsWatch(); 9402 9403 return function (scope, element, attrs, ctrl) { 9404 var mdThemeController = ctrl[0]; 9405 9406 var lastColors = {}; 9407 9408 var parseColors = function (theme) { 9409 if (typeof theme !== 'string') { 9410 theme = ''; 9411 } 9412 9413 if (!attrs.mdColors) { 9414 attrs.mdColors = '{}'; 9415 } 9416 9417 /** 9418 * Json.parse() does not work because the keys are not quoted; 9419 * use $parse to convert to a hash map 9420 */ 9421 var colors = $parse(attrs.mdColors)(scope); 9422 9423 /** 9424 * If mdTheme is defined up the DOM tree 9425 * we add mdTheme theme to colors who doesn't specified a theme 9426 * 9427 * # example 9428 * <hljs lang="html"> 9429 * <div md-theme="myTheme"> 9430 * <div md-colors="{background: 'primary-600'}"> 9431 * <span md-colors="{background: 'mySecondTheme-accent-200'}">Color demo</span> 9432 * </div> 9433 * </div> 9434 * </hljs> 9435 * 9436 * 'primary-600' will be 'myTheme-primary-600', 9437 * but 'mySecondTheme-accent-200' will stay the same cause it has a theme prefix 9438 */ 9439 if (mdThemeController) { 9440 Object.keys(colors).forEach(function (prop) { 9441 var color = colors[prop]; 9442 if (!$mdColors.hasTheme(color)) { 9443 colors[prop] = (theme || mdThemeController.$mdTheme) + '-' + color; 9444 } 9445 }); 9446 } 9447 9448 cleanElement(colors); 9449 9450 return colors; 9451 }; 9452 9453 var cleanElement = function (colors) { 9454 if (!angular.equals(colors, lastColors)) { 9455 var keys = Object.keys(lastColors); 9456 9457 if (lastColors.background && !keys.color) { 9458 keys.push('color'); 9459 } 9460 9461 keys.forEach(function (key) { 9462 element.css(key, ''); 9463 }); 9464 } 9465 9466 lastColors = colors; 9467 }; 9468 9469 /** 9470 * Registering for mgTheme changes and asking mdTheme controller run our callback whenever a theme changes 9471 */ 9472 var unregisterChanges = angular.noop; 9473 9474 if (mdThemeController) { 9475 unregisterChanges = mdThemeController.registerChanges(function (theme) { 9476 $mdColors.applyThemeColors(element, parseColors(theme)); 9477 }); 9478 } 9479 9480 scope.$on('$destroy', function () { 9481 unregisterChanges(); 9482 }); 9483 9484 try { 9485 if (shouldWatch) { 9486 scope.$watch(parseColors, angular.bind(this, 9487 $mdColors.applyThemeColors, element 9488 ), true); 9489 } 9490 else { 9491 $mdColors.applyThemeColors(element, parseColors()); 9492 } 9493 9494 } 9495 catch (e) { 9496 $log.error(e.message); 9497 } 9498 9499 }; 9500 9501 function shouldColorsWatch() { 9502 // Simulate 1x binding and mark mdColorsWatch == false 9503 var rawColorExpression = tAttrs.mdColors; 9504 var bindOnce = rawColorExpression.indexOf('::') > -1; 9505 var isStatic = bindOnce ? true : STATIC_COLOR_EXPRESSION.test(tAttrs.mdColors); 9506 9507 // Remove it for the postLink... 9508 tAttrs.mdColors = rawColorExpression.replace('::', ''); 9509 9510 var hasWatchAttr = angular.isDefined(tAttrs.mdColorsWatch); 9511 9512 return (bindOnce || isStatic) ? false : 9513 hasWatchAttr ? $mdUtil.parseAttributeBoolean(tAttrs.mdColorsWatch) : true; 9514 } 9515 } 9516 }; 9517 9518 } 9519 9520 9521})(); 9522 9523})(); 9524(function(){ 9525"use strict"; 9526 9527/** 9528 * @ngdoc module 9529 * @name material.components.content 9530 * 9531 * @description 9532 * Scrollable content 9533 */ 9534mdContentDirective.$inject = ["$mdTheming"]; 9535angular.module('material.components.content', [ 9536 'material.core' 9537]) 9538 .directive('mdContent', mdContentDirective); 9539 9540/** 9541 * @ngdoc directive 9542 * @name mdContent
9543 * @module material.components.content 9544 * 9545 * @restrict E 9546 * 9547 * @description 9548 * 9549 * The `<md-content>` directive is a container element useful for scrollable content. It achieves 9550 * this by setting the CSS `overflow` property to `auto` so that content can properly scroll. 9551 * 9552 * In general, `<md-content>` components are not designed to be nested inside one another. If 9553 * possible, it is better to make them siblings. This often results in a better user experience as 9554 * having nested scrollbars may confuse the user. 9555 * 9556 * ## Troubleshooting 9557 * 9558 * In some cases, you may wish to apply the `md-no-momentum` class to ensure that Safari's 9559 * momentum scrolling is disabled. Momentum scrolling can cause flickering issues while scrolling 9560 * SVG icons and some other components. 9561 * 9562 * Additionally, we now also offer the `md-no-flicker` class which can be applied to any element 9563 * and uses a Webkit-specific filter of `blur(0px)` that forces GPU rendering of all elements 9564 * inside (which eliminates the flicker on iOS devices). 9565 * 9566 * _<b>Note:</b> Forcing an element to render on the GPU can have unintended side-effects, especially 9567 * related to the z-index of elements. Please use with caution and only on the elements needed._ 9568 * 9569 * @usage 9570 * 9571 * Add the `[layout-padding]` attribute to make the content padded. 9572 * 9573 * <hljs lang="html"> 9574 * <md-content layout-padding> 9575 * Lorem ipsum dolor sit amet, ne quod novum mei. 9576 * </md-content> 9577 * </hljs> 9578 */ 9579 9580function mdContentDirective($mdTheming) { 9581 return { 9582 restrict: 'E', 9583 controller: ['$scope', '$element', ContentController], 9584 link: function(scope, element) { 9585 element.addClass('_md'); // private md component indicator for styling 9586 9587 $mdTheming(element); 9588 scope.$broadcast('$mdContentLoaded', element); 9589 9590 iosScrollFix(element[0]); 9591 } 9592 }; 9593 9594 function ContentController($scope, $element) { 9595 this.$scope = $scope; 9596 this.$element = $element; 9597 } 9598} 9599 9600function iosScrollFix(node) { 9601 // IOS FIX: 9602 // If we scroll where there is no more room for the webview to scroll, 9603 // by default the webview itself will scroll up and down, this looks really 9604 // bad. So if we are scrolling to the very top or bottom, add/subtract one 9605 angular.element(node).on('$md.pressdown', function(ev) { 9606 // Only touch events 9607 if (ev.pointer.type !== 't') return; 9608 // Don't let a child content's touchstart ruin it for us. 9609 if (ev.$materialScrollFixed) return; 9610 ev.$materialScrollFixed = true; 9611 9612 if (node.scrollTop === 0) { 9613 node.scrollTop = 1; 9614 } else if (node.scrollHeight === node.scrollTop + node.offsetHeight) { 9615 node.scrollTop -= 1; 9616 } 9617 }); 9618} 9619 9620})(); 9621(function(){ 9622"use strict"; 9623 9624/** 9625 * @ngdoc module 9626 * @name material.components.datepicker 9627 * @description Module for the datepicker component. 9628 */ 9629 9630angular.module('material.components.datepicker', [ 9631 'material.core', 9632 'material.components.icon', 9633 'material.components.virtualRepeat' 9634]); 9635 9636})(); 9637(function(){ 9638"use strict"; 9639 9640/** 9641 * @ngdoc module 9642 * @name material.components.dialog 9643 */ 9644MdDialogDirective.$inject = ["$$rAF", "$mdTheming", "$mdDialog"]; 9645MdDialogProvider.$inject = ["$$interimElementProvider"]; 9646angular 9647 .module('material.components.dialog', [ 9648 'material.core', 9649 'material.components.backdrop' 9650 ]) 9651 .directive('mdDialog', MdDialogDirective) 9652 .provider('$mdDialog', MdDialogProvider); 9653 9654/** 9655 * @ngdoc directive 9656 * @name mdDialog 9657 * @module material.components.dialog 9658 * 9659 * @restrict E 9660 * 9661 * @description 9662 * `<md-dialog>` - The dialog's template must be inside this element. 9663 * 9664 * Inside, use an `<md-dialog-content>` element for the dialog's content, and use 9665 * an `<md-dialog-actions>` element for the dialog's actions. 9666 * 9667 * ## CSS 9668 * - `.md-dialog-content` - class that sets the padding on the content as the spec file 9669 * 9670 * ## Notes 9671 * - If you specify an `id` for the `<md-dialog>`, the `<md-dialog-content>` will have the same `id` 9672 * prefixed with `dialogContent_`. 9673 * 9674 * @usage 9675 * ### Dialog template 9676 * <hljs lang="html"> 9677 * <md-dialog aria-label="List dialog"> 9678 * <md-dialog-content> 9679 * <md-list> 9680 * <md-list-item ng-repeat="item in items"> 9681 * <p>Number {{item}}</p> 9682 * </md-list-item> 9683 * </md-list> 9684 * </md-dialog-content> 9685 * <md-dialog-actions>
9686 * <md-button ng-click="closeDialog()" class="md-primary">Close Dialog</md-button> 9687 * </md-dialog-actions> 9688 * </md-dialog> 9689 * </hljs> 9690 */ 9691function MdDialogDirective($$rAF, $mdTheming, $mdDialog) { 9692 return { 9693 restrict: 'E', 9694 link: function(scope, element) { 9695 element.addClass('_md'); // private md component indicator for styling 9696 9697 $mdTheming(element); 9698 $$rAF(function() { 9699 var images; 9700 var content = element[0].querySelector('md-dialog-content'); 9701 9702 if (content) { 9703 images = content.getElementsByTagName('img'); 9704 addOverflowClass(); 9705 //-- delayed image loading may impact scroll height, check after images are loaded 9706 angular.element(images).on('load', addOverflowClass); 9707 } 9708 9709 scope.$on('$destroy', function() { 9710 $mdDialog.destroy(element); 9711 }); 9712 9713 /** 9714 * 9715 */ 9716 function addOverflowClass() { 9717 element.toggleClass('md-content-overflow', content.scrollHeight > content.clientHeight); 9718 } 9719 9720 9721 }); 9722 } 9723 }; 9724} 9725 9726/** 9727 * @ngdoc service 9728 * @name $mdDialog 9729 * @module material.components.dialog 9730 * 9731 * @description 9732 * `$mdDialog` opens a dialog over the app to inform users about critical information or require 9733 * them to make decisions. There are two approaches for setup: a simple promise API 9734 * and regular object syntax. 9735 * 9736 * ## Restrictions 9737 * 9738 * - The dialog is always given an isolate scope. 9739 * - The dialog's template must have an outer `<md-dialog>` element. 9740 * Inside, use an `<md-dialog-content>` element for the dialog's content, and use 9741 * an `<md-dialog-actions>` element for the dialog's actions. 9742 * - Dialogs must cover the entire application to keep interactions inside of them. 9743 * Use the `parent` option to change where dialogs are appended. 9744 * 9745 * ## Sizing 9746 * - Complex dialogs can be sized with `flex="percentage"`, i.e. `flex="66"`. 9747 * - Default max-width is 80% of the `rootElement` or `parent`. 9748 * 9749 * ## CSS 9750 * - `.md-dialog-content` - class that sets the padding on the content as the spec file 9751 * 9752 * @usage 9753 * <hljs lang="html"> 9754 * <div ng-app="demoApp" ng-controller="EmployeeController"> 9755 * <div> 9756 * <md-button ng-click="showAlert()" class="md-raised md-warn"> 9757 * Employee Alert! 9758 * </md-button> 9759 * </div> 9760 * <div> 9761 * <md-button ng-click="showDialog($event)" class="md-raised"> 9762 * Custom Dialog 9763 * </md-button> 9764 * </div> 9765 * <div> 9766 * <md-button ng-click="closeAlert()" ng-disabled="!hasAlert()" class="md-raised"> 9767 * Close Alert 9768 * </md-button> 9769 * </div> 9770 * <div> 9771 * <md-button ng-click="showGreeting($event)" class="md-raised md-primary" > 9772 * Greet Employee 9773 * </md-button> 9774 * </div> 9775 * </div> 9776 * </hljs> 9777 * 9778 * ### JavaScript: object syntax 9779 * <hljs lang="js"> 9780 * (function(angular, undefined){ 9781 * "use strict"; 9782 * 9783 * angular 9784 * .module('demoApp', ['ngMaterial']) 9785 * .controller('AppCtrl', AppController); 9786 * 9787 * function AppController($scope, $mdDialog) { 9788 * var alert; 9789 * $scope.showAlert = showAlert; 9790 * $scope.showDialog = showDialog; 9791 * $scope.items = [1, 2, 3]; 9792 * 9793 * // Internal method 9794 * function showAlert() { 9795 * alert = $mdDialog.alert({ 9796 * title: 'Attention', 9797 * textContent: 'This is an example of how easy dialogs can be!', 9798 * ok: 'Close' 9799 * }); 9800 * 9801 * $mdDialog 9802 * .show( alert ) 9803 * .finally(function() { 9804 * alert = undefined; 9805 * }); 9806 * } 9807 * 9808 * function showDialog($event) { 9809 * var parentEl = angular.element(document.body); 9810 * $mdDialog.show({ 9811 * parent: parentEl, 9812 * targetEvent: $event, 9813 * template: 9814 * '<md-dialog aria-label="List dialog">' + 9815 * ' <md-dialog-content>'+ 9816 * ' <md-list>'+ 9817 * ' <md-list-item ng-repeat="item in items">'+ 9818 * ' <p>Number {{item}}</p>' + 9819 * ' </md-item>'+ 9820 * ' </md-list>'+ 9821 * ' </md-dialog-content>' + 9822 * ' <md-dialog-actions>' +
9823 * ' <md-button ng-click="closeDialog()" class="md-primary">' + 9824 * ' Close Dialog' + 9825 * ' </md-button>' + 9826 * ' </md-dialog-actions>' + 9827 * '</md-dialog>', 9828 * locals: { 9829 * items: $scope.items 9830 * }, 9831 * controller: DialogController 9832 * }); 9833 * function DialogController($scope, $mdDialog, items) { 9834 * $scope.items = items; 9835 * $scope.closeDialog = function() { 9836 * $mdDialog.hide(); 9837 * } 9838 * } 9839 * } 9840 * } 9841 * })(angular); 9842 * </hljs> 9843 * 9844 * ### Multiple Dialogs 9845 * Using the `multiple` option for the `$mdDialog` service allows developers to show multiple dialogs 9846 * at the same time. 9847 * 9848 * <hljs lang="js"> 9849 * // From plain options 9850 * $mdDialog.show({ 9851 * multiple: true 9852 * }); 9853 * 9854 * // From a dialog preset 9855 * $mdDialog.show( 9856 * $mdDialog 9857 * .alert() 9858 * .multiple(true) 9859 * ); 9860 * 9861 * </hljs> 9862 * 9863 * ### Pre-Rendered Dialogs 9864 * By using the `contentElement` option, it is possible to use an already existing element in the DOM. 9865 * 9866 * > Pre-rendered dialogs will be not linked to any scope and will not instantiate any new controller.<br/> 9867 * > You can manually link the elements to a scope or instantiate a controller from the template (`ng-controller`) 9868 * 9869 * <hljs lang="js"> 9870 * $scope.showPrerenderedDialog = function() { 9871 * $mdDialog.show({ 9872 * contentElement: '#myStaticDialog', 9873 * parent: angular.element(document.body) 9874 * }); 9875 * }; 9876 * </hljs> 9877 * 9878 * When using a string as value, `$mdDialog` will automatically query the DOM for the specified CSS selector. 9879 * 9880 * <hljs lang="html"> 9881 * <div style="visibility: hidden"> 9882 * <div class="md-dialog-container" id="myStaticDialog"> 9883 * <md-dialog> 9884 * This is a pre-rendered dialog. 9885 * </md-dialog> 9886 * </div> 9887 * </div> 9888 * </hljs> 9889 * 9890 * **Notice**: It is important, to use the `.md-dialog-container` as the content element, otherwise the dialog 9891 * will not show up. 9892 * 9893 * It also possible to use a DOM element for the `contentElement` option. 9894 * - `contentElement: document.querySelector('#myStaticDialog')` 9895 * - `contentElement: angular.element(TEMPLATE)` 9896 * 9897 * When using a `template` as content element, it will be not compiled upon open. 9898 * This allows you to compile the element yourself and use it each time the dialog opens. 9899 * 9900 * ### Custom Presets 9901 * Developers are also able to create their own preset, which can be easily used without repeating 9902 * their options each time. 9903 * 9904 * <hljs lang="js"> 9905 * $mdDialogProvider.addPreset('testPreset', { 9906 * options: function() { 9907 * return { 9908 * template: 9909 * '<md-dialog>' + 9910 * 'This is a custom preset' + 9911 * '</md-dialog>', 9912 * controllerAs: 'dialog', 9913 * bindToController: true, 9914 * clickOutsideToClose: true, 9915 * escapeToClose: true 9916 * }; 9917 * } 9918 * }); 9919 * </hljs> 9920 * 9921 * After you created your preset at config phase, you can easily access it. 9922 * 9923 * <hljs lang="js"> 9924 * $mdDialog.show( 9925 * $mdDialog.testPreset() 9926 * ); 9927 * </hljs> 9928 * 9929 * ### JavaScript: promise API syntax, custom dialog template 9930 * <hljs lang="js"> 9931 * (function(angular, undefined){ 9932 * "use strict"; 9933 * 9934 * angular 9935 * .module('demoApp', ['ngMaterial']) 9936 * .controller('EmployeeController', EmployeeEditor) 9937 * .controller('GreetingController', GreetingController); 9938 * 9939 * // Fictitious Employee Editor to show how to use simple and complex dialogs. 9940 * 9941 * function EmployeeEditor($scope, $mdDialog) { 9942 * var alert; 9943 * 9944 * $scope.showAlert = showAlert; 9945 * $scope.closeAlert = closeAlert; 9946 * $scope.showGreeting = showCustomGreeting; 9947 * 9948 * $scope.hasAlert = function() { return !!alert }; 9949 * $scope.userName = $scope.userName || 'Bobby'; 9950 * 9951 * // Dialog #1 - Show simple alert dialog and cache 9952 * // reference to dialog instance 9953 * 9954 * function showAlert() { 9955 * alert = $mdDialog.alert() 9956 * .title('Attention, ' + $scope.userName) 9957 * .textContent('This is an example of how easy dialogs can be!') 9958 * .ok('Close'); 9959 * 9960 * $mdDialog 9961 * .show( alert ) 9962 * .finally(function() { 9963 * alert = undefined; 9964 * }); 9965 * } 9966 * 9967 * // Close the specified dialog instance and resolve with 'finished' flag 9968 * // Normally this is not needed, just use '$mdDialog.hide()' to close 9969 * // the most recent dialog popup. 9970 * 9971 * function closeAlert() { 9972 * $mdDialog.hide( alert, "finished" ); 9973 * alert = undefined; 9974 * } 9975 * 9976 * // Dialog #2 - Demonstrate more complex dialogs construction and popup. 9977 * 9978 * function showCustomGreeting($event) { 9979 * $mdDialog.show({ 9980 * targetEvent: $event, 9981 * template: 9982 * '<md-dialog>' + 9983 * 9984 * ' <md-dialog-content>Hello {{ employee }}!</md-dialog-content>' + 9985 * 9986 * ' <md-dialog-actions>' +
9987 * ' <md-button ng-click="closeDialog()" class="md-primary">' + 9988 * ' Close Greeting' + 9989 * ' </md-button>' + 9990 * ' </md-dialog-actions>' + 9991 * '</md-dialog>', 9992 * controller: 'GreetingController', 9993 * onComplete: afterShowAnimation, 9994 * locals: { employee: $scope.userName } 9995 * }); 9996 * 9997 * // When the 'enter' animation finishes... 9998 * 9999 * function afterShowAnimation(scope, element, options) { 10000 * // post-show code here: DOM element focus, etc. 10001 * } 10002 * } 10003 * 10004 * // Dialog #3 - Demonstrate use of ControllerAs and passing $scope to dialog 10005 * // Here we used ng-controller="GreetingController as vm" and 10006 * // $scope.vm === <controller instance> 10007 * 10008 * function showCustomGreeting() { 10009 * 10010 * $mdDialog.show({ 10011 * clickOutsideToClose: true, 10012 * 10013 * scope: $scope, // use parent scope in template 10014 * preserveScope: true, // do not forget this if use parent scope 10015 10016 * // Since GreetingController is instantiated with ControllerAs syntax 10017 * // AND we are passing the parent '$scope' to the dialog, we MUST 10018 * // use 'vm.<xxx>' in the template markup 10019 * 10020 * template: '<md-dialog>' + 10021 * ' <md-dialog-content>' + 10022 * ' Hi There {{vm.employee}}' + 10023 * ' </md-dialog-content>' + 10024 * '</md-dialog>', 10025 * 10026 * controller: function DialogController($scope, $mdDialog) { 10027 * $scope.closeDialog = function() { 10028 * $mdDialog.hide(); 10029 * } 10030 * } 10031 * }); 10032 * } 10033 * 10034 * } 10035 * 10036 * // Greeting controller used with the more complex 'showCustomGreeting()' custom dialog 10037 * 10038 * function GreetingController($scope, $mdDialog, employee) { 10039 * // Assigned from construction <code>locals</code> options... 10040 * $scope.employee = employee; 10041 * 10042 * $scope.closeDialog = function() { 10043 * // Easily hides most recent dialog shown... 10044 * // no specific instance reference is needed. 10045 * $mdDialog.hide(); 10046 * }; 10047 * } 10048 * 10049 * })(angular); 10050 * </hljs> 10051 */ 10052 10053/** 10054 * @ngdoc method 10055 * @name $mdDialog#alert 10056 * 10057 * @description 10058 * Builds a preconfigured dialog with the specified message. 10059 * 10060 * @returns {obj} an `$mdDialogPreset` with the chainable configuration methods: 10061 * 10062 * - $mdDialogPreset#title(string) - Sets the alert title. 10063 * - $mdDialogPreset#textContent(string) - Sets the alert message. 10064 * - $mdDialogPreset#htmlContent(string) - Sets the alert message as HTML. Requires ngSanitize 10065 * module to be loaded. HTML is not run through Angular's compiler. 10066 * - $mdDialogPreset#ok(string) - Sets the alert "Okay" button text. 10067 * - $mdDialogPreset#theme(string) - Sets the theme of the alert dialog. 10068 * - $mdDialogPreset#targetEvent(DOMClickEvent=) - A click's event object. When passed in as an option, 10069 * the location of the click will be used as the starting point for the opening animation 10070 * of the the dialog. 10071 * 10072 */ 10073 10074/** 10075 * @ngdoc method 10076 * @name $mdDialog#confirm 10077 * 10078 * @description 10079 * Builds a preconfigured dialog with the specified message. You can call show and the promise returned 10080 * will be resolved only if the user clicks the confirm action on the dialog. 10081 * 10082 * @returns {obj} an `$mdDialogPreset` with the chainable configuration methods: 10083 * 10084 * Additionally, it supports the following methods: 10085 * 10086 * - $mdDialogPreset#title(string) - Sets the confirm title. 10087 * - $mdDialogPreset#textContent(string) - Sets the confirm message. 10088 * - $mdDialogPreset#htmlContent(string) - Sets the confirm message as HTML. Requires ngSanitize 10089 * module to be loaded. HTML is not run through Angular's compiler. 10090 * - $mdDialogPreset#ok(string) - Sets the confirm "Okay" button text. 10091 * - $mdDialogPreset#cancel(string) - Sets the confirm "Cancel" button text. 10092 * - $mdDialogPreset#theme(string) - Sets the theme of the confirm dialog. 10093 * - $mdDialogPreset#targetEvent(DOMClickEvent=) - A click's event object. When passed in as an option, 10094 * the location of the click will be used as the starting point for the opening animation 10095 * of the the dialog. 10096 * 10097 */ 10098 10099/** 10100 * @ngdoc method 10101 * @name $mdDialog#prompt 10102 * 10103 * @description 10104 * Builds a preconfigured dialog with the specified message and input box. You can call show and the promise returned 10105 * will be resolved only if the user clicks the prompt action on the dialog, passing the input value as the first argument. 10106 * 10107 * @returns {obj} an `$mdDialogPreset` with the chainable configuration methods: 10108 * 10109 * Additionally, it supports the following methods: 10110 * 10111 * - $mdDialogPreset#title(string) - Sets the prompt title. 10112 * - $mdDialogPreset#textContent(string) - Sets the prompt message. 10113 * - $mdDialogPreset#htmlContent(string) - Sets the prompt message as HTML. Requires ngSanitize 10114 * module to be loaded. HTML is not run through Angular's compiler. 10115 * - $mdDialogPreset#placeholder(string) - Sets the placeholder text for the input. 10116 * - $mdDialogPreset#initialValue(string) - Sets the initial value for the prompt input. 10117 * - $mdDialogPreset#ok(string) - Sets the prompt "Okay" button text. 10118 * - $mdDialogPreset#cancel(string) - Sets the prompt "Cancel" button text. 10119 * - $mdDialogPreset#theme(string) - Sets the theme of the prompt dialog. 10120 * - $mdDialogPreset#targetEvent(DOMClickEvent=) - A click's event object. When passed in as an option, 10121 * the location of the click will be used as the starting point for the opening animation 10122 * of the the dialog. 10123 * 10124 */ 10125 10126/** 10127 * @ngdoc method 10128 * @name $mdDialog#show 10129 * 10130 * @description 10131 * Show a dialog with the specified options. 10132 * 10133 * @param {object} optionsOrPreset Either provide an `$mdDialogPreset` returned from `alert()`, and 10134 * `confirm()`, or an options object with the following properties: 10135 * - `templateUrl` - `{string=}`: The url of a template that will be used as the content 10136 * of the dialog. 10137 * - `template` - `{string=}`: HTML template to show in the dialog. This **must** be trusted HTML 10138 * with respect to Angular's [$sce service](https://docs.angularjs.org/api/ng/service/$sce). 10139 * This template should **never** be constructed with any kind of user input or user data. 10140 * - `contentElement` - `{string|Element}`: Instead of using a template, which will be compiled each time a 10141 * dialog opens, you can also use a DOM element.<br/> 10142 * * When specifying an element, which is present on the DOM, `$mdDialog` will temporary fetch the element into 10143 * the dialog and restores it at the old DOM position upon close. 10144 * * When specifying a string, the string be used as a CSS selector, to lookup for the element in the DOM. 10145 * - `autoWrap` - `{boolean=}`: Whether or not to automatically wrap the template with a 10146 * `<md-dialog>` tag if one is not provided. Defaults to true. Can be disabled if you provide a 10147 * custom dialog directive. 10148 * - `targetEvent` - `{DOMClickEvent=}`: A click's event object. When passed in as an option, 10149 * the location of the click will be used as the starting point for the opening animation 10150 * of the the dialog. 10151 * - `openFrom` - `{string|Element|object}`: The query selector, DOM element or the Rect object 10152 * that is used to determine the bounds (top, left, height, width) from which the Dialog will 10153 * originate. 10154 * - `closeTo` - `{string|Element|object}`: The query selector, DOM element or the Rect object 10155 * that is used to determine the bounds (top, left, height, width) to which the Dialog will 10156 * target. 10157 * - `scope` - `{object=}`: the scope to link the template / controller to. If none is specified, 10158 * it will create a new isolate scope. 10159 * This scope will be destroyed when the dialog is removed unless `preserveScope` is set to true. 10160 * - `preserveScope` - `{boolean=}`: whether to preserve the scope when the element is removed. Default is false 10161 * - `disableParentScroll` - `{boolean=}`: Whether to disable scrolling while the dialog is open. 10162 * Default true. 10163 * - `hasBackdrop` - `{boolean=}`: Whether there should be an opaque backdrop behind the dialog. 10164 * Default true. 10165 * - `clickOutsideToClose` - `{boolean=}`: Whether the user can click outside the dialog to 10166 * close it. Default false. 10167 * - `escapeToClose` - `{boolean=}`: Whether the user can press escape to close the dialog. 10168 * Default true. 10169 * - `focusOnOpen` - `{boolean=}`: An option to override focus behavior on open. Only disable if 10170 * focusing some other way, as focus management is required for dialogs to be accessible. 10171 * Defaults to true. 10172 * - `controller` - `{function|string=}`: The controller to associate with the dialog. The controller 10173 * will be injected with the local `$mdDialog`, which passes along a scope for the dialog. 10174 * - `locals` - `{object=}
10174`: An object containing key/value pairs. The keys will be used as names 10175 * of values to inject into the controller. For example, `locals: {three: 3}` would inject 10176 * `three` into the controller, with the value 3. If `bindToController` is true, they will be 10177 * copied to the controller instead. 10178 * - `bindToController` - `bool`: bind the locals to the controller, instead of passing them in. 10179 * - `controllerAs` - `{string=}`: An alias to assign the controller to on the scope. 10180 * - `parent` - `{element=}`: The element to append the dialog to. Defaults to appending 10181 * to the root element of the application. 10182 * - `onShowing` - `function(scope, element)`: Callback function used to announce the show() action is 10183 * starting. 10184 * - `onComplete` - `function(scope, element)`: Callback function used to announce when the show() action is 10185 * finished. 10186 * - `onRemoving` - `function(element, removePromise)`: Callback function used to announce the 10187 * close/hide() action is starting. This allows developers to run custom animations 10188 * in parallel the close animations. 10189 * - `fullscreen` `{boolean=}`: An option to toggle whether the dialog should show in fullscreen 10190 * or not. Defaults to `false`. 10191 * - `multiple` `{boolean=}`: An option to allow this dialog to display over one that's currently open. 10192 * @returns {promise} A promise that can be resolved with `$mdDialog.hide()` or 10193 * rejected with `$mdDialog.cancel()`. 10194 */ 10195 10196/** 10197 * @ngdoc method 10198 * @name $mdDialog#hide 10199 * 10200 * @description 10201 * Hide an existing dialog and resolve the promise returned from `$mdDialog.show()`. 10202 * 10203 * @param {*=} response An argument for the resolved promise. 10204 * 10205 * @returns {promise} A promise that is resolved when the dialog has been closed. 10206 */ 10207 10208/** 10209 * @ngdoc method 10210 * @name $mdDialog#cancel 10211 * 10212 * @description 10213 * Hide an existing dialog and reject the promise returned from `$mdDialog.show()`. 10214 * 10215 * @param {*=} response An argument for the rejected promise. 10216 * 10217 * @returns {promise} A promise that is resolved when the dialog has been closed. 10218 */ 10219 10220function MdDialogProvider($$interimElementProvider) { 10221 // Elements to capture and redirect focus when the user presses tab at the dialog boundary. 10222 MdDialogController.$inject = ["$mdDialog", "$mdConstant"]; 10223 dialogDefaultOptions.$inject = ["$mdDialog", "$mdAria", "$mdUtil", "$mdConstant", "$animate", "$document", "$window", "$rootElement", "$log", "$injector", "$mdTheming", "$interpolate", "$mdInteraction"]; 10224 var topFocusTrap, bottomFocusTrap; 10225 10226 return $$interimElementProvider('$mdDialog') 10227 .setDefaults({ 10228 methods: ['disableParentScroll', 'hasBackdrop', 'clickOutsideToClose', 'escapeToClose', 10229 'targetEvent', 'closeTo', 'openFrom', 'parent', 'fullscreen', 'multiple'], 10230 options: dialogDefaultOptions 10231 }) 10232 .addPreset('alert', { 10233 methods: ['title', 'htmlContent', 'textContent', 'content', 'ariaLabel', 'ok', 'theme', 10234 'css'], 10235 options: advancedDialogOptions 10236 }) 10237 .addPreset('confirm', { 10238 methods: ['title', 'htmlContent', 'textContent', 'content', 'ariaLabel', 'ok', 'cancel', 10239 'theme', 'css'], 10240 options: advancedDialogOptions 10241 }) 10242 .addPreset('prompt', { 10243 methods: ['title', 'htmlContent', 'textContent', 'initialValue', 'content', 'placeholder', 'ariaLabel', 10244 'ok', 'cancel', 'theme', 'css', 'required'], 10245 options: advancedDialogOptions 10246 }); 10247 10248 /* @ngInject */ 10249 function advancedDialogOptions() { 10250 return { 10251 template: [ 10252 '<md-dialog md-theme="{{ dialog.theme || dialog.defaultTheme }}" aria-label="{{ dialog.ariaLabel }}" ng-class="dialog.css">', 10253 ' <md-dialog-content class="md-dialog-content" role="document" tabIndex="-1">', 10254 ' <h2 class="md-title">{{ dialog.title }}</h2>', 10255 ' <div ng-if="::dialog.mdHtmlContent" class="md-dialog-content-body" ', 10256 ' ng-bind-html="::dialog.mdHtmlContent"></div>', 10257 ' <div ng-if="::!dialog.mdHtmlContent" class="md-dialog-content-body">', 10258 ' <p>{{::dialog.mdTextContent}}</p>', 10259 ' </div>', 10260 ' <md-input-container md-no-float ng-if="::dialog.$type == \'prompt\'" class="md-prompt-input-container">', 10261 ' <input ng-keypress="dialog.keypress($event)" md-autofocus ng-model="dialog.result" ' + 10262 ' placeholder="{{::dialog.placeholder}}" ng-required="dialog.required">', 10263 ' </md-input-container>', 10264 ' </md-dialog-content>', 10265 ' <md-dialog-actions>',
vendor: 45,284 bytes, lines 10266-11641
10266 ' <md-button ng-if="dialog.$type === \'confirm\' || dialog.$type === \'prompt\'"' + 10267 ' ng-click="dialog.abort()" class="md-primary md-cancel-button">', 10268 ' {{ dialog.cancel }}', 10269 ' </md-button>', 10270 ' <md-button ng-click="dialog.hide()" class="md-primary md-confirm-button" md-autofocus="dialog.$type===\'alert\'"' + 10271 ' ng-disabled="dialog.required && !dialog.result">', 10272 ' {{ dialog.ok }}', 10273 ' </md-button>', 10274 ' </md-dialog-actions>', 10275 '</md-dialog>' 10276 ].join('').replace(/\s\s+/g, ''), 10277 controller: MdDialogController, 10278 controllerAs: 'dialog', 10279 bindToController: true, 10280 }; 10281 } 10282 10283 /** 10284 * Controller for the md-dialog interim elements 10285 * @ngInject 10286 */ 10287 function MdDialogController($mdDialog, $mdConstant) { 10288 // For compatibility with AngularJS 1.6+, we should always use the $onInit hook in 10289 // interimElements. The $mdCompiler simulates the $onInit hook for all versions. 10290 this.$onInit = function() { 10291 var isPrompt = this.$type == 'prompt'; 10292 10293 if (isPrompt && this.initialValue) { 10294 this.result = this.initialValue; 10295 } 10296 10297 this.hide = function() { 10298 $mdDialog.hide(isPrompt ? this.result : true); 10299 }; 10300 this.abort = function() { 10301 $mdDialog.cancel(); 10302 }; 10303 this.keypress = function($event) { 10304 if ($event.keyCode === $mdConstant.KEY_CODE.ENTER) { 10305 $mdDialog.hide(this.result); 10306 } 10307 }; 10308 }; 10309 } 10310 10311 /* @ngInject */ 10312 function dialogDefaultOptions($mdDialog, $mdAria, $mdUtil, $mdConstant, $animate, $document, $window, $rootElement, 10313 $log, $injector, $mdTheming, $interpolate, $mdInteraction) { 10314 10315 return { 10316 hasBackdrop: true, 10317 isolateScope: true, 10318 onCompiling: beforeCompile, 10319 onShow: onShow, 10320 onShowing: beforeShow, 10321 onRemove: onRemove, 10322 clickOutsideToClose: false, 10323 escapeToClose: true, 10324 targetEvent: null, 10325 closeTo: null, 10326 openFrom: null, 10327 focusOnOpen: true, 10328 disableParentScroll: true, 10329 autoWrap: true, 10330 fullscreen: false, 10331 transformTemplate: function(template, options) { 10332 // Make the dialog container focusable, because otherwise the focus will be always redirected to 10333 // an element outside of the container, and the focus trap won't work probably.. 10334 // Also the tabindex is needed for the `escapeToClose` functionality, because 10335 // the keyDown event can't be triggered when the focus is outside of the container. 10336 var startSymbol = $interpolate.startSymbol(); 10337 var endSymbol = $interpolate.endSymbol(); 10338 var theme = startSymbol + (options.themeWatch ? '' : '::') + 'theme' + endSymbol; 10339 return '<div class="md-dialog-container" tabindex="-1" md-theme="' + theme + '">' + validatedTemplate(template) + '</div>'; 10340 10341 /** 10342 * The specified template should contain a <md-dialog> wrapper element.... 10343 */ 10344 function validatedTemplate(template) { 10345 if (options.autoWrap && !/<\/md-dialog>/g.test(template)) { 10346 return '<md-dialog>' + (template || '') + '</md-dialog>'; 10347 } else { 10348 return template || ''; 10349 } 10350 } 10351 } 10352 }; 10353 10354 function beforeCompile(options) { 10355 // Automatically apply the theme, if the user didn't specify a theme explicitly. 10356 // Those option changes need to be done, before the compilation has started, because otherwise 10357 // the option changes will be not available in the $mdCompilers locales. 10358 options.defaultTheme = $mdTheming.defaultTheme(); 10359 10360 detectTheming(options); 10361 } 10362 10363 function beforeShow(scope, element, options, controller) { 10364 10365 if (controller) { 10366 var mdHtmlContent = controller.htmlContent || options.htmlContent || ''; 10367 var mdTextContent = controller.textContent || options.textContent || 10368 controller.content || options.content || ''; 10369 10370 if (mdHtmlContent && !$injector.has('$sanitize')) { 10371 throw Error('The ngSanitize module must be loaded in order to use htmlContent.'); 10372 } 10373 10374 if (mdHtmlContent && mdTextContent) { 10375 throw Error('md-dialog cannot have both `htmlContent` and `textContent`'); 10376 } 10377 10378 // Only assign the content if nothing throws, otherwise it'll still be compiled. 10379 controller.mdHtmlContent = mdHtmlContent; 10380 controller.mdTextContent = mdTextContent; 10381 } 10382 } 10383 10384 /** Show method for dialogs */ 10385 function onShow(scope, element, options, controller) { 10386 angular.element($document[0].body).addClass('md-dialog-is-showing'); 10387 10388 var dialogElement = element.find('md-dialog'); 10389 10390 // Once a dialog has `ng-cloak` applied on his template the dialog animation will not work properly. 10391 // This is a very common problem, so we have to notify the developer about this. 10392 if (dialogElement.hasClass('ng-cloak')) { 10393 var message = '$mdDialog: using `<md-dialog ng-cloak>` will affect the dialog opening animations.'; 10394 $log.warn( message, element[0] ); 10395 } 10396 10397 captureParentAndFromToElements(options); 10398 configureAria(dialogElement, options); 10399 showBackdrop(scope, element, options); 10400 activateListeners(element, options); 10401 10402 return dialogPopIn(element, options) 10403 .then(function() { 10404 lockScreenReader(element, options); 10405 warnDeprecatedActions(); 10406 focusOnOpen(); 10407 }); 10408 10409 /** 10410 * Check to see if they used the deprecated .md-actions class and log a warning 10411 */ 10412 function warnDeprecatedActions() { 10413 if (element[0].querySelector('.md-actions')) { 10414 $log.warn('Using a class of md-actions is deprecated, please use <md-dialog-actions>.'); 10415 } 10416 } 10417 10418 /** 10419 * For alerts, focus on content... otherwise focus on 10420 * the close button (or equivalent) 10421 */ 10422 function focusOnOpen() { 10423 if (options.focusOnOpen) { 10424 var target = $mdUtil.findFocusTarget(element) || findCloseButton() || dialogElement; 10425 target.focus(); 10426 } 10427 10428 /** 10429 * If no element with class dialog-close, try to find the last 10430 * button child in md-actions and assume it is a close button. 10431 * 10432 * If we find no actions at all, log a warning to the console. 10433 */ 10434 function findCloseButton() { 10435 return element[0].querySelector('.dialog-close, md-dialog-actions button:last-child'); 10436 } 10437 } 10438 } 10439 10440 /** 10441 * Remove function for all dialogs 10442 */ 10443 function onRemove(scope, element, options) { 10444 options.deactivateListeners(); 10445 options.unlockScreenReader(); 10446 options.hideBackdrop(options.$destroy); 10447 10448 // Remove the focus traps that we added earlier for keeping focus within the dialog. 10449 if (topFocusTrap && topFocusTrap.parentNode) { 10450 topFocusTrap.parentNode.removeChild(topFocusTrap); 10451 } 10452 10453 if (bottomFocusTrap && bottomFocusTrap.parentNode) { 10454 bottomFocusTrap.parentNode.removeChild(bottomFocusTrap); 10455 } 10456 10457 // For navigation $destroy events, do a quick, non-animated removal, 10458 // but for normal closes (from clicks, etc) animate the removal 10459 return !!options.$destroy ? detachAndClean() : animateRemoval().then( detachAndClean ); 10460 10461 /** 10462 * For normal closes, animate the removal. 10463 * For forced closes (like $destroy events), skip the animations 10464 */ 10465 function animateRemoval() { 10466 return dialogPopOut(element, options); 10467 } 10468 10469 /** 10470 * Detach the element 10471 */ 10472 function detachAndClean() { 10473 angular.element($document[0].body).removeClass('md-dialog-is-showing'); 10474 10475 // Reverse the container stretch if using a content element. 10476 if (options.contentElement) { 10477 options.reverseContainerStretch(); 10478 } 10479 10480 // Exposed cleanup function from the $mdCompiler. 10481 options.cleanupElement(); 10482 10483 // Restores the focus to the origin element if the last interaction upon opening was a keyboard. 10484 if (!options.$destroy && options.originInteraction === 'keyboard') { 10485 options.origin.focus(); 10486 } 10487 } 10488 } 10489 10490 function detectTheming(options) { 10491 // Once the user specifies a targetEvent, we will automatically try to find the correct 10492 // nested theme. 10493 var targetEl; 10494 if (options.targetEvent && options.targetEvent.target) { 10495 targetEl = angular.element(options.targetEvent.target); 10496 } 10497 10498 var themeCtrl = targetEl && targetEl.controller('mdTheme'); 10499 10500 if (!themeCtrl) { 10501 return; 10502 } 10503 10504 options.themeWatch = themeCtrl.$shouldWatch; 10505 10506 var theme = options.theme || themeCtrl.$mdTheme; 10507 10508 if (theme) { 10509 options.scope.theme = theme; 10510 } 10511 10512 var unwatch = themeCtrl.registerChanges(function (newTheme) { 10513 options.scope.theme = newTheme; 10514 10515 if (!options.themeWatch) { 10516 unwatch(); 10517 } 10518 }); 10519 } 10520 10521 /** 10522 * Capture originator/trigger/from/to element information (if available) 10523 * and the parent container for the dialog; defaults to the $rootElement 10524 * unless overridden in the options.parent 10525 */ 10526 function captureParentAndFromToElements(options) { 10527 options.origin = angular.extend({ 10528 element: null, 10529 bounds: null, 10530 focus: angular.noop 10531 }, options.origin || {}); 10532 10533 options.parent = getDomElement(options.parent, $rootElement); 10534 options.closeTo = getBoundingClientRect(getDomElement(options.closeTo)); 10535 options.openFrom = getBoundingClientRect(getDomElement(options.openFrom)); 10536 10537 if ( options.targetEvent ) { 10538 options.origin = getBoundingClientRect(options.targetEvent.target, options.origin); 10539 options.originInteraction = $mdInteraction.getLastInteractionType(); 10540 } 10541 10542 10543 /** 10544 * Identify the bounding RECT for the target element 10545 * 10546 */ 10547 function getBoundingClientRect (element, orig) { 10548 var source = angular.element((element || {})); 10549 if (source && source.length) { 10550 // Compute and save the target element's bounding rect, so that if the 10551 // element is hidden when the dialog closes, we can shrink the dialog 10552 // back to the same position it expanded from. 10553 // 10554 // Checking if the source is a rect object or a DOM element 10555 var bounds = {top:0,left:0,height:0,width:0}; 10556 var hasFn = angular.isFunction(source[0].getBoundingClientRect); 10557 10558 return angular.extend(orig || {}, { 10559 element : hasFn ? source : undefined, 10560 bounds : hasFn ? source[0].getBoundingClientRect() : angular.extend({}, bounds, source[0]), 10561 focus : angular.bind(source, source.focus), 10562 }); 10563 } 10564 } 10565 10566 /** 10567 * If the specifier is a simple string selector, then query for 10568 * the DOM element. 10569 */ 10570 function getDomElement(element, defaultElement) { 10571 if (angular.isString(element)) { 10572 element = $document[0].querySelector(element); 10573 } 10574 10575 // If we have a reference to a raw dom element, always wrap it in jqLite 10576 return angular.element(element || defaultElement); 10577 } 10578 10579 } 10580 10581 /** 10582 * Listen for escape keys and outside clicks to auto close 10583 */ 10584 function activateListeners(element, options) { 10585 var window = angular.element($window); 10586 var onWindowResize = $mdUtil.debounce(function() { 10587 stretchDialogContainerToViewport(element, options); 10588 }, 60); 10589 10590 var removeListeners = []; 10591 var smartClose = function() { 10592 // Only 'confirm' dialogs have a cancel button... escape/clickOutside will 10593 // cancel or fallback to hide. 10594 var closeFn = ( options.$type == 'alert' ) ? $mdDialog.hide : $mdDialog.cancel; 10595 $mdUtil.nextTick(closeFn, true); 10596 }; 10597 10598 if (options.escapeToClose) { 10599 var parentTarget = options.parent; 10600 var keyHandlerFn = function(ev) { 10601 if (ev.keyCode === $mdConstant.KEY_CODE.ESCAPE) { 10602 ev.stopPropagation(); 10603 ev.preventDefault(); 10604 10605 smartClose(); 10606 } 10607 }; 10608 10609 // Add keydown listeners 10610 element.on('keydown', keyHandlerFn); 10611 parentTarget.on('keydown', keyHandlerFn); 10612 10613 // Queue remove listeners function 10614 removeListeners.push(function() { 10615 10616 element.off('keydown', keyHandlerFn); 10617 parentTarget.off('keydown', keyHandlerFn); 10618 10619 }); 10620 } 10621 10622 // Register listener to update dialog on window resize 10623 window.on('resize', onWindowResize); 10624 10625 removeListeners.push(function() { 10626 window.off('resize', onWindowResize); 10627 }); 10628 10629 if (options.clickOutsideToClose) { 10630 var target = element; 10631 var sourceElem; 10632 10633 // Keep track of the element on which the mouse originally went down 10634 // so that we can only close the backdrop when the 'click' started on it. 10635 // A simple 'click' handler does not work, 10636 // it sets the target object as the element the mouse went down on. 10637 var mousedownHandler = function(ev) { 10638 sourceElem = ev.target; 10639 }; 10640 10641 // We check if our original element and the target is the backdrop 10642 // because if the original was the backdrop and the target was inside the dialog 10643 // we don't want to dialog to close. 10644 var mouseupHandler = function(ev) { 10645 if (sourceElem === target[0] && ev.target === target[0]) { 10646 ev.stopPropagation(); 10647 ev.preventDefault(); 10648 10649 smartClose(); 10650 } 10651 }; 10652 10653 // Add listeners 10654 target.on('mousedown', mousedownHandler); 10655 target.on('mouseup', mouseupHandler); 10656 10657 // Queue remove listeners function 10658 removeListeners.push(function() { 10659 target.off('mousedown', mousedownHandler); 10660 target.off('mouseup', mouseupHandler); 10661 }); 10662 } 10663 10664 // Attach specific `remove` listener handler 10665 options.deactivateListeners = function() { 10666 removeListeners.forEach(function(removeFn) { 10667 removeFn(); 10668 }); 10669 options.deactivateListeners = null; 10670 }; 10671 } 10672 10673 /** 10674 * Show modal backdrop element... 10675 */ 10676 function showBackdrop(scope, element, options) { 10677 10678 if (options.disableParentScroll) { 10679 // !! DO this before creating the backdrop; since disableScrollAround() 10680 // configures the scroll offset; which is used by mdBackDrop postLink() 10681 options.restoreScroll = $mdUtil.disableScrollAround(element, options.parent); 10682 } 10683 10684 if (options.hasBackdrop) { 10685 options.backdrop = $mdUtil.createBackdrop(scope, "md-dialog-backdrop md-opaque"); 10686 $animate.enter(options.backdrop, options.parent); 10687 } 10688 10689 /** 10690 * Hide modal backdrop element... 10691 */ 10692 options.hideBackdrop = function hideBackdrop($destroy) { 10693 if (options.backdrop) { 10694 if ( !!$destroy ) options.backdrop.remove(); 10695 else $animate.leave(options.backdrop); 10696 } 10697 10698 10699 if (options.disableParentScroll) { 10700 options.restoreScroll && options.restoreScroll(); 10701 delete options.restoreScroll; 10702 } 10703 10704 options.hideBackdrop = null; 10705 }; 10706 } 10707 10708 /** 10709 * Inject ARIA-specific attributes appropriate for Dialogs 10710 */ 10711 function configureAria(element, options) { 10712 10713 var role = (options.$type === 'alert') ? 'alertdialog' : 'dialog'; 10714 var dialogContent = element.find('md-dialog-content'); 10715 var existingDialogId = element.attr('id'); 10716 var dialogContentId = 'dialogContent_' + (existingDialogId || $mdUtil.nextUid()); 10717 10718 element.attr({ 10719 'role': role, 10720 'tabIndex': '-1' 10721 }); 10722 10723 if (dialogContent.length === 0) { 10724 dialogContent = element; 10725 // If the dialog element already had an ID, don't clobber it. 10726 if (existingDialogId) { 10727 dialogContentId = existingDialogId; 10728 } 10729 } 10730 10731 dialogContent.attr('id', dialogContentId); 10732 element.attr('aria-describedby', dialogContentId); 10733 10734 if (options.ariaLabel) { 10735 $mdAria.expect(element, 'aria-label', options.ariaLabel); 10736 } 10737 else { 10738 $mdAria.expectAsync(element, 'aria-label', function() { 10739 // If dialog title is specified, set aria-label with it 10740 // See https://github.com/angular/material/issues/10582 10741 if (options.title) { 10742 return options.title; 10743 } else { 10744 var words = dialogContent.text().split(/\s+/); 10745 if (words.length > 3) words = words.slice(0, 3).concat('...'); 10746 return words.join(' '); 10747 } 10748 }); 10749 } 10750 10751 // Set up elements before and after the dialog content to capture focus and 10752 // redirect back into the dialog. 10753 topFocusTrap = document.createElement('div'); 10754 topFocusTrap.classList.add('md-dialog-focus-trap'); 10755 topFocusTrap.tabIndex = 0; 10756 10757 bottomFocusTrap = topFocusTrap.cloneNode(false); 10758 10759 // When focus is about to move out of the dialog, we want to intercept it and redirect it 10760 // back to the dialog element. 10761 var focusHandler = function() { 10762 element.focus(); 10763 }; 10764 topFocusTrap.addEventListener('focus', focusHandler); 10765 bottomFocusTrap.addEventListener('focus', focusHandler); 10766 10767 // The top focus trap inserted immeidately before the md-dialog element (as a sibling). 10768 // The bottom focus trap is inserted at the very end of the md-dialog element (as a child). 10769 element[0].parentNode.insertBefore(topFocusTrap, element[0]); 10770 element.after(bottomFocusTrap); 10771 } 10772 10773 /** 10774 * Prevents screen reader interaction behind modal window 10775 * on swipe interfaces 10776 */ 10777 function lockScreenReader(element, options) { 10778 var isHidden = true; 10779 10780 // get raw DOM node 10781 walkDOM(element[0]); 10782 10783 options.unlockScreenReader = function() { 10784 isHidden = false; 10785 walkDOM(element[0]); 10786 10787 options.unlockScreenReader = null; 10788 }; 10789 10790 /** 10791 * Walk DOM to apply or remove aria-hidden on sibling nodes 10792 * and parent sibling nodes 10793 * 10794 */ 10795 function walkDOM(element) { 10796 while (element.parentNode) { 10797 if (element === document.body) { 10798 return; 10799 } 10800 var children = element.parentNode.children; 10801 for (var i = 0; i < children.length; i++) { 10802 // skip over child if it is an ascendant of the dialog 10803 // or a script or style tag 10804 if (element !== children[i] && 10805 !isNodeOneOf(children[i], ['SCRIPT', 'STYLE']) && 10806 !children[i].hasAttribute('aria-live')) { 10807 children[i].setAttribute('aria-hidden', isHidden); 10808 } 10809 } 10810 10811 walkDOM(element = element.parentNode); 10812 } 10813 } 10814 } 10815 10816 /** 10817 * Ensure the dialog container fill-stretches to the viewport 10818 */ 10819 function stretchDialogContainerToViewport(container, options) { 10820 var isFixed = $window.getComputedStyle($document[0].body).position == 'fixed'; 10821 var backdrop = options.backdrop ? $window.getComputedStyle(options.backdrop[0]) : null; 10822 var height = backdrop ? Math.min($document[0].body.clientHeight, Math.ceil(Math.abs(parseInt(backdrop.height, 10)))) : 0; 10823 10824 var previousStyles = { 10825 top: container.css('top'), 10826 height: container.css('height') 10827 }; 10828 10829 // If the body is fixed, determine the distance to the viewport in relative from the parent. 10830 var parentTop = Math.abs(options.parent[0].getBoundingClientRect().top); 10831 10832 container.css({ 10833 top: (isFixed ? parentTop : 0) + 'px', 10834 height: height ? height + 'px' : '100%' 10835 }); 10836 10837 return function() { 10838 // Reverts the modified styles back to the previous values. 10839 // This is needed for contentElements, which should have the same styles after close 10840 // as before. 10841 container.css(previousStyles); 10842 }; 10843 } 10844 10845 /** 10846 * Dialog open and pop-in animation 10847 */ 10848 function dialogPopIn(container, options) { 10849 // Add the `md-dialog-container` to the DOM 10850 options.parent.append(container); 10851 options.reverseContainerStretch = stretchDialogContainerToViewport(container, options); 10852 10853 var dialogEl = container.find('md-dialog'); 10854 var animator = $mdUtil.dom.animator; 10855 var buildTranslateToOrigin = animator.calculateZoomToOrigin; 10856 var translateOptions = {transitionInClass: 'md-transition-in', transitionOutClass: 'md-transition-out'}; 10857 var from = animator.toTransformCss(buildTranslateToOrigin(dialogEl, options.openFrom || options.origin)); 10858 var to = animator.toTransformCss(""); // defaults to center display (or parent or $rootElement) 10859 10860 dialogEl.toggleClass('md-dialog-fullscreen', !!options.fullscreen); 10861 10862 return animator 10863 .translate3d(dialogEl, from, to, translateOptions) 10864 .then(function(animateReversal) { 10865 10866 // Build a reversal translate function synced to this translation... 10867 options.reverseAnimate = function() { 10868 delete options.reverseAnimate; 10869 10870 if (options.closeTo) { 10871 // Using the opposite classes to create a close animation to the closeTo element 10872 translateOptions = {transitionInClass: 'md-transition-out', transitionOutClass: 'md-transition-in'}; 10873 from = to; 10874 to = animator.toTransformCss(buildTranslateToOrigin(dialogEl, options.closeTo)); 10875 10876 return animator 10877 .translate3d(dialogEl, from, to,translateOptions); 10878 } 10879 10880 return animateReversal( 10881 to = animator.toTransformCss( 10882 // in case the origin element has moved or is hidden, 10883 // let's recalculate the translateCSS 10884 buildTranslateToOrigin(dialogEl, options.origin) 10885 ) 10886 ); 10887 10888 }; 10889 10890 // Function to revert the generated animation styles on the dialog element. 10891 // Useful when using a contentElement instead of a template. 10892 options.clearAnimate = function() { 10893 delete options.clearAnimate; 10894 10895 // Remove the transition classes, added from $animateCSS, since those can't be removed 10896 // by reversely running the animator. 10897 dialogEl.removeClass([ 10898 translateOptions.transitionOutClass, 10899 translateOptions.transitionInClass 10900 ].join(' ')); 10901 10902 // Run the animation reversely to remove the previous added animation styles. 10903 return animator.translate3d(dialogEl, to, animator.toTransformCss(''), {}); 10904 }; 10905 10906 return true; 10907 }); 10908 } 10909 10910 /** 10911 * Dialog close and pop-out animation 10912 */ 10913 function dialogPopOut(container, options) { 10914 return options.reverseAnimate().then(function() { 10915 if (options.contentElement) { 10916 // When we use a contentElement, we want the element to be the same as before. 10917 // That means, that we have to clear all the animation properties, like transform. 10918 options.clearAnimate(); 10919 } 10920 }); 10921 } 10922 10923 /** 10924 * Utility function to filter out raw DOM nodes 10925 */ 10926 function isNodeOneOf(elem, nodeTypeArray) { 10927 if (nodeTypeArray.indexOf(elem.nodeName) !== -1) { 10928 return true; 10929 } 10930 } 10931 10932 } 10933} 10934 10935})(); 10936(function(){ 10937"use strict"; 10938 10939/** 10940 * @ngdoc module 10941 * @name material.components.divider 10942 * @description Divider module! 10943 */ 10944MdDividerDirective.$inject = ["$mdTheming"]; 10945angular.module('material.components.divider', [ 10946 'material.core' 10947]) 10948 .directive('mdDivider', MdDividerDirective); 10949 10950/** 10951 * @ngdoc directive 10952 * @name mdDivider 10953 * @module material.components.divider 10954 * @restrict E 10955 * 10956 * @description 10957 * 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. 10958 * 10959 * @param {boolean=} md-inset Add this attribute to activate the inset divider style. 10960 * @usage 10961 * <hljs lang="html"> 10962 * <md-divider></md-divider> 10963 * 10964 * <md-divider md-inset></md-divider> 10965 * </hljs> 10966 * 10967 */ 10968function MdDividerDirective($mdTheming) { 10969 return { 10970 restrict: 'E', 10971 link: $mdTheming 10972 }; 10973} 10974 10975})(); 10976(function(){ 10977"use strict"; 10978 10979(function() { 10980 'use strict'; 10981 10982 /** 10983 * @ngdoc module 10984 * @name material.components.fabActions 10985 */ 10986 MdFabActionsDirective.$inject = ["$mdUtil"]; 10987 angular 10988 .module('material.components.fabActions', ['material.core']) 10989 .directive('mdFabActions', MdFabActionsDirective); 10990 10991 /** 10992 * @ngdoc directive 10993 * @name mdFabActions 10994 * @module material.components.fabActions 10995 * 10996 * @restrict E 10997 * 10998 * @description 10999 * The `<md-fab-actions>` directive is used inside of a `<md-fab-speed-dial>` or 11000 * `<md-fab-toolbar>` directive to mark an element (or elements) as the actions and setup the 11001 * proper event listeners. 11002 * 11003 * @usage 11004 * See the `<md-fab-speed-dial>` or `<md-fab-toolbar>` directives for example usage. 11005 */ 11006 function MdFabActionsDirective($mdUtil) { 11007 return { 11008 restrict: 'E', 11009 11010 require: ['^?mdFabSpeedDial', '^?mdFabToolbar'], 11011 11012 compile: function(element, attributes) { 11013 var children = element.children(); 11014 11015 var hasNgRepeat = $mdUtil.prefixer().hasAttribute(children, 'ng-repeat'); 11016 11017 // Support both ng-repeat and static content 11018 if (hasNgRepeat) { 11019 children.addClass('md-fab-action-item'); 11020 } else { 11021 // Wrap every child in a new div and add a class that we can scale/fling independently 11022 children.wrap('<div class="md-fab-action-item">'); 11023 } 11024 } 11025 }; 11026 } 11027 11028})(); 11029 11030})(); 11031(function(){ 11032"use strict"; 11033 11034(function() { 11035 'use strict'; 11036 11037 MdFabController.$inject = ["$scope", "$element", "$animate", "$mdUtil", "$mdConstant", "$timeout"]; 11038 angular.module('material.components.fabShared', ['material.core']) 11039 .controller('MdFabController', MdFabController); 11040 11041 function MdFabController($scope, $element, $animate, $mdUtil, $mdConstant, $timeout) { 11042 var vm = this; 11043 var initialAnimationAttempts = 0; 11044 11045 // NOTE: We use async eval(s) below to avoid conflicts with any existing digest loops 11046 11047 vm.open = function() { 11048 $scope.$evalAsync("vm.isOpen = true"); 11049 }; 11050 11051 vm.close = function() { 11052 // Async eval to avoid conflicts with existing digest loops 11053 $scope.$evalAsync("vm.isOpen = false"); 11054 11055 // Focus the trigger when the element closes so users can still tab to the next item 11056 $element.find('md-fab-trigger')[0].focus(); 11057 }; 11058 11059 // Toggle the open/close state when the trigger is clicked 11060 vm.toggle = function() { 11061 $scope.$evalAsync("vm.isOpen = !vm.isOpen"); 11062 }; 11063 11064 /* 11065 * AngularJS Lifecycle hook for newer AngularJS versions. 11066 * Bindings are not guaranteed to have been assigned in the controller, but they are in the $onInit hook. 11067 */ 11068 vm.$onInit = function() { 11069 setupDefaults(); 11070 setupListeners(); 11071 setupWatchers(); 11072 11073 fireInitialAnimations(); 11074 }; 11075 11076 // For AngularJS 1.4 and older, where there are no lifecycle hooks but bindings are pre-assigned, 11077 // manually call the $onInit hook. 11078 if (angular.version.major === 1 && angular.version.minor <= 4) { 11079 this.$onInit(); 11080 } 11081 11082 function setupDefaults() { 11083 // Set the default direction to 'down' if none is specified 11084 vm.direction = vm.direction || 'down'; 11085 11086 // Set the default to be closed 11087 vm.isOpen = vm.isOpen || false; 11088 11089 // Start the keyboard interaction at the first action 11090 resetActionIndex(); 11091 11092 // Add an animations waiting class so we know not to run 11093 $element.addClass('md-animations-waiting'); 11094 } 11095 11096 function setupListeners() { 11097 var eventTypes = [ 11098 'click', 'focusin', 'focusout' 11099 ]; 11100 11101 // Add our listeners 11102 angular.forEach(eventTypes, function(eventType) { 11103 $element.on(eventType, parseEvents); 11104 }); 11105 11106 // Remove our listeners when destroyed 11107 $scope.$on('$destroy', function() { 11108 angular.forEach(eventTypes, function(eventType) { 11109 $element.off(eventType, parseEvents); 11110 }); 11111 11112 // remove any attached keyboard handlers in case element is removed while 11113 // speed dial is open 11114 disableKeyboard(); 11115 }); 11116 } 11117 11118 var closeTimeout; 11119 function parseEvents(event) { 11120 // If the event is a click, just handle it 11121 if (event.type == 'click') { 11122 handleItemClick(event); 11123 } 11124 11125 // If we focusout, set a timeout to close the element 11126 if (event.type == 'focusout' && !closeTimeout) { 11127 closeTimeout = $timeout(function() { 11128 vm.close(); 11129 }, 100, false); 11130 } 11131 11132 // If we see a focusin and there is a timeout about to run, cancel it so we stay open 11133 if (event.type == 'focusin' && closeTimeout) { 11134 $timeout.cancel(closeTimeout); 11135 closeTimeout = null; 11136 } 11137 } 11138 11139 function resetActionIndex() { 11140 vm.currentActionIndex = -1; 11141 } 11142 11143 function setupWatchers() { 11144 // Watch for changes to the direction and update classes/attributes 11145 $scope.$watch('vm.direction', function(newDir, oldDir) { 11146 // Add the appropriate classes so we can target the direction in the CSS 11147 $animate.removeClass($element, 'md-' + oldDir); 11148 $animate.addClass($element, 'md-' + newDir); 11149 11150 // Reset the action index since it may have changed 11151 resetActionIndex(); 11152 }); 11153 11154 var trigger, actions; 11155 11156 // Watch for changes to md-open 11157 $scope.$watch('vm.isOpen', function(isOpen) { 11158 // Reset the action index since it may have changed 11159 resetActionIndex(); 11160 11161 // We can't get the trigger/actions outside of the watch because the component hasn't been 11162 // linked yet, so we wait until the first watch fires to cache them. 11163 if (!trigger || !actions) { 11164 trigger = getTriggerElement(); 11165 actions = getActionsElement(); 11166 } 11167 11168 if (isOpen) { 11169 enableKeyboard(); 11170 } else { 11171 disableKeyboard(); 11172 } 11173 11174 var toAdd = isOpen ? 'md-is-open' : ''; 11175 var toRemove = isOpen ? '' : 'md-is-open'; 11176 11177 // Set the proper ARIA attributes 11178 trigger.attr('aria-haspopup', true); 11179 trigger.attr('aria-expanded', isOpen); 11180 actions.attr('aria-hidden', !isOpen); 11181 11182 // Animate the CSS classes 11183 $animate.setClass($element, toAdd, toRemove); 11184 }); 11185 } 11186 11187 function fireInitialAnimations() { 11188 // If the element is actually visible on the screen 11189 if ($element[0].scrollHeight > 0) { 11190 // Fire our animation 11191 $animate.addClass($element, '_md-animations-ready').then(function() { 11192 // Remove the waiting class 11193 $element.removeClass('md-animations-waiting'); 11194 }); 11195 } 11196 11197 // Otherwise, try for up to 1 second before giving up 11198 else if (initialAnimationAttempts < 10) { 11199 $timeout(fireInitialAnimations, 100); 11200 11201 // Increment our counter 11202 initialAnimationAttempts = initialAnimationAttempts + 1; 11203 } 11204 } 11205 11206 function enableKeyboard() { 11207 $element.on('keydown', keyPressed); 11208 11209 // On the next tick, setup a check for outside clicks; we do this on the next tick to avoid 11210 // clicks/touches that result in the isOpen attribute changing (e.g. a bound radio button) 11211 $mdUtil.nextTick(function() { 11212 angular.element(document).on('click touchend', checkForOutsideClick); 11213 }); 11214 11215 // TODO: On desktop, we should be able to reset the indexes so you cannot tab through, but 11216 // this breaks accessibility, especially on mobile, since you have no arrow keys to press 11217 //resetActionTabIndexes(); 11218 } 11219 11220 function disableKeyboard() { 11221 $element.off('keydown', keyPressed); 11222 angular.element(document).off('click touchend', checkForOutsideClick); 11223 } 11224 11225 function checkForOutsideClick(event) { 11226 if (event.target) { 11227 var closestTrigger = $mdUtil.getClosest(event.target, 'md-fab-trigger'); 11228 var closestActions = $mdUtil.getClosest(event.target, 'md-fab-actions'); 11229 11230 if (!closestTrigger && !closestActions) { 11231 vm.close(); 11232 } 11233 } 11234 } 11235 11236 function keyPressed(event) { 11237 switch (event.which) { 11238 case $mdConstant.KEY_CODE.ESCAPE: vm.close(); event.preventDefault(); return false; 11239 case $mdConstant.KEY_CODE.LEFT_ARROW: doKeyLeft(event); return false; 11240 case $mdConstant.KEY_CODE.UP_ARROW: doKeyUp(event); return false; 11241 case $mdConstant.KEY_CODE.RIGHT_ARROW: doKeyRight(event); return false; 11242 case $mdConstant.KEY_CODE.DOWN_ARROW: doKeyDown(event); return false; 11243 } 11244 } 11245 11246 function doActionPrev(event) { 11247 focusAction(event, -1); 11248 } 11249 11250 function doActionNext(event) { 11251 focusAction(event, 1); 11252 } 11253 11254 function focusAction(event, direction) { 11255 var actions = resetActionTabIndexes(); 11256 11257 // Increment/decrement the counter with restrictions 11258 vm.currentActionIndex = vm.currentActionIndex + direction; 11259 vm.currentActionIndex = Math.min(actions.length - 1, vm.currentActionIndex); 11260 vm.currentActionIndex = Math.max(0, vm.currentActionIndex); 11261 11262 // Focus the element 11263 var focusElement = angular.element(actions[vm.currentActionIndex]).children()[0]; 11264 angular.element(focusElement).attr('tabindex', 0); 11265 focusElement.focus(); 11266 11267 // Make sure the event doesn't bubble and cause something else 11268 event.preventDefault(); 11269 event.stopImmediatePropagation(); 11270 } 11271 11272 function resetActionTabIndexes() { 11273 // Grab all of the actions 11274 var actions = getActionsElement()[0].querySelectorAll('.md-fab-action-item'); 11275 11276 // Disable all other actions for tabbing 11277 angular.forEach(actions, function(action) { 11278 angular.element(angular.element(action).children()[0]).attr('tabindex', -1); 11279 }); 11280 11281 return actions; 11282 } 11283 11284 function doKeyLeft(event) { 11285 if (vm.direction === 'left') { 11286 doActionNext(event); 11287 } else { 11288 doActionPrev(event); 11289 } 11290 } 11291 11292 function doKeyUp(event) { 11293 if (vm.direction === 'down') { 11294 doActionPrev(event); 11295 } else { 11296 doActionNext(event); 11297 } 11298 } 11299 11300 function doKeyRight(event) { 11301 if (vm.direction === 'left') { 11302 doActionPrev(event); 11303 } else { 11304 doActionNext(event); 11305 } 11306 } 11307 11308 function doKeyDown(event) { 11309 if (vm.direction === 'up') { 11310 doActionPrev(event); 11311 } else { 11312 doActionNext(event); 11313 } 11314 } 11315 11316 function isTrigger(element) { 11317 return $mdUtil.getClosest(element, 'md-fab-trigger'); 11318 } 11319 11320 function isAction(element) { 11321 return $mdUtil.getClosest(element, 'md-fab-actions'); 11322 } 11323 11324 function handleItemClick(event) { 11325 if (isTrigger(event.target)) { 11326 vm.toggle(); 11327 } 11328 11329 if (isAction(event.target)) { 11330 vm.close(); 11331 } 11332 } 11333 11334 function getTriggerElement() { 11335 return $element.find('md-fab-trigger'); 11336 } 11337 11338 function getActionsElement() { 11339 return $element.find('md-fab-actions'); 11340 } 11341 } 11342})(); 11343 11344})(); 11345(function(){ 11346"use strict"; 11347 11348(function() { 11349 'use strict'; 11350 11351 /** 11352 * The duration of the CSS animation in milliseconds. 11353 * 11354 * @type {number} 11355 */ 11356 MdFabSpeedDialFlingAnimation.$inject = ["$timeout"]; 11357 MdFabSpeedDialScaleAnimation.$inject = ["$timeout"]; 11358 var cssAnimationDuration = 300; 11359 11360 /** 11361 * @ngdoc module 11362 * @name material.components.fabSpeedDial 11363 */ 11364 angular 11365 // Declare our module 11366 .module('material.components.fabSpeedDial', [ 11367 'material.core', 11368 'material.components.fabShared', 11369 'material.components.fabActions' 11370 ]) 11371 11372 // Register our directive 11373 .directive('mdFabSpeedDial', MdFabSpeedDialDirective) 11374 11375 // Register our custom animations 11376 .animation('.md-fling', MdFabSpeedDialFlingAnimation) 11377 .animation('.md-scale', MdFabSpeedDialScaleAnimation) 11378 11379 // Register a service for each animation so that we can easily inject them into unit tests 11380 .service('mdFabSpeedDialFlingAnimation', MdFabSpeedDialFlingAnimation) 11381 .service('mdFabSpeedDialScaleAnimation', MdFabSpeedDialScaleAnimation); 11382 11383 /** 11384 * @ngdoc directive 11385 * @name mdFabSpeedDial 11386 * @module material.components.fabSpeedDial 11387 * 11388 * @restrict E 11389 * 11390 * @description 11391 * The `<md-fab-speed-dial>` directive is used to present a series of popup elements (usually 11392 * `<md-button>`s) for quick access to common actions. 11393 * 11394 * There are currently two animations available by applying one of the following classes to 11395 * the component: 11396 * 11397 * - `md-fling` - The speed dial items appear from underneath the trigger and move into their 11398 * appropriate positions. 11399 * - `md-scale` - The speed dial items appear in their proper places by scaling from 0% to 100%. 11400 * 11401 * You may also easily position the trigger by applying one one of the following classes to the 11402 * `<md-fab-speed-dial>` element: 11403 * - `md-fab-top-left` 11404 * - `md-fab-top-right` 11405 * - `md-fab-bottom-left` 11406 * - `md-fab-bottom-right` 11407 * 11408 * These CSS classes use `position: absolute`, so you need to ensure that the container element 11409 * also uses `position: absolute` or `position: relative` in order for them to work. 11410 * 11411 * Additionally, you may use the standard `ng-mouseenter` and `ng-mouseleave` directives to 11412 * open or close the speed dial. However, if you wish to allow users to hover over the empty 11413 * space where the actions will appear, you must also add the `md-hover-full` class to the speed 11414 * dial element. Without this, the hover effect will only occur on top of the trigger. 11415 * 11416 * See the demos for more information. 11417 * 11418 * ## Troubleshooting 11419 * 11420 * If your speed dial shows the closing animation upon launch, you may need to use `ng-cloak` on 11421 * the parent container to ensure that it is only visible once ready. We have plans to remove this 11422 * necessity in the future. 11423 * 11424 * @usage 11425 * <hljs lang="html"> 11426 * <md-fab-speed-dial md-direction="up" class="md-fling"> 11427 * <md-fab-trigger> 11428 * <md-button aria-label="Add..."><md-icon md-svg-src="/img/icons/plus.svg"></md-icon></md-button> 11429 * </md-fab-trigger> 11430 * 11431 * <md-fab-actions> 11432 * <md-button aria-label="Add User"> 11433 * <md-icon md-svg-src="/img/icons/user.svg"></md-icon> 11434 * </md-button> 11435 * 11436 * <md-button aria-label="Add Group"> 11437 * <md-icon md-svg-src="/img/icons/group.svg"></md-icon> 11438 * </md-button> 11439 * </md-fab-actions> 11440 * </md-fab-speed-dial> 11441 * </hljs> 11442 * 11443 * @param {string} md-direction From which direction you would like the speed dial to appear 11444 * relative to the trigger element. 11445 * @param {expression=} md-open Programmatically control whether or not the speed-dial is visible. 11446 */ 11447 function MdFabSpeedDialDirective() { 11448 return { 11449 restrict: 'E', 11450 11451 scope: { 11452 direction: '@?mdDirection', 11453 isOpen: '=?mdOpen' 11454 }, 11455 11456 bindToController: true, 11457 controller: 'MdFabController', 11458 controllerAs: 'vm', 11459 11460 link: FabSpeedDialLink 11461 }; 11462 11463 function FabSpeedDialLink(scope, element) { 11464 // Prepend an element to hold our CSS variables so we can use them in the animations below 11465 element.prepend('<div class="_md-css-variables"></div>'); 11466 } 11467 } 11468 11469 function MdFabSpeedDialFlingAnimation($timeout) { 11470 function delayDone(done) { $timeout(done, cssAnimationDuration, false); } 11471 11472 function runAnimation(element) { 11473 // Don't run if we are still waiting and we are not ready 11474 if (element.hasClass('md-animations-waiting') && !element.hasClass('_md-animations-ready')) { 11475 return; 11476 } 11477 11478 var el = element[0]; 11479 var ctrl = element.controller('mdFabSpeedDial'); 11480 var items = el.querySelectorAll('.md-fab-action-item'); 11481 11482 // Grab our trigger element 11483 var triggerElement = el.querySelector('md-fab-trigger'); 11484 11485 // Grab our element which stores CSS variables 11486 var variablesElement = el.querySelector('._md-css-variables'); 11487 11488 // Setup JS variables based on our CSS variables 11489 var startZIndex = parseInt(window.getComputedStyle(variablesElement).zIndex); 11490 11491 // Always reset the items to their natural position/state 11492 angular.forEach(items, function(item, index) { 11493 var styles = item.style; 11494 11495 styles.transform = styles.webkitTransform = ''; 11496 styles.transitionDelay = ''; 11497 styles.opacity = 1; 11498 11499 // Make the items closest to the trigger have the highest z-index 11500 styles.zIndex = (items.length - index) + startZIndex; 11501 }); 11502 11503 // Set the trigger to be above all of the actions so they disappear behind it. 11504 triggerElement.style.zIndex = startZIndex + items.length + 1; 11505 11506 // If the control is closed, hide the items behind the trigger 11507 if (!ctrl.isOpen) { 11508 angular.forEach(items, function(item, index) { 11509 var newPosition, axis; 11510 var styles = item.style; 11511 11512 // Make sure to account for differences in the dimensions of the trigger verses the items 11513 // so that we can properly center everything; this helps hide the item's shadows behind 11514 // the trigger. 11515 var triggerItemHeightOffset = (triggerElement.clientHeight - item.clientHeight) / 2; 11516 var triggerItemWidthOffset = (triggerElement.clientWidth - item.clientWidth) / 2; 11517 11518 switch (ctrl.direction) { 11519 case 'up': 11520 newPosition = (item.scrollHeight * (index + 1) + triggerItemHeightOffset); 11521 axis = 'Y'; 11522 break; 11523 case 'down': 11524 newPosition = -(item.scrollHeight * (index + 1) + triggerItemHeightOffset); 11525 axis = 'Y'; 11526 break; 11527 case 'left': 11528 newPosition = (item.scrollWidth * (index + 1) + triggerItemWidthOffset); 11529 axis = 'X'; 11530 break; 11531 case 'right': 11532 newPosition = -(item.scrollWidth * (index + 1) + triggerItemWidthOffset); 11533 axis = 'X'; 11534 break; 11535 } 11536 11537 var newTranslate = 'translate' + axis + '(' + newPosition + 'px)'; 11538 11539 styles.transform = styles.webkitTransform = newTranslate; 11540 }); 11541 } 11542 } 11543 11544 return { 11545 addClass: function(element, className, done) { 11546 if (element.hasClass('md-fling')) { 11547 runAnimation(element); 11548 delayDone(done); 11549 } else { 11550 done(); 11551 } 11552 }, 11553 removeClass: function(element, className, done) { 11554 runAnimation(element); 11555 delayDone(done); 11556 } 11557 }; 11558 } 11559 11560 function MdFabSpeedDialScaleAnimation($timeout) { 11561 function delayDone(done) { $timeout(done, cssAnimationDuration, false); } 11562 11563 var delay = 65; 11564 11565 function runAnimation(element) { 11566 var el = element[0]; 11567 var ctrl = element.controller('mdFabSpeedDial'); 11568 var items = el.querySelectorAll('.md-fab-action-item'); 11569 11570 // Grab our element which stores CSS variables 11571 var variablesElement = el.querySelector('._md-css-variables'); 11572 11573 // Setup JS variables based on our CSS variables 11574 var startZIndex = parseInt(window.getComputedStyle(variablesElement).zIndex); 11575 11576 // Always reset the items to their natural position/state 11577 angular.forEach(items, function(item, index) { 11578 var styles = item.style, 11579 offsetDelay = index * delay; 11580 11581 styles.opacity = ctrl.isOpen ? 1 : 0; 11582 styles.transform = styles.webkitTransform = ctrl.isOpen ? 'scale(1)' : 'scale(0)'; 11583 styles.transitionDelay = (ctrl.isOpen ? offsetDelay : (items.length - offsetDelay)) + 'ms'; 11584 11585 // Make the items closest to the trigger have the highest z-index 11586 styles.zIndex = (items.length - index) + startZIndex; 11587 }); 11588 } 11589 11590 return { 11591 addClass: function(element, className, done) { 11592 runAnimation(element); 11593 delayDone(done); 11594 }, 11595 11596 removeClass: function(element, className, done) { 11597 runAnimation(element); 11598 delayDone(done); 11599 } 11600 }; 11601 } 11602})(); 11603 11604})(); 11605(function(){ 11606"use strict"; 11607 11608(function() { 11609 'use strict'; 11610 11611 /** 11612 * @ngdoc module 11613 * @name material.components.fabToolbar 11614 */ 11615 angular 11616 // Declare our module 11617 .module('material.components.fabToolbar', [ 11618 'material.core', 11619 'material.components.fabShared', 11620 'material.components.fabActions' 11621 ]) 11622 11623 // Register our directive 11624 .directive('mdFabToolbar', MdFabToolbarDirective) 11625 11626 // Register our custom animations 11627 .animation('.md-fab-toolbar', MdFabToolbarAnimation) 11628 11629 // Register a service for the animation so that we can easily inject it into unit tests 11630 .service('mdFabToolbarAnimation', MdFabToolbarAnimation); 11631 11632 /** 11633 * @ngdoc directive 11634 * @name mdFabToolbar 11635 * @module material.components.fabToolbar 11636 * 11637 * @restrict E 11638 * 11639 * @description 11640 * 11641 * The `<md-fab-toolbar>
11641` directive is used to present a toolbar of elements (usually `<md-button>`s) 11642 * for quick access to common actions when a floating action button is activated (via click or 11643 * keyboard navigation). 11644 * 11645 * You may also easily position the trigger by applying one one of the following classes to the 11646 * `<md-fab-toolbar>` element: 11647 * - `md-fab-top-left` 11648 * - `md-fab-top-right` 11649 * - `md-fab-bottom-left` 11650 * - `md-fab-bottom-right` 11651 * 11652 * These CSS classes use `position: absolute`, so you need to ensure that the container element 11653 * also uses `position: absolute` or `position: relative` in order for them to work. 11654 * 11655 * @usage 11656 * 11657 * <hljs lang="html"> 11658 * <md-fab-toolbar md-direction='left'> 11659 * <md-fab-trigger> 11660 * <md-button aria-label="Add..."><md-icon md-svg-src="/img/icons/plus.svg"></md-icon></md-button> 11661 * </md-fab-trigger> 11662 * 11663 * <md-toolbar> 11664 * <md-fab-actions> 11665 * <md-button aria-label="Add User"> 11666 * <md-icon md-svg-src="/img/icons/user.svg"></md-icon> 11667 * </md-button> 11668 * 11669 * <md-button aria-label="Add Group"> 11670 * <md-icon md-svg-src="/img/icons/group.svg"></md-icon> 11671 * </md-button> 11672 * </md-fab-actions> 11673 * </md-toolbar> 11674 * </md-fab-toolbar> 11675 * </hljs> 11676 * 11677 * @param {string} md-direction From which direction you would like the toolbar items to appear 11678 * relative to the trigger element. Supports `left` and `right` directions. 11679 * @param {expression=} md-open Programmatically control whether or not the toolbar is visible. 11680 */ 11681 function MdFabToolbarDirective() { 11682 return { 11683 restrict: 'E', 11684 transclude: true, 11685 template: '<div class="md-fab-toolbar-wrapper">' + 11686 ' <div class="md-fab-toolbar-content" ng-transclude></div>' + 11687 '</div>', 11688 11689 scope: { 11690 direction: '@?mdDirection', 11691 isOpen: '=?mdOpen' 11692 }, 11693 11694 bindToController: true, 11695 controller: 'MdFabController', 11696 controllerAs: 'vm', 11697 11698 link: link 11699 }; 11700 11701 function link(scope, element, attributes) { 11702 // Add the base class for animations 11703 element.addClass('md-fab-toolbar'); 11704 11705 // Prepend the background element to the trigger's button 11706 element.find('md-fab-trigger').find('button') 11707 .prepend('<div class="md-fab-toolbar-background"></div>'); 11708 } 11709 } 11710 11711 function MdFabToolbarAnimation() { 11712 11713 function runAnimation(element, className, done) { 11714 // If no className was specified, don't do anything 11715 if (!className) { 11716 return; 11717 } 11718 11719 var el = element[0]; 11720 var ctrl = element.controller('mdFabToolbar'); 11721 11722 // Grab the relevant child elements 11723 var backgroundElement = el.querySelector('.md-fab-toolbar-background'); 11724 var triggerElement = el.querySelector('md-fab-trigger button'); 11725 var toolbarElement = el.querySelector('md-toolbar'); 11726 var iconElement = el.querySelector('md-fab-trigger button md-icon'); 11727 var actions = element.find('md-fab-actions').children(); 11728 11729 // If we have both elements, use them to position the new background 11730 if (triggerElement && backgroundElement) { 11731 // Get our variables 11732 var color = window.getComputedStyle(triggerElement).getPropertyValue('background-color'); 11733 var width = el.offsetWidth; 11734 var height = el.offsetHeight; 11735 11736 // Make it twice as big as it should be since we scale from the center 11737 var scale = 2 * (width / triggerElement.offsetWidth); 11738 11739 // Set some basic styles no matter what animation we're doing 11740 backgroundElement.style.backgroundColor = color; 11741 backgroundElement.style.borderRadius = width + 'px'; 11742 11743 // If we're open 11744 if (ctrl.isOpen) { 11745 // Turn on toolbar pointer events when closed 11746 toolbarElement.style.pointerEvents = 'inherit'; 11747 11748 backgroundElement.style.width = triggerElement.offsetWidth + 'px'; 11749 backgroundElement.style.height = triggerElement.offsetHeight + 'px'; 11750 backgroundElement.style.transform = 'scale(' + scale + ')'; 11751 11752 // Set the next close animation to have the proper delays 11753 backgroundElement.style.transitionDelay = '0ms'; 11754 iconElement && (iconElement.style.transitionDelay = '.3s'); 11755 11756 // Apply a transition delay to actions 11757 angular.forEach(actions, function(action, index) { 11758 action.style.transitionDelay = (actions.length - index) * 25 + 'ms'; 11759 }); 11760 } else { 11761 // Turn off toolbar pointer events when closed 11762 toolbarElement.style.pointerEvents = 'none'; 11763 11764 // Scale it back down to the trigger's size 11765 backgroundElement.style.transform = 'scale(1)'; 11766 11767 // Reset the position 11768 backgroundElement.style.top = '0'; 11769 11770 if (element.hasClass('md-right')) { 11771 backgroundElement.style.left = '0'; 11772 backgroundElement.style.right = null; 11773 } 11774 11775 if (element.hasClass('md-left')) { 11776 backgroundElement.style.right = '0'; 11777 backgroundElement.style.left = null; 11778 } 11779 11780 // Set the next open animation to have the proper delays 11781 backgroundElement.style.transitionDelay = '200ms'; 11782 iconElement && (iconElement.style.transitionDelay = '0ms'); 11783 11784 // Apply a transition delay to actions 11785 angular.forEach(actions, function(action, index) { 11786 action.style.transitionDelay = 200 + (index * 25) + 'ms'; 11787 }); 11788 } 11789 } 11790 } 11791 11792 return { 11793 addClass: function(element, className, done) { 11794 runAnimation(element, className, done); 11795 done(); 11796 }, 11797 11798 removeClass: function(element, className, done) { 11799 runAnimation(element, className, done); 11800 done(); 11801 } 11802 }; 11803 } 11804})(); 11805 11806})(); 11807(function(){ 11808"use strict"; 11809 11810/** 11811 * @ngdoc module 11812 * @name material.components.gridList 11813 */ 11814GridListController.$inject = ["$mdUtil"];
11815GridLayoutFactory.$inject = ["$mdUtil"]; 11816GridListDirective.$inject = ["$interpolate", "$mdConstant", "$mdGridLayout", "$mdMedia"]; 11817GridTileDirective.$inject = ["$mdMedia"]; 11818angular.module('material.components.gridList', ['material.core']) 11819 .directive('mdGridList', GridListDirective) 11820 .directive('mdGridTile', GridTileDirective) 11821 .directive('mdGridTileFooter', GridTileCaptionDirective) 11822 .directive('mdGridTileHeader', GridTileCaptionDirective) 11823 .factory('$mdGridLayout', GridLayoutFactory); 11824 11825/** 11826 * @ngdoc directive 11827 * @name mdGridList 11828 * @module material.components.gridList 11829 * @restrict E 11830 * @description 11831 * Grid lists are an alternative to standard list views. Grid lists are distinct 11832 * from grids used for layouts and other visual presentations. 11833 * 11834 * A grid list is best suited to presenting a homogenous data type, typically 11835 * images, and is optimized for visual comprehension and differentiating between 11836 * like data types. 11837 * 11838 * A grid list is a continuous element consisting of tessellated, regular 11839 * subdivisions called cells that contain tiles (`md-grid-tile`). 11840 * 11841 * <img src="//material-design.storage.googleapis.com/publish/v_2/material_ext_publish/0Bx4BSt6jniD7OVlEaXZ5YmU1Xzg/components_grids_usage2.png" 11842 * style="width: 300px; height: auto; margin-right: 16px;" alt="Concept of grid explained visually"> 11843 * <img src="//material-design.storage.googleapis.com/publish/v_2/material_ext_publish/0Bx4BSt6jniD7VGhsOE5idWlJWXM/components_grids_usage3.png" 11844 * style="width: 300px; height: auto;" alt="Grid concepts legend"> 11845 * 11846 * Cells are arrayed vertically and horizontally within the grid. 11847 * 11848 * Tiles hold content and can span one or more cells vertically or horizontally. 11849 * 11850 * ### Responsive Attributes 11851 * 11852 * The `md-grid-list` directive supports "responsive" attributes, which allow 11853 * different `md-cols`, `md-gutter` and `md-row-height` values depending on the 11854 * currently matching media query. 11855 * 11856 * In order to set a responsive attribute, first define the fallback value with 11857 * the standard attribute name, then add additional attributes with the 11858 * following convention: `{base-attribute-name}-{media-query-name}="{value}"` 11859 * (ie. `md-cols-lg="8"`) 11860 * 11861 * @param {number} md-cols Number of columns in the grid. 11862 * @param {string} md-row-height One of 11863 * <ul> 11864 * <li>CSS length - Fixed height rows (eg. `8px` or `1rem`)</li> 11865 * <li>`{width}:{height}` - Ratio of width to height (eg. 11866 * `md-row-height="16:9"`)</li> 11867 * <li>`"fit"` - Height will be determined by subdividing the available 11868 * height by the number of rows</li> 11869 * </ul> 11870 * @param {string=} md-gutter The amount of space between tiles in CSS units 11871 * (default 1px) 11872 * @param {expression=} md-on-layout Expression to evaluate after layout. Event 11873 * object is available as `$event`, and contains performance information. 11874 * 11875 * @usage 11876 * Basic: 11877 * <hljs lang="html"> 11878 * <md-grid-list md-cols="5" md-gutter="1em" md-row-height="4:3"> 11879 * <md-grid-tile></md-grid-tile> 11880 * </md-grid-list> 11881 * </hljs> 11882 * 11883 * Fixed-height rows: 11884 * <hljs lang="html"> 11885 * <md-grid-list md-cols="4" md-row-height="200px" ...> 11886 * <md-grid-tile></md-grid-tile> 11887 * </md-grid-list> 11888 * </hljs> 11889 * 11890 * Fit rows: 11891 * <hljs lang="html"> 11892 * <md-grid-list md-cols="4" md-row-height="fit" style="height: 400px;" ...> 11893 * <md-grid-tile></md-grid-tile> 11894 * </md-grid-list> 11895 * </hljs> 11896 * 11897 * Using responsive attributes: 11898 * <hljs lang="html"> 11899 * <md-grid-list 11900 * md-cols-sm="2" 11901 * md-cols-md="4" 11902 * md-cols-lg="8" 11903 * md-cols-gt-lg="12" 11904 * ...> 11905 * <md-grid-tile></md-grid-tile> 11906 * </md-grid-list> 11907 * </hljs> 11908 */ 11909function GridListDirective($interpolate, $mdConstant, $mdGridLayout, $mdMedia) { 11910 return { 11911 restrict: 'E', 11912 controller: GridListController, 11913 scope: { 11914 mdOnLayout: '&' 11915 }, 11916 link: postLink 11917 }; 11918 11919 function postLink(scope, element, attrs, ctrl) { 11920 element.addClass('_md'); // private md component indicator for styling 11921 11922 // Apply semantics 11923 element.attr('role', 'list'); 11924 11925 // Provide the controller with a way to trigger layouts. 11926 ctrl.layoutDelegate = layoutDelegate; 11927
vendor: 4,926 bytes, lines 11928-12056
11928 var invalidateLayout = angular.bind(ctrl, ctrl.invalidateLayout), 11929 unwatchAttrs = watchMedia(); 11930 scope.$on('$destroy', unwatchMedia); 11931 11932 /** 11933 * Watches for changes in media, invalidating layout as necessary. 11934 */ 11935 function watchMedia() { 11936 for (var mediaName in $mdConstant.MEDIA) { 11937 $mdMedia(mediaName); // initialize 11938 $mdMedia.getQuery($mdConstant.MEDIA[mediaName]) 11939 .addListener(invalidateLayout); 11940 } 11941 return $mdMedia.watchResponsiveAttributes( 11942 ['md-cols', 'md-row-height', 'md-gutter'], attrs, layoutIfMediaMatch); 11943 } 11944 11945 function unwatchMedia() { 11946 ctrl.layoutDelegate = angular.noop; 11947 11948 unwatchAttrs(); 11949 for (var mediaName in $mdConstant.MEDIA) { 11950 $mdMedia.getQuery($mdConstant.MEDIA[mediaName]) 11951 .removeListener(invalidateLayout); 11952 } 11953 } 11954 11955 /** 11956 * Performs grid layout if the provided mediaName matches the currently 11957 * active media type. 11958 */ 11959 function layoutIfMediaMatch(mediaName) { 11960 if (mediaName == null) { 11961 // TODO(shyndman): It would be nice to only layout if we have 11962 // instances of attributes using this media type 11963 ctrl.invalidateLayout(); 11964 } else if ($mdMedia(mediaName)) { 11965 ctrl.invalidateLayout(); 11966 } 11967 } 11968 11969 var lastLayoutProps; 11970 11971 /** 11972 * Invokes the layout engine, and uses its results to lay out our 11973 * tile elements. 11974 * 11975 * @param {boolean} tilesInvalidated Whether tiles have been 11976 * added/removed/moved since the last layout. This is to avoid situations 11977 * where tiles are replaced with properties identical to their removed 11978 * counterparts. 11979 */ 11980 function layoutDelegate(tilesInvalidated) { 11981 var tiles = getTileElements(); 11982 var props = { 11983 tileSpans: getTileSpans(tiles), 11984 colCount: getColumnCount(), 11985 rowMode: getRowMode(), 11986 rowHeight: getRowHeight(), 11987 gutter: getGutter() 11988 }; 11989 11990 if (!tilesInvalidated && angular.equals(props, lastLayoutProps)) { 11991 return; 11992 } 11993 11994 var performance = 11995 $mdGridLayout(props.colCount, props.tileSpans, tiles) 11996 .map(function(tilePositions, rowCount) { 11997 return { 11998 grid: { 11999 element: element, 12000 style: getGridStyle(props.colCount, rowCount, 12001 props.gutter, props.rowMode, props.rowHeight) 12002 }, 12003 tiles: tilePositions.map(function(ps, i) { 12004 return { 12005 element: angular.element(tiles[i]), 12006 style: getTileStyle(ps.position, ps.spans, 12007 props.colCount, rowCount, 12008 props.gutter, props.rowMode, props.rowHeight) 12009 } 12010 }) 12011 } 12012 }) 12013 .reflow() 12014 .performance(); 12015 12016 // Report layout 12017 scope.mdOnLayout({ 12018 $event: { 12019 performance: performance 12020 } 12021 }); 12022 12023 lastLayoutProps = props; 12024 } 12025 12026 // Use $interpolate to do some simple string interpolation as a convenience. 12027 12028 var startSymbol = $interpolate.startSymbol(); 12029 var endSymbol = $interpolate.endSymbol(); 12030 12031 // Returns an expression wrapped in the interpolator's start and end symbols. 12032 function expr(exprStr) { 12033 return startSymbol + exprStr + endSymbol; 12034 } 12035 12036 // The amount of space a single 1x1 tile would take up (either width or height), used as 12037 // a basis for other calculations. This consists of taking the base size percent (as would be 12038 // if evenly dividing the size between cells), and then subtracting the size of one gutter. 12039 // However, since there are no gutters on the edges, each tile only uses a fration 12040 // (gutterShare = numGutters / numCells) of the gutter size. (Imagine having one gutter per 12041 // tile, and then breaking up the extra gutter on the edge evenly among the cells). 12042 var UNIT = $interpolate(expr('share') + '% - (' + expr('gutter') + ' * ' + expr('gutterShare') + ')'); 12043 12044 // The horizontal or vertical position of a tile, e.g., the 'top' or 'left' property value. 12045 // The position comes the size of a 1x1 tile plus gutter for each previous tile in the 12046 // row/column (offset). 12047 var POSITION = $interpolate('calc((' + expr('unit') + ' + ' + expr('gutter') + ') * ' + expr('offset') + ')'); 12048 12049 // The actual size of a tile, e.g., width or height, taking rowSpan or colSpan into account. 12050 // This is computed by multiplying the base unit by the rowSpan/colSpan, and then adding back 12051 // in the space that the gutter would normally have used (which was already accounted for in 12052 // the base unit calculation). 12053 var DIMENSION = $interpolate('calc((' + expr('unit') + ') * ' + expr('span') + ' + (' + expr('span') + ' - 1) * ' + expr('gutter') + ')'); 12054 12055 /** 12056 * Gets the styles applied to a tile element described by the given parameters.
12057 * @param {{row: number, col: number}} position The row and column indices of the tile. 12058 * @param {{row: number, col: number}} spans The rowSpan and colSpan of the tile. 12059 * @param {number} colCount The number of columns. 12060 * @param {number} rowCount The number of rows. 12061 * @param {string} gutter The amount of space between tiles. This will be something like 12062 * '5px' or '2em'. 12063 * @param {string} rowMode The row height mode. Can be one of: 12064 * 'fixed': all rows have a fixed size, given by rowHeight, 12065 * 'ratio': row height defined as a ratio to width, or 12066 * 'fit': fit to the grid-list element height, divinding evenly among rows. 12067 * @param {string|number} rowHeight The height of a row. This is only used for 'fixed' mode and 12068 * for 'ratio' mode. For 'ratio' mode, this is the *ratio* of width-to-height (e.g., 0.75). 12069 * @returns {Object} Map of CSS properties to be applied to the style element. Will define 12070 * values for top, left, width, height, marginTop, and paddingTop. 12071 */ 12072 function getTileStyle(position, spans, colCount, rowCount, gutter, rowMode, rowHeight) { 12073 // TODO(shyndman): There are style caching opportunities here. 12074 12075 // Percent of the available horizontal space that one column takes up. 12076 var hShare = (1 / colCount) * 100; 12077 12078 // Fraction of the gutter size that each column takes up. 12079 var hGutterShare = (colCount - 1) / colCount; 12080 12081 // Base horizontal size of a column. 12082 var hUnit = UNIT({share: hShare, gutterShare: hGutterShare, gutter: gutter}); 12083 12084 // The width and horizontal position of each tile is always calculated the same way, but the 12085 // height and vertical position depends on the rowMode. 12086 var ltr = document.dir != 'rtl' && document.body.dir != 'rtl'; 12087 var style = ltr ? { 12088 left: POSITION({ unit: hUnit, offset: position.col, gutter: gutter }), 12089 width: DIMENSION({ unit: hUnit, span: spans.col, gutter: gutter }), 12090 // resets 12091 paddingTop: '', 12092 marginTop: '', 12093 top: '', 12094 height: '' 12095 } : { 12096 right: POSITION({ unit: hUnit, offset: position.col, gutter: gutter }), 12097 width: DIMENSION({ unit: hUnit, span: spans.col, gutter: gutter }), 12098 // resets 12099 paddingTop: '', 12100 marginTop: '', 12101 top: '', 12102 height: '' 12103 }; 12104 12105 switch (rowMode) { 12106 case 'fixed': 12107 // In fixed mode, simply use the given rowHeight. 12108 style.top = POSITION({ unit: rowHeight, offset: position.row, gutter: gutter }); 12109 style.height = DIMENSION({ unit: rowHeight, span: spans.row, gutter: gutter }); 12110 break; 12111 12112 case 'ratio': 12113 // Percent of the available vertical space that one row takes up. Here, rowHeight holds 12114 // the ratio value. For example, if the width:height ratio is 4:3, rowHeight = 1.333. 12115 var vShare = hShare / rowHeight; 12116 12117 // Base veritcal size of a row. 12118 var vUnit = UNIT({ share: vShare, gutterShare: hGutterShare, gutter: gutter }); 12119 12120 // padidngTop and marginTop are used to maintain the given aspect ratio, as 12121 // a percentage-based value for these properties is applied to the *width* of the 12122 // containing block. See http://www.w3.org/TR/CSS2/box.html#margin-properties 12123 style.paddingTop = DIMENSION({ unit: vUnit, span: spans.row, gutter: gutter}); 12124 style.marginTop = POSITION({ unit: vUnit, offset: position.row, gutter: gutter }); 12125 break; 12126 12127 case 'fit': 12128 // Fraction of the gutter size that each column takes up. 12129 var vGutterShare = (rowCount - 1) / rowCount; 12130 12131 // Percent of the available vertical space that one row takes up. 12132 var vShare = (1 / rowCount) * 100; 12133 12134 // Base vertical size of a row. 12135 var vUnit = UNIT({share: vShare, gutterShare: vGutterShare, gutter: gutter}); 12136 12137 style.top = POSITION({unit: vUnit, offset: position.row, gutter: gutter}); 12138 style.height = DIMENSION({unit: vUnit, span: spans.row, gutter: gutter}); 12139 break; 12140 } 12141 12142 return style; 12143 } 12144 12145 function getGridStyle(colCount, rowCount, gutter, rowMode, rowHeight) { 12146 var style = {}; 12147 12148 switch
vendor: 6,126 bytes, lines 12148-12366
12148(rowMode) { 12149 case 'fixed': 12150 style.height = DIMENSION({ unit: rowHeight, span: rowCount, gutter: gutter }); 12151 style.paddingBottom = ''; 12152 break; 12153 12154 case 'ratio': 12155 // rowHeight is width / height 12156 var hGutterShare = colCount === 1 ? 0 : (colCount - 1) / colCount, 12157 hShare = (1 / colCount) * 100, 12158 vShare = hShare * (1 / rowHeight), 12159 vUnit = UNIT({ share: vShare, gutterShare: hGutterShare, gutter: gutter }); 12160 12161 style.height = ''; 12162 style.paddingBottom = DIMENSION({ unit: vUnit, span: rowCount, gutter: gutter}); 12163 break; 12164 12165 case 'fit': 12166 // noop, as the height is user set 12167 break; 12168 } 12169 12170 return style; 12171 } 12172 12173 function getTileElements() { 12174 return [].filter.call(element.children(), function(ele) { 12175 return ele.tagName == 'MD-GRID-TILE' && !ele.$$mdDestroyed; 12176 }); 12177 } 12178 12179 /** 12180 * Gets an array of objects containing the rowspan and colspan for each tile. 12181 * @returns {Array<{row: number, col: number}>} 12182 */ 12183 function getTileSpans(tileElements) { 12184 return [].map.call(tileElements, function(ele) { 12185 var ctrl = angular.element(ele).controller('mdGridTile'); 12186 return { 12187 row: parseInt( 12188 $mdMedia.getResponsiveAttribute(ctrl.$attrs, 'md-rowspan'), 10) || 1, 12189 col: parseInt( 12190 $mdMedia.getResponsiveAttribute(ctrl.$attrs, 'md-colspan'), 10) || 1 12191 }; 12192 }); 12193 } 12194 12195 function getColumnCount() { 12196 var colCount = parseInt($mdMedia.getResponsiveAttribute(attrs, 'md-cols'), 10); 12197 if (isNaN(colCount)) { 12198 throw 'md-grid-list: md-cols attribute was not found, or contained a non-numeric value'; 12199 } 12200 return colCount; 12201 } 12202 12203 function getGutter() { 12204 return applyDefaultUnit($mdMedia.getResponsiveAttribute(attrs, 'md-gutter') || 1); 12205 } 12206 12207 function getRowHeight() { 12208 var rowHeight = $mdMedia.getResponsiveAttribute(attrs, 'md-row-height'); 12209 if (!rowHeight) { 12210 throw 'md-grid-list: md-row-height attribute was not found'; 12211 } 12212 12213 switch (getRowMode()) { 12214 case 'fixed': 12215 return applyDefaultUnit(rowHeight); 12216 case 'ratio': 12217 var whRatio = rowHeight.split(':'); 12218 return parseFloat(whRatio[0]) / parseFloat(whRatio[1]); 12219 case 'fit': 12220 return 0; // N/A 12221 } 12222 } 12223 12224 function getRowMode() { 12225 var rowHeight = $mdMedia.getResponsiveAttribute(attrs, 'md-row-height'); 12226 if (!rowHeight) { 12227 throw 'md-grid-list: md-row-height attribute was not found'; 12228 } 12229 12230 if (rowHeight == 'fit') { 12231 return 'fit'; 12232 } else if (rowHeight.indexOf(':') !== -1) { 12233 return 'ratio'; 12234 } else { 12235 return 'fixed'; 12236 } 12237 } 12238 12239 function applyDefaultUnit(val) { 12240 return /\D$/.test(val) ? val : val + 'px'; 12241 } 12242 } 12243} 12244 12245/* @ngInject */ 12246function GridListController($mdUtil) { 12247 this.layoutInvalidated = false; 12248 this.tilesInvalidated = false; 12249 this.$timeout_ = $mdUtil.nextTick; 12250 this.layoutDelegate = angular.noop; 12251} 12252 12253GridListController.prototype = { 12254 invalidateTiles: function() { 12255 this.tilesInvalidated = true; 12256 this.invalidateLayout(); 12257 }, 12258 12259 invalidateLayout: function() { 12260 if (this.layoutInvalidated) { 12261 return; 12262 } 12263 this.layoutInvalidated = true; 12264 this.$timeout_(angular.bind(this, this.layout)); 12265 }, 12266 12267 layout: function() { 12268 try { 12269 this.layoutDelegate(this.tilesInvalidated); 12270 } finally { 12271 this.layoutInvalidated = false; 12272 this.tilesInvalidated = false; 12273 } 12274 } 12275}; 12276 12277 12278/* @ngInject */ 12279function GridLayoutFactory($mdUtil) { 12280 var defaultAnimator = GridTileAnimator; 12281 12282 /** 12283 * Set the reflow animator callback 12284 */ 12285 GridLayout.animateWith = function(customAnimator) { 12286 defaultAnimator = !angular.isFunction(customAnimator) ? GridTileAnimator : customAnimator; 12287 }; 12288 12289 return GridLayout; 12290 12291 /** 12292 * Publish layout function 12293 */ 12294 function GridLayout(colCount, tileSpans) { 12295 var self, layoutInfo, gridStyles, layoutTime, mapTime, reflowTime; 12296 12297 layoutTime = $mdUtil.time(function() { 12298 layoutInfo = calculateGridFor(colCount, tileSpans); 12299 }); 12300 12301 return self = { 12302 12303 /** 12304 * An array of objects describing each tile's position in the grid. 12305 */ 12306 layoutInfo: function() { 12307 return layoutInfo; 12308 }, 12309 12310 /** 12311 * Maps grid positioning to an element and a set of styles using the 12312 * provided updateFn. 12313 */ 12314 map: function(updateFn) { 12315 mapTime = $mdUtil.time(function() { 12316 var info = self.layoutInfo(); 12317 gridStyles = updateFn(info.positioning, info.rowCount); 12318 }); 12319 return self; 12320 }, 12321 12322 /** 12323 * Default animator simply sets the element.css( <styles> ). An alternate 12324 * animator can be provided as an argument. The function has the following 12325 * signature: 12326 * 12327 * function({grid: {element: JQLite, style: Object}, tiles: Array<{element: JQLite, style: Object}>) 12328 */ 12329 reflow: function(animatorFn) { 12330 reflowTime = $mdUtil.time(function() { 12331 var animator = animatorFn || defaultAnimator; 12332 animator(gridStyles.grid, gridStyles.tiles); 12333 }); 12334 return self; 12335 }, 12336 12337 /** 12338 * Timing for the most recent layout run. 12339 */ 12340 performance: function() { 12341 return { 12342 tileCount: tileSpans.length, 12343 layoutTime: layoutTime, 12344 mapTime: mapTime, 12345 reflowTime: reflowTime, 12346 totalTime: layoutTime + mapTime + reflowTime 12347 }; 12348 } 12349 }; 12350 } 12351 12352 /** 12353 * Default Gridlist animator simple sets the css for each element; 12354 * NOTE: any transitions effects must be manually set in the CSS. 12355 * e.g. 12356 * 12357 * md-grid-tile { 12358 * transition: all 700ms ease-out 50ms; 12359 * } 12360 * 12361 */ 12362 function GridTileAnimator(grid, tiles) { 12363 grid.element.css(grid.style); 12364 tiles.forEach(function(t) { 12365 t.element.css(t.style); 12366 })
vendor: 4,262 bytes, lines 12366-12510
12366 12367 } 12368 12369 /** 12370 * Calculates the positions of tiles. 12371 * 12372 * The algorithm works as follows: 12373 * An Array<Number> with length colCount (spaceTracker) keeps track of 12374 * available tiling positions, where elements of value 0 represents an 12375 * empty position. Space for a tile is reserved by finding a sequence of 12376 * 0s with length <= than the tile's colspan. When such a space has been 12377 * found, the occupied tile positions are incremented by the tile's 12378 * rowspan value, as these positions have become unavailable for that 12379 * many rows. 12380 * 12381 * If the end of a row has been reached without finding space for the 12382 * tile, spaceTracker's elements are each decremented by 1 to a minimum 12383 * of 0. Rows are searched in this fashion until space is found. 12384 */ 12385 function calculateGridFor(colCount, tileSpans) { 12386 var curCol = 0, 12387 curRow = 0, 12388 spaceTracker = newSpaceTracker(); 12389 12390 return { 12391 positioning: tileSpans.map(function(spans, i) { 12392 return { 12393 spans: spans, 12394 position: reserveSpace(spans, i) 12395 }; 12396 }), 12397 rowCount: curRow + Math.max.apply(Math, spaceTracker) 12398 }; 12399 12400 function reserveSpace(spans, i) { 12401 if (spans.col > colCount) { 12402 throw 'md-grid-list: Tile at position ' + i + ' has a colspan ' + 12403 '(' + spans.col + ') that exceeds the column count ' + 12404 '(' + colCount + ')'; 12405 } 12406 12407 var start = 0, 12408 end = 0; 12409 12410 // TODO(shyndman): This loop isn't strictly necessary if you can 12411 // determine the minimum number of rows before a space opens up. To do 12412 // this, recognize that you've iterated across an entire row looking for 12413 // space, and if so fast-forward by the minimum rowSpan count. Repeat 12414 // until the required space opens up. 12415 while (end - start < spans.col) { 12416 if (curCol >= colCount) { 12417 nextRow(); 12418 continue; 12419 } 12420 12421 start = spaceTracker.indexOf(0, curCol); 12422 if (start === -1 || (end = findEnd(start + 1)) === -1) { 12423 start = end = 0; 12424 nextRow(); 12425 continue; 12426 } 12427 12428 curCol = end + 1; 12429 } 12430 12431 adjustRow(start, spans.col, spans.row); 12432 curCol = start + spans.col; 12433 12434 return { 12435 col: start, 12436 row: curRow 12437 }; 12438 } 12439 12440 function nextRow() { 12441 curCol = 0; 12442 curRow++; 12443 adjustRow(0, colCount, -1); // Decrement row spans by one 12444 } 12445 12446 function adjustRow(from, cols, by) { 12447 for (var i = from; i < from + cols; i++) { 12448 spaceTracker[i] = Math.max(spaceTracker[i] + by, 0); 12449 } 12450 } 12451 12452 function findEnd(start) { 12453 var i; 12454 for (i = start; i < spaceTracker.length; i++) { 12455 if (spaceTracker[i] !== 0) { 12456 return i; 12457 } 12458 } 12459 12460 if (i === spaceTracker.length) { 12461 return i; 12462 } 12463 } 12464 12465 function newSpaceTracker() { 12466 var tracker = []; 12467 for (var i = 0; i < colCount; i++) { 12468 tracker.push(0); 12469 } 12470 return tracker; 12471 } 12472 } 12473} 12474 12475/** 12476 * @ngdoc directive 12477 * @name mdGridTile 12478 * @module material.components.gridList 12479 * @restrict E 12480 * @description 12481 * Tiles contain the content of an `md-grid-list`. They span one or more grid 12482 * cells vertically or horizontally, and use `md-grid-tile-{footer,header}` to 12483 * display secondary content. 12484 * 12485 * ### Responsive Attributes 12486 * 12487 * The `md-grid-tile` directive supports "responsive" attributes, which allow 12488 * different `md-rowspan` and `md-colspan` values depending on the currently 12489 * matching media query. 12490 * 12491 * In order to set a responsive attribute, first define the fallback value with 12492 * the standard attribute name, then add additional attributes with the 12493 * following convention: `{base-attribute-name}-{media-query-name}="{value}"` 12494 * (ie. `md-colspan-sm="4"`) 12495 * 12496 * @param {number=} md-colspan The number of columns to span (default 1). Cannot 12497 * exceed the number of columns in the grid. Supports interpolation. 12498 * @param {number=} md-rowspan The number of rows to span (default 1). Supports 12499 * interpolation. 12500 * 12501 * @usage 12502 * With header: 12503 * <hljs lang="html"> 12504 * <md-grid-tile> 12505 * <md-grid-tile-header> 12506 * <h3>This is a header</h3> 12507 * </md-grid-tile-header> 12508 * </md-grid-tile> 12509 * </hljs> 12510 *
12511 * With footer: 12512 * <hljs lang="html"> 12513 * <md-grid-tile> 12514 * <md-grid-tile-footer> 12515 * <h3>This is a footer</h3> 12516 * </md-grid-tile-footer> 12517 * </md-grid-tile> 12518 * </hljs> 12519 * 12520 * Spanning multiple rows/columns: 12521 * <hljs lang="html"> 12522 * <md-grid-tile md-colspan="2" md-rowspan="3"> 12523 * </md-grid-tile> 12524 * </hljs> 12525 * 12526 * Responsive attributes: 12527 * <hljs lang="html"> 12528 * <md-grid-tile md-colspan="1" md-colspan-sm="3" md-colspan-md="5"> 12529 * </md-grid-tile> 12530 * </hljs> 12531 */ 12532function GridTileDirective($mdMedia) { 12533 return { 12534 restrict: 'E', 12535 require: '^mdGridList', 12536 template: '<figure ng-transclude></figure>', 12537 transclude: true, 12538 scope: {}, 12539 // Simple controller that exposes attributes to the grid directive 12540 controller: ["$attrs", function($attrs) { 12541 this.$attrs = $attrs; 12542 }], 12543 link: postLink 12544 }; 12545 12546 function postLink(scope, element, attrs, gridCtrl) { 12547 // Apply semantics 12548 element.attr('role', 'listitem'); 12549 12550 // If our colspan or rowspan changes, trigger a layout 12551 var unwatchAttrs = $mdMedia.watchResponsiveAttributes(['md-colspan', 'md-rowspan'], 12552 attrs, angular.bind(gridCtrl, gridCtrl.invalidateLayout)); 12553 12554 // Tile registration/deregistration 12555 gridCtrl.invalidateTiles(); 12556 scope.$on('$destroy', function() { 12557 // Mark the tile as destroyed so it is no longer considered in layout, 12558 // even if the DOM element sticks around (like during a leave animation) 12559 element[0].$$mdDestroyed = true; 12560 unwatchAttrs(); 12561 gridCtrl.invalidateLayout(); 12562 }); 12563 12564 if (angular.isDefined(scope.$parent.$index)) { 12565 scope.$watch(function() { return scope.$parent.$index; }, 12566 function indexChanged(newIdx, oldIdx) { 12567 if (newIdx === oldIdx) { 12568 return; 12569 } 12570 gridCtrl.invalidateTiles(); 12571 }); 12572 } 12573 } 12574} 12575 12576 12577function GridTileCaptionDirective() { 12578 return { 12579 template: '<figcaption ng-transclude></figcaption>', 12580 transclude: true 12581 }; 12582} 12583 12584})(); 12585(function(){ 12586"use strict"; 12587 12588/** 12589 * @ngdoc module 12590 * @name material.components.icon 12591 * @description 12592 * Icon 12593 */ 12594angular.module('material.components.icon', ['material.core']); 12595 12596})(); 12597(function(){ 12598"use strict"; 12599 12600/** 12601 * @ngdoc module 12602 * @name material.components.input 12603 */ 12604mdInputContainerDirective.$inject = ["$mdTheming", "$parse"]; 12605inputTextareaDirective.$inject = ["$mdUtil", "$window", "$mdAria", "$timeout", "$mdGesture"]; 12606mdMaxlengthDirective.$inject = ["$animate", "$mdUtil"]; 12607placeholderDirective.$inject = ["$compile"]; 12608ngMessageDirective.$inject = ["$mdUtil"]; 12609mdSelectOnFocusDirective.$inject = ["$timeout"]; 12610mdInputInvalidMessagesAnimation.$inject = ["$$AnimateRunner", "$animateCss", "$mdUtil", "$log"]; 12611ngMessagesAnimation.$inject = ["$$AnimateRunner", "$animateCss", "$mdUtil", "$log"]; 12612ngMessageAnimation.$inject = ["$$AnimateRunner", "$animateCss", "$mdUtil", "$log"]; 12613var inputModule = angular.module('material.components.input', [ 12614 'material.core' 12615 ]) 12616 .directive('mdInputContainer', mdInputContainerDirective) 12617 .directive('label', labelDirective) 12618 .directive('input', inputTextareaDirective) 12619 .directive('textarea', inputTextareaDirective) 12620 .directive('mdMaxlength', mdMaxlengthDirective) 12621 .directive('placeholder', placeholderDirective) 12622 .directive('ngMessages', ngMessagesDirective) 12623 .directive('ngMessage', ngMessageDirective) 12624 .directive('ngMessageExp', ngMessageDirective) 12625 .directive('mdSelectOnFocus', mdSelectOnFocusDirective) 12626 12627 .animation('.md-input-invalid', mdInputInvalidMessagesAnimation) 12628 .animation('.md-input-messages-animation', ngMessagesAnimation) 12629 .animation('.md-input-message-animation', ngMessageAnimation); 12630 12631// If we are running inside of tests; expose some extra services so that we can test them 12632if (window._mdMocksIncluded) { 12633 inputModule.service('$$mdInput', function() { 12634 return { 12635 // special accessor to internals... useful for testing 12636 messages: { 12637 show : showInputMessages, 12638 hide : hideInputMessages, 12639 getElement : getMessagesElement 12640 } 12641 } 12642 }) 12643 12644 // Register a service for each animation so that we can easily inject them into unit tests 12645 .service('mdInputInvalidAnimation', mdInputInvalidMessagesAnimation) 12646 .service('mdInputMessagesAnimation', ngMessagesAnimation) 12647 .service('mdInputMessageAnimation', ngMessageAnimation); 12648} 12649 12650/** 12651 * @ngdoc directive 12652 * @name mdInputContainer 12653 * @module material.components.input 12654 * 12655 * @restrict E 12656 * 12657 * @description 12658 * `<md-input-container>` is the parent of any input or textarea element. 12659 * 12660 * Input and textarea elements will not behave properly unless the md-input-container 12661 * parent is provided. 12662 * 12663 * A single `<md-input-container>` should contain only one `<input>` element, otherwise it will throw an error. 12664 * 12665 * <b>Exception:</b>
12665 Hidden inputs (`<input type="hidden" />`) are ignored and will not throw an error, so 12666 * you may combine these with other inputs. 12667 * 12668 * <b>Note:</b> When using `ngMessages` with your input element, make sure the message and container elements 12669 * are *block* elements, otherwise animations applied to the messages will not look as intended. Either use a `div` and 12670 * apply the `ng-message` and `ng-messages` classes respectively, or use the `md-block` class on your element. 12671 * 12672 * @param md-is-error {expression=} When the given expression evaluates to true, the input container 12673 * will go into error state. Defaults to erroring if the input has been touched and is invalid. 12674 * @param md-no-float {boolean=} When present, `placeholder` attributes on the input will not be converted to floating 12675 * labels. 12676 * 12677 * @usage 12678 * <hljs lang="html"> 12679 * 12680 * <md-input-container> 12681 * <label>Username</label> 12682 * <input type="text" ng-model="user.name"> 12683 * </md-input-container> 12684 * 12685 * <md-input-container> 12686 * <label>Description</label> 12687 * <textarea ng-model="user.description"></textarea> 12688 * </md-input-container> 12689 * 12690 * </hljs> 12691 * 12692 * <h3>When disabling floating labels</h3> 12693 * <hljs lang="html"> 12694 * 12695 * <md-input-container md-no-float> 12696 * <input type="text" placeholder="Non-Floating Label"> 12697 * </md-input-container> 12698 * 12699 * </hljs> 12700 */ 12701function mdInputContainerDirective($mdTheming, $parse) { 12702 12703 ContainerCtrl.$inject = ["$scope", "$element", "$attrs", "$animate"]; 12704 var INPUT_TAGS = ['INPUT', 'TEXTAREA', 'SELECT', 'MD-SELECT']; 12705 12706 var LEFT_SELECTORS = INPUT_TAGS.reduce(function(selectors, isel) { 12707 return selectors.concat(['md-icon ~ ' + isel, '.md-icon ~ ' + isel]); 12708 }, []).join(","); 12709 12710 var RIGHT_SELECTORS = INPUT_TAGS.reduce(function(selectors, isel) { 12711 return selectors.concat([isel + ' ~ md-icon', isel + ' ~ .md-icon']); 12712 }, []).join(","); 12713 12714 return { 12715 restrict: 'E', 12716 compile: compile, 12717 controller: ContainerCtrl 12718 }; 12719 12720 function compile(tElement) { 12721 // Check for both a left & right icon 12722 var leftIcon = tElement[0].querySelector(LEFT_SELECTORS); 12723 var rightIcon = tElement[0].querySelector(RIGHT_SELECTORS); 12724 12725 if (leftIcon) { tElement.addClass('md-icon-left'); } 12726 if (rightIcon) { tElement.addClass('md-icon-right'); } 12727 12728 return function postLink(scope, element) { 12729 $mdTheming(element); 12730 }; 12731 } 12732 12733 function ContainerCtrl($scope, $element, $attrs, $animate) { 12734 var self = this; 12735 12736 self.isErrorGetter = $attrs.mdIsError && $parse($attrs.mdIsError); 12737 12738 self.delegateClick = function() { 12739 self.input.focus(); 12740 }; 12741 self.element = $element; 12742 self.setFocused = function(isFocused) { 12743 $element.toggleClass('md-input-focused', !!isFocused); 12744 }; 12745 self.setHasValue = function(hasValue) { 12746 $element.toggleClass('md-input-has-value', !!hasValue); 12747 }; 12748 self.setHasPlaceholder = function(hasPlaceholder) { 12749 $element.toggleClass('md-input-has-placeholder', !!hasPlaceholder); 12750 }; 12751 self.setInvalid = function(isInvalid) { 12752 if (isInvalid) { 12753 $animate.addClass($element, 'md-input-invalid'); 12754 } else { 12755 $animate.removeClass($element, 'md-input-invalid'); 12756 } 12757 }; 12758 $scope.$watch(function() { 12759 return self.label && self.input; 12760 }, function(hasLabelAndInput) { 12761 if (hasLabelAndInput && !self.label.attr('for')) { 12762 self.label.attr('for', self.input.attr('id')); 12763 } 12764 }); 12765 } 12766} 12767 12768function labelDirective() { 12769 return { 12770 restrict: 'E', 12771 require: '^?mdInputContainer', 12772 link: function(scope, element, attr, containerCtrl) { 12773 if (!containerCtrl || attr.mdNoFloat || element.hasClass('md-container-ignore')) return; 12774 12775 containerCtrl.label = element; 12776 scope.$on('$destroy', function() { 12777 containerCtrl.label = null; 12778 }); 12779 } 12780 }; 12781} 12782 12783/** 12784 * @ngdoc directive 12785 * @name mdInput 12786 * @restrict E 12787 * @module material.components.input 12788 * 12789 * @description 12790 * You can use any `<input>` or `<textarea>` element as a child of an `<md-input-container>`. This 12791 * allows you to build complex forms for data entry. 12792 * 12793 * When the input is required and uses a floating label, then the label will automatically contain 12794 * an asterisk (`*`).<br/> 12795 * This behavior can be disabled by using the `md-no-asterisk` attribute. 12796 * 12797 * @param {number=} md-maxlength The maximum number of characters allowed in this input. If this is 12798 * specified, a character counter will be shown underneath the input.<br/><br/> 12799 * The purpose of **`md-maxlength`** is exactly to show the max length counter text. If you don't 12800 * want the counter text and only need "plain" validation, you can use the "simple" `ng-maxlength` 12801 * or maxlength attributes.<br/><br/> 12802 * **Note:** Only valid for text/string inputs (not numeric). 12803 * 12804 * @param {string=} aria-label Aria-label is required when no label is present. A warning message 12805 * will be logged in the console if not present. 12806 * @param {string=} placeholder An alternative approach to using aria-label when the label is not 12807 * PRESENT. The placeholder text is copied to the aria-label attribute. 12808 * @param md-no-autogrow {boolean=} When present, textareas will not grow automatically. 12809 * @param md-no-asterisk {boolean=} When present, an asterisk will not be appended to the inputs floating label 12810 * @param md-no-resize {boolean=} Disables the textarea resize handle. 12811 * @param {number=} max-rows The maximum amount of rows for a textarea. 12812 * @param md-detect-hidden {boolean=} When present, textareas will be sized properly when they are 12813 * revealed after being hidden. This is off by default for performance reasons because it 12814 * guarantees a reflow every digest cycle. 12815 * 12816 * @usage 12817 * <hljs lang="html"> 12818 * <md-input-container> 12819 * <label>Color</label> 12820 * <input type="text" ng-model="color" required md-maxlength="10"> 12821 * </md-input-container> 12822 * </hljs> 12823 * 12824 * <h3>With Errors</h3> 12825 * 12826 * `md-input-container` also supports errors using the standard `ng-messages` directives and 12827 * animates the messages when they become visible using from the `ngEnter`/`ngLeave` events or 12828 * the `ngShow`/`ngHide` events. 12829 * 12830 * By default, the messages will be hidden until the input is in an error state. This is based off 12831 * of the `md-is-error` expression of the `md-input-container`. This gives the user a chance to 12832 * fill out the form before the errors become visible. 12833 * 12834 * <hljs lang="html"> 12835 * <form name="colorForm"> 12836 * <md-input-container> 12837 * <label>Favorite Color</label> 12838 * <input name="favoriteColor" ng-model="favoriteColor" required> 12839 * <div ng-messages="colorForm.favoriteColor.$error"> 12840 * <div ng-message="required">This is required!</div>
12841 * </div> 12842 * </md-input-container> 12843 * </form> 12844 * </hljs> 12845 * 12846 * We automatically disable this auto-hiding functionality if you provide any of the following 12847 * visibility directives on the `ng-messages` container: 12848 * 12849 * - `ng-if` 12850 * - `ng-show`/`ng-hide` 12851 * - `ng-switch-when`/`ng-switch-default` 12852 * 12853 * You can also disable this functionality manually by adding the `md-auto-hide="false"` expression 12854 * to the `ng-messages` container. This may be helpful if you always want to see the error messages 12855 * or if you are building your own visibilty directive. 12856 * 12857 * _<b>Note:</b> The `md-auto-hide` attribute is a static string that is only checked upon 12858 * initialization of the `ng-messages` directive to see if it equals the string `false`._ 12859 * 12860 * <hljs lang="html"> 12861 * <form name="userForm"> 12862 * <md-input-container> 12863 * <label>Last Name</label> 12864 * <input name="lastName" ng-model="lastName" required md-maxlength="10" minlength="4"> 12865 * <div ng-messages="userForm.lastName.$error" ng-show="userForm.lastName.$dirty"> 12866 * <div ng-message="required">This is required!</div> 12867 * <div ng-message="md-maxlength">That's too long!</div> 12868 * <div ng-message="minlength">That's too short!</div> 12869 * </div> 12870 * </md-input-container> 12871 * <md-input-container> 12872 * <label>Biography</label> 12873 * <textarea name="bio" ng-model="biography" required md-maxlength="150"></textarea> 12874 * <div ng-messages="userForm.bio.$error" ng-show="userForm.bio.$dirty"> 12875 * <div ng-message="required">This is required!</div> 12876 * <div ng-message="md-maxlength">That's too long!</div> 12877 * </div> 12878 * </md-input-container> 12879 * <md-input-container> 12880 * <input aria-label='title' ng-model='title'> 12881 * </md-input-container> 12882 * <md-input-container> 12883 * <input placeholder='title' ng-model='title'> 12884 * </md-input-container> 12885 * </form> 12886 * </hljs> 12887 * 12888 * <h3>Notes</h3> 12889 * 12890 * - Requires [ngMessages](https://docs.angularjs.org/api/ngMessages). 12891 * - Behaves like the [AngularJS input directive](https://docs.angularjs.org/api/ng/directive/input). 12892 * 12893 * The `md-input` and `md-input-container` directives use very specific positioning to achieve the 12894 * error animation effects. Therefore, it is *not* advised to use the Layout system inside of the 12895 * `<md-input-container>` tags. Instead, use relative or absolute positioning. 12896 * 12897 * 12898 * <h3>Textarea directive</h3> 12899 * The `textarea` element within a `md-input-container` has the following specific behavior: 12900 * - By default the `textarea` grows as the user types. This can be disabled via the `md-no-autogrow` 12901 * attribute. 12902 * - If a `textarea` has the `rows` attribute, it will treat the `rows` as the minimum height and will 12903 * continue growing as the user types. For example a textarea with `rows="3"` will be 3 lines of text 12904 * high initially. If no rows are specified, the directive defaults to 1. 12905 * - The textarea's height gets set on initialization, as well as while the user is typing. In certain situations 12906 * (e.g. while animating) the directive might have been initialized, before the element got it's final height. In 12907 * those cases, you can trigger a resize manually by broadcasting a `md-resize-textarea` event on the scope. 12908 * - If you want a `textarea` to stop growing at a certain point, you can specify the `max-rows` attribute. 12909 * - The textarea's bottom border acts as a handle which users can drag, in order to resize the element vertically. 12910 * Once the user has resized a `textarea`, the autogrowing functionality becomes disabled. If you don't want a 12911 * `textarea` to be resizeable by the user, you can add the `md-no-resize` attribute. 12912 */ 12913 12914function inputTextareaDirective($mdUtil, $window, $mdAria, $timeout, $mdGesture) { 12915 return { 12916 restrict: 'E', 12917 require: ['^?mdInputContainer', '?ngModel', '?^form'], 12918 link: postLink 12919 }; 12920 12921 function postLink(scope, element, attr, ctrls) { 12922 12923 var containerCtrl = ctrls[0]; 12924 var hasNgModel = !!ctrls[1]; 12925 var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel(); 12926 var parentForm = ctrls[2]; 12927 var isReadonly = angular.isDefined(attr.readonly); 12928 var mdNoAsterisk = $mdUtil.parseAttributeBoolean(attr.mdNoAsterisk); 12929 var tagName = element[0].tagName.toLowerCase(); 12930 12931 12932 if (!containerCtrl) return; 12933 if (attr.type === 'hidden') { 12934 element.attr('aria-hidden', 'true'); 12935 return; 12936 } else if (containerCtrl.input) { 12937 if (containerCtrl.input[0].contains(element[0])) { 12938 return; 12939 } else { 12940 throw new Error("<md-input-container> can only have *one* <input>, <textarea> or <md-select> child element!"); 12941 } 12942 } 12943 containerCtrl.input = element; 12944 12945 setupAttributeWatchers(); 12946 12947 // Add an error spacer div after our input to provide space for the char counter and any ng-messages
vendor: 10,434 bytes, lines 12948-13267
12948 var errorsSpacer = angular.element('<div class="md-errors-spacer">'); 12949 element.after(errorsSpacer); 12950 12951 var placeholderText = angular.isString(attr.placeholder) ? attr.placeholder.trim() : ''; 12952 if (!containerCtrl.label && !placeholderText.length) { 12953 $mdAria.expect(element, 'aria-label'); 12954 } 12955 12956 element.addClass('md-input'); 12957 if (!element.attr('id')) { 12958 element.attr('id', 'input_' + $mdUtil.nextUid()); 12959 } 12960 12961 // This works around a Webkit issue where number inputs, placed in a flexbox, that have 12962 // a `min` and `max` will collapse to about 1/3 of their proper width. Please check #7349 12963 // for more info. Also note that we don't override the `step` if the user has specified it, 12964 // in order to prevent some unexpected behaviour. 12965 if (tagName === 'input' && attr.type === 'number' && attr.min && attr.max && !attr.step) { 12966 element.attr('step', 'any'); 12967 } else if (tagName === 'textarea') { 12968 setupTextarea(); 12969 } 12970 12971 // If the input doesn't have an ngModel, it may have a static value. For that case, 12972 // we have to do one initial check to determine if the container should be in the 12973 // "has a value" state. 12974 if (!hasNgModel) { 12975 inputCheckValue(); 12976 } 12977 12978 var isErrorGetter = containerCtrl.isErrorGetter || function() { 12979 return ngModelCtrl.$invalid && (ngModelCtrl.$touched || (parentForm && parentForm.$submitted)); 12980 }; 12981 12982 scope.$watch(isErrorGetter, containerCtrl.setInvalid); 12983 12984 // When the developer uses the ngValue directive for the input, we have to observe the attribute, because 12985 // AngularJS's ngValue directive is just setting the `value` attribute. 12986 if (attr.ngValue) { 12987 attr.$observe('value', inputCheckValue); 12988 } 12989 12990 ngModelCtrl.$parsers.push(ngModelPipelineCheckValue); 12991 ngModelCtrl.$formatters.push(ngModelPipelineCheckValue); 12992 12993 element.on('input', inputCheckValue); 12994 12995 if (!isReadonly) { 12996 element 12997 .on('focus', function(ev) { 12998 $mdUtil.nextTick(function() { 12999 containerCtrl.setFocused(true); 13000 }); 13001 }) 13002 .on('blur', function(ev) { 13003 $mdUtil.nextTick(function() { 13004 containerCtrl.setFocused(false); 13005 inputCheckValue(); 13006 }); 13007 }); 13008 } 13009 13010 scope.$on('$destroy', function() { 13011 containerCtrl.setFocused(false); 13012 containerCtrl.setHasValue(false); 13013 containerCtrl.input = null; 13014 }); 13015 13016 /** Gets run through ngModel's pipeline and set the `has-value` class on the container. */ 13017 function ngModelPipelineCheckValue(arg) { 13018 containerCtrl.setHasValue(!ngModelCtrl.$isEmpty(arg)); 13019 return arg; 13020 } 13021 13022 function setupAttributeWatchers() { 13023 if (containerCtrl.label) { 13024 attr.$observe('required', function (value) { 13025 // We don't need to parse the required value, it's always a boolean because of angular's 13026 // required directive. 13027 containerCtrl.label.toggleClass('md-required', value && !mdNoAsterisk); 13028 }); 13029 } 13030 } 13031 13032 function inputCheckValue() { 13033 // An input's value counts if its length > 0, 13034 // or if the input's validity state says it has bad input (eg string in a number input) 13035 containerCtrl.setHasValue(element.val().length > 0 || (element[0].validity || {}).badInput); 13036 } 13037 13038 function setupTextarea() { 13039 var isAutogrowing = !attr.hasOwnProperty('mdNoAutogrow'); 13040 13041 attachResizeHandle(); 13042 13043 if (!isAutogrowing) return; 13044 13045 // Can't check if height was or not explicity set, 13046 // so rows attribute will take precedence if present 13047 var minRows = attr.hasOwnProperty('rows') ? parseInt(attr.rows) : NaN; 13048 var maxRows = attr.hasOwnProperty('maxRows') ? parseInt(attr.maxRows) : NaN; 13049 var scopeResizeListener = scope.$on('md-resize-textarea', growTextarea); 13050 var lineHeight = null; 13051 var node = element[0]; 13052 13053 // This timeout is necessary, because the browser needs a little bit 13054 // of time to calculate the `clientHeight` and `scrollHeight`. 13055 $timeout(function() { 13056 $mdUtil.nextTick(growTextarea); 13057 }, 10, false); 13058 13059 // We could leverage ngModel's $parsers here, however it 13060 // isn't reliable, because AngularJS trims the input by default, 13061 // which means that growTextarea won't fire when newlines and 13062 // spaces are added. 13063 element.on('input', growTextarea); 13064 13065 // We should still use the $formatters, because they fire when 13066 // the value was changed from outside the textarea. 13067 if (hasNgModel) { 13068 ngModelCtrl.$formatters.push(formattersListener); 13069 } 13070 13071 if (!minRows) { 13072 element.attr('rows', 1); 13073 } 13074 13075 angular.element($window).on('resize', growTextarea); 13076 scope.$on('$destroy', disableAutogrow); 13077 13078 function growTextarea() { 13079 // temporarily disables element's flex so its height 'runs free' 13080 element 13081 .attr('rows', 1) 13082 .css('height', 'auto') 13083 .addClass('md-no-flex'); 13084 13085 var height = getHeight(); 13086 13087 if (!lineHeight) { 13088 // offsetHeight includes padding which can throw off our value 13089 var originalPadding = element[0].style.padding || ''; 13090 lineHeight = element.css('padding', 0).prop('offsetHeight'); 13091 element[0].style.padding = originalPadding; 13092 } 13093 13094 if (minRows && lineHeight) { 13095 height = Math.max(height, lineHeight * minRows); 13096 } 13097 13098 if (maxRows && lineHeight) { 13099 var maxHeight = lineHeight * maxRows; 13100 13101 if (maxHeight < height) { 13102 element.attr('md-no-autogrow', ''); 13103 height = maxHeight; 13104 } else { 13105 element.removeAttr('md-no-autogrow'); 13106 } 13107 } 13108 13109 if (lineHeight) { 13110 element.attr('rows', Math.round(height / lineHeight)); 13111 } 13112 13113 element 13114 .css('height', height + 'px') 13115 .removeClass('md-no-flex'); 13116 } 13117 13118 function getHeight() { 13119 var offsetHeight = node.offsetHeight; 13120 var line = node.scrollHeight - offsetHeight; 13121 return offsetHeight + Math.max(line, 0); 13122 } 13123 13124 function formattersListener(value) { 13125 $mdUtil.nextTick(growTextarea); 13126 return value; 13127 } 13128 13129 function disableAutogrow() { 13130 if (!isAutogrowing) return; 13131 13132 isAutogrowing = false; 13133 angular.element($window).off('resize', growTextarea); 13134 scopeResizeListener && scopeResizeListener(); 13135 element 13136 .attr('md-no-autogrow', '') 13137 .off('input', growTextarea); 13138 13139 if (hasNgModel) { 13140 var listenerIndex = ngModelCtrl.$formatters.indexOf(formattersListener); 13141 13142 if (listenerIndex > -1) { 13143 ngModelCtrl.$formatters.splice(listenerIndex, 1); 13144 } 13145 } 13146 } 13147 13148 function attachResizeHandle() { 13149 if (attr.hasOwnProperty('mdNoResize')) return; 13150 13151 var handle = angular.element('<div class="md-resize-handle"></div>'); 13152 var isDragging = false; 13153 var dragStart = null; 13154 var startHeight = 0; 13155 var container = containerCtrl.element; 13156 var dragGestureHandler = $mdGesture.register(handle, 'drag', { horizontal: false }); 13157 13158 13159 element.wrap('<div class="md-resize-wrapper">').after(handle); 13160 handle.on('mousedown', onMouseDown); 13161 13162 container 13163 .on('$md.dragstart', onDragStart) 13164 .on('$md.drag', onDrag) 13165 .on('$md.dragend', onDragEnd); 13166 13167 scope.$on('$destroy', function() { 13168 handle 13169 .off('mousedown', onMouseDown) 13170 .remove(); 13171 13172 container 13173 .off('$md.dragstart', onDragStart) 13174 .off('$md.drag', onDrag) 13175 .off('$md.dragend', onDragEnd); 13176 13177 dragGestureHandler(); 13178 handle = null; 13179 container = null; 13180 dragGestureHandler = null; 13181 }); 13182 13183 function onMouseDown(ev) { 13184 ev.preventDefault(); 13185 isDragging = true; 13186 dragStart = ev.clientY; 13187 startHeight = parseFloat(element.css('height')) || element.prop('offsetHeight'); 13188 } 13189 13190 function onDragStart(ev) { 13191 if (!isDragging) return; 13192 ev.preventDefault(); 13193 disableAutogrow(); 13194 container.addClass('md-input-resized'); 13195 } 13196 13197 function onDrag(ev) { 13198 if (!isDragging) return; 13199 13200 element.css('height', (startHeight + ev.pointer.distanceY) + 'px'); 13201 } 13202 13203 function onDragEnd(ev) { 13204 if (!isDragging) return; 13205 isDragging = false; 13206 container.removeClass('md-input-resized'); 13207 } 13208 } 13209 13210 // Attach a watcher to detect when the textarea gets shown. 13211 if (attr.hasOwnProperty('mdDetectHidden')) { 13212 13213 var handleHiddenChange = function() { 13214 var wasHidden = false; 13215 13216 return function() { 13217 var isHidden = node.offsetHeight === 0; 13218 13219 if (isHidden === false && wasHidden === true) { 13220 growTextarea(); 13221 } 13222 13223 wasHidden = isHidden; 13224 }; 13225 }(); 13226 13227 // Check every digest cycle whether the visibility of the textarea has changed. 13228 // Queue up to run after the digest cycle is complete. 13229 scope.$watch(function() { 13230 $mdUtil.nextTick(handleHiddenChange, false); 13231 return true; 13232 }); 13233 } 13234 } 13235 } 13236} 13237 13238function mdMaxlengthDirective($animate, $mdUtil) { 13239 return { 13240 restrict: 'A', 13241 require: ['ngModel', '^mdInputContainer'], 13242 link: postLink 13243 }; 13244 13245 function postLink(scope, element, attr, ctrls) { 13246 var maxlength; 13247 var ngModelCtrl = ctrls[0]; 13248 var containerCtrl = ctrls[1]; 13249 var charCountEl, errorsSpacer; 13250 13251 // Wait until the next tick to ensure that the input has setup the errors spacer where we will 13252 // append our counter 13253 $mdUtil.nextTick(function() { 13254 errorsSpacer = angular.element(containerCtrl.element[0].querySelector('.md-errors-spacer')); 13255 charCountEl = angular.element('<div class="md-char-counter">'); 13256 13257 // Append our character counter inside the errors spacer 13258 errorsSpacer.append(charCountEl); 13259 13260 // Stop model from trimming. This makes it so whitespace 13261 // over the maxlength still counts as invalid. 13262 attr.$set('ngTrim', 'false'); 13263 13264 scope.$watch(attr.mdMaxlength, function(value) { 13265 maxlength = value; 13266 if (angular.isNumber(value) && value > 0) { 13267 if (!charCountEl.parent().length) {
13268 $animate.enter(charCountEl, errorsSpacer); 13269 } 13270 renderCharCount(); 13271 } else { 13272 $animate.leave(charCountEl); 13273 } 13274 }); 13275 13276 ngModelCtrl.$validators['md-maxlength'] = function(modelValue, viewValue) { 13277 if (!angular.isNumber(maxlength) || maxlength < 0) { 13278 return true; 13279 } 13280 13281 // We always update the char count, when the modelValue has changed. 13282 // Using the $validators for triggering the update works very well. 13283 renderCharCount(); 13284 13285 return ( modelValue || element.val() || viewValue || '' ).length <= maxlength; 13286 }; 13287 }); 13288 13289 function renderCharCount(value) { 13290 // If we have not been appended to the body yet; do not render 13291 if (!charCountEl.parent) { 13292 return value; 13293 } 13294 13295 // Force the value into a string since it may be a number, 13296 // which does not have a length property. 13297 charCountEl.text(String(element.val() || value || '').length + ' / ' + maxlength); 13298 return value; 13299 } 13300 } 13301} 13302 13303function placeholderDirective($compile) { 13304 return { 13305 restrict: 'A', 13306 require: '^^?mdInputContainer', 13307 priority: 200, 13308 link: { 13309 // Note that we need to do this in the pre-link, as opposed to the post link, if we want to 13310 // support data bindings in the placeholder. This is necessary, because we have a case where 13311 // we transfer the placeholder value to the `<label>` and we remove it from the original `<input>`. 13312 // If we did this in the post-link, AngularJS would have set up the observers already and would be 13313 // re-adding the attribute, even though we removed it from the element. 13314 pre: preLink 13315 } 13316 }; 13317 13318 function preLink(scope, element, attr, inputContainer) { 13319 // If there is no input container, just return 13320 if (!inputContainer) return; 13321 13322 var label = inputContainer.element.find('label'); 13323 var noFloat = inputContainer.element.attr('md-no-float'); 13324 13325 // If we have a label, or they specify the md-no-float attribute, just return 13326 if ((label && label.length) || noFloat === '' || scope.$eval(noFloat)) { 13327 // Add a placeholder class so we can target it in the CSS 13328 inputContainer.setHasPlaceholder(true); 13329 return; 13330 } 13331 13332 // md-select handles placeholders on it's own 13333 if (element[0].nodeName != 'MD-SELECT') { 13334 // Move the placeholder expression to the label 13335 var newLabel = angular.element('<label ng-click="delegateClick()" tabindex="-1">' + attr.placeholder + '</label>'); 13336 13337 // Note that we unset it via `attr`, in order to get AngularJS 13338 // to remove any observers that it might have set up. Otherwise 13339 // the attribute will be added on the next digest. 13340 attr.$set('placeholder', null); 13341 13342 // We need to compile the label manually in case it has any bindings. 13343 // A gotcha here is that we first add the element to the DOM and we compile 13344 // it later. This is necessary, because if we compile the element beforehand, 13345 // it won't be able to find the `mdInputContainer` controller. 13346 inputContainer.element 13347 .addClass('md-icon-float') 13348 .prepend(newLabel); 13349 13350 $compile(newLabel)(scope); 13351 } 13352 } 13353} 13354 13355/** 13356 * @ngdoc directive 13357 * @name mdSelectOnFocus 13358 * @module material.components.input 13359 * 13360 * @restrict A 13361 * 13362 * @description 13363 * The `md-select-on-focus` directive allows you to automatically select the element's input text on focus. 13364 * 13365 * <h3>Notes</h3> 13366 * - The use of `md-select-on-focus` is restricted to `<input>` and `<textarea>` elements. 13367 * 13368 * @usage 13369 * <h3>Using with an Input</h3> 13370 * <hljs lang="html"> 13371 * 13372 * <md-input-container> 13373 * <label>Auto Select</label> 13374 * <input type="text" md-select-on-focus> 13375 * </md-input-container> 13376 * </hljs> 13377 * 13378 * <h3>Using with a Textarea</h3> 13379 * <hljs lang="html"> 13380 * 13381 * <md-input-container> 13382 * <label>Auto Select</label> 13383 * <textarea md-select-on-focus>This text will be selected on focus.</textarea> 13384 * </md-input-container> 13385 * 13386 * </hljs> 13387 */ 13388function mdSelectOnFocusDirective($timeout) { 13389 13390 return { 13391 restrict: 'A', 13392 link: postLink 13393 }; 13394 13395 function postLink(scope, element, attr) { 13396 if (element[0].nodeName !== 'INPUT' && element[0].nodeName !== "TEXTAREA") return; 13397 13398 var preventMouseUp = false;
vendor: 10,130 bytes, lines 13399-13763
13399 13400 element 13401 .on('focus', onFocus) 13402 .on('mouseup', onMouseUp); 13403 13404 scope.$on('$destroy', function() { 13405 element 13406 .off('focus', onFocus) 13407 .off('mouseup', onMouseUp); 13408 }); 13409 13410 function onFocus() { 13411 preventMouseUp = true; 13412 13413 $timeout(function() { 13414 // Use HTMLInputElement#select to fix firefox select issues. 13415 // The debounce is here for Edge's sake, otherwise the selection doesn't work. 13416 element[0].select(); 13417 13418 // This should be reset from inside the `focus`, because the event might 13419 // have originated from something different than a click, e.g. a keyboard event. 13420 preventMouseUp = false; 13421 }, 1, false); 13422 } 13423 13424 // Prevents the default action of the first `mouseup` after a focus. 13425 // This is necessary, because browsers fire a `mouseup` right after the element 13426 // has been focused. In some browsers (Firefox in particular) this can clear the 13427 // selection. There are examples of the problem in issue #7487. 13428 function onMouseUp(event) { 13429 if (preventMouseUp) { 13430 event.preventDefault(); 13431 } 13432 } 13433 } 13434} 13435 13436var visibilityDirectives = ['ngIf', 'ngShow', 'ngHide', 'ngSwitchWhen', 'ngSwitchDefault']; 13437function ngMessagesDirective() { 13438 return { 13439 restrict: 'EA', 13440 link: postLink, 13441 13442 // This is optional because we don't want target *all* ngMessage instances, just those inside of 13443 // mdInputContainer. 13444 require: '^^?mdInputContainer' 13445 }; 13446 13447 function postLink(scope, element, attrs, inputContainer) { 13448 // If we are not a child of an input container, don't do anything 13449 if (!inputContainer) return; 13450 13451 // Add our animation class 13452 element.toggleClass('md-input-messages-animation', true); 13453 13454 // Add our md-auto-hide class to automatically hide/show messages when container is invalid 13455 element.toggleClass('md-auto-hide', true); 13456 13457 // If we see some known visibility directives, remove the md-auto-hide class 13458 if (attrs.mdAutoHide == 'false' || hasVisibiltyDirective(attrs)) { 13459 element.toggleClass('md-auto-hide', false); 13460 } 13461 } 13462 13463 function hasVisibiltyDirective(attrs) { 13464 return visibilityDirectives.some(function(attr) { 13465 return attrs[attr]; 13466 }); 13467 } 13468} 13469 13470function ngMessageDirective($mdUtil) { 13471 return { 13472 restrict: 'EA', 13473 compile: compile, 13474 priority: 100 13475 }; 13476 13477 function compile(tElement) { 13478 if (!isInsideInputContainer(tElement)) { 13479 13480 // When the current element is inside of a document fragment, then we need to check for an input-container 13481 // in the postLink, because the element will be later added to the DOM and is currently just in a temporary 13482 // fragment, which causes the input-container check to fail. 13483 if (isInsideFragment()) { 13484 return function (scope, element) { 13485 if (isInsideInputContainer(element)) { 13486 // Inside of the postLink function, a ngMessage directive will be a comment element, because it's 13487 // currently hidden. To access the shown element, we need to use the element from the compile function. 13488 initMessageElement(tElement); 13489 } 13490 }; 13491 } 13492 } else { 13493 initMessageElement(tElement); 13494 } 13495 13496 function isInsideFragment() { 13497 var nextNode = tElement[0]; 13498 while (nextNode = nextNode.parentNode) { 13499 if (nextNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { 13500 return true; 13501 } 13502 } 13503 return false; 13504 } 13505 13506 function isInsideInputContainer(element) { 13507 return !!$mdUtil.getClosest(element, "md-input-container"); 13508 } 13509 13510 function initMessageElement(element) { 13511 // Add our animation class 13512 element.toggleClass('md-input-message-animation', true); 13513 } 13514 } 13515} 13516 13517var $$AnimateRunner, $animateCss, $mdUtil, $log; 13518 13519function mdInputInvalidMessagesAnimation($$AnimateRunner, $animateCss, $mdUtil, $log) { 13520 saveSharedServices($$AnimateRunner, $animateCss, $mdUtil, $log); 13521 13522 return { 13523 addClass: function(element, className, done) { 13524 showInputMessages(element, done); 13525 } 13526 13527 // NOTE: We do not need the removeClass method, because the message ng-leave animation will fire 13528 }; 13529} 13530 13531function ngMessagesAnimation($$AnimateRunner, $animateCss, $mdUtil, $log) { 13532 saveSharedServices($$AnimateRunner, $animateCss, $mdUtil, $log); 13533 13534 return { 13535 enter: function(element, done) { 13536 showInputMessages(element, done); 13537 }, 13538 13539 leave: function(element, done) { 13540 hideInputMessages(element, done); 13541 }, 13542 13543 addClass: function(element, className, done) { 13544 if (className == "ng-hide") { 13545 hideInputMessages(element, done); 13546 } else { 13547 done(); 13548 } 13549 }, 13550 13551 removeClass: function(element, className, done) { 13552 if (className == "ng-hide") { 13553 showInputMessages(element, done); 13554 } else { 13555 done(); 13556 } 13557 } 13558 }; 13559} 13560 13561function ngMessageAnimation($$AnimateRunner, $animateCss, $mdUtil, $log) { 13562 saveSharedServices($$AnimateRunner, $animateCss, $mdUtil, $log); 13563 13564 return { 13565 enter: function(element, done) { 13566 var animator = showMessage(element); 13567 13568 animator.start().done(done); 13569 }, 13570 13571 leave: function(element, done) { 13572 var animator = hideMessage(element); 13573 13574 animator.start().done(done); 13575 } 13576 }; 13577} 13578 13579function showInputMessages(element, done) { 13580 var animators = [], animator; 13581 var messages = getMessagesElement(element); 13582 var children = messages.children(); 13583 13584 if (messages.length == 0 || children.length == 0) { 13585 $log.warn('mdInput messages show animation called on invalid messages element: ', element); 13586 done(); 13587 return; 13588 } 13589 13590 angular.forEach(children, function(child) { 13591 animator = showMessage(angular.element(child)); 13592 13593 animators.push(animator.start()); 13594 }); 13595 13596 $$AnimateRunner.all(animators, done); 13597} 13598 13599function hideInputMessages(element, done) { 13600 var animators = [], animator; 13601 var messages = getMessagesElement(element); 13602 var children = messages.children(); 13603 13604 if (messages.length == 0 || children.length == 0) { 13605 $log.warn('mdInput messages hide animation called on invalid messages element: ', element); 13606 done(); 13607 return; 13608 } 13609 13610 angular.forEach(children, function(child) { 13611 animator = hideMessage(angular.element(child)); 13612 13613 animators.push(animator.start()); 13614 }); 13615 13616 $$AnimateRunner.all(animators, done); 13617} 13618 13619function showMessage(element) { 13620 var height = parseInt(window.getComputedStyle(element[0]).height); 13621 var topMargin = parseInt(window.getComputedStyle(element[0]).marginTop); 13622 13623 var messages = getMessagesElement(element); 13624 var container = getInputElement(element); 13625 13626 // Check to see if the message is already visible so we can skip 13627 var alreadyVisible = (topMargin > -height); 13628 13629 // If we have the md-auto-hide class, the md-input-invalid animation will fire, so we can skip 13630 if (alreadyVisible || (messages.hasClass('md-auto-hide') && !container.hasClass('md-input-invalid'))) { 13631 return $animateCss(element, {}); 13632 } 13633 13634 return $animateCss(element, { 13635 event: 'enter', 13636 structural: true, 13637 from: {"opacity": 0, "margin-top": -height + "px"}, 13638 to: {"opacity": 1, "margin-top": "0"}, 13639 duration: 0.3 13640 }); 13641} 13642 13643function hideMessage(element) { 13644 var height = element[0].offsetHeight; 13645 var styles = window.getComputedStyle(element[0]); 13646 13647 // If we are already hidden, just return an empty animation 13648 if (parseInt(styles.opacity) === 0) { 13649 return $animateCss(element, {}); 13650 } 13651 13652 // Otherwise, animate 13653 return $animateCss(element, { 13654 event: 'leave', 13655 structural: true, 13656 from: {"opacity": 1, "margin-top": 0}, 13657 to: {"opacity": 0, "margin-top": -height + "px"}, 13658 duration: 0.3 13659 }); 13660} 13661 13662function getInputElement(element) { 13663 var inputContainer = element.controller('mdInputContainer'); 13664 13665 return inputContainer.element; 13666} 13667 13668function getMessagesElement(element) { 13669 // If we ARE the messages element, just return ourself 13670 if (element.hasClass('md-input-messages-animation')) { 13671 return element; 13672 } 13673 13674 // If we are a ng-message element, we need to traverse up the DOM tree 13675 if (element.hasClass('md-input-message-animation')) { 13676 return angular.element($mdUtil.getClosest(element, function(node) { 13677 return node.classList.contains('md-input-messages-animation'); 13678 })); 13679 } 13680 13681 // Otherwise, we can traverse down 13682 return angular.element(element[0].querySelector('.md-input-messages-animation')); 13683} 13684 13685function saveSharedServices(_$$AnimateRunner_, _$animateCss_, _$mdUtil_, _$log_) { 13686 $$AnimateRunner = _$$AnimateRunner_; 13687 $animateCss = _$animateCss_; 13688 $mdUtil = _$mdUtil_; 13689 $log = _$log_; 13690} 13691 13692})(); 13693(function(){ 13694"use strict"; 13695 13696/** 13697 * @ngdoc module 13698 * @name material.components.list 13699 * @description 13700 * List module 13701 */ 13702MdListController.$inject = ["$scope", "$element", "$mdListInkRipple"]; 13703mdListDirective.$inject = ["$mdTheming"]; 13704mdListItemDirective.$inject = ["$mdAria", "$mdConstant", "$mdUtil", "$timeout"]; 13705angular.module('material.components.list', [ 13706 'material.core' 13707]) 13708 .controller('MdListController', MdListController) 13709 .directive('mdList', mdListDirective) 13710 .directive('mdListItem', mdListItemDirective); 13711 13712/** 13713 * @ngdoc directive 13714 * @name mdList 13715 * @module material.components.list 13716 * 13717 * @restrict E 13718 * 13719 * @description 13720 * The `<md-list>` directive is a list container for 1..n `<md-list-item>` tags. 13721 * 13722 * @usage 13723 * <hljs lang="html"> 13724 * <md-list> 13725 * <md-list-item class="md-2-line" ng-repeat="item in todos"> 13726 * <md-checkbox ng-model="item.done"></md-checkbox> 13727 * <div class="md-list-item-text"> 13728 * <h3>{{item.title}}</h3> 13729 * <p>{{item.description}}</p> 13730 * </div> 13731 * </md-list-item> 13732 * </md-list> 13733 * </hljs> 13734 */ 13735 13736function mdListDirective($mdTheming) { 13737 return { 13738 restrict: 'E', 13739 compile: function(tEl) { 13740 tEl[0].setAttribute('role', 'list'); 13741 return $mdTheming; 13742 } 13743 }; 13744} 13745/** 13746 * @ngdoc directive 13747 * @name mdListItem 13748 * @module material.components.list 13749 * 13750 * @restrict E 13751 * 13752 * @description 13753 * A `md-list-item` element can be used to represent some information in a row.<br/> 13754 * 13755 * @usage 13756 * ### Single Row Item 13757 * <hljs lang="html"> 13758 * <md-list-item> 13759 * <span>Single Row Item</span> 13760 * </md-list-item> 13761 * </hljs> 13762 * 13763 * ### Multiple Lines
13764 * By using the following markup, you will be able to have two lines inside of one `md-list-item`. 13765 * 13766 * <hljs lang="html"> 13767 * <md-list-item class="md-2-line"> 13768 * <div class="md-list-item-text" layout="column"> 13769 * <p>First Line</p> 13770 * <p>Second Line</p> 13771 * </div> 13772 * </md-list-item> 13773 * </hljs> 13774 * 13775 * It is also possible to have three lines inside of one list item. 13776 * 13777 * <hljs lang="html"> 13778 * <md-list-item class="md-3-line"> 13779 * <div class="md-list-item-text" layout="column"> 13780 * <p>First Line</p> 13781 * <p>Second Line</p> 13782 * <p>Third Line</p> 13783 * </div> 13784 * </md-list-item> 13785 * </hljs> 13786 * 13787 * ### Secondary Items 13788 * Secondary items are elements which will be aligned at the end of the `md-list-item`. 13789 * 13790 * <hljs lang="html"> 13791 * <md-list-item> 13792 * <span>Single Row Item</span> 13793 * <md-button class="md-secondary"> 13794 * Secondary Button 13795 * </md-button> 13796 * </md-list-item> 13797 * </hljs> 13798 * 13799 * It also possible to have multiple secondary items inside of one `md-list-item`. 13800 * 13801 * <hljs lang="html"> 13802 * <md-list-item> 13803 * <span>Single Row Item</span> 13804 * <md-button class="md-secondary">First Button</md-button> 13805 * <md-button class="md-secondary">Second Button</md-button> 13806 * </md-list-item> 13807 * </hljs> 13808 * 13809 * ### Proxy Item 13810 * Proxies are elements, which will execute their specific action on click<br/> 13811 * Currently supported proxy items are 13812 * - `md-checkbox` (Toggle) 13813 * - `md-switch` (Toggle) 13814 * - `md-menu` (Open) 13815 * 13816 * This means, when using a supported proxy item inside of `md-list-item`, the list item will 13817 * automatically become clickable and executes the associated action of the proxy element on click. 13818 * 13819 * It is possible to disable this behavior by applying the `md-no-proxy` class to the list item. 13820 * 13821 * <hljs lang="html"> 13822 * <md-list-item class="md-no-proxy"> 13823 * <span>No Proxy List</span> 13824 * <md-checkbox class="md-secondary"></md-checkbox> 13825 * </md-list-item> 13826 * </hljs> 13827 * 13828 * Here are a few examples of proxy elements inside of a list item. 13829 * 13830 * <hljs lang="html"> 13831 * <md-list-item> 13832 * <span>First Line</span> 13833 * <md-checkbox class="md-secondary"></md-checkbox> 13834 * </md-list-item> 13835 * </hljs> 13836 * 13837 * The `md-checkbox` element will be automatically detected as a proxy element and will toggle on click. 13838 * 13839 * <hljs lang="html"> 13840 * <md-list-item> 13841 * <span>First Line</span> 13842 * <md-switch class="md-secondary"></md-switch> 13843 * </md-list-item> 13844 * </hljs> 13845 * 13846 * The recognized `md-switch` will toggle its state, when the user clicks on the `md-list-item`. 13847 * 13848 * It is also possible to have a `md-menu` inside of a `md-list-item`. 13849 * <hljs lang="html"> 13850 * <md-list-item> 13851 * <p>Click anywhere to fire the secondary action</p> 13852 * <md-menu class="md-secondary"> 13853 * <md-button class="md-icon-button"> 13854 * <md-icon md-svg-icon="communication:message"></md-icon> 13855 * </md-button> 13856 * <md-menu-content width="4"> 13857 * <md-menu-item> 13858 * <md-button> 13859 * Redial 13860 * </md-button> 13861 * </md-menu-item> 13862 * <md-menu-item> 13863 * <md-button> 13864 * Check voicemail 13865 * </md-button> 13866 * </md-menu-item> 13867 * <md-menu-divider></md-menu-divider> 13868 * <md-menu-item> 13869 * <md-button> 13870 * Notifications 13871 * </md-button> 13872 * </md-menu-item> 13873 * </md-menu-content> 13874 * </md-menu> 13875 * </md-list-item> 13876 * </hljs> 13877 * 13878 * The menu will automatically open, when the users clicks on the `md-list-item`.<br/> 13879 * 13880 * If the developer didn't specify any position mode on the menu, the `md-list-item` will automatically detect the 13881 * position mode and applies it to the `md-menu`. 13882 * 13883 * ### Avatars 13884 * Sometimes you may want to have some avatars inside of the `md-list-item `.<br/> 13885 * You are able to create a optimized icon for the list item, by applying the `.md-avatar` class on the `<img>` element. 13886 * 13887 * <hljs lang="html"> 13888 * <md-list-item> 13889 * <img src="my-avatar.png" class="md-avatar"> 13890 * <span>Alan Turing</span> 13891 * </hljs> 13892 * 13893 * When using `<md-icon>` for an avatar, you have to use the `.md-avatar-icon` class. 13894 * <hljs lang="html"> 13895 * <md-list-item> 13896 * <md-icon class="md-avatar-icon" md-svg-icon="avatars:timothy"></md-icon> 13897 * <span>Timothy Kopra</span> 13898 * </md-list-item> 13899 * </hljs> 13900 * 13901 * In cases, you have a `md-list-item`, which doesn't have any avatar, 13902 * but you want to align it with the other avatar items, you have to use the `.md-offset` class. 13903 * 13904 * <hljs lang="html"> 13905 * <md-list-item class="md-offset"> 13906 * <span>Jon Doe</span> 13907 * </md-list-item> 13908 * </hljs> 13909 * 13910 * ### DOM modification 13911 * The `md-list-item` component automatically detects if the list item should be clickable. 13912 * 13913 * --- 13914 * If the `md-list-item` is clickable, we wrap all content inside of a `<div>` and create 13915 * an overlaying button, which will will execute the given actions (like `ng-href`, `ng-click`) 13916 * 13917 * We create an overlaying button, instead of wrapping all content inside of the button, 13918 * because otherwise some elements may not be clickable inside of the button. 13919 * 13920 * --- 13921 * When using a secondary item inside of your list item, the `md-list-item` component will automatically create 13922 * a secondary container at the end of the `md-list-item`, which contains all secondary items. 13923 * 13924 * The secondary item container is not static, because otherwise the overflow will not work properly on the 13925 * list item. 13926 * 13927 */ 13928function mdListItemDirective($mdAria, $mdConstant, $mdUtil, $timeout) { 13929 var proxiedTypes = ['md-checkbox', 'md-switch', 'md-menu']; 13930 return { 13931 restrict: 'E', 13932 controller: 'MdListController',
vendor: 15,580 bytes, lines 13933-14391
13933 compile: function(tEl, tAttrs) { 13934 13935 // Check for proxy controls (no ng-click on parent, and a control inside) 13936 var secondaryItems = tEl[0].querySelectorAll('.md-secondary'); 13937 var hasProxiedElement; 13938 var proxyElement; 13939 var itemContainer = tEl; 13940 13941 tEl[0].setAttribute('role', 'listitem'); 13942 13943 if (tAttrs.ngClick || tAttrs.ngDblclick || tAttrs.ngHref || tAttrs.href || tAttrs.uiSref || tAttrs.ngAttrUiSref) { 13944 wrapIn('button'); 13945 } else if (!tEl.hasClass('md-no-proxy')) { 13946 13947 for (var i = 0, type; type = proxiedTypes[i]; ++i) { 13948 if (proxyElement = tEl[0].querySelector(type)) { 13949 hasProxiedElement = true; 13950 break; 13951 } 13952 } 13953 13954 if (hasProxiedElement) { 13955 wrapIn('div'); 13956 } else { 13957 tEl.addClass('md-no-proxy'); 13958 } 13959 13960 } 13961 13962 wrapSecondaryItems(); 13963 setupToggleAria(); 13964 13965 if (hasProxiedElement && proxyElement.nodeName === "MD-MENU") { 13966 setupProxiedMenu(); 13967 } 13968 13969 function setupToggleAria() { 13970 var toggleTypes = ['md-switch', 'md-checkbox']; 13971 var toggle; 13972 13973 for (var i = 0, toggleType; toggleType = toggleTypes[i]; ++i) { 13974 if (toggle = tEl.find(toggleType)[0]) { 13975 if (!toggle.hasAttribute('aria-label')) { 13976 var p = tEl.find('p')[0]; 13977 if (!p) return; 13978 toggle.setAttribute('aria-label', 'Toggle ' + p.textContent); 13979 } 13980 } 13981 } 13982 } 13983 13984 function setupProxiedMenu() { 13985 var menuEl = angular.element(proxyElement); 13986 13987 var isEndAligned = menuEl.parent().hasClass('md-secondary-container') || 13988 proxyElement.parentNode.firstElementChild !== proxyElement; 13989 13990 var xAxisPosition = 'left'; 13991 13992 if (isEndAligned) { 13993 // When the proxy item is aligned at the end of the list, we have to set the origin to the end. 13994 xAxisPosition = 'right'; 13995 } 13996 13997 // Set the position mode / origin of the proxied menu. 13998 if (!menuEl.attr('md-position-mode')) { 13999 menuEl.attr('md-position-mode', xAxisPosition + ' target'); 14000 } 14001 14002 // Apply menu open binding to menu button 14003 var menuOpenButton = menuEl.children().eq(0); 14004 if (!hasClickEvent(menuOpenButton[0])) { 14005 menuOpenButton.attr('ng-click', '$mdMenu.open($event)'); 14006 } 14007 14008 if (!menuOpenButton.attr('aria-label')) { 14009 menuOpenButton.attr('aria-label', 'Open List Menu'); 14010 } 14011 } 14012 14013 function wrapIn(type) { 14014 if (type == 'div') { 14015 itemContainer = angular.element('<div class="md-no-style md-list-item-inner">'); 14016 itemContainer.append(tEl.contents()); 14017 tEl.addClass('md-proxy-focus'); 14018 } else { 14019 // Element which holds the default list-item content. 14020 itemContainer = angular.element( 14021 '<div class="md-button md-no-style">'+ 14022 ' <div class="md-list-item-inner"></div>'+ 14023 '</div>' 14024 ); 14025 14026 // Button which shows ripple and executes primary action. 14027 var buttonWrap = angular.element( 14028 '<md-button class="md-no-style"></md-button>' 14029 ); 14030 14031 copyAttributes(tEl[0], buttonWrap[0]); 14032 14033 // If there is no aria-label set on the button (previously copied over if present) 14034 // we determine the label from the content and copy it to the button. 14035 if (!buttonWrap.attr('aria-label')) { 14036 buttonWrap.attr('aria-label', $mdAria.getText(tEl)); 14037 } 14038 14039 // We allow developers to specify the `md-no-focus` class, to disable the focus style 14040 // on the button executor. Once more classes should be forwarded, we should probably make the 14041 // class forward more generic. 14042 if (tEl.hasClass('md-no-focus')) { 14043 buttonWrap.addClass('md-no-focus'); 14044 } 14045 14046 // Append the button wrap before our list-item content, because it will overlay in relative. 14047 itemContainer.prepend(buttonWrap); 14048 itemContainer.children().eq(1).append(tEl.contents()); 14049 14050 tEl.addClass('_md-button-wrap'); 14051 } 14052 14053 tEl[0].setAttribute('tabindex', '-1'); 14054 tEl.append(itemContainer); 14055 } 14056 14057 function wrapSecondaryItems() { 14058 var secondaryItemsWrapper = angular.element('<div class="md-secondary-container">'); 14059 14060 angular.forEach(secondaryItems, function(secondaryItem) { 14061 wrapSecondaryItem(secondaryItem, secondaryItemsWrapper); 14062 }); 14063 14064 itemContainer.append(secondaryItemsWrapper); 14065 } 14066 14067 function wrapSecondaryItem(secondaryItem, container) { 14068 // If the current secondary item is not a button, but contains a ng-click attribute, 14069 // the secondary item will be automatically wrapped inside of a button. 14070 if (secondaryItem && !isButton(secondaryItem) && secondaryItem.hasAttribute('ng-click')) { 14071 14072 $mdAria.expect(secondaryItem, 'aria-label'); 14073 var buttonWrapper = angular.element('<md-button class="md-secondary md-icon-button">'); 14074 14075 // Copy the attributes from the secondary item to the generated button. 14076 // We also support some additional attributes from the secondary item, 14077 // because some developers may use a ngIf, ngHide, ngShow on their item. 14078 copyAttributes(secondaryItem, buttonWrapper[0], ['ng-if', 'ng-hide', 'ng-show']); 14079 14080 secondaryItem.setAttribute('tabindex', '-1'); 14081 buttonWrapper.append(secondaryItem); 14082 14083 secondaryItem = buttonWrapper[0]; 14084 } 14085 14086 if (secondaryItem && (!hasClickEvent(secondaryItem) || (!tAttrs.ngClick && isProxiedElement(secondaryItem)))) { 14087 // In this case we remove the secondary class, so we can identify it later, when we searching for the 14088 // proxy items. 14089 angular.element(secondaryItem).removeClass('md-secondary'); 14090 } 14091 14092 tEl.addClass('md-with-secondary'); 14093 container.append(secondaryItem); 14094 } 14095 14096 /** 14097 * Copies attributes from a source element to the destination element 14098 * By default the function will copy the most necessary attributes, supported 14099 * by the button executor for clickable list items. 14100 * @param source Element with the specified attributes 14101 * @param destination Element which will retrieve the attributes 14102 * @param extraAttrs Additional attributes, which will be copied over. 14103 */ 14104 function copyAttributes(source, destination, extraAttrs) { 14105 var copiedAttrs = $mdUtil.prefixer([ 14106 'ng-if', 'ng-click', 'ng-dblclick', 'aria-label', 'ng-disabled', 'ui-sref', 14107 'href', 'ng-href', 'rel', 'target', 'ng-attr-ui-sref', 'ui-sref-opts' 14108 ]); 14109 14110 if (extraAttrs) { 14111 copiedAttrs = copiedAttrs.concat($mdUtil.prefixer(extraAttrs)); 14112 } 14113 14114 angular.forEach(copiedAttrs, function(attr) { 14115 if (source.hasAttribute(attr)) { 14116 destination.setAttribute(attr, source.getAttribute(attr)); 14117 source.removeAttribute(attr); 14118 } 14119 }); 14120 } 14121 14122 function isProxiedElement(el) { 14123 return proxiedTypes.indexOf(el.nodeName.toLowerCase()) != -1; 14124 } 14125 14126 function isButton(el) { 14127 var nodeName = el.nodeName.toUpperCase(); 14128 14129 return nodeName == "MD-BUTTON" || nodeName == "BUTTON"; 14130 } 14131 14132 function hasClickEvent (element) { 14133 var attr = element.attributes; 14134 for (var i = 0; i < attr.length; i++) { 14135 if (tAttrs.$normalize(attr[i].name) === 'ngClick') return true; 14136 } 14137 return false; 14138 } 14139 14140 return postLink; 14141 14142 function postLink($scope, $element, $attr, ctrl) { 14143 $element.addClass('_md'); // private md component indicator for styling 14144 14145 var proxies = [], 14146 firstElement = $element[0].firstElementChild, 14147 isButtonWrap = $element.hasClass('_md-button-wrap'), 14148 clickChild = isButtonWrap ? firstElement.firstElementChild : firstElement, 14149 hasClick = clickChild && hasClickEvent(clickChild), 14150 noProxies = $element.hasClass('md-no-proxy'); 14151 14152 computeProxies(); 14153 computeClickable(); 14154 14155 if (proxies.length) { 14156 angular.forEach(proxies, function(proxy) { 14157 proxy = angular.element(proxy); 14158 14159 $scope.mouseActive = false; 14160 proxy.on('mousedown', function() { 14161 $scope.mouseActive = true; 14162 $timeout(function(){ 14163 $scope.mouseActive = false; 14164 }, 100); 14165 }) 14166 .on('focus', function() { 14167 if ($scope.mouseActive === false) { $element.addClass('md-focused'); } 14168 proxy.on('blur', function proxyOnBlur() { 14169 $element.removeClass('md-focused'); 14170 proxy.off('blur', proxyOnBlur); 14171 }); 14172 }); 14173 }); 14174 } 14175 14176 14177 function computeProxies() { 14178 14179 if (firstElement && firstElement.children && !hasClick && !noProxies) { 14180 14181 angular.forEach(proxiedTypes, function(type) { 14182 14183 // All elements which are not capable for being used a proxy have the .md-secondary class 14184 // applied. These items had been sorted out in the secondary wrap function. 14185 angular.forEach(firstElement.querySelectorAll(type + ':not(.md-secondary)'), function(child) { 14186 proxies.push(child); 14187 }); 14188 }); 14189 14190 } 14191 } 14192 14193 function computeClickable() { 14194 if (proxies.length == 1 || hasClick) { 14195 $element.addClass('md-clickable'); 14196 14197 if (!hasClick) { 14198 ctrl.attachRipple($scope, angular.element($element[0].querySelector('.md-no-style'))); 14199 } 14200 } 14201 } 14202 14203 function isEventFromControl(event) { 14204 var forbiddenControls = ['md-slider']; 14205 14206 // If there is no path property in the event, then we can assume that the event was not bubbled. 14207 if (!event.path) { 14208 return forbiddenControls.indexOf(event.target.tagName.toLowerCase()) !== -1; 14209 } 14210 14211 // We iterate the event path up and check for a possible component. 14212 // Our maximum index to search, is the list item root. 14213 var maxPath = event.path.indexOf($element.children()[0]); 14214 14215 for (var i = 0; i < maxPath; i++) { 14216 if (forbiddenControls.indexOf(event.path[i].tagName.toLowerCase()) !== -1) { 14217 return true; 14218 } 14219 } 14220 } 14221 14222 var clickChildKeypressListener = function(e) { 14223 if (e.target.nodeName != 'INPUT' && e.target.nodeName != 'TEXTAREA' && !e.target.isContentEditable) { 14224 var keyCode = e.which || e.keyCode; 14225 if (keyCode == $mdConstant.KEY_CODE.SPACE) { 14226 if (clickChild) { 14227 clickChild.click(); 14228 e.preventDefault(); 14229 e.stopPropagation(); 14230 } 14231 } 14232 } 14233 }; 14234 14235 if (!hasClick && !proxies.length) { 14236 clickChild && clickChild.addEventListener('keypress', clickChildKeypressListener); 14237 } 14238 14239 $element.off('click'); 14240 $element.off('keypress'); 14241 14242 if (proxies.length == 1 && clickChild) { 14243 $element.children().eq(0).on('click', function(e) { 14244 // When the event is coming from an control and it should not trigger the proxied element 14245 // then we are skipping. 14246 if (isEventFromControl(e)) return; 14247 14248 var parentButton = $mdUtil.getClosest(e.target, 'BUTTON'); 14249 if (!parentButton && clickChild.contains(e.target)) { 14250 angular.forEach(proxies, function(proxy) { 14251 if (e.target !== proxy && !proxy.contains(e.target)) { 14252 if (proxy.nodeName === 'MD-MENU') { 14253 proxy = proxy.children[0]; 14254 } 14255 angular.element(proxy).triggerHandler('click'); 14256 } 14257 }); 14258 } 14259 }); 14260 } 14261 14262 $scope.$on('$destroy', function () { 14263 clickChild && clickChild.removeEventListener('keypress', clickChildKeypressListener); 14264 }); 14265 } 14266 } 14267 }; 14268} 14269 14270/* 14271 * @private 14272 * @ngdoc controller 14273 * @name MdListController 14274 * @module material.components.list 14275 * 14276 */ 14277function MdListController($scope, $element, $mdListInkRipple) { 14278 var ctrl = this; 14279 ctrl.attachRipple = attachRipple; 14280 14281 function attachRipple (scope, element) { 14282 var options = {}; 14283 $mdListInkRipple.attach(scope, element, options); 14284 } 14285} 14286 14287})(); 14288(function(){ 14289"use strict"; 14290 14291/** 14292 * @ngdoc module 14293 * @name material.components.menu 14294 */ 14295 14296angular.module('material.components.menu', [ 14297 'material.core', 14298 'material.components.backdrop' 14299]); 14300 14301})(); 14302(function(){ 14303"use strict"; 14304 14305/** 14306 * @ngdoc module 14307 * @name material.components.menuBar 14308 */ 14309 14310angular.module('material.components.menuBar', [ 14311 'material.core', 14312 'material.components.icon', 14313 'material.components.menu' 14314]); 14315 14316})(); 14317(function(){ 14318"use strict"; 14319 14320/** 14321 * @ngdoc module 14322 * @name material.components.navBar 14323 */ 14324 14325 14326MdNavBarController.$inject = ["$element", "$scope", "$timeout", "$mdConstant"]; 14327MdNavItem.$inject = ["$mdAria", "$$rAF"]; 14328MdNavItemController.$inject = ["$element"]; 14329MdNavBar.$inject = ["$mdAria", "$mdTheming"]; 14330angular.module('material.components.navBar', ['material.core']) 14331 .controller('MdNavBarController', MdNavBarController) 14332 .directive('mdNavBar', MdNavBar) 14333 .controller('MdNavItemController', MdNavItemController) 14334 .directive('mdNavItem', MdNavItem); 14335 14336 14337/***************************************************************************** 14338 * PUBLIC DOCUMENTATION * 14339 *****************************************************************************/ 14340/** 14341 * @ngdoc directive 14342 * @name mdNavBar 14343 * @module material.components.navBar 14344 * 14345 * @restrict E 14346 * 14347 * @description 14348 * The `<md-nav-bar>` directive renders a list of material tabs that can be used 14349 * for top-level page navigation. Unlike `<md-tabs>`, it has no concept of a tab 14350 * body and no bar pagination. 14351 * 14352 * Because it deals with page navigation, certain routing concepts are built-in. 14353 * Route changes via via ng-href, ui-sref, or ng-click events are supported. 14354 * Alternatively, the user could simply watch currentNavItem for changes. 14355 * 14356 * Accessibility functionality is implemented as a site navigator with a 14357 * listbox, according to 14358 * https://www.w3.org/TR/wai-aria-practices/#Site_Navigator_Tabbed_Style 14359 * 14360 * @param {string=} mdSelectedNavItem The name of the current tab; this must 14361 * match the name attribute of `<md-nav-item>` 14362 * @param {boolean=} mdNoInkBar If set to true, the ink bar will be hidden. 14363 * @param {string=} navBarAriaLabel An aria-label for the nav-bar 14364 * 14365 * @usage 14366 * <hljs lang="html"> 14367 * <md-nav-bar md-selected-nav-item="currentNavItem"> 14368 * <md-nav-item md-nav-click="goto('page1')" name="page1"> 14369 * Page One 14370 * </md-nav-item> 14371 * <md-nav-item md-nav-href="#page2" name="page3">Page Two</md-nav-item> 14372 * <md-nav-item md-nav-sref="page3" name="page2">Page Three</md-nav-item> 14373 * <md-nav-item 14374 * md-nav-sref="app.page4" 14375 * sref-opts="{reload: true, notify: true}" 14376 * name="page4"> 14377 * Page Four 14378 * </md-nav-item> 14379 * </md-nav-bar> 14380 *</hljs> 14381 * <hljs lang="js"> 14382 * (function() { 14383 * 'use strict'; 14384 * 14385 * $rootScope.$on('$routeChangeSuccess', function(event, current) { 14386 * $scope.currentLink = getCurrentLinkFromRoute(current); 14387 * }); 14388 * }); 14389 * </hljs> 14390 */ 14391
14392/***************************************************************************** 14393 * mdNavItem 14394 *****************************************************************************/ 14395/** 14396 * @ngdoc directive 14397 * @name mdNavItem 14398 * @module material.components.navBar 14399 * 14400 * @restrict E 14401 * 14402 * @description 14403 * `<md-nav-item>` describes a page navigation link within the `<md-nav-bar>` 14404 * component. It renders an md-button as the actual link. 14405 * 14406 * Exactly one of the mdNavClick, mdNavHref, mdNavSref attributes are required 14407 * to be specified. 14408 * 14409 * @param {Function=} mdNavClick Function which will be called when the 14410 * link is clicked to change the page. Renders as an `ng-click`. 14411 * @param {string=} mdNavHref url to transition to when this link is clicked. 14412 * Renders as an `ng-href`. 14413 * @param {string=} mdNavSref Ui-router state to transition to when this link is 14414 * clicked. Renders as a `ui-sref`. 14415 * @param {!Object=} srefOpts Ui-router options that are passed to the 14416 * `$state.go()` function. See the [Ui-router documentation for details] 14417 * (https://ui-router.github.io/docs/latest/interfaces/transition.transitionoptions.html). 14418 * @param {string=} name The name of this link. Used by the nav bar to know 14419 * which link is currently selected. 14420 * @param {string=} aria-label Adds alternative text for accessibility 14421 * 14422 * @usage 14423 * See `<md-nav-bar>` for usage. 14424 */ 14425 14426 14427/***************************************************************************** 14428 * IMPLEMENTATION * 14429 *****************************************************************************/ 14430 14431function MdNavBar($mdAria, $mdTheming) { 14432 return { 14433 restrict: 'E', 14434 transclude: true, 14435 controller: MdNavBarController, 14436 controllerAs: 'ctrl', 14437 bindToController: true, 14438 scope: { 14439 'mdSelectedNavItem': '=?', 14440 'mdNoInkBar': '=?', 14441 'navBarAriaLabel': '@?', 14442 }, 14443 template: 14444 '<div class="md-nav-bar">' + 14445 '<nav role="navigation">' + 14446 '<ul class="_md-nav-bar-list" ng-transclude role="listbox"' + 14447 'tabindex="0"' + 14448 'ng-focus="ctrl.onFocus()"' + 14449 'ng-keydown="ctrl.onKeydown($event)"' + 14450 'aria-label="{{ctrl.navBarAriaLabel}}">' + 14451 '</ul>' + 14452 '</nav>' + 14453 '<md-nav-ink-bar ng-hide="ctrl.mdNoInkBar"></md-nav-ink-bar>' + 14454 '</div>', 14455 link: function(scope, element, attrs, ctrl) { 14456 $mdTheming(element); 14457 if (!ctrl.navBarAriaLabel) { 14458 $mdAria.expectAsync(element, 'aria-label', angular.noop); 14459 } 14460 }, 14461 }; 14462} 14463 14464/** 14465 * Controller for the nav-bar component. 14466 * 14467 * Accessibility functionality is implemented as a site navigator with a 14468 * listbox, according to 14469 * https://www.w3.org/TR/wai-aria-practices/#Site_Navigator_Tabbed_Style 14470 * @param {!angular.JQLite} $element 14471 * @param {!angular.Scope} $scope 14472 * @param {!angular.Timeout} $timeout 14473 * @param {!Object} $mdConstant 14474 * @constructor 14475 * @final 14476 * @ngInject 14477 */ 14478function MdNavBarController($element, $scope, $timeout, $mdConstant) { 14479 // Injected variables 14480 /** @private @const {!angular.Timeout} */ 14481 this._$timeout = $timeout; 14482 14483 /** @private @const {!angular.Scope} */ 14484 this._$scope = $scope; 14485 14486 /** @private @const {!Object} */ 14487 this._$mdConstant = $mdConstant; 14488 14489 // Data-bound variables. 14490 /** @type {string} */ 14491 this.mdSelectedNavItem; 14492 14493 /** @type {string} */ 14494 this.navBarAriaLabel; 14495 14496 // State variables. 14497 14498 /** @type {?angular.JQLite} */ 14499 this._navBarEl = $element[0]; 14500 14501 /** @type {?angular.JQLite} */ 14502 this._inkbar; 14503 14504 var self = this; 14505 // need to wait for transcluded content to be available 14506 var deregisterTabWatch = this._$scope.$watch(function() { 14507 return self._navBarEl.querySelectorAll('._md-nav-button').length; 14508 }, 14509 function(newLength) { 14510 if (newLength > 0) { 14511 self._initTabs(); 14512 deregisterTabWatch(); 14513 } 14514 }); 14515} 14516 14517 14518 14519/** 14520 * Initializes the tab components once they exist. 14521 * @private 14522 */ 14523MdNavBarController.prototype._initTabs = function() { 14524 this._inkbar = angular.element(this._navBarEl.querySelector('md-nav-ink-bar')); 14525 14526 var self = this; 14527 this._$timeout(function() { 14528 self._updateTabs(self.mdSelectedNavItem, undefined); 14529 }); 14530 14531 this._$scope.$watch('ctrl.mdSelectedNavItem', function(newValue, oldValue) { 14532 // Wait a digest before update tabs for products doing 14533 // anything dynamic in the template. 14534 self._$timeout(function() { 14535 self._updateTabs(newValue, oldValue); 14536 }); 14537 }); 14538}; 14539 14540/** 14541 * Set the current tab to be selected. 14542 * @param {string|undefined} newValue New current tab name. 14543 * @param {string|undefined} oldValue Previous tab name. 14544 * @private 14545 */ 14546MdNavBarController.prototype._updateTabs = function(newValue, oldValue) { 14547 var self = this; 14548 var tabs = this._getTabs(); 14549 14550 // this._getTabs can return null if nav-bar has not yet been initialized 14551 if(!tabs) 14552 return; 14553 14554 var oldIndex = -1; 14555 var newIndex = -1; 14556 var newTab = this._getTabByName(newValue); 14557 var oldTab = this._getTabByName(oldValue); 14558 14559 if (oldTab) { 14560 oldTab.setSelected(false); 14561 oldIndex = tabs.indexOf(oldTab); 14562 } 14563 14564 if (newTab) { 14565 newTab.setSelected(true); 14566 newIndex = tabs.indexOf(newTab); 14567 } 14568 14569 this._$timeout(function() { 14570 self._updateInkBarStyles(newTab, newIndex, oldIndex); 14571 }); 14572}; 14573 14574/** 14575 * Repositions the ink bar to the selected tab. 14576 * @private 14577 */ 14578MdNavBarController.prototype._updateInkBarStyles = function(tab, newIndex, oldIndex) { 14579 this._inkbar.toggleClass('_md-left', newIndex < oldIndex) 14580 .toggleClass('_md-right', newIndex > oldIndex); 14581 14582 this._inkbar.css({display: newIndex < 0 ? 'none' : ''}); 14583 14584 if (tab) { 14585 var tabEl = tab.getButtonEl(); 14586 var left = tabEl.offsetLeft; 14587 14588 this._inkbar.css({left: left + 'px', width: tabEl.offsetWidth + 'px'}); 14589 } 14590}; 14591 14592/** 14593 * Returns an array of the current tabs. 14594 * @return {!Array<!NavItemController>} 14595 * @private 14596 */ 14597MdNavBarController.prototype._getTabs = function() { 14598 var controllers = Array.prototype.slice.call( 14599 this._navBarEl.querySelectorAll('.md-nav-item')) 14600 .map(function(el) { 14601 return angular.element(el).controller('mdNavItem') 14602 }); 14603 return controllers.indexOf(undefined) ? controllers : null; 14604}; 14605 14606/** 14607 * Returns the tab with the specified name. 14608 * @param {string} name The name of the tab, found in its name attribute. 14609 * @return {!NavItemController|undefined} 14610 * @private 14611 */ 14612MdNavBarController.prototype._getTabByName = function(name) { 14613 return this._findTab(function(tab) { 14614 return tab.getName() == name; 14615 }); 14616}; 14617 14618/** 14619 * Returns the selected tab. 14620 * @return {!NavItemController|undefined} 14621 * @private 14622 */ 14623MdNavBarController.prototype._getSelectedTab = function() { 14624 return this._findTab(function(tab) { 14625 return tab.isSelected(); 14626 }); 14627}; 14628 14629/** 14630 * Returns the focused tab. 14631 * @return {!NavItemController|undefined} 14632 */ 14633MdNavBarController.prototype.getFocusedTab = function() { 14634 return this._findTab(function(tab) { 14635 return tab.hasFocus(); 14636 }); 14637}; 14638 14639/** 14640 * Find a tab that matches the specified function. 14641 * @private 14642 */ 14643MdNavBarController.prototype._findTab = function(fn) { 14644 var tabs = this._getTabs(); 14645 for (var i = 0; i < tabs.length; i++) { 14646 if (fn(tabs[i])) { 14647 return tabs[i]; 14648 } 14649 } 14650 14651 return null; 14652}; 14653 14654/**
14655 * Direct focus to the selected tab when focus enters the nav bar. 14656 */ 14657MdNavBarController.prototype.onFocus = function() { 14658 var tab = this._getSelectedTab(); 14659 if (tab) { 14660 tab.setFocused(true); 14661 } 14662}; 14663 14664/** 14665 * Move focus from oldTab to newTab. 14666 * @param {!NavItemController} oldTab 14667 * @param {!NavItemController} newTab 14668 * @private 14669 */ 14670MdNavBarController.prototype._moveFocus = function(oldTab, newTab) { 14671 oldTab.setFocused(false); 14672 newTab.setFocused(true); 14673}; 14674 14675/** 14676 * Responds to keypress events. 14677 * @param {!Event} e 14678 */ 14679MdNavBarController.prototype.onKeydown = function(e) { 14680 var keyCodes = this._$mdConstant.KEY_CODE; 14681 var tabs = this._getTabs(); 14682 var focusedTab = this.getFocusedTab(); 14683 if (!focusedTab) return; 14684 14685 var focusedTabIndex = tabs.indexOf(focusedTab); 14686 14687 // use arrow keys to navigate between tabs 14688 switch (e.keyCode) { 14689 case keyCodes.UP_ARROW: 14690 case keyCodes.LEFT_ARROW: 14691 if (focusedTabIndex > 0) { 14692 this._moveFocus(focusedTab, tabs[focusedTabIndex - 1]); 14693 } 14694 break; 14695 case keyCodes.DOWN_ARROW: 14696 case keyCodes.RIGHT_ARROW: 14697 if (focusedTabIndex < tabs.length - 1) { 14698 this._moveFocus(focusedTab, tabs[focusedTabIndex + 1]); 14699 } 14700 break; 14701 case keyCodes.SPACE: 14702 case keyCodes.ENTER: 14703 // timeout to avoid a "digest already in progress" console error 14704 this._$timeout(function() { 14705 focusedTab.getButtonEl().click(); 14706 }); 14707 break; 14708 } 14709}; 14710 14711/** 14712 * @ngInject 14713 */ 14714function MdNavItem($mdAria, $$rAF) { 14715 return { 14716 restrict: 'E', 14717 require: ['mdNavItem', '^mdNavBar'], 14718 controller: MdNavItemController, 14719 bindToController: true, 14720 controllerAs: 'ctrl', 14721 replace: true, 14722 transclude: true,
vendor: 6,215 bytes, lines 14723-14952
14723 template: function(tElement, tAttrs) { 14724 var hasNavClick = tAttrs.mdNavClick; 14725 var hasNavHref = tAttrs.mdNavHref; 14726 var hasNavSref = tAttrs.mdNavSref; 14727 var hasSrefOpts = tAttrs.srefOpts; 14728 var navigationAttribute; 14729 var navigationOptions; 14730 var buttonTemplate; 14731 14732 // Cannot specify more than one nav attribute 14733 if ((hasNavClick ? 1:0) + (hasNavHref ? 1:0) + (hasNavSref ? 1:0) > 1) { 14734 throw Error( 14735 'Must not specify more than one of the md-nav-click, md-nav-href, ' + 14736 'or md-nav-sref attributes per nav-item directive.' 14737 ); 14738 } 14739 14740 if (hasNavClick) { 14741 navigationAttribute = 'ng-click="ctrl.mdNavClick()"'; 14742 } else if (hasNavHref) { 14743 navigationAttribute = 'ng-href="{{ctrl.mdNavHref}}"'; 14744 } else if (hasNavSref) { 14745 navigationAttribute = 'ui-sref="{{ctrl.mdNavSref}}"'; 14746 } 14747 14748 navigationOptions = hasSrefOpts ? 'ui-sref-opts="{{ctrl.srefOpts}}" ' : ''; 14749 14750 if (navigationAttribute) { 14751 buttonTemplate = '' + 14752 '<md-button class="_md-nav-button md-accent" ' + 14753 'ng-class="ctrl.getNgClassMap()" ' + 14754 'ng-blur="ctrl.setFocused(false)" ' + 14755 'tabindex="-1" ' + 14756 navigationOptions + 14757 navigationAttribute + '>' + 14758 '<span ng-transclude class="_md-nav-button-text"></span>' + 14759 '</md-button>'; 14760 } 14761 14762 return '' + 14763 '<li class="md-nav-item" ' + 14764 'role="option" ' + 14765 'aria-selected="{{ctrl.isSelected()}}">' + 14766 (buttonTemplate || '') + 14767 '</li>'; 14768 }, 14769 scope: { 14770 'mdNavClick': '&?', 14771 'mdNavHref': '@?', 14772 'mdNavSref': '@?', 14773 'srefOpts': '=?', 14774 'name': '@', 14775 }, 14776 link: function(scope, element, attrs, controllers) { 14777 // When accessing the element's contents synchronously, they 14778 // may not be defined yet because of transclusion. There is a higher 14779 // chance that it will be accessible if we wait one frame. 14780 $$rAF(function() { 14781 var mdNavItem = controllers[0]; 14782 var mdNavBar = controllers[1]; 14783 var navButton = angular.element(element[0].querySelector('._md-nav-button')); 14784 14785 if (!mdNavItem.name) { 14786 mdNavItem.name = angular.element(element[0] 14787 .querySelector('._md-nav-button-text')).text().trim(); 14788 } 14789 14790 navButton.on('click', function() { 14791 mdNavBar.mdSelectedNavItem = mdNavItem.name; 14792 scope.$apply(); 14793 }); 14794 14795 $mdAria.expectWithText(element, 'aria-label'); 14796 }); 14797 } 14798 }; 14799} 14800 14801/** 14802 * Controller for the nav-item component. 14803 * @param {!angular.JQLite} $element 14804 * @constructor 14805 * @final 14806 * @ngInject 14807 */ 14808function MdNavItemController($element) { 14809 14810 /** @private @const {!angular.JQLite} */ 14811 this._$element = $element; 14812 14813 // Data-bound variables 14814 14815 /** @const {?Function} */ 14816 this.mdNavClick; 14817 14818 /** @const {?string} */ 14819 this.mdNavHref; 14820 14821 /** @const {?string} */ 14822 this.mdNavSref; 14823 /** @const {?Object} */ 14824 this.srefOpts; 14825 /** @const {?string} */ 14826 this.name; 14827 14828 // State variables 14829 /** @private {boolean} */ 14830 this._selected = false; 14831 14832 /** @private {boolean} */ 14833 this._focused = false; 14834} 14835 14836/** 14837 * Returns a map of class names and values for use by ng-class. 14838 * @return {!Object<string,boolean>} 14839 */ 14840MdNavItemController.prototype.getNgClassMap = function() { 14841 return { 14842 'md-active': this._selected, 14843 'md-primary': this._selected, 14844 'md-unselected': !this._selected, 14845 'md-focused': this._focused, 14846 }; 14847}; 14848 14849/** 14850 * Get the name attribute of the tab. 14851 * @return {string} 14852 */ 14853MdNavItemController.prototype.getName = function() { 14854 return this.name; 14855}; 14856 14857/** 14858 * Get the button element associated with the tab. 14859 * @return {!Element} 14860 */ 14861MdNavItemController.prototype.getButtonEl = function() { 14862 return this._$element[0].querySelector('._md-nav-button'); 14863}; 14864 14865/** 14866 * Set the selected state of the tab. 14867 * @param {boolean} isSelected 14868 */ 14869MdNavItemController.prototype.setSelected = function(isSelected) { 14870 this._selected = isSelected; 14871}; 14872 14873/** 14874 * @return {boolean} 14875 */ 14876MdNavItemController.prototype.isSelected = function() { 14877 return this._selected; 14878}; 14879 14880/** 14881 * Set the focused state of the tab. 14882 * @param {boolean} isFocused 14883 */ 14884MdNavItemController.prototype.setFocused = function(isFocused) { 14885 this._focused = isFocused; 14886 14887 if (isFocused) { 14888 this.getButtonEl().focus(); 14889 } 14890}; 14891 14892/** 14893 * @return {boolean} 14894 */ 14895MdNavItemController.prototype.hasFocus = function() { 14896 return this._focused; 14897}; 14898 14899})(); 14900(function(){ 14901"use strict"; 14902 14903/** 14904 * @ngdoc module 14905 * @name material.components.panel 14906 */ 14907MdPanelService.$inject = ["presets", "$rootElement", "$rootScope", "$injector", "$window"]; 14908angular 14909 .module('material.components.panel', [ 14910 'material.core', 14911 'material.components.backdrop' 14912 ]) 14913 .provider('$mdPanel', MdPanelProvider); 14914 14915 14916/***************************************************************************** 14917 * PUBLIC DOCUMENTATION * 14918 *****************************************************************************/ 14919 14920 14921/** 14922 * @ngdoc service 14923 * @name $mdPanelProvider 14924 * @module material.components.panel 14925 * 14926 * @description 14927 * `$mdPanelProvider` allows users to create configuration presets that will be 14928 * stored within a cached presets object. When the configuration is needed, the 14929 * user can request the preset by passing it as the first parameter in the 14930 * `$mdPanel.create` or `$mdPanel.open` methods. 14931 * 14932 * @usage 14933 * <hljs lang="js"> 14934 * (function(angular, undefined) { 14935 * 'use strict'; 14936 * 14937 * angular 14938 * .module('demoApp', ['ngMaterial']) 14939 * .config(DemoConfig) 14940 * .controller('DemoCtrl', DemoCtrl) 14941 * .controller('DemoMenuCtrl', DemoMenuCtrl); 14942 * 14943 * function DemoConfig($mdPanelProvider) { 14944 * $mdPanelProvider.definePreset('demoPreset', { 14945 * attachTo: angular.element(document.body), 14946 * controller: DemoMenuCtrl, 14947 * controllerAs: 'ctrl', 14948 * template: '' + 14949 * '<div class="menu-panel" md-whiteframe="4">' + 14950 * ' <div class="menu-content">' + 14951 * ' <div class="menu-item" ng-repeat="item in ctrl.items">' + 14952 * ' <button class="md-button">' +
14953 * ' <span>{{item}}</span>' + 14954 * ' </button>' + 14955 * ' </div>' + 14956 * ' <md-divider></md-divider>' + 14957 * ' <div class="menu-item">' + 14958 * ' <button class="md-button" ng-click="ctrl.closeMenu()">' + 14959 * ' <span>Close Menu</span>' + 14960 * ' </button>' + 14961 * ' </div>' + 14962 * ' </div>' + 14963 * '</div>', 14964 * panelClass: 'menu-panel-container', 14965 * focusOnOpen: false, 14966 * zIndex: 100, 14967 * propagateContainerEvents: true, 14968 * groupName: 'menus' 14969 * }); 14970 * } 14971 * 14972 * function PanelProviderCtrl($mdPanel) { 14973 * this.navigation = { 14974 * name: 'navigation', 14975 * items: [ 14976 * 'Home', 14977 * 'About', 14978 * 'Contact' 14979 * ] 14980 * }; 14981 * this.favorites = { 14982 * name: 'favorites', 14983 * items: [ 14984 * 'Add to Favorites' 14985 * ] 14986 * }; 14987 * this.more = { 14988 * name: 'more', 14989 * items: [ 14990 * 'Account', 14991 * 'Sign Out' 14992 * ] 14993 * }; 14994 * 14995 * $mdPanel.newPanelGroup('menus', { 14996 * maxOpen: 2 14997 * }); 14998 * 14999 * this.showMenu = function($event, menu) { 15000 * $mdPanel.open('demoPreset', { 15001 * id: 'menu_' + menu.name, 15002 * position: $mdPanel.newPanelPosition() 15003 * .relativeTo($event.srcElement) 15004 * .addPanelPosition( 15005 * $mdPanel.xPosition.ALIGN_START, 15006 * $mdPanel.yPosition.BELOW 15007 * ), 15008 * locals: { 15009 * items: menu.items 15010 * }, 15011 * openFrom: $event 15012 * }); 15013 * }; 15014 * } 15015 * 15016 * function PanelMenuCtrl(mdPanelRef) { 15017 * // The controller is provided with an import named 'mdPanelRef' 15018 * this.closeMenu = function() { 15019 * mdPanelRef && mdPanelRef.close(); 15020 * }; 15021 * } 15022 * })(angular); 15023 * </hljs> 15024 */ 15025 15026/** 15027 * @ngdoc method 15028 * @name $mdPanelProvider#definePreset 15029 * @description 15030 * Takes the passed in preset name and preset configuration object and adds it 15031 * to the `_presets` object of the provider. This `_presets` object is then 15032 * passed along to the `$mdPanel` service. 15033 * 15034 * @param {string} name Preset name. 15035 * @param {!Object} preset Specific configuration object that can contain any 15036 * and all of the parameters avaialble within the `$mdPanel.create` method. 15037 * However, parameters that pertain to id, position, animation, and user 15038 * interaction are not allowed and will be removed from the preset 15039 * configuration. 15040 */ 15041 15042 15043/***************************************************************************** 15044 * MdPanel Service * 15045 *****************************************************************************/ 15046 15047 15048/** 15049 * @ngdoc service 15050 * @name $mdPanel 15051 * @module material.components.panel 15052 * 15053 * @description 15054 * `$mdPanel` is a robust, low-level service for creating floating panels on 15055 * the screen. It can be used to implement tooltips, dialogs, pop-ups, etc. 15056 * 15057 * @usage 15058 * <hljs lang="js"> 15059 * (function(angular, undefined) { 15060 * 'use strict'; 15061 * 15062 * angular 15063 * .module('demoApp', ['ngMaterial']) 15064 * .controller('DemoDialogController', DialogController); 15065 * 15066 * var panelRef; 15067 * 15068 * function showPanel($event) { 15069 * var panelPosition = $mdPanel.newPanelPosition() 15070 * .absolute() 15071 * .top('50%') 15072 * .left('50%'); 15073 * 15074 * var panelAnimation = $mdPanel.newPanelAnimation() 15075 * .targetEvent($event) 15076 * .defaultAnimation('md-panel-animate-fly') 15077 * .closeTo('.show-button'); 15078 * 15079 * var config = { 15080 * attachTo: angular.element(document.body), 15081 * controller: DialogController, 15082 * controllerAs: 'ctrl', 15083 * position: panelPosition, 15084 * animation: panelAnimation, 15085 * targetEvent: $event, 15086 * templateUrl: 'dialog-template.html', 15087 * clickOutsideToClose: true, 15088 * escapeToClose: true, 15089 * focusOnOpen: true 15090 * }; 15091 * 15092 * $mdPanel.open(config) 15093 * .then(function(result) { 15094 * panelRef = result; 15095 * }); 15096 * } 15097 * 15098 * function DialogController(MdPanelRef) { 15099 * function closeDialog() { 15100 * if (MdPanelRef) MdPanelRef.close(); 15101 * } 15102 * } 15103 * })(angular); 15104 * </hljs> 15105 */ 15106
15107/** 15108 * @ngdoc method 15109 * @name $mdPanel#create 15110 * @description 15111 * Creates a panel with the specified options. 15112 * 15113 * @param config {!Object=} Specific configuration object that may contain the 15114 * following properties: 15115 * 15116 * - `id` - `{string=}`: An ID to track the panel by. When an ID is provided, 15117 * the created panel is added to a tracked panels object. Any subsequent 15118 * requests made to create a panel with that ID are ignored. This is useful 15119 * in having the panel service not open multiple panels from the same user 15120 * interaction when there is no backdrop and events are propagated. Defaults 15121 * to an arbitrary string that is not tracked. 15122 * - `template` - `{string=}`: HTML template to show in the panel. This 15123 * **must** be trusted HTML with respect to AngularJS’s 15124 * [$sce service](https://docs.angularjs.org/api/ng/service/$sce). 15125 * - `templateUrl` - `{string=}`: The URL that will be used as the content of 15126 * the panel. 15127 * - `contentElement` - `{(string|!angular.JQLite|!Element)=}`: Pre-compiled 15128 * element to be used as the panel's content. 15129 * - `controller` - `{(function|string)=}`: The controller to associate with 15130 * the panel. The controller can inject a reference to the returned 15131 * panelRef, which allows the panel to be closed, hidden, and shown. Any 15132 * fields passed in through locals or resolve will be bound to the 15133 * controller. 15134 * - `controllerAs` - `{string=}`: An alias to assign the controller to on 15135 * the scope. 15136 * - `bindToController` - `{boolean=}`: Binds locals to the controller 15137 * instead of passing them in. Defaults to true, as this is a best 15138 * practice. 15139 * - `locals` - `{Object=}`: An object containing key/value pairs. The keys 15140 * will be used as names of values to inject into the controller. For 15141 * example, `locals: {three: 3}` would inject `three` into the controller, 15142 * with the value 3. 'mdPanelRef' is a reserved key, and will always 15143 * be set to the created MdPanelRef instance. 15144 * - `resolve` - `{Object=}`: Similar to locals, except it takes promises as 15145 * values. The panel will not open until all of the promises resolve. 15146 * - `attachTo` - `{(string|!angular.JQLite|!Element)=}`: The element to 15147 * attach the panel to. Defaults to appending to the root element of the 15148 * application. 15149 * - `propagateContainerEvents` - `{boolean=}`: Whether pointer or touch 15150 * events should be allowed to propagate 'go through' the container, aka the 15151 * wrapper, of the panel. Defaults to false. 15152 * - `panelClass` - `{string=}`: A css class to apply to the panel element. 15153 * This class should define any borders, box-shadow, etc. for the panel. 15154 * - `zIndex` - `{number=}`: The z-index to place the panel at. 15155 * Defaults to 80. 15156 * - `position` - `{MdPanelPosition=}`: An MdPanelPosition object that 15157 * specifies the alignment of the panel. For more information, see 15158 * `MdPanelPosition`. 15159 * - `clickOutsideToClose` - `{boolean=}`: Whether the user can click 15160 * outside the panel to close it. Defaults to false. 15161 * - `escapeToClose` - `{boolean=}`: Whether the user can press escape to 15162 * close the panel. Defaults to false. 15163 * - `onCloseSuccess` - `{function(!panelRef, string)=}`: Function that is 15164 * called after the close successfully finishes. The first parameter passed 15165 * into this function is the current panelRef and the 2nd is an optional 15166 * string explaining the close reason. The currently supported closeReasons 15167 * can be found in the MdPanelRef.closeReasons enum. These are by default 15168 * passed along by the panel. 15169 * - `trapFocus` - `{boolean=}`: Whether focus should be trapped within the 15170 * panel. If `trapFocus` is true, the user will not be able to interact 15171 * with the rest of the page until the panel is dismissed. Defaults to 15172 * false. 15173 * - `focusOnOpen` - `{boolean=}`: An option to override focus behavior on 15174 * open. Only disable if focusing some other way, as focus management is 15175 * required for panels to be accessible. Defaults to true. 15176 * - `fullscreen` - `{boolean=}`: Whether the panel should be full screen. 15177 * Applies the class `._md-panel-fullscreen` to the panel on open. Defaults 15178 * to false. 15179 * - `animation` - `{MdPanelAnimation=}`: An MdPanelAnimation object that 15180 * specifies the animation of the panel. For more information, see 15181 * `MdPanelAnimation`. 15182 * - `hasBackdrop` - `{boolean=}`: Whether there should be an opaque backdrop 15183 * behind the panel. Defaults to false. 15184 * - `disableParentScroll` - `{boolean=}`: Whether the user can scroll the 15185 * page behind the panel. Defaults to false. 15186 * - `onDomAdded` - `{function=}`: Callback function used to announce when 15187 * the panel is added to the DOM. 15188 * - `onOpenComplete` - `{function=}`: Callback function used to announce 15189 * when the open() action is finished. 15190 * - `onRemoving` - `{function=}`: Callback function used to announce the 15191 * close/hide() action is starting. 15192 * - `onDomRemoved` - `{function=}`: Callback function used to announce when 15193 * the panel is removed from the DOM. 15194 * - `origin` - `{(string|!angular.JQLite|!Element)=}`: The element to focus 15195 * on when the panel closes. This is commonly the element which triggered 15196 * the opening of the panel. If you do not use `origin`, you need to control 15197 * the focus manually. 15198 * - `groupName` - `{(string|!Array<string>)=}`: A group name or an array of 15199 * group names. The group name is used for creating a group of panels. The
15200 * group is used for configuring the number of open panels and identifying 15201 * specific behaviors for groups. For instance, all tooltips could be 15202 * identified using the same groupName. 15203 * 15204 * @returns {!MdPanelRef} panelRef 15205 */ 15206 15207/** 15208 * @ngdoc method 15209 * @name $mdPanel#open 15210 * @description 15211 * Calls the create method above, then opens the panel. This is a shortcut for 15212 * creating and then calling open manually. If custom methods need to be 15213 * called when the panel is added to the DOM or opened, do not use this method. 15214 * Instead create the panel, chain promises on the domAdded and openComplete 15215 * methods, and call open from the returned panelRef. 15216 * 15217 * @param {!Object=} config Specific configuration object that may contain 15218 * the properties defined in `$mdPanel.create`. 15219 * @returns {!angular.$q.Promise<!MdPanelRef>} panelRef A promise that resolves 15220 * to an instance of the panel. 15221 */ 15222 15223/** 15224 * @ngdoc method 15225 * @name $mdPanel#newPanelPosition 15226 * @description 15227 * Returns a new instance of the MdPanelPosition object. Use this to create 15228 * the position config object. 15229 * 15230 * @returns {!MdPanelPosition} panelPosition 15231 */ 15232 15233/** 15234 * @ngdoc method 15235 * @name $mdPanel#newPanelAnimation 15236 * @description 15237 * Returns a new instance of the MdPanelAnimation object. Use this to create 15238 * the animation config object. 15239 * 15240 * @returns {!MdPanelAnimation} panelAnimation 15241 */ 15242 15243/** 15244 * @ngdoc method 15245 * @name $mdPanel#newPanelGroup 15246 * @description 15247 * Creates a panel group and adds it to a tracked list of panel groups. 15248 * 15249 * @param {string} groupName Name of the group to create. 15250 * @param {!Object=} config Specific configuration object that may contain the 15251 * following properties: 15252 * 15253 * - `maxOpen` - `{number=}`: The maximum number of panels that are allowed to 15254 * be open within a defined panel group. 15255 * 15256 * @returns {!Object<string, 15257 * {panels: !Array<!MdPanelRef>, 15258 * openPanels: !Array<!MdPanelRef>, 15259 * maxOpen: number}>} panelGroup 15260 */ 15261 15262/** 15263 * @ngdoc method
15264 * @name $mdPanel#setGroupMaxOpen 15265 * @description 15266 * Sets the maximum number of panels in a group that can be opened at a given 15267 * time. 15268 * 15269 * @param {string} groupName The name of the group to configure. 15270 * @param {number} maxOpen The maximum number of panels that can be 15271 * opened. Infinity can be passed in to remove the maxOpen limit. 15272 */ 15273 15274 15275/***************************************************************************** 15276 * MdPanelRef * 15277 *****************************************************************************/ 15278 15279 15280/** 15281 * @ngdoc type 15282 * @name MdPanelRef 15283 * @module material.components.panel 15284 * @description 15285 * A reference to a created panel. This reference contains a unique id for the 15286 * panel, along with the following properties: 15287 * 15288 * - `id` - `{string}`: The unique id for the panel. This id is used to track 15289 * when a panel was interacted with. 15290 * - `config` - `{!Object=}`: The entire config object that was used in 15291 * create. 15292 * - `isAttached` - `{boolean}`: Whether the panel is attached to the DOM. 15293 * Visibility to the user does not factor into isAttached. 15294 * - `panelContainer` - `{angular.JQLite}`: The wrapper element containing the 15295 * panel. This property is added in order to have access to the `addClass`, 15296 * `removeClass`, `toggleClass`, etc methods. 15297 * - `panelEl` - `{angular.JQLite}`: The panel element. This property is added 15298 * in order to have access to the `addClass`, `removeClass`, `toggleClass`, 15299 * etc methods. 15300 */ 15301 15302/** 15303 * @ngdoc method 15304 * @name MdPanelRef#open 15305 * @description 15306 * Attaches and shows the panel. 15307 * 15308 * @returns {!angular.$q.Promise} A promise that is resolved when the panel is 15309 * opened. 15310 */ 15311 15312/** 15313 * @ngdoc method 15314 * @name MdPanelRef#close 15315 * @description 15316 * Hides and detaches the panel. Note that this will **not** destroy the panel. 15317 * If you don't intend on using the panel again, call the {@link #destroy 15318 * destroy} method afterwards. 15319 * 15320 * @returns {!angular.$q.Promise} A promise that is resolved when the panel is 15321 * closed. 15322 */ 15323 15324/** 15325 * @ngdoc method 15326 * @name MdPanelRef#attach 15327 * @description 15328 * Create the panel elements and attach them to the DOM. The panel will be 15329 * hidden by default. 15330 * 15331 * @returns {!angular.$q.Promise} A promise that is resolved when the panel is 15332 * attached. 15333 */ 15334 15335/** 15336 * @ngdoc method 15337 * @name MdPanelRef#detach 15338 * @description 15339 * Removes the panel from the DOM. This will NOT hide the panel before removing 15340 * it. 15341 * 15342 * @returns {!angular.$q.Promise} A promise that is resolved when the panel is 15343 * detached. 15344 */ 15345 15346/** 15347 * @ngdoc method 15348 * @name MdPanelRef#show 15349 * @description 15350 * Shows the panel. 15351 * 15352 * @returns {!angular.$q.Promise} A promise that is resolved when the panel has 15353 * shown and animations are completed. 15354 */ 15355 15356/** 15357 * @ngdoc method 15358 * @name MdPanelRef#hide 15359 * @description 15360 * Hides the panel. 15361 * 15362 * @returns {!angular.$q.Promise} A promise that is resolved when the panel has 15363 * hidden and animations are completed. 15364 */ 15365 15366/** 15367 * @ngdoc method 15368 * @name MdPanelRef#destroy 15369 * @description 15370 * Destroys the panel. The panel cannot be opened again after this is called. 15371 */ 15372 15373/** 15374 * @ngdoc method 15375 * @name MdPanelRef#addClass 15376 * @deprecated
15377 * This method is in the process of being deprecated in favor of using the panel 15378 * and container JQLite elements that are referenced in the MdPanelRef object. 15379 * Full deprecation is scheduled for material 1.2. 15380 * @description 15381 * Adds a class to the panel. DO NOT use this hide/show the panel. 15382 * 15383 * @param {string} newClass class to be added. 15384 * @param {boolean} toElement Whether or not to add the class to the panel 15385 * element instead of the container. 15386 */ 15387 15388/** 15389 * @ngdoc method 15390 * @name MdPanelRef#removeClass 15391 * @deprecated 15392 * This method is in the process of being deprecated in favor of using the panel 15393 * and container JQLite elements that are referenced in the MdPanelRef object. 15394 * Full deprecation is scheduled for material 1.2. 15395 * @description 15396 * Removes a class from the panel. DO NOT use this to hide/show the panel. 15397 * 15398 * @param {string} oldClass Class to be removed. 15399 * @param {boolean} fromElement Whether or not to remove the class from the 15400 * panel element instead of the container. 15401 */ 15402 15403/** 15404 * @ngdoc method 15405 * @name MdPanelRef#toggleClass 15406 * @deprecated
15407 * This method is in the process of being deprecated in favor of using the panel 15408 * and container JQLite elements that are referenced in the MdPanelRef object. 15409 * Full deprecation is scheduled for material 1.2. 15410 * @description 15411 * Toggles a class on the panel. DO NOT use this to hide/show the panel. 15412 * 15413 * @param {string} toggleClass Class to be toggled. 15414 * @param {boolean} onElement Whether or not to remove the class from the panel 15415 * element instead of the container. 15416 */ 15417 15418/** 15419 * @ngdoc method 15420 * @name MdPanelRef#updatePosition 15421 * @description 15422 * Updates the position configuration of a panel. Use this to update the 15423 * position of a panel that is open, without having to close and re-open the 15424 * panel. 15425 * 15426 * @param {!MdPanelPosition} position 15427 */ 15428 15429/** 15430 * @ngdoc method 15431 * @name MdPanelRef#addToGroup 15432 * @description 15433 * Adds a panel to a group if the panel does not exist within the group already. 15434 * A panel can only exist within a single group. 15435 * 15436 * @param {string} groupName The name of the group to add the panel to. 15437 */ 15438 15439/** 15440 * @ngdoc method 15441 * @name MdPanelRef#removeFromGroup 15442 * @description 15443 * Removes a panel from a group if the panel exists within that group. The group 15444 * must be created ahead of time. 15445 * 15446 * @param {string} groupName The name of the group. 15447 */ 15448 15449/** 15450 * @ngdoc method 15451 * @name MdPanelRef#registerInterceptor 15452 * @description 15453 * Registers an interceptor with the panel. The callback should return a promise, 15454 * which will allow the action to continue when it gets resolved, or will 15455 * prevent an action if it is rejected. The interceptors are called sequentially 15456 * and it reverse order. `type` must be one of the following 15457 * values available on `$mdPanel.interceptorTypes`: 15458 * * `CLOSE` - Gets called before the panel begins closing. 15459 * 15460 * @param {string} type Type of interceptor. 15461 * @param {!angular.$q.Promise<any>} callback Callback to be registered. 15462 * @returns {!MdPanelRef} 15463 */ 15464 15465/** 15466 * @ngdoc method 15467 * @name MdPanelRef#removeInterceptor 15468 * @description 15469 * Removes a registered interceptor. 15470 * 15471 * @param {string} type Type of interceptor to be removed. 15472 * @param {function(): !angular.$q.Promise<any>} callback Interceptor to be removed. 15473 * @returns {!MdPanelRef} 15474 */ 15475 15476/** 15477 * @ngdoc method 15478 * @name MdPanelRef#removeAllInterceptors 15479 * @description 15480 * Removes all interceptors. If a type is supplied, only the 15481 * interceptors of that type will be cleared. 15482 * 15483 * @param {string=} type Type of interceptors to be removed. 15484 * @returns {!MdPanelRef} 15485 */ 15486 15487/** 15488 * @ngdoc method 15489 * @name MdPanelRef#updateAnimation 15490 * @description 15491 * Updates the animation configuration for a panel. You can use this to change 15492 * the panel's animation without having to re-create it. 15493 * 15494 * @param {!MdPanelAnimation} animation 15495 */ 15496 15497 15498/***************************************************************************** 15499 * MdPanelPosition * 15500 *****************************************************************************/ 15501 15502 15503/** 15504 * @ngdoc type 15505 * @name MdPanelPosition 15506 * @module material.components.panel 15507 * @description 15508 * 15509 * Object for configuring the position of the panel. 15510 * 15511 * @usage 15512 * 15513 * #### Centering the panel 15514 * 15515 * <hljs lang="js"> 15516 * new MdPanelPosition().absolute().center(); 15517 * </hljs> 15518 * 15519 * #### Overlapping the panel with an element 15520 * 15521 * <hljs lang="js"> 15522 * new MdPanelPosition() 15523 * .relativeTo(someElement) 15524 * .addPanelPosition( 15525 * $mdPanel.xPosition.ALIGN_START, 15526 * $mdPanel.yPosition.ALIGN_TOPS 15527 * ); 15528 * </hljs> 15529 * 15530 * #### Aligning the panel with the bottom of an element 15531 * 15532 * <hljs lang="js"> 15533 * new MdPanelPosition() 15534 * .relativeTo(someElement) 15535 * .addPanelPosition($mdPanel.xPosition.CENTER, $mdPanel.yPosition.BELOW); 15536 * </hljs> 15537 */ 15538 15539/** 15540 * @ngdoc method 15541 * @name MdPanelPosition#absolute 15542 * @description 15543 * Positions the panel absolutely relative to the parent element. If the parent 15544 * is document.body, this is equivalent to positioning the panel absolutely 15545 * within the viewport. 15546 * 15547 * @returns {!MdPanelPosition} 15548 */ 15549 15550/** 15551 * @ngdoc method 15552 * @name MdPanelPosition#relativeTo 15553 * @description 15554 * Positions the panel relative to a specific element. 15555 * 15556 * @param {string|!Element|!angular.JQLite} element Query selector, DOM element, 15557 * or angular element to position the panel with respect to. 15558 * @returns {!MdPanelPosition} 15559 */ 15560
15561/** 15562 * @ngdoc method 15563 * @name MdPanelPosition#top 15564 * @description 15565 * Sets the value of `top` for the panel. Clears any previously set vertical 15566 * position. 15567 * 15568 * @param {string=} top Value of `top`. Defaults to '0'. 15569 * @returns {!MdPanelPosition} 15570 */ 15571 15572/** 15573 * @ngdoc method 15574 * @name MdPanelPosition#bottom 15575 * @description 15576 * Sets the value of `bottom` for the panel. Clears any previously set vertical 15577 * position. 15578 * 15579 * @param {string=} bottom Value of `bottom`. Defaults to '0'. 15580 * @returns {!MdPanelPosition} 15581 */ 15582 15583/** 15584 * @ngdoc method 15585 * @name MdPanelPosition#start 15586 * @description 15587 * Sets the panel to the start of the page - `left` if `ltr` or `right` for 15588 * `rtl`. Clears any previously set horizontal position. 15589 * 15590 * @param {string=} start Value of position. Defaults to '0'. 15591 * @returns {!MdPanelPosition} 15592 */ 15593 15594/** 15595 * @ngdoc method 15596 * @name MdPanelPosition#end 15597 * @description 15598 * Sets the panel to the end of the page - `right` if `ltr` or `left` for `rtl`. 15599 * Clears any previously set horizontal position. 15600 * 15601 * @param {string=} end Value of position. Defaults to '0'. 15602 * @returns {!MdPanelPosition} 15603 */ 15604 15605/** 15606 * @ngdoc method 15607 * @name MdPanelPosition#left 15608 * @description 15609 * Sets the value of `left` for the panel. Clears any previously set 15610 * horizontal position. 15611 * 15612 * @param {string=} left Value of `left`. Defaults to '0'. 15613 * @returns {!MdPanelPosition} 15614 */ 15615 15616/** 15617 * @ngdoc method 15618 * @name MdPanelPosition#right 15619 * @description 15620 * Sets the value of `right` for the panel. Clears any previously set 15621 * horizontal position. 15622 * 15623 * @param {string=} right Value of `right`. Defaults to '0'. 15624 * @returns {!MdPanelPosition} 15625 */ 15626 15627/** 15628 * @ngdoc method 15629 * @name MdPanelPosition#centerHorizontally 15630 * @description 15631 * Centers the panel horizontally in the viewport. Clears any previously set 15632 * horizontal position. 15633 * 15634 * @returns {!MdPanelPosition} 15635 */ 15636 15637/** 15638 * @ngdoc method 15639 * @name MdPanelPosition#centerVertically 15640 * @description 15641 * Centers the panel vertically in the viewport. Clears any previously set 15642 * vertical position. 15643 * 15644 * @returns {!MdPanelPosition} 15645 */ 15646 15647/** 15648 * @ngdoc method 15649 * @name MdPanelPosition#center 15650 * @description 15651 * Centers the panel horizontally and vertically in the viewport. This is 15652 * equivalent to calling both `centerHorizontally` and `centerVertically`. 15653 * Clears any previously set horizontal and vertical positions. 15654 * 15655 * @returns {!MdPanelPosition} 15656 */ 15657 15658/** 15659 * @ngdoc method 15660 * @name MdPanelPosition#addPanelPosition 15661 * @description 15662 * Sets the x and y position for the panel relative to another element. Can be 15663 * called multiple times to specify an ordered list of panel positions. The 15664 * first position which allows the panel to be completely on-screen will be 15665 * chosen; the last position will be chose whether it is on-screen or not. 15666 * 15667 * xPosition must be one of the following values available on 15668 * $mdPanel.xPosition: 15669 * 15670 * 15671 * CENTER | ALIGN_START | ALIGN_END | OFFSET_START | OFFSET_END 15672 * 15673 * <pre> 15674 * ************* 15675 * * * 15676 * * PANEL * 15677 * * * 15678 * ************* 15679 * A B C D E 15680 * 15681 * A: OFFSET_START (for LTR displays) 15682 * B: ALIGN_START (for LTR displays) 15683 * C: CENTER 15684 * D: ALIGN_END (for LTR displays) 15685 * E: OFFSET_END (for LTR displays) 15686 * </pre> 15687 * 15688 * yPosition must be one of the following values available on 15689 * $mdPanel.yPosition: 15690 * 15691 * CENTER | ALIGN_TOPS | ALIGN_BOTTOMS | ABOVE | BELOW 15692 * 15693 * <pre> 15694 * F 15695 * G ************* 15696 * * * 15697 * H * PANEL * 15698 * * * 15699 * I ************* 15700 * J 15701 * 15702 * F: BELOW 15703 * G: ALIGN_TOPS 15704 * H: CENTER 15705 * I: ALIGN_BOTTOMS 15706 * J: ABOVE 15707 * </pre> 15708 * 15709 * @param {string} xPosition 15710 * @param {string} yPosition 15711 * @returns {!MdPanelPosition} 15712 */ 15713 15714/** 15715 * @ngdoc method 15716 * @name MdPanelPosition#withOffsetX 15717 * @description 15718 * Sets the value of the offset in the x-direction. 15719 * 15720 * @param {string} offsetX 15721 * @returns {!MdPanelPosition} 15722 */ 15723 15724/** 15725 * @ngdoc method 15726 * @name MdPanelPosition#withOffsetY 15727 * @description 15728 * Sets the value of the offset in the y-direction. 15729 * 15730 * @param {string} offsetY 15731 * @returns {!MdPanelPosition} 15732 */ 15733 15734 15735/***************************************************************************** 15736 * MdPanelAnimation * 15737 *****************************************************************************/ 15738 15739 15740/** 15741 * @ngdoc type 15742 * @name MdPanelAnimation 15743 * @module material.components.panel 15744 * @description 15745 * Animation configuration object. To use, create an MdPanelAnimation with the 15746 * desired properties, then pass the object as part of $mdPanel creation. 15747 * 15748 * @usage 15749 * 15750 * <hljs lang="js"> 15751 * var panelAnimation = new MdPanelAnimation()
15752 * .openFrom(myButtonEl) 15753 * .duration(1337) 15754 * .closeTo('.my-button') 15755 * .withAnimation($mdPanel.animation.SCALE); 15756 * 15757 * $mdPanel.create({ 15758 * animation: panelAnimation 15759 * }); 15760 * </hljs> 15761 */ 15762 15763/** 15764 * @ngdoc method 15765 * @name MdPanelAnimation#openFrom 15766 * @description 15767 * Specifies where to start the open animation. `openFrom` accepts a 15768 * click event object, query selector, DOM element, or a Rect object that 15769 * is used to determine the bounds. When passed a click event, the location 15770 * of the click will be used as the position to start the animation. 15771 * 15772 * @param {string|!Element|!Event|{top: number, left: number}} 15773 * @returns {!MdPanelAnimation} 15774 */ 15775 15776/** 15777 * @ngdoc method 15778 * @name MdPanelAnimation#closeTo 15779 * @description 15780 * Specifies where to animate the panel close. `closeTo` accepts a 15781 * query selector, DOM element, or a Rect object that is used to determine 15782 * the bounds. 15783 * 15784 * @param {string|!Element|{top: number, left: number}} 15785 * @returns {!MdPanelAnimation} 15786 */ 15787 15788/** 15789 * @ngdoc method 15790 * @name MdPanelAnimation#withAnimation 15791 * @description 15792 * Specifies the animation class. 15793 * 15794 * There are several default animations that can be used: 15795 * ($mdPanel.animation) 15796 * SLIDE: The panel slides in and out from the specified 15797 * elements. It will not fade in or out. 15798 * SCALE: The panel scales in and out. Slide and fade are 15799 * included in this animation. 15800 * FADE: The panel fades in and out. 15801 * 15802 * Custom classes will by default fade in and out unless 15803 * "transition: opacity 1ms" is added to the to custom class. 15804 * 15805 * @param {string|{open: string, close: string}} cssClass 15806 * @returns {!MdPanelAnimation} 15807 */ 15808 15809/** 15810 * @ngdoc method 15811 * @name MdPanelAnimation#duration 15812 * @description 15813 * Specifies the duration of the animation in milliseconds. The `duration` 15814 * method accepts either a number or an object with separate open and close 15815 * durations. 15816 * 15817 * @param {number|{open: number, close: number}} duration 15818 * @returns {!MdPanelAnimation} 15819 */ 15820 15821 15822/***************************************************************************** 15823 * PUBLIC DOCUMENTATION * 15824 *****************************************************************************/ 15825 15826 15827var MD_PANEL_Z_INDEX = 80; 15828var MD_PANEL_HIDDEN = '_md-panel-hidden'; 15829var FOCUS_TRAP_TEMPLATE = angular.element( 15830 '<div class="_md-panel-focus-trap" tabindex="0"></div>'); 15831 15832var _presets = {}; 15833 15834 15835/** 15836 * A provider that is used for creating presets for the panel API. 15837 * @final @constructor @ngInject 15838 */ 15839function MdPanelProvider() { 15840 return { 15841 'definePreset': definePreset, 15842 'getAllPresets': getAllPresets, 15843 'clearPresets': clearPresets, 15844 '$get': $getProvider() 15845 }; 15846} 15847 15848 15849/** 15850 * Takes the passed in panel configuration object and adds it to the `_presets` 15851 * object at the specified name. 15852 * @param {string} name Name of the preset to set. 15853 * @param {!Object} preset Specific configuration object that can contain any 15854 * and all of the parameters avaialble within the `$mdPanel.create` method. 15855 * However, parameters that pertain to id, position, animation, and user 15856 * interaction are not allowed and will be removed from the preset 15857 * configuration. 15858 */ 15859function definePreset(name, preset) { 15860 if (!name || !preset) { 15861 throw new Error('mdPanelProvider: The panel preset definition is ' + 15862 'malformed. The name and preset object are required.'); 15863 } else if (_presets.hasOwnProperty(name)) { 15864 throw new Error('mdPanelProvider: The panel preset you have requested ' + 15865 'has already been defined.'); 15866 } 15867 15868 // Delete any property on the preset that is not allowed. 15869 delete preset.id; 15870 delete preset.position; 15871 delete preset.animation; 15872 15873 _presets[name] = preset; 15874} 15875 15876 15877/** 15878 * Gets a clone of the `_presets`. 15879 * @return {!Object} 15880 */ 15881function getAllPresets() { 15882 return angular.copy(_presets); 15883} 15884 15885 15886/** 15887 * Clears all of the stored presets. 15888 */ 15889function clearPresets() { 15890 _presets = {}; 15891} 15892 15893 15894/** 15895 * Represents the `$get` method of the AngularJS provider. From here, a new 15896 * reference to the MdPanelService is returned where the needed arguments are 15897 * passed in including the MdPanelProvider `_presets`. 15898 * @param {!Object} _presets 15899 * @param {!angular.JQLite} $rootElement 15900 * @param {!angular.Scope} $rootScope 15901 * @param {!angular.$injector} $injector 15902 * @param {!angular.$window} $window 15903 */ 15904function $getProvider() { 15905 return [ 15906 '$rootElement', '$rootScope', '$injector', '$window', 15907 function($rootElement, $rootScope, $injector, $window) { 15908 return new MdPanelService(_presets, $rootElement, $rootScope, 15909 $injector, $window); 15910 } 15911 ]; 15912} 15913 15914 15915/***************************************************************************** 15916 * MdPanel Service * 15917 *****************************************************************************/ 15918 15919 15920/**
15921 * A service that is used for controlling/displaying panels on the screen. 15922 * @param {!Object} presets 15923 * @param {!angular.JQLite} $rootElement 15924 * @param {!angular.Scope} $rootScope 15925 * @param {!angular.$injector} $injector 15926 * @param {!angular.$window} $window 15927 * @final @constructor @ngInject 15928 */ 15929function MdPanelService(presets, $rootElement, $rootScope, $injector, $window) { 15930 /** 15931 * Default config options for the panel. 15932 * Anything angular related needs to be done later. Therefore 15933 * scope: $rootScope.$new(true), 15934 * attachTo: $rootElement, 15935 * are added later. 15936 * @private {!Object} 15937 */ 15938 this._defaultConfigOptions = { 15939 bindToController: true, 15940 clickOutsideToClose: false, 15941 disableParentScroll: false, 15942 escapeToClose: false, 15943 focusOnOpen: true, 15944 fullscreen: false, 15945 hasBackdrop: false, 15946 propagateContainerEvents: false, 15947 transformTemplate: angular.bind(this, this._wrapTemplate), 15948 trapFocus: false, 15949 zIndex: MD_PANEL_Z_INDEX 15950 }; 15951 15952 /** @private {!Object} */ 15953 this._config = {}; 15954 15955 /** @private {!Object} */ 15956 this._presets = presets; 15957 15958 /** @private @const */ 15959 this._$rootElement = $rootElement; 15960 15961 /** @private @const */ 15962 this._$rootScope = $rootScope; 15963 15964 /** @private @const */ 15965 this._$injector = $injector; 15966 15967 /** @private @const */ 15968 this._$window = $window; 15969 15970 /** @private @const */ 15971 this._$mdUtil = this._$injector.get('$mdUtil'); 15972 15973 /** @private {!Object<string, !MdPanelRef>} */ 15974 this._trackedPanels = {}; 15975 15976 /** 15977 * @private {!Object<string, 15978 * {panels: !Array<!MdPanelRef>, 15979 * openPanels: !Array<!MdPanelRef>, 15980 * maxOpen: number}>} 15981 */ 15982 this._groups = Object.create(null); 15983 15984 /** 15985 * Default animations that can be used within the panel. 15986 * @type {enum} 15987 */ 15988 this.animation = MdPanelAnimation.animation; 15989 15990 /** 15991 * Possible values of xPosition for positioning the panel relative to 15992 * another element. 15993 * @type {enum} 15994 */ 15995 this.xPosition = MdPanelPosition.xPosition; 15996 15997 /** 15998 * Possible values of yPosition for positioning the panel relative to 15999 * another element. 16000 * @type {enum} 16001 */ 16002 this.yPosition = MdPanelPosition.yPosition; 16003 16004 /** 16005 * Possible values for the interceptors that can be registered on a panel. 16006 * @type {enum} 16007 */ 16008 this.interceptorTypes = MdPanelRef.interceptorTypes; 16009 16010 /** 16011 * Possible values for closing of a panel. 16012 * @type {enum} 16013 */ 16014 this.closeReasons = MdPanelRef.closeReasons; 16015 16016 /** 16017 * Possible values of absolute position. 16018 * @type {enum} 16019 */ 16020 this.absPosition = MdPanelPosition.absPosition; 16021} 16022 16023 16024/** 16025 * Creates a panel with the specified options. 16026 * @param {string=} preset Name of a preset configuration that can be used to 16027 * extend the panel configuration. 16028 * @param {!Object=} config Configuration object for the panel. 16029 * @returns {!MdPanelRef} 16030 */ 16031MdPanelService.prototype.create = function(preset, config) { 16032 if (typeof preset === 'string') { 16033 preset = this._getPresetByName(preset); 16034 } else if (typeof preset === 'object' && 16035 (angular.isUndefined(config) || !config)) { 16036 config = preset; 16037 preset = {}; 16038 } 16039 16040 preset = preset || {}; 16041 config = config || {}; 16042 16043 // If the passed-in config contains an ID and the ID is within _trackedPanels, 16044 // return the tracked panel after updating its config with the passed-in 16045 // config. 16046 if (angular.isDefined(config.id) && this._trackedPanels[config.id]) { 16047 var trackedPanel = this._trackedPanels[config.id]; 16048 angular.extend(trackedPanel.config, config); 16049 return trackedPanel; 16050 } 16051 16052 // Combine the passed-in config, the _defaultConfigOptions, and the preset 16053 // configuration into the `_config`. 16054 this._config = angular.extend({ 16055 // If no ID is set within the passed-in config, then create an arbitrary ID. 16056 id: config.id || 'panel_' + this._$mdUtil.nextUid(), 16057 scope: this._$rootScope.$new(true), 16058 attachTo: this._$rootElement 16059 }, this._defaultConfigOptions, config, preset); 16060 16061 // Create the panelRef and add it to the `_trackedPanels` object. 16062 var panelRef = new MdPanelRef(this._config, this._$injector); 16063 this._trackedPanels[config.id] = panelRef; 16064 16065 // Add the panel to each of its requested groups. 16066 if (this._config.groupName) { 16067 if (angular.isString(this._config.groupName)) { 16068 this._config.groupName = [this._config.groupName]; 16069 } 16070 angular.forEach(this._config.groupName, function(group) { 16071 panelRef.addToGroup(group); 16072 }); 16073 } 16074 16075 this._config.scope.$on('$destroy', angular.bind(panelRef, panelRef.detach)); 16076 16077 return panelRef; 16078}; 16079 16080 16081/** 16082 * Creates and opens a panel with the specified options. 16083 * @param {string=} preset Name of a preset configuration that can be used to 16084 * extend the panel configuration. 16085 * @param {!Object=} config Configuration object for the panel. 16086 * @returns {!angular.$q.Promise<!MdPanelRef>}
16086 The panel created from create. 16087 */ 16088MdPanelService.prototype.open = function(preset, config) { 16089 var panelRef = this.create(preset, config); 16090 return panelRef.open().then(function() { 16091 return panelRef; 16092 }); 16093}; 16094 16095 16096/** 16097 * Gets a specific preset configuration object saved within `_presets`. 16098 * @param {string} preset Name of the preset to search for. 16099 * @returns {!Object} The preset configuration object. 16100 */ 16101MdPanelService.prototype._getPresetByName = function(preset) { 16102 if (!this._presets[preset]) { 16103 throw new Error('mdPanel: The panel preset configuration that you ' + 16104 'requested does not exist. Use the $mdPanelProvider to create a ' + 16105 'preset before requesting one.'); 16106 } 16107 return this._presets[preset]; 16108}; 16109 16110 16111/** 16112 * Returns a new instance of the MdPanelPosition. Use this to create the 16113 * positioning object. 16114 * @returns {!MdPanelPosition} 16115 */ 16116MdPanelService.prototype.newPanelPosition = function() { 16117 return new MdPanelPosition(this._$injector); 16118}; 16119 16120 16121/** 16122 * Returns a new instance of the MdPanelAnimation. Use this to create the 16123 * animation object. 16124 * @returns {!MdPanelAnimation} 16125 */ 16126MdPanelService.prototype.newPanelAnimation = function() { 16127 return new MdPanelAnimation(this._$injector); 16128}; 16129 16130 16131/** 16132 * Creates a panel group and adds it to a tracked list of panel groups. 16133 * @param groupName {string} Name of the group to create. 16134 * @param config {!Object=} Specific configuration object that may contain the 16135 * following properties: 16136 * 16137 * - `maxOpen` - `{number=}`: The maximum number of panels that are allowed 16138 * open within a defined panel group. 16139 * 16140 * @returns {!Object<string, 16141 * {panels: !Array<!MdPanelRef>, 16142 * openPanels: !Array<!MdPanelRef>, 16143 * maxOpen: number}>} panelGroup 16144 */ 16145MdPanelService.prototype.newPanelGroup = function(groupName, config) { 16146 if (!this._groups[groupName]) { 16147 config = config || {}; 16148 var group = { 16149 panels: [], 16150 openPanels: [], 16151 maxOpen: config.maxOpen > 0 ? config.maxOpen : Infinity 16152 }; 16153 this._groups[groupName] = group; 16154 } 16155 return this._groups[groupName]; 16156}; 16157 16158 16159/** 16160 * Sets the maximum number of panels in a group that can be opened at a given 16161 * time. 16162 * @param {string} groupName The name of the group to configure. 16163 * @param {number} maxOpen The maximum number of panels that can be 16164 * opened. Infinity can be passed in to remove the maxOpen limit. 16165 */
16166MdPanelService.prototype.setGroupMaxOpen = function(groupName, maxOpen) { 16167 if (this._groups[groupName]) { 16168 this._groups[groupName].maxOpen = maxOpen; 16169 } else { 16170 throw new Error('mdPanel: Group does not exist yet. Call newPanelGroup().'); 16171 } 16172}; 16173 16174 16175/** 16176 * Determines if the current number of open panels within a group exceeds the 16177 * limit of allowed open panels. 16178 * @param {string} groupName The name of the group to check. 16179 * @returns {boolean} true if open count does exceed maxOpen and false if not. 16180 * @private 16181 */ 16182MdPanelService.prototype._openCountExceedsMaxOpen = function(groupName) { 16183 if (this._groups[groupName]) { 16184 var group = this._groups[groupName]; 16185 return group.maxOpen > 0 && group.openPanels.length > group.maxOpen; 16186 } 16187 return false; 16188}; 16189 16190 16191/** 16192 * Closes the first open panel within a specific group. 16193 * @param {string} groupName The name of the group. 16194 * @private 16195 */ 16196MdPanelService.prototype._closeFirstOpenedPanel = function(groupName) { 16197 this._groups[groupName].openPanels[0].close(); 16198}; 16199 16200 16201/** 16202 * Wraps the users template in two elements, md-panel-outer-wrapper, which 16203 * covers the entire attachTo element, and md-panel, which contains only the 16204 * template. This allows the panel control over positioning, animations, 16205 * and similar properties. 16206 * @param {string} origTemplate The original template. 16207 * @returns {string} The wrapped template. 16208 * @private 16209 */ 16210MdPanelService.prototype._wrapTemplate = function(origTemplate) { 16211 var template = origTemplate || ''; 16212 16213 // The panel should be initially rendered offscreen so we can calculate 16214 // height and width for positioning. 16215 return '' + 16216 '<div class="md-panel-outer-wrapper">' + 16217 ' <div class="md-panel _md-panel-offscreen">' + template + '</div>' + 16218 '</div>'; 16219}; 16220 16221 16222/** 16223 * Wraps a content element in a md-panel-outer wrapper and 16224 * positions it off-screen. Allows for proper control over positoning 16225 * and animations. 16226 * @param {!angular.JQLite} contentElement Element to be wrapped. 16227 * @return {!angular.JQLite} Wrapper element. 16228 * @private 16229 */ 16230MdPanelService.prototype._wrapContentElement = function(contentElement) { 16231 var wrapper = angular.element('<div class="md-panel-outer-wrapper">'); 16232 16233 contentElement.addClass('md-panel _md-panel-offscreen'); 16234 wrapper.append(contentElement); 16235 16236 return wrapper; 16237}; 16238 16239 16240/***************************************************************************** 16241 * MdPanelRef * 16242 *****************************************************************************/ 16243 16244 16245/** 16246 * A reference to a created panel. This reference contains a unique id for the 16247 * panel, along with properties/functions used to control the panel. 16248 * @param {!Object} config 16249 * @param {!angular.$injector} $injector 16250 * @final @constructor 16251 */ 16252function MdPanelRef(config, $injector) { 16253 // Injected variables. 16254 /** @private @const {!angular.$q} */ 16255 this._$q = $injector.get('$q'); 16256 16257 /** @private @const {!angular.$mdCompiler} */ 16258 this._$mdCompiler = $injector.get('$mdCompiler'); 16259 16260 /** @private @const {!angular.$mdConstant} */ 16261 this._$mdConstant = $injector.get('$mdConstant'); 16262 16263 /** @private @const {!angular.$mdUtil} */ 16264 this._$mdUtil = $injector.get('$mdUtil'); 16265 16266 /** @private @const {!angular.$mdTheming} */ 16267 this._$mdTheming = $injector.get('$mdTheming'); 16268 16269 /** @private @const {!angular.Scope} */ 16270 this._$rootScope = $injector.get('$rootScope'); 16271 16272 /** @private @const {!angular.$animate} */ 16273 this._$animate = $injector.get('$animate'); 16274 16275 /** @private @const {!MdPanelRef} */ 16276 this._$mdPanel = $injector.get('$mdPanel'); 16277 16278 /** @private @const {!angular.$log} */ 16279 this._$log = $injector.get('$log'); 16280 16281 /** @private @const {!angular.$window} */ 16282 this._$window = $injector.get('$window'); 16283 16284 /** @private @const {!Function} */ 16285 this._$$rAF = $injector.get('$$rAF'); 16286 16287 // Public variables. 16288 /** 16289 * Unique id for the panelRef. 16290 * @type {string} 16291 */ 16292 this.id = config.id; 16293 16294 /** @type {!Object} */ 16295 this.config = config; 16296 16297 /** @type {!angular.JQLite|undefined} */ 16298 this.panelContainer; 16299 16300 /** @type {!angular.JQLite|undefined} */ 16301 this.panelEl; 16302 16303 /** 16304 * Whether the panel is attached. This is synchronous. When attach is called, 16305 * isAttached is set to true. When detach is called, isAttached is set to 16306 * false. 16307 * @type {boolean} 16308 */ 16309 this.isAttached = false;
vendor: 8,363 bytes, lines 16310-16620
16310 16311 // Private variables. 16312 /** @private {Array<function()>} */ 16313 this._removeListeners = []; 16314 16315 /** @private {!angular.JQLite|undefined} */ 16316 this._topFocusTrap; 16317 16318 /** @private {!angular.JQLite|undefined} */ 16319 this._bottomFocusTrap; 16320 16321 /** @private {!$mdPanel|undefined} */ 16322 this._backdropRef; 16323 16324 /** @private {Function?} */ 16325 this._restoreScroll = null; 16326 16327 /** 16328 * Keeps track of all the panel interceptors. 16329 * @private {!Object} 16330 */ 16331 this._interceptors = Object.create(null); 16332 16333 /** 16334 * Cleanup function, provided by `$mdCompiler` and assigned after the element 16335 * has been compiled. When `contentElement` is used, the function is used to 16336 * restore the element to it's proper place in the DOM. 16337 * @private {!Function} 16338 */ 16339 this._compilerCleanup = null; 16340 16341 /** 16342 * Cache for saving and restoring element inline styles, CSS classes etc. 16343 * @type {{styles: string, classes: string}} 16344 */ 16345 this._restoreCache = { 16346 styles: '', 16347 classes: '' 16348 }; 16349} 16350 16351 16352MdPanelRef.interceptorTypes = { 16353 CLOSE: 'onClose' 16354}; 16355 16356 16357/** 16358 * Opens an already created and configured panel. If the panel is already 16359 * visible, does nothing. 16360 * @returns {!angular.$q.Promise<!MdPanelRef>} A promise that is resolved when 16361 * the panel is opened and animations finish. 16362 */ 16363MdPanelRef.prototype.open = function() { 16364 var self = this; 16365 return this._$q(function(resolve, reject) { 16366 var done = self._done(resolve, self); 16367 var show = self._simpleBind(self.show, self); 16368 var checkGroupMaxOpen = function() { 16369 if (self.config.groupName) { 16370 angular.forEach(self.config.groupName, function(group) { 16371 if (self._$mdPanel._openCountExceedsMaxOpen(group)) { 16372 self._$mdPanel._closeFirstOpenedPanel(group); 16373 } 16374 }); 16375 } 16376 }; 16377 16378 self.attach() 16379 .then(show) 16380 .then(checkGroupMaxOpen) 16381 .then(done) 16382 .catch(reject); 16383 }); 16384}; 16385 16386 16387/** 16388 * Closes the panel. 16389 * @param {string} closeReason The event type that triggered the close. 16390 * @returns {!angular.$q.Promise<!MdPanelRef>} A promise that is resolved when 16391 * the panel is closed and animations finish. 16392 */ 16393MdPanelRef.prototype.close = function(closeReason) { 16394 var self = this; 16395 16396 return this._$q(function(resolve, reject) { 16397 self._callInterceptors(MdPanelRef.interceptorTypes.CLOSE).then(function() { 16398 var done = self._done(resolve, self); 16399 var detach = self._simpleBind(self.detach, self); 16400 var onCloseSuccess = self.config['onCloseSuccess'] || angular.noop; 16401 onCloseSuccess = angular.bind(self, onCloseSuccess, self, closeReason); 16402 16403 self.hide() 16404 .then(detach) 16405 .then(done) 16406 .then(onCloseSuccess) 16407 .catch(reject); 16408 }, reject); 16409 }); 16410}; 16411 16412 16413/** 16414 * Attaches the panel. The panel will be hidden afterwards. 16415 * @returns {!angular.$q.Promise<!MdPanelRef>} A promise that is resolved when 16416 * the panel is attached. 16417 */ 16418MdPanelRef.prototype.attach = function() { 16419 if (this.isAttached && this.panelEl) { 16420 return this._$q.when(this); 16421 } 16422 16423 var self = this; 16424 return this._$q(function(resolve, reject) { 16425 var done = self._done(resolve, self); 16426 var onDomAdded = self.config['onDomAdded'] || angular.noop; 16427 var addListeners = function(response) { 16428 self.isAttached = true; 16429 self._addEventListeners(); 16430 return response; 16431 }; 16432 16433 self._$q.all([ 16434 self._createBackdrop(), 16435 self._createPanel() 16436 .then(addListeners) 16437 .catch(reject) 16438 ]).then(onDomAdded) 16439 .then(done) 16440 .catch(reject); 16441 }); 16442}; 16443 16444 16445/** 16446 * Only detaches the panel. Will NOT hide the panel first. 16447 * @returns {!angular.$q.Promise<!MdPanelRef>} A promise that is resolved when 16448 * the panel is detached. 16449 */ 16450MdPanelRef.prototype.detach = function() { 16451 if (!this.isAttached) { 16452 return this._$q.when(this); 16453 } 16454 16455 var self = this; 16456 var onDomRemoved = self.config['onDomRemoved'] || angular.noop; 16457 16458 var detachFn = function() { 16459 self._removeEventListeners(); 16460 16461 // Remove the focus traps that we added earlier for keeping focus within 16462 // the panel. 16463 if (self._topFocusTrap && self._topFocusTrap.parentNode) { 16464 self._topFocusTrap.parentNode.removeChild(self._topFocusTrap); 16465 } 16466 16467 if (self._bottomFocusTrap && self._bottomFocusTrap.parentNode) { 16468 self._bottomFocusTrap.parentNode.removeChild(self._bottomFocusTrap); 16469 } 16470 16471 if (self._restoreCache.classes) { 16472 self.panelEl[0].className = self._restoreCache.classes; 16473 } 16474 16475 // Either restore the saved styles or clear the ones set by mdPanel. 16476 self.panelEl[0].style.cssText = self._restoreCache.styles || ''; 16477 16478 self._compilerCleanup(); 16479 self.panelContainer.remove(); 16480 self.isAttached = false; 16481 return self._$q.when(self); 16482 }; 16483 16484 if (this._restoreScroll) { 16485 this._restoreScroll(); 16486 this._restoreScroll = null; 16487 } 16488 16489 return this._$q(function(resolve, reject) { 16490 var done = self._done(resolve, self); 16491 16492 self._$q.all([ 16493 detachFn(), 16494 self._backdropRef ? self._backdropRef.detach() : true 16495 ]).then(onDomRemoved) 16496 .then(done) 16497 .catch(reject); 16498 }); 16499}; 16500 16501 16502/** 16503 * Destroys the panel. The Panel cannot be opened again after this. 16504 */ 16505MdPanelRef.prototype.destroy = function() { 16506 var self = this; 16507 if (this.config.groupName) { 16508 angular.forEach(this.config.groupName, function(group) { 16509 self.removeFromGroup(group); 16510 }); 16511 } 16512 this.config.scope.$destroy(); 16513 this.config.locals = null; 16514 this._interceptors = null; 16515}; 16516 16517 16518/** 16519 * Shows the panel. 16520 * @returns {!angular.$q.Promise<!MdPanelRef>} A promise that is resolved when 16521 * the panel has shown and animations finish. 16522 */ 16523MdPanelRef.prototype.show = function() { 16524 if (!this.panelContainer) { 16525 return this._$q(function(resolve, reject) { 16526 reject('mdPanel: Panel does not exist yet. Call open() or attach().'); 16527 }); 16528 } 16529 16530 if (!this.panelContainer.hasClass(MD_PANEL_HIDDEN)) { 16531 return this._$q.when(this); 16532 } 16533 16534 var self = this; 16535 var animatePromise = function() { 16536 self.panelContainer.removeClass(MD_PANEL_HIDDEN); 16537 return self._animateOpen(); 16538 }; 16539 16540 return this._$q(function(resolve, reject) { 16541 var done = self._done(resolve, self); 16542 var onOpenComplete = self.config['onOpenComplete'] || angular.noop; 16543 var addToGroupOpen = function() { 16544 if (self.config.groupName) { 16545 angular.forEach(self.config.groupName, function(group) { 16546 self._$mdPanel._groups[group].openPanels.push(self); 16547 }); 16548 } 16549 }; 16550 16551 self._$q.all([ 16552 self._backdropRef ? self._backdropRef.show() : self, 16553 animatePromise().then(function() { self._focusOnOpen(); }, reject) 16554 ]).then(onOpenComplete) 16555 .then(addToGroupOpen) 16556 .then(done) 16557 .catch(reject); 16558 }); 16559}; 16560 16561 16562/** 16563 * Hides the panel. 16564 * @returns {!angular.$q.Promise<!MdPanelRef>} A promise that is resolved when 16565 * the panel has hidden and animations finish. 16566 */ 16567MdPanelRef.prototype.hide = function() { 16568 if (!this.panelContainer) { 16569 return this._$q(function(resolve, reject) { 16570 reject('mdPanel: Panel does not exist yet. Call open() or attach().'); 16571 }); 16572 } 16573 16574 if (this.panelContainer.hasClass(MD_PANEL_HIDDEN)) { 16575 return this._$q.when(this); 16576 } 16577 16578 var self = this; 16579 16580 return this._$q(function(resolve, reject) { 16581 var done = self._done(resolve, self); 16582 var onRemoving = self.config['onRemoving'] || angular.noop; 16583 var hidePanel = function() { 16584 self.panelContainer.addClass(MD_PANEL_HIDDEN); 16585 }; 16586 var removeFromGroupOpen = function() { 16587 if (self.config.groupName) { 16588 var group, index; 16589 angular.forEach(self.config.groupName, function(group) { 16590 group = self._$mdPanel._groups[group]; 16591 index = group.openPanels.indexOf(self); 16592 if (index > -1) { 16593 group.openPanels.splice(index, 1); 16594 } 16595 }); 16596 } 16597 }; 16598 var focusOnOrigin = function() { 16599 var origin = self.config['origin']; 16600 if (origin) { 16601 getElement(origin).focus(); 16602 } 16603 }; 16604 16605 self._$q.all([ 16606 self._backdropRef ? self._backdropRef.hide() : self, 16607 self._animateClose() 16608 .then(onRemoving) 16609 .then(hidePanel) 16610 .then(removeFromGroupOpen) 16611 .then(focusOnOrigin) 16612 .catch(reject) 16613 ]).then(done, reject); 16614 }); 16615}; 16616 16617 16618/** 16619 * Add a class to the panel. DO NOT use this to hide/show the panel. 16620 * @deprecated
16621 * This method is in the process of being deprecated in favor of using the panel 16622 * and container JQLite elements that are referenced in the MdPanelRef object. 16623 * Full deprecation is scheduled for material 1.2. 16624 * 16625 * @param {string} newClass Class to be added. 16626 * @param {boolean} toElement Whether or not to add the class to the panel 16627 * element instead of the container. 16628 */ 16629MdPanelRef.prototype.addClass = function(newClass, toElement) { 16630 this._$log.warn( 16631 'mdPanel: The addClass method is in the process of being deprecated. ' + 16632 'Full deprecation is scheduled for the AngularJS Material 1.2 release. ' + 16633 'To achieve the same results, use the panelContainer or panelEl ' + 16634 'JQLite elements that are referenced in MdPanelRef.'); 16635 16636 if (!this.panelContainer) { 16637 throw new Error( 16638 'mdPanel: Panel does not exist yet. Call open() or attach().'); 16639 } 16640 16641 if (!toElement && !this.panelContainer.hasClass(newClass)) { 16642 this.panelContainer.addClass(newClass); 16643 } else if (toElement && !this.panelEl.hasClass(newClass)) { 16644 this.panelEl.addClass(newClass); 16645 } 16646}; 16647 16648 16649/** 16650 * Remove a class from the panel. DO NOT use this to hide/show the panel. 16651 * @deprecated
16652 * This method is in the process of being deprecated in favor of using the panel 16653 * and container JQLite elements that are referenced in the MdPanelRef object. 16654 * Full deprecation is scheduled for material 1.2. 16655 * 16656 * @param {string} oldClass Class to be removed. 16657 * @param {boolean} fromElement Whether or not to remove the class from the 16658 * panel element instead of the container. 16659 */ 16660MdPanelRef.prototype.removeClass = function(oldClass, fromElement) { 16661 this._$log.warn( 16662 'mdPanel: The removeClass method is in the process of being deprecated. ' + 16663 'Full deprecation is scheduled for the AngularJS Material 1.2 release. ' + 16664 'To achieve the same results, use the panelContainer or panelEl ' + 16665 'JQLite elements that are referenced in MdPanelRef.'); 16666 16667 if (!this.panelContainer) { 16668 throw new Error( 16669 'mdPanel: Panel does not exist yet. Call open() or attach().'); 16670 } 16671 16672 if (!fromElement && this.panelContainer.hasClass(oldClass)) { 16673 this.panelContainer.removeClass(oldClass); 16674 } else if (fromElement && this.panelEl.hasClass(oldClass)) { 16675 this.panelEl.removeClass(oldClass); 16676 } 16677}; 16678 16679 16680/** 16681 * Toggle a class on the panel. DO NOT use this to hide/show the panel. 16682 * @deprecated
16683 * This method is in the process of being deprecated in favor of using the panel 16684 * and container JQLite elements that are referenced in the MdPanelRef object. 16685 * Full deprecation is scheduled for material 1.2. 16686 * 16687 * @param {string} toggleClass The class to toggle. 16688 * @param {boolean} onElement Whether or not to toggle the class on the panel 16689 * element instead of the container. 16690 */ 16691MdPanelRef.prototype.toggleClass = function(toggleClass, onElement) { 16692 this._$log.warn( 16693 'mdPanel: The toggleClass method is in the process of being deprecated. ' + 16694 'Full deprecation is scheduled for the AngularJS Material 1.2 release. ' + 16695 'To achieve the same results, use the panelContainer or panelEl ' + 16696 'JQLite elements that are referenced in MdPanelRef.'); 16697 16698 if (!this.panelContainer) { 16699 throw new Error( 16700 'mdPanel: Panel does not exist yet. Call open() or attach().'); 16701 } 16702 16703 if (!onElement) { 16704 this.panelContainer.toggleClass(toggleClass); 16705 } else { 16706 this.panelEl.toggleClass(toggleClass); 16707 } 16708}; 16709 16710 16711/** 16712 * Compiles the panel, according to the passed in config and appends it to 16713 * the DOM. Helps normalize differences in the compilation process between 16714 * using a string template and a content element. 16715 * @returns {!angular.$q.Promise<!MdPanelRef>} Promise that is resolved when 16716 * the element has been compiled and added to the DOM. 16717 * @private 16718 */ 16719MdPanelRef.prototype._compile = function() { 16720 var self = this; 16721 16722 // Compile the element via $mdCompiler. Note that when using a 16723 // contentElement, the element isn't actually being compiled, rather the 16724 // compiler saves it's place in the DOM and provides a way of restoring it. 16725 return self._$mdCompiler.compile(self.config).then(function(compileData) { 16726 var config = self.config; 16727 16728 if (config.contentElement) { 16729 var panelEl = compileData.element; 16730 16731 // Since mdPanel modifies the inline styles and CSS classes, we need 16732 // to save them in order to be able to restore on close. 16733 self._restoreCache.styles = panelEl[0].style.cssText; 16734 self._restoreCache.classes = panelEl[0].className; 16735 16736 self.panelContainer = self._$mdPanel._wrapContentElement(panelEl); 16737 self.panelEl = panelEl; 16738 } else { 16739 self.panelContainer = compileData.link(config['scope']); 16740 self.panelEl = angular.element( 16741 self.panelContainer[0].querySelector('.md-panel') 16742 ); 16743 } 16744 16745 // Save a reference to the cleanup function from the compiler. 16746 self._compilerCleanup = compileData.cleanup; 16747 16748 // Attach the panel to the proper place in the DOM. 16749 getElement(self.config['attachTo']).append(self.panelContainer); 16750 16751 return self; 16752 }); 16753}; 16754 16755 16756/** 16757 * Creates a panel and adds it to the dom. 16758 * @returns {!angular.$q.Promise} A promise that is resolved when the panel is 16759 * created. 16760 * @private 16761 */ 16762MdPanelRef.prototype._createPanel = function() { 16763 var self = this; 16764 16765 return this._$q(function(resolve, reject) { 16766 if (!self.config.locals) { 16767 self.config.locals = {}; 16768 } 16769 16770 self.config.locals.mdPanelRef = self; 16771 16772 self._compile().then(function() { 16773 if (self.config['disableParentScroll']) { 16774 self._restoreScroll = self._$mdUtil.disableScrollAround( 16775 null, 16776 self.panelContainer, 16777 { disableScrollMask: true } 16778 ); 16779 } 16780 16781 // Add a custom CSS class to the panel element. 16782 if (self.config['panelClass']) { 16783 self.panelEl.addClass(self.config['panelClass']); 16784 } 16785 16786 // Handle click and touch events for the panel container. 16787 if (self.config['propagateContainerEvents']) { 16788 self.panelContainer.css('pointer-events', 'none'); 16789 self.panelEl.css('pointer-events', 'all'); 16790 } 16791 16792 // Panel may be outside the $rootElement, tell ngAnimate to animate 16793 // regardless. 16794 if (self._$animate.pin) { 16795 self._$animate.pin( 16796 self.panelContainer, 16797 getElement(self.config['attachTo']) 16798 ); 16799 } 16800 16801 self._configureTrapFocus(); 16802 self._addStyles().then(function() { 16803 resolve(self); 16804 }, reject); 16805 }, reject); 16806 16807 }); 16808}; 16809 16810 16811/** 16812 * Adds the styles for the panel, such as positioning and z-index. Also,
vendor: 10,063 bytes, lines 16813-17165
16813 * themes the panel element and panel container using `$mdTheming`. 16814 * @returns {!angular.$q.Promise<!MdPanelRef>} 16815 * @private 16816 */ 16817MdPanelRef.prototype._addStyles = function() { 16818 var self = this; 16819 return this._$q(function(resolve) { 16820 self.panelContainer.css('z-index', self.config['zIndex']); 16821 self.panelEl.css('z-index', self.config['zIndex'] + 1); 16822 16823 var hideAndResolve = function() { 16824 // Theme the element and container. 16825 self._setTheming(); 16826 16827 // Remove offscreen class and add hidden class. 16828 self.panelEl.removeClass('_md-panel-offscreen'); 16829 self.panelContainer.addClass(MD_PANEL_HIDDEN); 16830 16831 resolve(self); 16832 }; 16833 16834 if (self.config['fullscreen']) { 16835 self.panelEl.addClass('_md-panel-fullscreen'); 16836 hideAndResolve(); 16837 return; // Don't setup positioning. 16838 } 16839 16840 var positionConfig = self.config['position']; 16841 if (!positionConfig) { 16842 hideAndResolve(); 16843 return; // Don't setup positioning. 16844 } 16845 16846 // Wait for angular to finish processing the template 16847 self._$rootScope['$$postDigest'](function() { 16848 // Position it correctly. This is necessary so that the panel will have a 16849 // defined height and width. 16850 self._updatePosition(true); 16851 16852 // Theme the element and container. 16853 self._setTheming(); 16854 16855 resolve(self); 16856 }); 16857 }); 16858}; 16859 16860 16861/** 16862 * Sets the `$mdTheming` classes on the `panelContainer` and `panelEl`. 16863 * @private 16864 */ 16865MdPanelRef.prototype._setTheming = function() { 16866 this._$mdTheming(this.panelEl); 16867 this._$mdTheming(this.panelContainer); 16868}; 16869 16870 16871/** 16872 * Updates the position configuration of a panel 16873 * @param {!MdPanelPosition} position 16874 */ 16875MdPanelRef.prototype.updatePosition = function(position) { 16876 if (!this.panelContainer) { 16877 throw new Error( 16878 'mdPanel: Panel does not exist yet. Call open() or attach().'); 16879 } 16880 16881 this.config['position'] = position; 16882 this._updatePosition(); 16883}; 16884 16885 16886/** 16887 * Calculates and updates the position of the panel. 16888 * @param {boolean=} init 16889 * @private 16890 */ 16891MdPanelRef.prototype._updatePosition = function(init) { 16892 var positionConfig = this.config['position']; 16893 16894 if (positionConfig) { 16895 positionConfig._setPanelPosition(this.panelEl); 16896 16897 // Hide the panel now that position is known. 16898 if (init) { 16899 this.panelEl.removeClass('_md-panel-offscreen'); 16900 this.panelContainer.addClass(MD_PANEL_HIDDEN); 16901 } 16902 16903 this.panelEl.css( 16904 MdPanelPosition.absPosition.TOP, 16905 positionConfig.getTop() 16906 ); 16907 this.panelEl.css( 16908 MdPanelPosition.absPosition.BOTTOM, 16909 positionConfig.getBottom() 16910 ); 16911 this.panelEl.css( 16912 MdPanelPosition.absPosition.LEFT, 16913 positionConfig.getLeft() 16914 ); 16915 this.panelEl.css( 16916 MdPanelPosition.absPosition.RIGHT, 16917 positionConfig.getRight() 16918 ); 16919 } 16920}; 16921 16922 16923/** 16924 * Focuses on the panel or the first focus target. 16925 * @private 16926 */ 16927MdPanelRef.prototype._focusOnOpen = function() { 16928 if (this.config['focusOnOpen']) { 16929 // Wait for the template to finish rendering to guarantee md-autofocus has 16930 // finished adding the class md-autofocus, otherwise the focusable element 16931 // isn't available to focus. 16932 var self = this; 16933 this._$rootScope['$$postDigest'](function() { 16934 var target = self._$mdUtil.findFocusTarget(self.panelEl) || 16935 self.panelEl; 16936 target.focus(); 16937 }); 16938 } 16939}; 16940 16941 16942/** 16943 * Shows the backdrop. 16944 * @returns {!angular.$q.Promise} A promise that is resolved when the backdrop 16945 * is created and attached. 16946 * @private 16947 */ 16948MdPanelRef.prototype._createBackdrop = function() { 16949 if (this.config.hasBackdrop) { 16950 if (!this._backdropRef) { 16951 var backdropAnimation = this._$mdPanel.newPanelAnimation() 16952 .openFrom(this.config.attachTo) 16953 .withAnimation({ 16954 open: '_md-opaque-enter', 16955 close: '_md-opaque-leave' 16956 }); 16957 16958 if (this.config.animation) { 16959 backdropAnimation.duration(this.config.animation._rawDuration); 16960 } 16961 16962 var backdropConfig = { 16963 animation: backdropAnimation, 16964 attachTo: this.config.attachTo, 16965 focusOnOpen: false, 16966 panelClass: '_md-panel-backdrop', 16967 zIndex: this.config.zIndex - 1 16968 }; 16969 16970 this._backdropRef = this._$mdPanel.create(backdropConfig); 16971 } 16972 if (!this._backdropRef.isAttached) { 16973 return this._backdropRef.attach(); 16974 } 16975 } 16976}; 16977 16978 16979/** 16980 * Listen for escape keys and outside clicks to auto close. 16981 * @private 16982 */ 16983MdPanelRef.prototype._addEventListeners = function() { 16984 this._configureEscapeToClose(); 16985 this._configureClickOutsideToClose(); 16986 this._configureScrollListener(); 16987}; 16988 16989 16990/** 16991 * Remove event listeners added in _addEventListeners. 16992 * @private 16993 */ 16994MdPanelRef.prototype._removeEventListeners = function() { 16995 this._removeListeners && this._removeListeners.forEach(function(removeFn) { 16996 removeFn(); 16997 }); 16998 this._removeListeners = []; 16999}; 17000 17001 17002/** 17003 * Setup the escapeToClose event listeners. 17004 * @private 17005 */ 17006MdPanelRef.prototype._configureEscapeToClose = function() { 17007 if (this.config['escapeToClose']) { 17008 var parentTarget = getElement(this.config['attachTo']); 17009 var self = this; 17010 17011 var keyHandlerFn = function(ev) { 17012 if (ev.keyCode === self._$mdConstant.KEY_CODE.ESCAPE) { 17013 ev.stopPropagation(); 17014 ev.preventDefault(); 17015 17016 self.close(MdPanelRef.closeReasons.ESCAPE); 17017 } 17018 }; 17019 17020 // Add keydown listeners 17021 this.panelContainer.on('keydown', keyHandlerFn); 17022 parentTarget.on('keydown', keyHandlerFn); 17023 17024 // Queue remove listeners function 17025 this._removeListeners.push(function() { 17026 self.panelContainer.off('keydown', keyHandlerFn); 17027 parentTarget.off('keydown', keyHandlerFn); 17028 }); 17029 } 17030}; 17031 17032 17033/** 17034 * Setup the clickOutsideToClose event listeners. 17035 * @private 17036 */ 17037MdPanelRef.prototype._configureClickOutsideToClose = function() { 17038 if (this.config['clickOutsideToClose']) { 17039 var target = this.config['propagateContainerEvents'] ? 17040 angular.element(document.body) : 17041 this.panelContainer; 17042 var sourceEl; 17043 17044 // Keep track of the element on which the mouse originally went down 17045 // so that we can only close the backdrop when the 'click' started on it. 17046 // A simple 'click' handler does not work, it sets the target object as the 17047 // element the mouse went down on. 17048 var mousedownHandler = function(ev) { 17049 sourceEl = ev.target; 17050 }; 17051 17052 // We check if our original element and the target is the backdrop 17053 // because if the original was the backdrop and the target was inside the 17054 // panel we don't want to panel to close. 17055 var self = this; 17056 var mouseupHandler = function(ev) { 17057 if (self.config['propagateContainerEvents']) { 17058 17059 // We check if the sourceEl of the event is the panel element or one 17060 // of it's children. If it is not, then close the panel. 17061 if (sourceEl !== self.panelEl[0] && !self.panelEl[0].contains(sourceEl)) { 17062 self.close(); 17063 } 17064 17065 } else if (sourceEl === target[0] && ev.target === target[0]) { 17066 ev.stopPropagation(); 17067 ev.preventDefault(); 17068 17069 self.close(MdPanelRef.closeReasons.CLICK_OUTSIDE); 17070 } 17071 }; 17072 17073 // Add listeners 17074 target.on('mousedown', mousedownHandler); 17075 target.on('mouseup', mouseupHandler); 17076 17077 // Queue remove listeners function 17078 this._removeListeners.push(function() { 17079 target.off('mousedown', mousedownHandler); 17080 target.off('mouseup', mouseupHandler); 17081 }); 17082 } 17083}; 17084 17085 17086/** 17087 * Configures the listeners for updating the panel position on scroll. 17088 * @private 17089*/ 17090MdPanelRef.prototype._configureScrollListener = function() { 17091 // No need to bind the event if scrolling is disabled. 17092 if (!this.config['disableParentScroll']) { 17093 var updatePosition = angular.bind(this, this._updatePosition); 17094 var debouncedUpdatePosition = this._$$rAF.throttle(updatePosition); 17095 var self = this; 17096 17097 var onScroll = function() { 17098 debouncedUpdatePosition(); 17099 }; 17100 17101 // Add listeners. 17102 this._$window.addEventListener('scroll', onScroll, true); 17103 17104 // Queue remove listeners function. 17105 this._removeListeners.push(function() { 17106 self._$window.removeEventListener('scroll', onScroll, true); 17107 }); 17108 } 17109}; 17110 17111 17112/** 17113 * Setup the focus traps. These traps will wrap focus when tabbing past the 17114 * panel. When shift-tabbing, the focus will stick in place. 17115 * @private 17116 */ 17117MdPanelRef.prototype._configureTrapFocus = function() { 17118 // Focus doesn't remain inside of the panel without this. 17119 this.panelEl.attr('tabIndex', '-1'); 17120 if (this.config['trapFocus']) { 17121 var element = this.panelEl; 17122 // Set up elements before and after the panel to capture focus and 17123 // redirect back into the panel. 17124 this._topFocusTrap = FOCUS_TRAP_TEMPLATE.clone()[0]; 17125 this._bottomFocusTrap = FOCUS_TRAP_TEMPLATE.clone()[0]; 17126 17127 // When focus is about to move out of the panel, we want to intercept it 17128 // and redirect it back to the panel element. 17129 var focusHandler = function() { 17130 element.focus(); 17131 }; 17132 this._topFocusTrap.addEventListener('focus', focusHandler); 17133 this._bottomFocusTrap.addEventListener('focus', focusHandler); 17134 17135 // Queue remove listeners function 17136 this._removeListeners.push(this._simpleBind(function() { 17137 this._topFocusTrap.removeEventListener('focus', focusHandler); 17138 this._bottomFocusTrap.removeEventListener('focus', focusHandler); 17139 }, this)); 17140 17141 // The top focus trap inserted immediately before the md-panel element (as 17142 // a sibling). The bottom focus trap inserted immediately after the 17143 // md-panel element (as a sibling). 17144 element[0].parentNode.insertBefore(this._topFocusTrap, element[0]); 17145 element.after(this._bottomFocusTrap); 17146 } 17147}; 17148 17149 17150/** 17151 * Updates the animation of a panel. 17152 * @param {!MdPanelAnimation} animation 17153 */ 17154MdPanelRef.prototype.updateAnimation = function(animation) { 17155 this.config['animation'] = animation; 17156 17157 if (this._backdropRef) { 17158 this._backdropRef.config.animation.duration(animation._rawDuration); 17159 } 17160}; 17161 17162 17163/** 17164 * Animate the panel opening. 17165 * @returns {!angular.$q.Promise} A promise that is resolved when the panel has
17166 * animated open. 17167 * @private 17168 */ 17169MdPanelRef.prototype._animateOpen = function() { 17170 this.panelContainer.addClass('md-panel-is-showing'); 17171 var animationConfig = this.config['animation']; 17172 if (!animationConfig) { 17173 // Promise is in progress, return it. 17174 this.panelContainer.addClass('_md-panel-shown'); 17175 return this._$q.when(this); 17176 } 17177 17178 var self = this; 17179 return this._$q(function(resolve) { 17180 var done = self._done(resolve, self); 17181 var warnAndOpen = function() { 17182 self._$log.warn( 17183 'mdPanel: MdPanel Animations failed. ' + 17184 'Showing panel without animating.'); 17185 done(); 17186 }; 17187 17188 animationConfig.animateOpen(self.panelEl) 17189 .then(done, warnAndOpen); 17190 }); 17191}; 17192 17193 17194/** 17195 * Animate the panel closing. 17196 * @returns {!angular.$q.Promise} A promise that is resolved when the panel has 17197 * animated closed. 17198 * @private 17199 */ 17200MdPanelRef.prototype._animateClose = function() { 17201 var animationConfig = this.config['animation']; 17202 if (!animationConfig) { 17203 this.panelContainer.removeClass('md-panel-is-showing');
vendor: 35,225 bytes, lines 17204-18496
17204 this.panelContainer.removeClass('_md-panel-shown'); 17205 return this._$q.when(this); 17206 } 17207 17208 var self = this; 17209 return this._$q(function(resolve) { 17210 var done = function() { 17211 self.panelContainer.removeClass('md-panel-is-showing'); 17212 resolve(self); 17213 }; 17214 var warnAndClose = function() { 17215 self._$log.warn( 17216 'mdPanel: MdPanel Animations failed. ' + 17217 'Hiding panel without animating.'); 17218 done(); 17219 }; 17220 17221 animationConfig.animateClose(self.panelEl) 17222 .then(done, warnAndClose); 17223 }); 17224}; 17225 17226 17227/** 17228 * Registers a interceptor with the panel. The callback should return a promise, 17229 * which will allow the action to continue when it gets resolved, or will 17230 * prevent an action if it is rejected. 17231 * @param {string} type Type of interceptor. 17232 * @param {!angular.$q.Promise<!any>} callback Callback to be registered. 17233 * @returns {!MdPanelRef} 17234 */ 17235MdPanelRef.prototype.registerInterceptor = function(type, callback) { 17236 var error = null; 17237 17238 if (!angular.isString(type)) { 17239 error = 'Interceptor type must be a string, instead got ' + typeof type; 17240 } else if (!angular.isFunction(callback)) { 17241 error = 'Interceptor callback must be a function, instead got ' + typeof callback; 17242 } 17243 17244 if (error) { 17245 throw new Error('MdPanel: ' + error); 17246 } 17247 17248 var interceptors = this._interceptors[type] = this._interceptors[type] || []; 17249 17250 if (interceptors.indexOf(callback) === -1) { 17251 interceptors.push(callback); 17252 } 17253 17254 return this; 17255}; 17256 17257 17258/** 17259 * Removes a registered interceptor. 17260 * @param {string} type Type of interceptor to be removed. 17261 * @param {Function} callback Interceptor to be removed. 17262 * @returns {!MdPanelRef} 17263 */ 17264MdPanelRef.prototype.removeInterceptor = function(type, callback) { 17265 var index = this._interceptors[type] ? 17266 this._interceptors[type].indexOf(callback) : -1; 17267 17268 if (index > -1) { 17269 this._interceptors[type].splice(index, 1); 17270 } 17271 17272 return this; 17273}; 17274 17275 17276/** 17277 * Removes all interceptors. 17278 * @param {string=} type Type of interceptors to be removed. 17279 * If ommited, all interceptors types will be removed. 17280 * @returns {!MdPanelRef} 17281 */ 17282MdPanelRef.prototype.removeAllInterceptors = function(type) { 17283 if (type) { 17284 this._interceptors[type] = []; 17285 } else { 17286 this._interceptors = Object.create(null); 17287 } 17288 17289 return this; 17290}; 17291 17292 17293/** 17294 * Invokes all the interceptors of a certain type sequantially in 17295 * reverse order. Works in a similar way to `$q.all`, except it 17296 * respects the order of the functions. 17297 * @param {string} type Type of interceptors to be invoked. 17298 * @returns {!angular.$q.Promise<!MdPanelRef>} 17299 * @private 17300 */ 17301MdPanelRef.prototype._callInterceptors = function(type) { 17302 var self = this; 17303 var $q = self._$q; 17304 var interceptors = self._interceptors && self._interceptors[type] || []; 17305 17306 return interceptors.reduceRight(function(promise, interceptor) { 17307 var isPromiseLike = interceptor && angular.isFunction(interceptor.then); 17308 var response = isPromiseLike ? interceptor : null; 17309 17310 /** 17311 * For interceptors to reject/cancel subsequent portions of the chain, simply 17312 * return a `$q.reject(<value>)` 17313 */ 17314 return promise.then(function() { 17315 if (!response) { 17316 try { 17317 response = interceptor(self); 17318 } catch(e) { 17319 response = $q.reject(e); 17320 } 17321 } 17322 17323 return response; 17324 }); 17325 }, $q.resolve(self)); 17326}; 17327 17328 17329/** 17330 * Faster, more basic than angular.bind 17331 * http://jsperf.com/angular-bind-vs-custom-vs-native 17332 * @param {function} callback 17333 * @param {!Object} self 17334 * @return {function} Callback function with a bound self. 17335 */ 17336MdPanelRef.prototype._simpleBind = function(callback, self) { 17337 return function(value) { 17338 return callback.apply(self, value); 17339 }; 17340}; 17341 17342 17343/** 17344 * @param {function} callback 17345 * @param {!Object} self 17346 * @return {function} Callback function with a self param. 17347 */ 17348MdPanelRef.prototype._done = function(callback, self) { 17349 return function() { 17350 callback(self); 17351 }; 17352}; 17353 17354 17355/** 17356 * Adds a panel to a group if the panel does not exist within the group already. 17357 * A panel can only exist within a single group. 17358 * @param {string} groupName The name of the group. 17359 */ 17360MdPanelRef.prototype.addToGroup = function(groupName) { 17361 if (!this._$mdPanel._groups[groupName]) { 17362 this._$mdPanel.newPanelGroup(groupName); 17363 } 17364 17365 var group = this._$mdPanel._groups[groupName]; 17366 var index = group.panels.indexOf(this); 17367 17368 if (index < 0) { 17369 group.panels.push(this); 17370 } 17371}; 17372 17373 17374/** 17375 * Removes a panel from a group if the panel exists within that group. The group 17376 * must be created ahead of time. 17377 * @param {string} groupName The name of the group. 17378 */ 17379MdPanelRef.prototype.removeFromGroup = function(groupName) { 17380 if (!this._$mdPanel._groups[groupName]) { 17381 throw new Error('mdPanel: The group ' + groupName + ' does not exist.'); 17382 } 17383 17384 var group = this._$mdPanel._groups[groupName]; 17385 var index = group.panels.indexOf(this); 17386 17387 if (index > -1) { 17388 group.panels.splice(index, 1); 17389 } 17390}; 17391 17392 17393/** 17394 * Possible default closeReasons for the close function. 17395 * @enum {string} 17396 */ 17397MdPanelRef.closeReasons = { 17398 CLICK_OUTSIDE: 'clickOutsideToClose', 17399 ESCAPE: 'escapeToClose', 17400}; 17401 17402 17403/***************************************************************************** 17404 * MdPanelPosition * 17405 *****************************************************************************/ 17406 17407 17408/** 17409 * Position configuration object. To use, create an MdPanelPosition with the 17410 * desired properties, then pass the object as part of $mdPanel creation. 17411 * 17412 * Example: 17413 * 17414 * var panelPosition = new MdPanelPosition() 17415 * .relativeTo(myButtonEl) 17416 * .addPanelPosition( 17417 * $mdPanel.xPosition.CENTER, 17418 * $mdPanel.yPosition.ALIGN_TOPS 17419 * ); 17420 * 17421 * $mdPanel.create({ 17422 * position: panelPosition 17423 * }); 17424 * 17425 * @param {!angular.$injector} $injector 17426 * @final @constructor 17427 */ 17428function MdPanelPosition($injector) { 17429 /** @private @const {!angular.$window} */ 17430 this._$window = $injector.get('$window'); 17431 17432 /** @private {boolean} */ 17433 this._isRTL = $injector.get('$mdUtil').bidi() === 'rtl'; 17434 17435 /** @private @const {!angular.$mdConstant} */ 17436 this._$mdConstant = $injector.get('$mdConstant'); 17437 17438 /** @private {boolean} */ 17439 this._absolute = false; 17440 17441 /** @private {!angular.JQLite} */ 17442 this._relativeToEl; 17443 17444 /** @private {string} */ 17445 this._top = ''; 17446 17447 /** @private {string} */ 17448 this._bottom = ''; 17449 17450 /** @private {string} */ 17451 this._left = ''; 17452 17453 /** @private {string} */ 17454 this._right = ''; 17455 17456 /** @private {!Array<string>} */ 17457 this._translateX = []; 17458 17459 /** @private {!Array<string>} */ 17460 this._translateY = []; 17461 17462 /** @private {!Array<{x:string, y:string}>} */ 17463 this._positions = []; 17464 17465 /** @private {?{x:string, y:string}} */ 17466 this._actualPosition; 17467} 17468 17469 17470/** 17471 * Possible values of xPosition. 17472 * @enum {string} 17473 */ 17474MdPanelPosition.xPosition = { 17475 CENTER: 'center', 17476 ALIGN_START: 'align-start', 17477 ALIGN_END: 'align-end', 17478 OFFSET_START: 'offset-start', 17479 OFFSET_END: 'offset-end' 17480}; 17481 17482 17483/** 17484 * Possible values of yPosition. 17485 * @enum {string} 17486 */ 17487MdPanelPosition.yPosition = { 17488 CENTER: 'center', 17489 ALIGN_TOPS: 'align-tops', 17490 ALIGN_BOTTOMS: 'align-bottoms', 17491 ABOVE: 'above', 17492 BELOW: 'below' 17493}; 17494 17495 17496/** 17497 * Possible values of absolute position. 17498 * @enum {string} 17499 */ 17500MdPanelPosition.absPosition = { 17501 TOP: 'top', 17502 RIGHT: 'right', 17503 BOTTOM: 'bottom', 17504 LEFT: 'left' 17505}; 17506 17507/** 17508 * Margin between the edges of a panel and the viewport. 17509 * @const {number} 17510 */ 17511MdPanelPosition.viewportMargin = 8; 17512 17513 17514/** 17515 * Sets absolute positioning for the panel. 17516 * @return {!MdPanelPosition} 17517 */ 17518MdPanelPosition.prototype.absolute = function() { 17519 this._absolute = true; 17520 return this; 17521}; 17522 17523 17524/** 17525 * Sets the value of a position for the panel. Clears any previously set 17526 * position. 17527 * @param {string} position Position to set 17528 * @param {string=} value Value of the position. Defaults to '0'. 17529 * @returns {!MdPanelPosition} 17530 * @private 17531 */ 17532MdPanelPosition.prototype._setPosition = function(position, value) { 17533 if (position === MdPanelPosition.absPosition.RIGHT || 17534 position === MdPanelPosition.absPosition.LEFT) { 17535 this._left = this._right = ''; 17536 } else if ( 17537 position === MdPanelPosition.absPosition.BOTTOM || 17538 position === MdPanelPosition.absPosition.TOP) { 17539 this._top = this._bottom = ''; 17540 } else { 17541 var positions = Object.keys(MdPanelPosition.absPosition).join() 17542 .toLowerCase(); 17543 17544 throw new Error('mdPanel: Position must be one of ' + positions + '.'); 17545 } 17546 17547 this['_' + position] = angular.isString(value) ? value : '0'; 17548 17549 return this; 17550}; 17551 17552 17553/** 17554 * Sets the value of `top` for the panel. Clears any previously set vertical 17555 * position. 17556 * @param {string=} top Value of `top`. Defaults to '0'. 17557 * @returns {!MdPanelPosition} 17558 */ 17559MdPanelPosition.prototype.top = function(top) { 17560 return this._setPosition(MdPanelPosition.absPosition.TOP, top); 17561}; 17562 17563 17564/** 17565 * Sets the value of `bottom` for the panel. Clears any previously set vertical 17566 * position. 17567 * @param {string=} bottom Value of `bottom`. Defaults to '0'. 17568 * @returns {!MdPanelPosition} 17569 */ 17570MdPanelPosition.prototype.bottom = function(bottom) { 17571 return this._setPosition(MdPanelPosition.absPosition.BOTTOM, bottom); 17572}; 17573 17574 17575/** 17576 * Sets the panel to the start of the page - `left` if `ltr` or `right` for 17577 * `rtl`. Clears any previously set horizontal position. 17578 * @param {string=} start Value of position. Defaults to '0'. 17579 * @returns {!MdPanelPosition} 17580 */ 17581MdPanelPosition.prototype.start = function(start) { 17582 var position = this._isRTL ? MdPanelPosition.absPosition.RIGHT : MdPanelPosition.absPosition.LEFT; 17583 return this._setPosition(position, start); 17584}; 17585 17586 17587/** 17588 * Sets the panel to the end of the page - `right` if `ltr` or `left` for `rtl`. 17589 * Clears any previously set horizontal position. 17590 * @param {string=} end Value of position. Defaults to '0'. 17591 * @returns {!MdPanelPosition} 17592 */ 17593MdPanelPosition.prototype.end = function(end) { 17594 var position = this._isRTL ? MdPanelPosition.absPosition.LEFT : MdPanelPosition.absPosition.RIGHT; 17595 return this._setPosition(position, end); 17596}; 17597 17598 17599/** 17600 * Sets the value of `left` for the panel. Clears any previously set 17601 * horizontal position. 17602 * @param {string=} left Value of `left`. Defaults to '0'. 17603 * @returns {!MdPanelPosition} 17604 */ 17605MdPanelPosition.prototype.left = function(left) { 17606 return this._setPosition(MdPanelPosition.absPosition.LEFT, left); 17607}; 17608 17609 17610/** 17611 * Sets the value of `right` for the panel. Clears any previously set 17612 * horizontal position. 17613 * @param {string=} right Value of `right`. Defaults to '0'. 17614 * @returns {!MdPanelPosition} 17615*/ 17616MdPanelPosition.prototype.right = function(right) { 17617 return this._setPosition(MdPanelPosition.absPosition.RIGHT, right); 17618}; 17619 17620 17621/** 17622 * Centers the panel horizontally in the viewport. Clears any previously set 17623 * horizontal position. 17624 * @returns {!MdPanelPosition} 17625 */ 17626MdPanelPosition.prototype.centerHorizontally = function() { 17627 this._left = '50%'; 17628 this._right = ''; 17629 this._translateX = ['-50%']; 17630 return this; 17631}; 17632 17633 17634/** 17635 * Centers the panel vertically in the viewport. Clears any previously set 17636 * vertical position. 17637 * @returns {!MdPanelPosition} 17638 */ 17639MdPanelPosition.prototype.centerVertically = function() { 17640 this._top = '50%'; 17641 this._bottom = ''; 17642 this._translateY = ['-50%']; 17643 return this; 17644}; 17645 17646 17647/** 17648 * Centers the panel horizontally and vertically in the viewport. This is 17649 * equivalent to calling both `centerHorizontally` and `centerVertically`. 17650 * Clears any previously set horizontal and vertical positions. 17651 * @returns {!MdPanelPosition} 17652 */ 17653MdPanelPosition.prototype.center = function() { 17654 return this.centerHorizontally().centerVertically(); 17655}; 17656 17657 17658/** 17659 * Sets element for relative positioning. 17660 * @param {string|!Element|!angular.JQLite} element Query selector, DOM element, 17661 * or angular element to set the panel relative to. 17662 * @returns {!MdPanelPosition} 17663 */ 17664MdPanelPosition.prototype.relativeTo = function(element) { 17665 this._absolute = false; 17666 this._relativeToEl = getElement(element); 17667 return this; 17668}; 17669 17670 17671/** 17672 * Sets the x and y positions for the panel relative to another element. 17673 * @param {string} xPosition must be one of the MdPanelPosition.xPosition 17674 * values. 17675 * @param {string} yPosition must be one of the MdPanelPosition.yPosition 17676 * values. 17677 * @returns {!MdPanelPosition} 17678 */ 17679MdPanelPosition.prototype.addPanelPosition = function(xPosition, yPosition) { 17680 if (!this._relativeToEl) { 17681 throw new Error('mdPanel: addPanelPosition can only be used with ' + 17682 'relative positioning. Set relativeTo first.'); 17683 } 17684 17685 this._validateXPosition(xPosition); 17686 this._validateYPosition(yPosition); 17687 17688 this._positions.push({ 17689 x: xPosition, 17690 y: yPosition, 17691 }); 17692 return this; 17693}; 17694 17695 17696/** 17697 * Ensures that yPosition is a valid position name. Throw an exception if not. 17698 * @param {string} yPosition 17699 */ 17700MdPanelPosition.prototype._validateYPosition = function(yPosition) { 17701 // empty is ok 17702 if (yPosition == null) { 17703 return; 17704 } 17705 17706 var positionKeys = Object.keys(MdPanelPosition.yPosition); 17707 var positionValues = []; 17708 for (var key, i = 0; key = positionKeys[i]; i++) { 17709 var position = MdPanelPosition.yPosition[key]; 17710 positionValues.push(position); 17711 17712 if (position === yPosition) { 17713 return; 17714 } 17715 } 17716 17717 throw new Error('mdPanel: Panel y position only accepts the following ' + 17718 'values:\n' + positionValues.join(' | ')); 17719}; 17720 17721 17722/** 17723 * Ensures that xPosition is a valid position name. Throw an exception if not. 17724 * @param {string} xPosition 17725 */ 17726MdPanelPosition.prototype._validateXPosition = function(xPosition) { 17727 // empty is ok 17728 if (xPosition == null) { 17729 return; 17730 } 17731 17732 var positionKeys = Object.keys(MdPanelPosition.xPosition); 17733 var positionValues = []; 17734 for (var key, i = 0; key = positionKeys[i]; i++) { 17735 var position = MdPanelPosition.xPosition[key]; 17736 positionValues.push(position); 17737 if (position === xPosition) { 17738 return; 17739 } 17740 } 17741 17742 throw new Error('mdPanel: Panel x Position only accepts the following ' + 17743 'values:\n' + positionValues.join(' | ')); 17744}; 17745 17746 17747/** 17748 * Sets the value of the offset in the x-direction. This will add to any 17749 * previously set offsets. 17750 * @param {string|function(MdPanelPosition): string} offsetX 17751 * @returns {!MdPanelPosition} 17752 */ 17753MdPanelPosition.prototype.withOffsetX = function(offsetX) { 17754 this._translateX.push(offsetX); 17755 return this; 17756}; 17757 17758 17759/** 17760 * Sets the value of the offset in the y-direction. This will add to any 17761 * previously set offsets. 17762 * @param {string|function(MdPanelPosition): string} offsetY 17763 * @returns {!MdPanelPosition} 17764 */ 17765MdPanelPosition.prototype.withOffsetY = function(offsetY) { 17766 this._translateY.push(offsetY); 17767 return this; 17768}; 17769 17770 17771/** 17772 * Gets the value of `top` for the panel. 17773 * @returns {string} 17774 */ 17775MdPanelPosition.prototype.getTop = function() { 17776 return this._top; 17777}; 17778 17779 17780/** 17781 * Gets the value of `bottom` for the panel. 17782 * @returns {string} 17783 */ 17784MdPanelPosition.prototype.getBottom = function() { 17785 return this._bottom; 17786}; 17787 17788 17789/** 17790 * Gets the value of `left` for the panel. 17791 * @returns {string} 17792 */ 17793MdPanelPosition.prototype.getLeft = function() { 17794 return this._left; 17795}; 17796 17797 17798/** 17799 * Gets the value of `right` for the panel. 17800 * @returns {string} 17801 */ 17802MdPanelPosition.prototype.getRight = function() { 17803 return this._right; 17804}; 17805 17806 17807/** 17808 * Gets the value of `transform` for the panel. 17809 * @returns {string} 17810 */ 17811MdPanelPosition.prototype.getTransform = function() { 17812 var translateX = this._reduceTranslateValues('translateX', this._translateX); 17813 var translateY = this._reduceTranslateValues('translateY', this._translateY); 17814 17815 // It's important to trim the result, because the browser will ignore the set 17816 // operation if the string contains only whitespace. 17817 return (translateX + ' ' + translateY).trim(); 17818}; 17819 17820 17821/** 17822 * Sets the `transform` value for a panel element. 17823 * @param {!angular.JQLite} panelEl 17824 * @returns {!angular.JQLite} 17825 * @private 17826 */ 17827MdPanelPosition.prototype._setTransform = function(panelEl) { 17828 return panelEl.css(this._$mdConstant.CSS.TRANSFORM, this.getTransform()); 17829}; 17830 17831 17832/** 17833 * True if the panel is completely on-screen with this positioning; false 17834 * otherwise. 17835 * @param {!angular.JQLite} panelEl 17836 * @return {boolean} 17837 * @private 17838 */ 17839MdPanelPosition.prototype._isOnscreen = function(panelEl) { 17840 // this works because we always use fixed positioning for the panel, 17841 // which is relative to the viewport. 17842 var left = parseInt(this.getLeft()); 17843 var top = parseInt(this.getTop()); 17844 17845 if (this._translateX.length || this._translateY.length) { 17846 var prefixedTransform = this._$mdConstant.CSS.TRANSFORM; 17847 var offsets = getComputedTranslations(panelEl, prefixedTransform); 17848 left += offsets.x; 17849 top += offsets.y; 17850 } 17851 17852 var right = left + panelEl[0].offsetWidth; 17853 var bottom = top + panelEl[0].offsetHeight; 17854 17855 return (left >= 0) && 17856 (top >= 0) && 17857 (bottom <= this._$window.innerHeight) && 17858 (right <= this._$window.innerWidth); 17859}; 17860 17861 17862/** 17863 * Gets the first x/y position that can fit on-screen. 17864 * @returns {{x: string, y: string}} 17865 */ 17866MdPanelPosition.prototype.getActualPosition = function() { 17867 return this._actualPosition; 17868}; 17869 17870 17871/** 17872 * Reduces a list of translate values to a string that can be used within 17873 * transform. 17874 * @param {string} translateFn 17875 * @param {!Array<string>} values 17876 * @returns {string} 17877 * @private 17878 */ 17879MdPanelPosition.prototype._reduceTranslateValues = 17880 function(translateFn, values) { 17881 return values.map(function(translation) { 17882 // TODO(crisbeto): this should add the units after #9609 is merged. 17883 var translationValue = angular.isFunction(translation) ? 17884 translation(this) : translation; 17885 return translateFn + '(' + translationValue + ')'; 17886 }, this).join(' '); 17887 }; 17888 17889 17890/** 17891 * Sets the panel position based on the created panel element and best x/y 17892 * positioning. 17893 * @param {!angular.JQLite} panelEl 17894 * @private 17895 */ 17896MdPanelPosition.prototype._setPanelPosition = function(panelEl) { 17897 // Remove the "position adjusted" class in case it has been added before. 17898 panelEl.removeClass('_md-panel-position-adjusted'); 17899 17900 // Only calculate the position if necessary. 17901 if (this._absolute) { 17902 this._setTransform(panelEl); 17903 return; 17904 } 17905 17906 if (this._actualPosition) { 17907 this._calculatePanelPosition(panelEl, this._actualPosition); 17908 this._setTransform(panelEl); 17909 this._constrainToViewport(panelEl); 17910 return; 17911 } 17912 17913 for (var i = 0; i < this._positions.length; i++) { 17914 this._actualPosition = this._positions[i]; 17915 this._calculatePanelPosition(panelEl, this._actualPosition); 17916 this._setTransform(panelEl); 17917 17918 if (this._isOnscreen(panelEl)) { 17919 return; 17920 } 17921 } 17922 17923 this._constrainToViewport(panelEl); 17924}; 17925 17926 17927/** 17928 * Constrains a panel's position to the viewport. 17929 * @param {!angular.JQLite} panelEl 17930 * @private 17931 */ 17932MdPanelPosition.prototype._constrainToViewport = function(panelEl) { 17933 var margin = MdPanelPosition.viewportMargin; 17934 var initialTop = this._top; 17935 var initialLeft = this._left; 17936 17937 if (this.getTop()) { 17938 var top = parseInt(this.getTop()); 17939 var bottom = panelEl[0].offsetHeight + top; 17940 var viewportHeight = this._$window.innerHeight; 17941 17942 if (top < margin) { 17943 this._top = margin + 'px'; 17944 } else if (bottom > viewportHeight) { 17945 this._top = top - (bottom - viewportHeight + margin) + 'px'; 17946 } 17947 } 17948 17949 if (this.getLeft()) { 17950 var left = parseInt(this.getLeft()); 17951 var right = panelEl[0].offsetWidth + left; 17952 var viewportWidth = this._$window.innerWidth; 17953 17954 if (left < margin) { 17955 this._left = margin + 'px'; 17956 } else if (right > viewportWidth) { 17957 this._left = left - (right - viewportWidth + margin) + 'px'; 17958 } 17959 } 17960 17961 // Class that can be used to re-style the panel if it was repositioned. 17962 panelEl.toggleClass( 17963 '_md-panel-position-adjusted', 17964 this._top !== initialTop || this._left !== initialLeft 17965 ); 17966}; 17967 17968 17969/** 17970 * Switches between 'start' and 'end'. 17971 * @param {string} position Horizontal position of the panel 17972 * @returns {string} Reversed position 17973 * @private 17974 */ 17975MdPanelPosition.prototype._reverseXPosition = function(position) { 17976 if (position === MdPanelPosition.xPosition.CENTER) { 17977 return position; 17978 } 17979 17980 var start = 'start'; 17981 var end = 'end'; 17982 17983 return position.indexOf(start) > -1 ? position.replace(start, end) : position.replace(end, start); 17984}; 17985 17986 17987/** 17988 * Handles horizontal positioning in rtl or ltr environments. 17989 * @param {string} position Horizontal position of the panel 17990 * @returns {string} The correct position according the page direction 17991 * @private 17992 */ 17993MdPanelPosition.prototype._bidi = function(position) { 17994 return this._isRTL ? this._reverseXPosition(position) : position; 17995}; 17996 17997 17998/** 17999 * Calculates the panel position based on the created panel element and the 18000 * provided positioning. 18001 * @param {!angular.JQLite} panelEl 18002 * @param {!{x:string, y:string}} position 18003 * @private 18004 */ 18005MdPanelPosition.prototype._calculatePanelPosition = function(panelEl, position) { 18006 18007 var panelBounds = panelEl[0].getBoundingClientRect(); 18008 var panelWidth = panelBounds.width; 18009 var panelHeight = panelBounds.height; 18010 18011 var targetBounds = this._relativeToEl[0].getBoundingClientRect(); 18012 18013 var targetLeft = targetBounds.left; 18014 var targetRight = targetBounds.right; 18015 var targetWidth = targetBounds.width; 18016 18017 switch (this._bidi(position.x)) { 18018 case MdPanelPosition.xPosition.OFFSET_START: 18019 this._left = targetLeft - panelWidth + 'px'; 18020 break; 18021 case MdPanelPosition.xPosition.ALIGN_END: 18022 this._left = targetRight - panelWidth + 'px'; 18023 break; 18024 case MdPanelPosition.xPosition.CENTER: 18025 var left = targetLeft + (0.5 * targetWidth) - (0.5 * panelWidth); 18026 this._left = left + 'px'; 18027 break; 18028 case MdPanelPosition.xPosition.ALIGN_START: 18029 this._left = targetLeft + 'px'; 18030 break; 18031 case MdPanelPosition.xPosition.OFFSET_END: 18032 this._left = targetRight + 'px'; 18033 break; 18034 } 18035 18036 var targetTop = targetBounds.top; 18037 var targetBottom = targetBounds.bottom; 18038 var targetHeight = targetBounds.height; 18039 18040 switch (position.y) { 18041 case MdPanelPosition.yPosition.ABOVE: 18042 this._top = targetTop - panelHeight + 'px'; 18043 break; 18044 case MdPanelPosition.yPosition.ALIGN_BOTTOMS: 18045 this._top = targetBottom - panelHeight + 'px'; 18046 break; 18047 case MdPanelPosition.yPosition.CENTER: 18048 var top = targetTop + (0.5 * targetHeight) - (0.5 * panelHeight); 18049 this._top = top + 'px'; 18050 break; 18051 case MdPanelPosition.yPosition.ALIGN_TOPS: 18052 this._top = targetTop + 'px'; 18053 break; 18054 case MdPanelPosition.yPosition.BELOW: 18055 this._top = targetBottom + 'px'; 18056 break; 18057 } 18058}; 18059 18060 18061/***************************************************************************** 18062 * MdPanelAnimation * 18063 *****************************************************************************/ 18064 18065 18066/** 18067 * Animation configuration object. To use, create an MdPanelAnimation with the 18068 * desired properties, then pass the object as part of $mdPanel creation. 18069 * 18070 * Example: 18071 * 18072 * var panelAnimation = new MdPanelAnimation() 18073 * .openFrom(myButtonEl) 18074 * .closeTo('.my-button') 18075 * .withAnimation($mdPanel.animation.SCALE); 18076 * 18077 * $mdPanel.create({ 18078 * animation: panelAnimation 18079 * }); 18080 * 18081 * @param {!angular.$injector} $injector 18082 * @final @constructor 18083 */ 18084function MdPanelAnimation($injector) { 18085 /** @private @const {!angular.$mdUtil} */ 18086 this._$mdUtil = $injector.get('$mdUtil'); 18087 18088 /** 18089 * @private {{element: !angular.JQLite|undefined, bounds: !DOMRect}| 18090 * undefined} 18091 */ 18092 this._openFrom; 18093 18094 /** 18095 * @private {{element: !angular.JQLite|undefined, bounds: !DOMRect}| 18096 * undefined} 18097 */ 18098 this._closeTo; 18099 18100 /** @private {string|{open: string, close: string}} */ 18101 this._animationClass = ''; 18102 18103 /** @private {number} */ 18104 this._openDuration; 18105 18106 /** @private {number} */ 18107 this._closeDuration; 18108 18109 /** @private {number|{open: number, close: number}} */ 18110 this._rawDuration; 18111} 18112 18113 18114/** 18115 * Possible default animations. 18116 * @enum {string} 18117 */ 18118MdPanelAnimation.animation = { 18119 SLIDE: 'md-panel-animate-slide', 18120 SCALE: 'md-panel-animate-scale', 18121 FADE: 'md-panel-animate-fade' 18122}; 18123 18124 18125/** 18126 * Specifies where to start the open animation. `openFrom` accepts a 18127 * click event object, query selector, DOM element, or a Rect object that 18128 * is used to determine the bounds. When passed a click event, the location 18129 * of the click will be used as the position to start the animation. 18130 * @param {string|!Element|!Event|{top: number, left: number}} openFrom 18131 * @returns {!MdPanelAnimation} 18132 */ 18133MdPanelAnimation.prototype.openFrom = function(openFrom) { 18134 // Check if 'openFrom' is an Event. 18135 openFrom = openFrom.target ? openFrom.target : openFrom; 18136 18137 this._openFrom = this._getPanelAnimationTarget(openFrom); 18138 18139 if (!this._closeTo) { 18140 this._closeTo = this._openFrom; 18141 } 18142 return this; 18143}; 18144 18145 18146/** 18147 * Specifies where to animate the panel close. `closeTo` accepts a 18148 * query selector, DOM element, or a Rect object that is used to determine 18149 * the bounds. 18150 * @param {string|!Element|{top: number, left: number}} closeTo 18151 * @returns {!MdPanelAnimation} 18152 */ 18153MdPanelAnimation.prototype.closeTo = function(closeTo) { 18154 this._closeTo = this._getPanelAnimationTarget(closeTo); 18155 return this; 18156}; 18157 18158 18159/** 18160 * Specifies the duration of the animation in milliseconds. 18161 * @param {number|{open: number, close: number}} duration 18162 * @returns {!MdPanelAnimation} 18163 */ 18164MdPanelAnimation.prototype.duration = function(duration) { 18165 if (duration) { 18166 if (angular.isNumber(duration)) { 18167 this._openDuration = this._closeDuration = toSeconds(duration); 18168 } else if (angular.isObject(duration)) { 18169 this._openDuration = toSeconds(duration.open); 18170 this._closeDuration = toSeconds(duration.close); 18171 } 18172 } 18173 18174 // Save the original value so it can be passed to the backdrop. 18175 this._rawDuration = duration; 18176 18177 return this; 18178 18179 function toSeconds(value) { 18180 if (angular.isNumber(value)) return value / 1000; 18181 } 18182}; 18183 18184 18185/** 18186 * Returns the element and bounds for the animation target. 18187 * @param {string|!Element|{top: number, left: number}} location 18188 * @returns {{element: !angular.JQLite|undefined, bounds: !DOMRect}} 18189 * @private 18190 */ 18191MdPanelAnimation.prototype._getPanelAnimationTarget = function(location) { 18192 if (angular.isDefined(location.top) || angular.isDefined(location.left)) { 18193 return { 18194 element: undefined, 18195 bounds: { 18196 top: location.top || 0, 18197 left: location.left || 0 18198 } 18199 }; 18200 } else { 18201 return this._getBoundingClientRect(getElement(location)); 18202 } 18203}; 18204 18205 18206/** 18207 * Specifies the animation class. 18208 * 18209 * There are several default animations that can be used: 18210 * (MdPanelAnimation.animation) 18211 * SLIDE: The panel slides in and out from the specified 18212 * elements. 18213 * SCALE: The panel scales in and out. 18214 * FADE: The panel fades in and out. 18215 * 18216 * @param {string|{open: string, close: string}} cssClass 18217 * @returns {!MdPanelAnimation} 18218 */ 18219MdPanelAnimation.prototype.withAnimation = function(cssClass) { 18220 this._animationClass = cssClass; 18221 return this; 18222}; 18223 18224 18225/** 18226 * Animate the panel open. 18227 * @param {!angular.JQLite} panelEl 18228 * @returns {!angular.$q.Promise} A promise that is resolved when the open 18229 * animation is complete. 18230 */ 18231MdPanelAnimation.prototype.animateOpen = function(panelEl) { 18232 var animator = this._$mdUtil.dom.animator; 18233 18234 this._fixBounds(panelEl); 18235 var animationOptions = {}; 18236 18237 // Include the panel transformations when calculating the animations. 18238 var panelTransform = panelEl[0].style.transform || ''; 18239 18240 var openFrom = animator.toTransformCss(panelTransform); 18241 var openTo = animator.toTransformCss(panelTransform); 18242 18243 switch (this._animationClass) { 18244 case MdPanelAnimation.animation.SLIDE: 18245 // Slide should start with opacity: 1. 18246 panelEl.css('opacity', '1'); 18247 18248 animationOptions = { 18249 transitionInClass: '_md-panel-animate-enter' 18250 }; 18251 18252 var openSlide = animator.calculateSlideToOrigin( 18253 panelEl, this._openFrom) || ''; 18254 openFrom = animator.toTransformCss(openSlide + ' ' + panelTransform); 18255 break; 18256 18257 case MdPanelAnimation.animation.SCALE: 18258 animationOptions = { 18259 transitionInClass: '_md-panel-animate-enter' 18260 }; 18261 18262 var openScale = animator.calculateZoomToOrigin( 18263 panelEl, this._openFrom) || ''; 18264 openFrom = animator.toTransformCss(openScale + ' ' + panelTransform); 18265 break; 18266 18267 case MdPanelAnimation.animation.FADE: 18268 animationOptions = { 18269 transitionInClass: '_md-panel-animate-enter' 18270 }; 18271 break; 18272 18273 default: 18274 if (angular.isString(this._animationClass)) { 18275 animationOptions = { 18276 transitionInClass: this._animationClass 18277 }; 18278 } else { 18279 animationOptions = { 18280 transitionInClass: this._animationClass['open'], 18281 transitionOutClass: this._animationClass['close'], 18282 }; 18283 } 18284 } 18285 18286 animationOptions.duration = this._openDuration; 18287 18288 return animator 18289 .translate3d(panelEl, openFrom, openTo, animationOptions); 18290}; 18291 18292 18293/** 18294 * Animate the panel close. 18295 * @param {!angular.JQLite} panelEl 18296 * @returns {!angular.$q.Promise} A promise that resolves when the close 18297 * animation is complete. 18298 */ 18299MdPanelAnimation.prototype.animateClose = function(panelEl) { 18300 var animator = this._$mdUtil.dom.animator; 18301 var reverseAnimationOptions = {}; 18302 18303 // Include the panel transformations when calculating the animations. 18304 var panelTransform = panelEl[0].style.transform || ''; 18305 18306 var closeFrom = animator.toTransformCss(panelTransform); 18307 var closeTo = animator.toTransformCss(panelTransform); 18308 18309 switch (this._animationClass) { 18310 case MdPanelAnimation.animation.SLIDE: 18311 // Slide should start with opacity: 1. 18312 panelEl.css('opacity', '1'); 18313 reverseAnimationOptions = { 18314 transitionInClass: '_md-panel-animate-leave' 18315 }; 18316 18317 var closeSlide = animator.calculateSlideToOrigin( 18318 panelEl, this._closeTo) || ''; 18319 closeTo = animator.toTransformCss(closeSlide + ' ' + panelTransform); 18320 break; 18321 18322 case MdPanelAnimation.animation.SCALE: 18323 reverseAnimationOptions = { 18324 transitionInClass: '_md-panel-animate-scale-out _md-panel-animate-leave' 18325 }; 18326 18327 var closeScale = animator.calculateZoomToOrigin( 18328 panelEl, this._closeTo) || ''; 18329 closeTo = animator.toTransformCss(closeScale + ' ' + panelTransform); 18330 break; 18331 18332 case MdPanelAnimation.animation.FADE: 18333 reverseAnimationOptions = { 18334 transitionInClass: '_md-panel-animate-fade-out _md-panel-animate-leave' 18335 }; 18336 break; 18337 18338 default: 18339 if (angular.isString(this._animationClass)) { 18340 reverseAnimationOptions = { 18341 transitionOutClass: this._animationClass 18342 }; 18343 } else { 18344 reverseAnimationOptions = { 18345 transitionInClass: this._animationClass['close'], 18346 transitionOutClass: this._animationClass['open'] 18347 }; 18348 } 18349 } 18350 18351 reverseAnimationOptions.duration = this._closeDuration; 18352 18353 return animator 18354 .translate3d(panelEl, closeFrom, closeTo, reverseAnimationOptions); 18355}; 18356 18357 18358/** 18359 * Set the height and width to match the panel if not provided. 18360 * @param {!angular.JQLite} panelEl 18361 * @private 18362 */ 18363MdPanelAnimation.prototype._fixBounds = function(panelEl) { 18364 var panelWidth = panelEl[0].offsetWidth; 18365 var panelHeight = panelEl[0].offsetHeight; 18366 18367 if (this._openFrom && this._openFrom.bounds.height == null) { 18368 this._openFrom.bounds.height = panelHeight; 18369 } 18370 if (this._openFrom && this._openFrom.bounds.width == null) { 18371 this._openFrom.bounds.width = panelWidth; 18372 } 18373 if (this._closeTo && this._closeTo.bounds.height == null) { 18374 this._closeTo.bounds.height = panelHeight; 18375 } 18376 if (this._closeTo && this._closeTo.bounds.width == null) { 18377 this._closeTo.bounds.width = panelWidth; 18378 } 18379}; 18380 18381 18382/** 18383 * Identify the bounding RECT for the target element. 18384 * @param {!angular.JQLite} element 18385 * @returns {{element: !angular.JQLite|undefined, bounds: !DOMRect}} 18386 * @private 18387 */ 18388MdPanelAnimation.prototype._getBoundingClientRect = function(element) { 18389 if (element instanceof angular.element) { 18390 return { 18391 element: element, 18392 bounds: element[0].getBoundingClientRect() 18393 }; 18394 } 18395}; 18396 18397 18398/***************************************************************************** 18399 * Util Methods * 18400 *****************************************************************************/ 18401 18402 18403/** 18404 * Returns the angular element associated with a css selector or element. 18405 * @param el {string|!angular.JQLite|!Element} 18406 * @returns {!angular.JQLite} 18407 */ 18408function getElement(el) { 18409 var queryResult = angular.isString(el) ? 18410 document.querySelector(el) : el; 18411 return angular.element(queryResult); 18412} 18413 18414 18415/** 18416 * Gets the computed values for an element's translateX and translateY in px. 18417 * @param {!angular.JQLite|!Element} el 18418 * @param {string} property 18419 * @return {{x: number, y: number}} 18420 */ 18421function getComputedTranslations(el, property) { 18422 // The transform being returned by `getComputedStyle` is in the format: 18423 // `matrix(a, b, c, d, translateX, translateY)` if defined and `none` 18424 // if the element doesn't have a transform. 18425 var transform = getComputedStyle(el[0] || el)[property]; 18426 var openIndex = transform.indexOf('('); 18427 var closeIndex = transform.lastIndexOf(')'); 18428 var output = { x: 0, y: 0 }; 18429 18430 if (openIndex > -1 && closeIndex > -1) { 18431 var parsedValues = transform 18432 .substring(openIndex + 1, closeIndex) 18433 .split(', ') 18434 .slice(-2); 18435 18436 output.x = parseInt(parsedValues[0]); 18437 output.y = parseInt(parsedValues[1]); 18438 } 18439 18440 return output; 18441} 18442 18443})(); 18444(function(){ 18445"use strict"; 18446 18447/** 18448 * @ngdoc module 18449 * @name material.components.progressCircular 18450 * @description Module for a circular progressbar 18451 */ 18452 18453angular.module('material.components.progressCircular', ['material.core']); 18454 18455})(); 18456(function(){ 18457"use strict"; 18458 18459/** 18460 * @ngdoc module 18461 * @name material.components.progressLinear 18462 * @description Linear Progress module! 18463 */ 18464MdProgressLinearDirective.$inject = ["$mdTheming", "$mdUtil", "$log"]; 18465angular.module('material.components.progressLinear', [ 18466 'material.core' 18467]) 18468 .directive('mdProgressLinear', MdProgressLinearDirective); 18469 18470/** 18471 * @ngdoc directive 18472 * @name mdProgressLinear 18473 * @module material.components.progressLinear 18474 * @restrict E 18475 * 18476 * @description 18477 * The linear progress directive is used to make loading content 18478 * in your app as delightful and painless as possible by minimizing 18479 * the amount of visual change a user sees before they can view 18480 * and interact with content. 18481 * 18482 * Each operation should only be represented by one activity indicator 18483 * For example: one refresh operation should not display both a 18484 * refresh bar and an activity circle. 18485 * 18486 * For operations where the percentage of the operation completed 18487 * can be determined, use a determinate indicator. They give users 18488 * a quick sense of how long an operation will take. 18489 * 18490 * For operations where the user is asked to wait a moment while 18491 * something finishes up, and it’s not necessary to expose what's 18492 * happening behind the scenes and how long it will take, use an 18493 * indeterminate indicator. 18494 * 18495 * @param {string} md-mode Select from one of four modes: determinate, indeterminate, buffer or query. 18496 *
18497 * Note: if the `md-mode` value is set as undefined or specified as 1 of the four (4) valid modes, then `indeterminate` 18498 * will be auto-applied as the mode. 18499 * 18500 * Note: if not configured, the `md-mode="indeterminate"` will be auto injected as an attribute. If `value=""` is also specified, however, 18501 * then `md-mode="determinate"` would be auto-injected instead. 18502 * @param {number=} value In determinate and buffer modes, this number represents the percentage of the primary progress bar. Default: 0 18503 * @param {number=} md-buffer-value In the buffer mode, this number represents the percentage of the secondary progress bar. Default: 0 18504 * @param {boolean=} ng-disabled Determines whether to disable the progress element. 18505 * 18506 * @usage 18507 * <hljs lang="html"> 18508 * <md-progress-linear md-mode="determinate" value="..."></md-progress-linear> 18509 * 18510 * <md-progress-linear md-mode="determinate" ng-value="..."></md-progress-linear> 18511 * 18512 * <md-progress-linear md-mode="indeterminate"></md-progress-linear> 18513 * 18514 * <md-progress-linear md-mode="buffer" value="..." md-buffer-value="..."></md-progress-linear> 18515 * 18516 * <md-progress-linear md-mode="query"></md-progress-linear> 18517 * </hljs> 18518 */ 18519function MdProgressLinearDirective($mdTheming, $mdUtil, $log) { 18520 var MODE_DETERMINATE = "determinate"; 18521 var MODE_INDETERMINATE = "indeterminate"; 18522 var MODE_BUFFER = "buffer"; 18523 var MODE_QUERY = "query"; 18524 var DISABLED_CLASS = "_md-progress-linear-disabled"; 18525 18526 return { 18527 restrict: 'E', 18528 template: '<div class="md-container">' + 18529 '<div class="md-dashed"></div>' + 18530 '<div class="md-bar md-bar1"></div>' + 18531 '<div class="md-bar md-bar2"></div>' + 18532 '</div>', 18533 compile: compile 18534 }; 18535 18536 function compile(tElement, tAttrs, transclude) { 18537 tElement.attr('aria-valuemin', 0); 18538 tElement.attr('aria-valuemax', 100); 18539 tElement.attr('role', 'progressbar'); 18540 18541 return postLink; 18542 } 18543 function postLink(scope, element, attr) { 18544 $mdTheming(element); 18545 18546 var lastMode; 18547 var isDisabled = attr.hasOwnProperty('disabled'); 18548 var toVendorCSS = $mdUtil.dom.animator.toCss; 18549 var bar1 = angular.element(element[0].querySelector('.md-bar1')); 18550 var bar2 = angular.element(element[0].querySelector('.md-bar2')); 18551 var container = angular.element(element[0].querySelector('.md-container')); 18552 18553 element 18554 .attr('md-mode', mode()) 18555 .toggleClass(DISABLED_CLASS, isDisabled); 18556 18557 validateMode(); 18558 watchAttributes(); 18559 18560 /** 18561 * Watch the value, md-buffer-value, and md-mode attributes 18562 */ 18563 function watchAttributes() { 18564 attr.$observe('value', function(value) { 18565 var percentValue = clamp(value); 18566 element.attr('aria-valuenow', percentValue); 18567 18568 if (mode() != MODE_QUERY) animateIndicator(bar2, percentValue); 18569 }); 18570 18571 attr.$observe('mdBufferValue', function(value) { 18572 animateIndicator(bar1, clamp(value)); 18573 }); 18574 18575 attr.$observe('disabled', function(value) { 18576 if (value === true || value === false) { 18577 isDisabled = !!value; 18578 } else { 18579 isDisabled = angular.isDefined(value); 18580 } 18581 18582 element.toggleClass(DISABLED_CLASS, isDisabled); 18583 container.toggleClass(lastMode, !isDisabled); 18584 }); 18585 18586 attr.$observe('mdMode', function(mode) { 18587 if (lastMode) container.removeClass( lastMode ); 18588 18589 switch( mode ) { 18590 case MODE_QUERY: 18591 case MODE_BUFFER: 18592 case MODE_DETERMINATE: 18593 case MODE_INDETERMINATE: 18594 container.addClass( lastMode = "md-mode-" + mode ); 18595 break; 18596 default: 18597 container.addClass( lastMode = "md-mode-" + MODE_INDETERMINATE ); 18598 break; 18599 } 18600 }); 18601 } 18602 18603 /** 18604 * Auto-defaults the mode to either `determinate` or `indeterminate` mode; if not specified 18605 */ 18606 function validateMode() { 18607 if ( angular.isUndefined(attr.mdMode) ) { 18608 var hasValue = angular.isDefined(attr.value); 18609 var mode = hasValue ? MODE_DETERMINATE : MODE_INDETERMINATE; 18610 var info = "Auto-adding the missing md-mode='{0}' to the ProgressLinear element"; 18611 18612 //$log.debug( $mdUtil.supplant(info, [mode]) ); 18613 18614 element.attr("md-mode", mode); 18615 attr.mdMode = mode; 18616 } 18617 } 18618 18619 /** 18620 * Is the md-mode a valid option? 18621 */ 18622 function mode() { 18623 var value = (attr.mdMode || "").trim(); 18624 if ( value ) { 18625 switch(value) { 18626 case MODE_DETERMINATE: 18627 case MODE_INDETERMINATE: 18628 case MODE_BUFFER: 18629 case MODE_QUERY: 18630 break; 18631 default: 18632 value = MODE_INDETERMINATE; 18633 break; 18634 } 18635 } 18636 return value; 18637 } 18638 18639 /** 18640 * Manually set CSS to animate the Determinate indicator based on the specified 18641 * percentage value (0-100). 18642 */ 18643 function animateIndicator(target, value) { 18644 if ( isDisabled || !mode() ) return; 18645 18646 var to = $mdUtil.supplant("translateX({0}%) scale({1},1)", [ (value-100)/2, value/100 ]); 18647 var styles = toVendorCSS({ transform : to }); 18648 angular.element(target).css( styles ); 18649 } 18650 } 18651 18652 /** 18653 * Clamps the value to be between 0 and 100. 18654 * @param {number} value The value to clamp. 18655 * @returns {number} 18656 */ 18657 function clamp(value) { 18658 return Math.max(0, Math.min(value || 0, 100)); 18659 } 18660} 18661 18662 18663})(); 18664(function(){ 18665"use strict"; 18666 18667/** 18668 * @ngdoc module 18669 * @name material.components.radioButton 18670 * @description radioButton module! 18671 */ 18672mdRadioGroupDirective.$inject = ["$mdUtil", "$mdConstant", "$mdTheming", "$timeout"]; 18673mdRadioButtonDirective.$inject = ["$mdAria", "$mdUtil", "$mdTheming"]; 18674angular.module('material.components.radioButton', [ 18675 'material.core' 18676]) 18677 .directive('mdRadioGroup', mdRadioGroupDirective) 18678 .directive('mdRadioButton', mdRadioButtonDirective); 18679 18680/** 18681 * @ngdoc directive 18682 * @module material.components.radioButton 18683 * @name mdRadioGroup 18684 * 18685 * @restrict E 18686 * 18687 * @description 18688 * The `<md-radio-group>` directive identifies a grouping 18689 * container for the 1..n grouped radio buttons; specified using nested 18690 * `<md-radio-button>
18690` tags. 18691 * 18692 * As per the [material design spec](http://www.google.com/design/spec/style/color.html#color-ui-color-application) 18693 * the radio button is in the accent color by default. The primary color palette may be used with 18694 * the `md-primary` class. 18695 * 18696 * Note: `<md-radio-group>` and `<md-radio-button>` handle tabindex differently 18697 * than the native `<input type='radio'>` controls. Whereas the native controls 18698 * force the user to tab through all the radio buttons, `<md-radio-group>` 18699 * is focusable, and by default the `<md-radio-button>`s are not. 18700 * 18701 * @param {string} ng-model Assignable angular expression to data-bind to. 18702 * @param {boolean=} md-no-ink Use of attribute indicates flag to disable ink ripple effects. 18703 * @param {string} ngModel Assignable angular expression to data-bind to. 18704 * @param {string=} ngChange AngularJS expression to be executed when input changes due to user 18705 * interaction with the input element. 18706 * 18707 * @usage 18708 * <hljs lang="html"> 18709 * <md-radio-group ng-model="selected"> 18710 * 18711 * <md-radio-button 18712 * ng-repeat="d in colorOptions" 18713 * ng-value="d.value" aria-label="{{ d.label }}"> 18714 * 18715 * {{ d.label }} 18716 * 18717 * </md-radio-button>
18717 18718 * 18719 * </md-radio-group> 18720 * </hljs> 18721 * 18722 */ 18723function mdRadioGroupDirective($mdUtil, $mdConstant, $mdTheming, $timeout) { 18724 RadioGroupController.prototype = createRadioGroupControllerProto(); 18725 18726 return { 18727 restrict: 'E', 18728 controller: ['$element', RadioGroupController], 18729 require: ['mdRadioGroup', '?ngModel'], 18730 link: { pre: linkRadioGroup } 18731 }; 18732 18733 function linkRadioGroup(scope, element, attr, ctrls) { 18734 element.addClass('_md'); // private md component indicator for styling 18735 $mdTheming(element); 18736 18737 var rgCtrl = ctrls[0]; 18738 var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel(); 18739 18740 rgCtrl.init(ngModelCtrl); 18741 18742 scope.mouseActive = false; 18743 18744 element 18745 .attr({ 18746 'role': 'radiogroup', 18747 'tabIndex': element.attr('tabindex') || '0' 18748 }) 18749 .on('keydown', keydownListener) 18750 .on('mousedown', function(event) { 18751 scope.mouseActive = true; 18752 $timeout(function() { 18753 scope.mouseActive = false; 18754 }, 100); 18755 }) 18756 .on('focus', function() { 18757 if(scope.mouseActive === false) { 18758 rgCtrl.$element.addClass('md-focused'); 18759 } 18760 }) 18761 .on('blur', function() { 18762 rgCtrl.$element.removeClass('md-focused'); 18763 }); 18764 18765 /** 18766 * 18767 */ 18768 function setFocus() { 18769 if (!element.hasClass('md-focused')) { element.addClass('md-focused'); } 18770 } 18771 18772 /** 18773 * 18774 */ 18775 function keydownListener(ev) { 18776 var keyCode = ev.which || ev.keyCode; 18777 18778 // Only listen to events that we originated ourselves 18779 // so that we don't trigger on things like arrow keys in 18780 // inputs. 18781 18782 if (keyCode != $mdConstant.KEY_CODE.ENTER && 18783 ev.currentTarget != ev.target) { 18784 return; 18785 } 18786 18787 switch (keyCode) { 18788 case $mdConstant.KEY_CODE.LEFT_ARROW: 18789 case $mdConstant.KEY_CODE.UP_ARROW: 18790 ev.preventDefault(); 18791 rgCtrl.selectPrevious(); 18792 setFocus(); 18793 break; 18794 18795 case $mdConstant.KEY_CODE.RIGHT_ARROW: 18796 case $mdConstant.KEY_CODE.DOWN_ARROW: 18797 ev.preventDefault(); 18798 rgCtrl.selectNext(); 18799 setFocus(); 18800 break; 18801 18802 case $mdConstant.KEY_CODE.ENTER: 18803 var form = angular.element($mdUtil.getClosest(element[0], 'form')); 18804 if (form.length > 0) { 18805 form.triggerHandler('submit'); 18806 } 18807 break; 18808 } 18809 18810 } 18811 } 18812 18813 function RadioGroupController($element) { 18814 this._radioButtonRenderFns = []; 18815 this.$element = $element; 18816 } 18817 18818 function createRadioGroupControllerProto() { 18819 return { 18820 init: function(ngModelCtrl) { 18821 this._ngModelCtrl = ngModelCtrl; 18822 this._ngModelCtrl.$render = angular.bind(this, this.render); 18823 }, 18824 add: function(rbRender) { 18825 this._radioButtonRenderFns.push(rbRender); 18826 }, 18827 remove: function(rbRender) { 18828 var index = this._radioButtonRenderFns.indexOf(rbRender); 18829 if (index !== -1) { 18830 this._radioButtonRenderFns.splice(index, 1); 18831 } 18832 }, 18833 render: function() { 18834 this._radioButtonRenderFns.forEach(function(rbRender) { 18835 rbRender(); 18836 }); 18837 }, 18838 setViewValue: function(value, eventType) { 18839 this._ngModelCtrl.$setViewValue(value, eventType); 18840 // update the other radio buttons as well 18841 this.render(); 18842 }, 18843 getViewValue: function() { 18844 return this._ngModelCtrl.$viewValue; 18845 }, 18846 selectNext: function() { 18847 return changeSelectedButton(this.$element, 1); 18848 }, 18849 selectPrevious: function() { 18850 return changeSelectedButton(this.$element, -1); 18851 }, 18852 setActiveDescendant: function (radioId) { 18853 this.$element.attr('aria-activedescendant', radioId); 18854 }, 18855 isDisabled: function() { 18856 return this.$element[0].hasAttribute('disabled'); 18857 } 18858 }; 18859 } 18860 /** 18861 * Change the radio group's selected button by a given increment. 18862 * If no button is selected, select the first button. 18863 */ 18864 function changeSelectedButton(parent, increment) { 18865 // Coerce all child radio buttons into an array, then wrap then in an iterator 18866 var buttons = $mdUtil.iterator(parent[0].querySelectorAll('md-radio-button'), true); 18867 18868 if (buttons.count()) {
18869 var validate = function (button) { 18870 // If disabled, then NOT valid 18871 return !angular.element(button).attr("disabled"); 18872 }; 18873 18874 var selected = parent[0].querySelector('md-radio-button.md-checked'); 18875 var target = buttons[increment < 0 ? 'previous' : 'next'](selected, validate) || buttons.first(); 18876 18877 // Activate radioButton's click listener (triggerHandler won't create a real click event) 18878 angular.element(target).triggerHandler('click'); 18879 } 18880 } 18881 18882} 18883 18884/** 18885 * @ngdoc directive 18886 * @module material.components.radioButton 18887 * @name mdRadioButton 18888 * 18889 * @restrict E 18890 * 18891 * @description 18892 * The `<md-radio-button>`directive is the child directive required to be used within `<md-radio-group>` elements. 18893 * 18894 * While similar to the `<input type="radio" ng-model="" value="">` directive, 18895 * the `<md-radio-button>` directive provides ink effects, ARIA support, and 18896 * supports use within named radio groups. 18897 * 18898 * @param {string} ngValue AngularJS expression which sets the value to which the expression should 18899 * be set when selected. 18900 * @param {string} value The value to which the expression should be set when selected. 18901 * @param {string=} name Property name of the form under which the control is published. 18902 * @param {string=} aria-label Adds label to radio button for accessibility. 18903 * Defaults to radio button's text. If no text content is available, a warning will be logged. 18904 * 18905 * @usage 18906 * <hljs lang="html"> 18907 * 18908 * <md-radio-button value="1" aria-label="Label 1"> 18909 * Label 1 18910 * </md-radio-button>
vendor: 4,115 bytes, lines 18910-19061
18910 18911 * 18912 * <md-radio-button ng-value="specialValue" aria-label="Green"> 18913 * Green 18914 * </md-radio-button> 18915 * 18916 * </hljs> 18917 * 18918 */ 18919function mdRadioButtonDirective($mdAria, $mdUtil, $mdTheming) { 18920 18921 var CHECKED_CSS = 'md-checked'; 18922 18923 return { 18924 restrict: 'E', 18925 require: '^mdRadioGroup', 18926 transclude: true, 18927 template: '<div class="md-container" md-ink-ripple md-ink-ripple-checkbox>' + 18928 '<div class="md-off"></div>' + 18929 '<div class="md-on"></div>' + 18930 '</div>' + 18931 '<div ng-transclude class="md-label"></div>', 18932 link: link 18933 }; 18934 18935 function link(scope, element, attr, rgCtrl) { 18936 var lastChecked; 18937 18938 $mdTheming(element); 18939 configureAria(element, scope); 18940 18941 // ngAria overwrites the aria-checked inside a $watch for ngValue. 18942 // We should defer the initialization until all the watches have fired. 18943 // This can also be fixed by removing the `lastChecked` check, but that'll 18944 // cause more DOM manipulation on each digest. 18945 if (attr.ngValue) { 18946 $mdUtil.nextTick(initialize, false); 18947 } else { 18948 initialize(); 18949 } 18950 18951 /** 18952 * Initializes the component. 18953 */ 18954 function initialize() { 18955 if (!rgCtrl) { 18956 throw 'RadioButton: No RadioGroupController could be found.'; 18957 } 18958 18959 rgCtrl.add(render); 18960 attr.$observe('value', render); 18961 18962 element 18963 .on('click', listener) 18964 .on('$destroy', function() { 18965 rgCtrl.remove(render); 18966 }); 18967 } 18968 18969 /** 18970 * On click functionality. 18971 */ 18972 function listener(ev) { 18973 if (element[0].hasAttribute('disabled') || rgCtrl.isDisabled()) return; 18974 18975 scope.$apply(function() { 18976 rgCtrl.setViewValue(attr.value, ev && ev.type); 18977 }); 18978 } 18979 18980 /** 18981 * Add or remove the `.md-checked` class from the RadioButton (and conditionally its parent). 18982 * Update the `aria-activedescendant` attribute. 18983 */ 18984 function render() { 18985 var checked = rgCtrl.getViewValue() == attr.value; 18986 18987 if (checked === lastChecked) return; 18988 18989 if (element[0].parentNode.nodeName.toLowerCase() !== 'md-radio-group') { 18990 // If the radioButton is inside a div, then add class so highlighting will work 18991 element.parent().toggleClass(CHECKED_CSS, checked); 18992 } 18993 18994 if (checked) { 18995 rgCtrl.setActiveDescendant(element.attr('id')); 18996 } 18997 18998 lastChecked = checked; 18999 19000 element 19001 .attr('aria-checked', checked) 19002 .toggleClass(CHECKED_CSS, checked); 19003 } 19004 19005 /** 19006 * Inject ARIA-specific attributes appropriate for each radio button 19007 */ 19008 function configureAria(element, scope){ 19009 element.attr({ 19010 id: attr.id || 'radio_' + $mdUtil.nextUid(), 19011 role: 'radio', 19012 'aria-checked': 'false' 19013 }); 19014 19015 $mdAria.expectWithText(element, 'aria-label'); 19016 } 19017 } 19018} 19019 19020})(); 19021(function(){ 19022"use strict"; 19023 19024/** 19025 * @ngdoc module 19026 * @name material.components.select 19027 */ 19028 19029/*************************************************** 19030 19031 ### TODO - POST RC1 ### 19032 - [ ] Abstract placement logic in $mdSelect service to $mdMenu service 19033 19034 ***************************************************/ 19035 19036SelectDirective.$inject = ["$mdSelect", "$mdUtil", "$mdConstant", "$mdTheming", "$mdAria", "$parse", "$sce", "$injector"]; 19037SelectMenuDirective.$inject = ["$parse", "$mdUtil", "$mdConstant", "$mdTheming"]; 19038OptionDirective.$inject = ["$mdButtonInkRipple", "$mdUtil", "$mdTheming"]; 19039SelectProvider.$inject = ["$$interimElementProvider"]; 19040var SELECT_EDGE_MARGIN = 8; 19041var selectNextId = 0; 19042var CHECKBOX_SELECTION_INDICATOR = 19043 angular.element('<div class="md-container"><div class="md-icon"></div></div>'); 19044 19045angular.module('material.components.select', [ 19046 'material.core', 19047 'material.components.backdrop' 19048 ]) 19049 .directive('mdSelect', SelectDirective) 19050 .directive('mdSelectMenu', SelectMenuDirective) 19051 .directive('mdOption', OptionDirective) 19052 .directive('mdOptgroup', OptgroupDirective) 19053 .directive('mdSelectHeader', SelectHeaderDirective) 19054 .provider('$mdSelect', SelectProvider); 19055 19056/** 19057 * @ngdoc directive 19058 * @name mdSelect 19059 * @restrict E 19060 * @module material.components.select 19061 *
19062 * @description Displays a select box, bound to an ng-model. 19063 * 19064 * When the select is required and uses a floating label, then the label will automatically contain 19065 * an asterisk (`*`). This behavior can be disabled by using the `md-no-asterisk` attribute. 19066 * 19067 * By default, the select will display with an underline to match other form elements. This can be 19068 * disabled by applying the `md-no-underline` CSS class. 19069 * 19070 * ### Option Params 19071 * 19072 * When applied, `md-option-empty` will mark the option as "empty" allowing the option to clear the 19073 * select and put it back in it's default state. You may supply this attribute on any option you 19074 * wish, however, it is automatically applied to an option whose `value` or `ng-value` are not 19075 * defined. 19076 * 19077 * **Automatically Applied** 19078 * 19079 * - `<md-option>` 19080 * - `<md-option value>` 19081 * - `<md-option value="">` 19082 * - `<md-option ng-value>` 19083 * - `<md-option ng-value="">` 19084 * 19085 * **NOT Automatically Applied** 19086 * 19087 * - `<md-option ng-value="1">` 19088 * - `<md-option ng-value="''">` 19089 * - `<md-option ng-value="undefined">` 19090 * - `<md-option value="undefined">` (this evaluates to the string `"undefined"`) 19091 * - <code ng-non-bindable><md-option ng-value="{{someValueThatMightBeUndefined}}"></code> 19092 * 19093 * **Note:** A value of `undefined` ***is considered a valid value*** (and does not auto-apply this 19094 * attribute) since you may wish this to be your "Not Available" or "None" option. 19095 * 19096 * **Note:** Using the `value` attribute (as opposed to `ng-value`) always evaluates to a string, so 19097 * `value="null"` will require the test `ng-if="myValue != 'null'"` rather than `ng-if="!myValue"`. 19098 * 19099 * @param {expression} ng-model The model! 19100 * @param {boolean=} multiple When set to true, allows for more than one option to be selected. The model is an array with the selected choices. 19101 * @param {expression=} md-on-close Expression to be evaluated when the select is closed. 19102 * @param {expression=} md-on-open Expression to be evaluated when opening the select. 19103 * Will hide the select options and show a spinner until the evaluated promise resolves. 19104 * @param {expression=} md-selected-text Expression to be evaluated that will return a string 19105 * to be displayed as a placeholder in the select input box when it is closed. The value 19106 * will be treated as *text* (not html). 19107 * @param {expression=} md-selected-html Expression to be evaluated that will return a string 19108 * to be displayed as a placeholder in the select input box when it is closed. The value 19109 * will be treated as *html*. The value must either be explicitly marked as trustedHtml or 19110 * the ngSanitize module must be loaded. 19111 * @param {string=} placeholder Placeholder hint text. 19112 * @param md-no-asterisk {boolean=} When set to true, an asterisk will not be appended to the 19113 * floating label. **Note:** This attribute is only evaluated once; it is not watched. 19114 * @param {string=} aria-label Optional label for accessibility. Only necessary if no placeholder or 19115 * explicit label is present. 19116 * @param {string=} md-container-class Class list to get applied to the `.md-select-menu-container` 19117 * element (for custom styling). 19118 * 19119 * @usage 19120 * With a placeholder (label and aria-label are added dynamically) 19121 * <hljs lang="html"> 19122 * <md-input-container> 19123 * <md-select 19124 * ng-model="someModel" 19125 * placeholder="Select a state"> 19126 * <md-option ng-value="opt" ng-repeat="opt in neighborhoods2">{{ opt }}</md-option> 19127 * </md-select> 19128 * </md-input-container> 19129 * </hljs> 19130 * 19131 * With an explicit label 19132 * <hljs lang="html"> 19133 * <md-input-container> 19134 * <label>State</label> 19135 * <md-select 19136 * ng-model="someModel"> 19137 * <md-option ng-value="opt" ng-repeat="opt in neighborhoods2">{{ opt }}</md-option> 19138 * </md-select> 19139 * </md-input-container> 19140 * </hljs> 19141 * 19142 * With a select-header 19143 * 19144 * When a developer needs to put more than just a text label in the 19145 * md-select-menu, they should use the md-select-header. 19146 * The user can put custom HTML inside of the header and style it to their liking. 19147 * One common use case of this would be a sticky search bar. 19148 * 19149 * When using the md-select-header the labels that would previously be added to the 19150 * OptGroupDirective are ignored. 19151 * 19152 * <hljs lang="html"> 19153 * <md-input-container> 19154 * <md-select ng-model="someModel"> 19155 * <md-select-header>
19156 * <span> Neighborhoods - </span> 19157 * </md-select-header> 19158 * <md-option ng-value="opt" ng-repeat="opt in neighborhoods2">{{ opt }}</md-option> 19159 * </md-select> 19160 * </md-input-container> 19161 * </hljs> 19162 * 19163 * ## Selects and object equality 19164 * When using a `md-select` to pick from a list of objects, it is important to realize how javascript handles 19165 * equality. Consider the following example: 19166 * <hljs lang="js"> 19167 * angular.controller('MyCtrl', function($scope) { 19168 * $scope.users = [ 19169 * { id: 1, name: 'Bob' }, 19170 * { id: 2, name: 'Alice' }, 19171 * { id: 3, name: 'Steve' } 19172 * ]; 19173 * $scope.selectedUser = { id: 1, name: 'Bob' }; 19174 * }); 19175 * </hljs> 19176 * <hljs lang="html"> 19177 * <div ng-controller="MyCtrl"> 19178 * <md-select ng-model="selectedUser"> 19179 * <md-option ng-value="user" ng-repeat="user in users">{{ user.name }}</md-option> 19180 * </md-select> 19181 * </div> 19182 * </hljs> 19183 * 19184 * At first one might expect that the select should be populated with "Bob" as the selected user. However, 19185 * this is not true. To determine whether something is selected, 19186 * `ngModelController` is looking at whether `$scope.selectedUser == (any user in $scope.users);`; 19187 * 19188 * Javascript's `==` operator does not check for deep equality (ie. that all properties 19189 * on the object are the same), but instead whether the objects are *the same object in memory*. 19190 * In this case, we have two instances of identical objects, but they exist in memory as unique 19191 * entities. Because of this, the select will have no value populated for a selected user. 19192 * 19193 * To get around this, `ngModelController` provides a `track by` option that allows us to specify a different 19194 * expression which will be used for the equality operator. As such, we can update our `html` to 19195 * make use of this by specifying the `ng-model-options="{trackBy: '$value.id'}"` on the `md-select` 19196 * element. This converts our equality expression to be 19197 * `$scope.selectedUser.id == (any id in $scope.users.map(function(u) { return u.id; }));` 19198 * which results in Bob being selected as desired. 19199 * 19200 * Working HTML: 19201 * <hljs lang="html"> 19202 * <div ng-controller="MyCtrl"> 19203 * <md-select ng-model="selectedUser" ng-model-options="{trackBy: '$value.id'}"> 19204 * <md-option ng-value="user" ng-repeat="user in users">{{ user.name }}</md-option> 19205 * </md-select> 19206 * </div> 19207 * </hljs> 19208 */ 19209function SelectDirective($mdSelect, $mdUtil, $mdConstant, $mdTheming, $mdAria, $parse, $sce, 19210 $injector) { 19211 var keyCodes = $mdConstant.KEY_CODE; 19212 var NAVIGATION_KEYS = [keyCodes.SPACE, keyCodes.ENTER, keyCodes.UP_ARROW, keyCodes.DOWN_ARROW]; 19213 19214 return { 19215 restrict: 'E', 19216 require: ['^?mdInputContainer', 'mdSelect', 'ngModel', '?^form'], 19217 compile: compile, 19218 controller: function() { 19219 } // empty placeholder controller to be initialized in link 19220 }; 19221 19222 function compile(element, attr) { 19223 // add the select value that will hold our placeholder or selected option value 19224 var valueEl = angular.element('<md-select-value><span></span></md-select-value>'); 19225 valueEl.append('<span class="md-select-icon" aria-hidden="true"></span>'); 19226 valueEl.addClass('md-select-value'); 19227 if (!valueEl[0].hasAttribute('id')) { 19228 valueEl.attr('id', 'select_value_label_' + $mdUtil.nextUid()); 19229 } 19230 19231 // There's got to be an md-content inside. If there's not one, let's add it. 19232 var mdContentEl = element.find('md-content'); 19233 if (!mdContentEl.length) { 19234 element.append(angular.element('<md-content>').append(element.contents())); 19235 } 19236 mdContentEl.attr('role', 'presentation'); 19237 19238 19239 // Add progress spinner for md-options-loading 19240 if (attr.mdOnOpen) { 19241 19242 // Show progress indicator while loading async 19243 // Use ng-hide for `display:none` so the indicator does not interfere with the options list 19244 element 19245 .find('md-content') 19246 .prepend(angular.element( 19247 '<div>' + 19248 ' <md-progress-circular md-mode="indeterminate" ng-if="$$loadingAsyncDone === false" md-diameter="25px"></md-progress-circular>' + 19249 '</div>' 19250 )); 19251 19252 // Hide list [of item options] while loading async 19253 element 19254 .find('md-option') 19255 .attr('ng-show', '$$loadingAsyncDone'); 19256 } 19257 19258 if (attr.name) { 19259 var autofillClone = angular.element('<select class="md-visually-hidden">'); 19260 autofillClone.attr({ 19261 'name': attr.name, 19262 'aria-hidden': 'true', 19263 'tabindex': '-1' 19264 }); 19265 var opts = element.find('md-option'); 19266 angular.forEach(opts, function(el) { 19267 var newEl = angular.element('<option>' + el.innerHTML + '</option>'); 19268 if (el.hasAttribute('ng-value')) newEl.attr('ng-value', el.getAttribute('ng-value')); 19269 else if (el.hasAttribute('value')) newEl.attr('value', el.getAttribute('value')); 19270 autofillClone.append(newEl); 19271 }); 19272 19273 // Adds an extra option that will hold the selected value for the 19274 // cases where the select is a part of a non-angular form. This can be done with a ng-model, 19275 // however if the `md-option` is being `ng-repeat`-ed, AngularJS seems to insert a similar
19276 // `option` node, but with a value of `? string: <value> ?` which would then get submitted. 19277 // This also goes around having to prepend a dot to the name attribute. 19278 autofillClone.append( 19279 '<option ng-value="' + attr.ngModel + '" selected></option>' 19280 ); 19281 19282 element.parent().append(autofillClone); 19283 } 19284 19285 var isMultiple = $mdUtil.parseAttributeBoolean(attr.multiple); 19286 19287 // Use everything that's left inside element.contents() as the contents of the menu 19288 var multipleContent = isMultiple ? 'multiple' : ''; 19289 var selectTemplate = '' + 19290 '<div class="md-select-menu-container" aria-hidden="true" role="presentation">' + 19291 '<md-select-menu role="presentation" {0}>{1}</md-select-menu>' + 19292 '</div>'; 19293 19294 selectTemplate = $mdUtil.supplant(selectTemplate, [multipleContent, element.html()]); 19295 element.empty().append(valueEl); 19296 element.append(selectTemplate); 19297 19298 if(!attr.tabindex){ 19299 attr.$set('tabindex', 0); 19300 } 19301 19302 return function postLink(scope, element, attr, ctrls) { 19303 var untouched = true; 19304 var isDisabled, ariaLabelBase; 19305 19306 var containerCtrl = ctrls[0]; 19307 var mdSelectCtrl = ctrls[1]; 19308 var ngModelCtrl = ctrls[2]; 19309 var formCtrl = ctrls[3]; 19310 // grab a reference to the select menu value label 19311 var valueEl = element.find('md-select-value'); 19312 var isReadonly = angular.isDefined(attr.readonly); 19313 var disableAsterisk = $mdUtil.parseAttributeBoolean(attr.mdNoAsterisk); 19314 19315 if (disableAsterisk) { 19316 element.addClass('md-no-asterisk'); 19317 } 19318 19319 if (containerCtrl) { 19320 var isErrorGetter = containerCtrl.isErrorGetter || function() { 19321 return ngModelCtrl.$invalid && (ngModelCtrl.$touched || (formCtrl && formCtrl.$submitted)); 19322 }; 19323 19324 if (containerCtrl.input) { 19325 // We ignore inputs that are in the md-select-header (one 19326 // case where this might be useful would be adding as searchbox) 19327 if (element.find('md-select-header').find('input')[0] !== containerCtrl.input[0]) { 19328 throw new Error("<md-input-container> can only have *one* child <input>, <textarea> or <select> element!"); 19329 } 19330 } 19331 19332 containerCtrl.input = element; 19333 if (!containerCtrl.label) { 19334 $mdAria.expect(element, 'aria-label', element.attr('placeholder')); 19335 } 19336 19337 scope.$watch(isErrorGetter, containerCtrl.setInvalid); 19338 } 19339 19340 var selectContainer, selectScope, selectMenuCtrl; 19341 19342 findSelectContainer(); 19343 $mdTheming(element); 19344 19345 if (formCtrl && angular.isDefined(attr.multiple)) { 19346 $mdUtil.nextTick(function() { 19347 var hasModelValue = ngModelCtrl.$modelValue || ngModelCtrl.$viewValue; 19348 if (hasModelValue) { 19349 formCtrl.$setPristine(); 19350 } 19351 }); 19352 } 19353 19354 var originalRender = ngModelCtrl.$render; 19355 ngModelCtrl.$render = function() { 19356 originalRender(); 19357 syncLabelText(); 19358 syncAriaLabel(); 19359 inputCheckValue(); 19360 }; 19361 19362 attr.$observe('placeholder', ngModelCtrl.$render); 19363 19364 if (containerCtrl && containerCtrl.label) { 19365 attr.$observe('required', function (value) { 19366 // Toggle the md-required class on the input containers label, because the input container is automatically 19367 // applying the asterisk indicator on the label. 19368 containerCtrl.label.toggleClass('md-required', value && !disableAsterisk); 19369 }); 19370 } 19371 19372 mdSelectCtrl.setLabelText = function(text) { 19373 mdSelectCtrl.setIsPlaceholder(!text); 19374 19375 // Whether the select label has been given via user content rather than the internal 19376 // template of <md-option> 19377 var isSelectLabelFromUser = false; 19378 19379 if (attr.mdSelectedText && attr.mdSelectedHtml) { 19380 throw Error('md-select cannot have both `md-selected-text` and `md-selected-html`'); 19381 } 19382 19383 if (attr.mdSelectedText || attr.mdSelectedHtml) { 19384 text = $parse(attr.mdSelectedText || attr.mdSelectedHtml)(scope); 19385 isSelectLabelFromUser = true; 19386 } else if (!text) { 19387 // Use placeholder attribute, otherwise fallback to the md-input-container label 19388 var tmpPlaceholder = attr.placeholder || 19389 (containerCtrl && containerCtrl.label ? containerCtrl.label.text() : ''); 19390 19391 text = tmpPlaceholder || ''; 19392 isSelectLabelFromUser = true; 19393 } 19394 19395 var target = valueEl.children().eq(0); 19396 19397 if (attr.mdSelectedHtml) { 19398 // Using getTrustedHtml will run the content through $sanitize if it is not already 19399 // explicitly trusted. If the ngSanitize module is not loaded, this will 19400 // *correctly* throw an sce error. 19401 target.html($sce.getTrustedHtml(text)); 19402 } else if (isSelectLabelFromUser) { 19403 target.text(text); 19404 } else { 19405 // If we've reached this point, the text is not user-prov
19405ided. 19406 target.html(text); 19407 } 19408 }; 19409 19410 mdSelectCtrl.setIsPlaceholder = function(isPlaceholder) { 19411 if (isPlaceholder) { 19412 valueEl.addClass('md-select-placeholder'); 19413 if (containerCtrl && containerCtrl.label) { 19414 containerCtrl.label.addClass('md-placeholder'); 19415 } 19416 } else { 19417 valueEl.removeClass('md-select-placeholder'); 19418 if (containerCtrl && containerCtrl.label) { 19419 containerCtrl.label.removeClass('md-placeholder'); 19420 } 19421 } 19422 }; 19423 19424 if (!isReadonly) { 19425 element 19426 .on('focus', function(ev) { 19427 // Always focus the container (if we have one) so floating labels and other styles are 19428 // applied properly 19429 containerCtrl && containerCtrl.setFocused(true); 19430 }); 19431 19432 // Attach before ngModel's blur listener to stop propagation of blur event 19433 // to prevent from setting $touched. 19434 element.on('blur', function(event) { 19435 if (untouched) { 19436 untouched = false; 19437 if (selectScope._mdSelectIsOpen) { 19438 event.stopImmediatePropagation(); 19439 } 19440 } 19441 19442 if (selectScope._mdSelectIsOpen) return; 19443 containerCtrl && containerCtrl.setFocused(false); 19444 inputCheckValue(); 19445 }); 19446 } 19447 19448 mdSelectCtrl.triggerClose = function() { 19449 $parse(attr.mdOnClose)(scope); 19450 }; 19451 19452 scope.$$postDigest(function() { 19453 initAriaLabel(); 19454 syncLabelText(); 19455 syncAriaLabel(); 19456 }); 19457 19458 function initAriaLabel() { 19459 var labelText = element.attr('aria-label') || element.attr('placeholder'); 19460 if (!labelText && containerCtrl && containerCtrl.label) { 19461 labelText = containerCtrl.label.text(); 19462 } 19463 ariaLabelBase = labelText; 19464 $mdAria.expect(element, 'aria-label', labelText); 19465 } 19466 19467 scope.$watch(function() { 19468 return selectMenuCtrl.selectedLabels(); 19469 }, syncLabelText); 19470 19471 function syncLabelText() { 19472 if (selectContainer) { 19473 selectMenuCtrl = selectMenuCtrl || selectContainer.find('md-select-menu').controller('mdSelectMenu'); 19474 mdSelectCtrl.setLabelText(selectMenuCtrl.selectedLabels()); 19475 } 19476 } 19477 19478 function syncAriaLabel() { 19479 if (!ariaLabelBase) return; 19480 var ariaLabels = selectMenuCtrl.selectedLabels({mode: 'aria'}); 19481 element.attr('aria-label', ariaLabels.length ? ariaLabelBase + '
19481: ' + ariaLabels : ariaLabelBase); 19482 } 19483 19484 var deregisterWatcher; 19485 attr.$observe('ngMultiple', function(val) { 19486 if (deregisterWatcher) deregisterWatcher(); 19487 var parser = $parse(val); 19488 deregisterWatcher = scope.$watch(function() { 19489 return parser(scope); 19490 }, function(multiple, prevVal) { 19491 if (multiple === undefined && prevVal === undefined) return; // assume compiler did a good job 19492 if (multiple) { 19493 element.attr('multiple', 'multiple'); 19494 } else { 19495 element.removeAttr('multiple'); 19496 } 19497 element.attr('aria-multiselectable', multiple ? 'true' : 'false'); 19498 if (selectContainer) { 19499 selectMenuCtrl.setMultiple(multiple); 19500 originalRender = ngModelCtrl.$render; 19501 ngModelCtrl.$render = function() { 19502 originalRender(); 19503 syncLabelText(); 19504 syncAriaLabel(); 19505 inputCheckValue(); 19506 }; 19507 ngModelCtrl.$render(); 19508 } 19509 }); 19510 }); 19511 19512 attr.$observe('disabled', function(disabled) { 19513 if (angular.isString(disabled)) { 19514 disabled = true; 19515 } 19516 // Prevent click event being registered twice 19517 if (isDisabled !== undefined && isDisabled === disabled) { 19518 return; 19519 } 19520 isDisabled = disabled; 19521 if (disabled) { 19522 element 19523 .attr({'aria-disabled': 'true'}) 19524 .removeAttr('tabindex') 19525 .off('click', openSelect) 19526 .off('keydown', handleKeypress); 19527 } else { 19528 element 19529 .attr({'tabindex': attr.tabindex, 'aria-disabled': 'false'}) 19530 .on('click', openSelect) 19531 .on('keydown', handleKeypress); 19532 } 19533 }); 19534 19535 if (!attr.hasOwnProperty('disabled') && !attr.hasOwnProperty('ngDisabled')) { 19536 element.attr({'aria-disabled': 'false'}); 19537 element.on('click', openSelect); 19538 element.on('keydown', handleKeypress); 19539 } 19540 19541 var ariaAttrs = { 19542 role: 'listbox', 19543 'aria-expanded': 'false', 19544 'aria-multiselectable': isMultiple && !attr.ngMultiple ? 'true' : 'false' 19545 }; 19546 19547 if (!element[0].hasAttribute('id')) { 19548 ariaAttrs.id = 'select_' + $mdUtil.nextUid(); 19549 } 19550 19551 var containerId = 'select_container_' + $mdUtil.nextUid(); 19552 selectContainer.attr('id', containerId); 19553 // Only add aria-owns if element ownership is NOT represented in the DOM. 19554 if (!element.find('md-select-menu').length) { 19555 ariaAttrs['aria-owns'] = containerId; 19556 } 19557 element.attr(ariaAttrs); 19558 19559 scope.$on('$destroy', function() { 19560 $mdSelect 19561 .destroy() 19562 .finally(function() { 19563 if (containerCtrl) { 19564 containerCtrl.setFocused(false); 19565 containerCtrl.setHasValue(false); 19566 containerCtrl.input = null; 19567 } 19568 ngModelCtrl.$setTouched(); 19569 }); 19570 }); 19571 19572 19573 19574 function inputCheckValue() { 19575 // The select counts as having a value if one or more options are selected, 19576 // or if the input's validity state says it has bad input (eg string in a number input) 19577 containerCtrl && containerCtrl.setHasValue(selectMenuCtrl.selectedLabels().length > 0 || (element[0].validity || {}).badInput); 19578 } 19579 19580 function findSelectContainer() { 19581 selectContainer = angular.element( 19582 element[0].querySelector('.md-select-menu-container') 19583 ); 19584 selectScope = scope; 19585 if (attr.mdContainerClass) { 19586 var value = selectContainer[0].getAttribute('class') + ' ' + attr.mdContainerClass; 19587 selectContainer[0].setAttribute('class', value); 19588 } 19589 selectMenuCtrl = selectContainer.find('md-select-menu').controller('mdSelectMenu'); 19590 selectMenuCtrl.init(ngModelCtrl, attr.ngModel); 19591 element.on('$destroy', function() { 19592 selectContainer.remove(); 19593 }); 19594 } 19595 19596 function handleKeypress(e) { 19597 if ($mdConstant.isNavigationKey(e)) { 19598 // prevent page scrolling on interaction 19599 e.preventDefault(); 19600 openSelect(e); 19601 } else { 19602 if (shouldHandleKey(e, $mdConstant)) { 19603 e.preventDefault(); 19604 19605 var node = selectMenuCtrl.optNodeForKeyboardSearch(e); 19606 if (!node || node.hasAttribute('disabled')) return; 19607 var optionCtrl = angular.element(node).controller('mdOption'); 19608 if (!selectMenuCtrl.isMultiple) { 19609 selectMenuCtrl.deselect(Object.keys(selectMenuCtrl.selected)[0]); 19610 } 19611 selectMenuCtrl.select(optionCtrl.hashKey, optionCtrl.value); 19612 selectMenuCtrl.refreshViewValue(); 19613 } 19614 } 19615 } 19616 19617 function openSelect() { 19618 selectScope._mdSelectIsOpen = true; 19619 element.attr('aria-expanded', 'true'); 19620 19621 $mdSelect.show({ 19622 scope: selectScope, 19623 preserveScope: true, 19624 skipCompile: true, 19625 element: selectContainer, 19626 target: element[0], 19627 selectCtrl: mdSelectCtrl, 19628 preserveElement: true, 19629 hasBackdrop: true, 19630 loadingAsync: attr.mdOnOpen ? scope.$eval(attr.mdOnOpen) || true : false 19631 }).finally(function() { 19632 selectScope._mdSelectIsOpen = false;
19633 element.focus(); 19634 element.attr('aria-expanded', 'false'); 19635 ngModelCtrl.$setTouched(); 19636 }); 19637 } 19638 19639 }; 19640 } 19641} 19642 19643function SelectMenuDirective($parse, $mdUtil, $mdConstant, $mdTheming) { 19644 // We want the scope to be set to 'false' so an isolated scope is not created 19645 // which would interfere with the md-select-header's access to the 19646 // parent scope. 19647 SelectMenuController.$inject = ["$scope", "$attrs", "$element"]; 19648 return { 19649 restrict: 'E', 19650 require: ['mdSelectMenu'], 19651 scope: false, 19652 controller: SelectMenuController, 19653 link: {pre: preLink} 19654 }; 19655 19656 // We use preLink instead of postLink to ensure that the select is initialized before 19657 // its child options run postLink. 19658 function preLink(scope, element, attr, ctrls) { 19659 var selectCtrl = ctrls[0]; 19660 19661 element.addClass('_md'); // private md component indicator for styling 19662 19663 $mdTheming(element); 19664 element.on('click', clickListener); 19665 element.on('keypress', keyListener); 19666 19667 function keyListener(e) { 19668 if (e.keyCode == 13 || e.keyCode == 32) { 19669 clickListener(e); 19670 } 19671 } 19672 19673 function clickListener(ev) { 19674 var option = $mdUtil.getClosest(ev.target, 'md-option'); 19675 var optionCtrl = option && angular.element(option).data('$mdOptionController'); 19676 if (!option || !optionCtrl) return; 19677 if (option.hasAttribute('disabled')) { 19678 ev.stopImmediatePropagation(); 19679 return false; 19680 } 19681 19682 var optionHashKey = selectCtrl.hashGetter(optionCtrl.value); 19683 var isSelected = angular.isDefined(selectCtrl.selected[optionHashKey]); 19684 19685 scope.$apply(function() { 19686 if (selectCtrl.isMultiple) { 19687 if (isSelected) { 19688 selectCtrl.deselect(optionHashKey); 19689 } else { 19690 selectCtrl.select(optionHashKey, optionCtrl.value); 19691 } 19692 } else { 19693 if (!isSelected) { 19694 selectCtrl.deselect(Object.keys(selectCtrl.selected)[0]); 19695 selectCtrl.select(optionHashKey, optionCtrl.value); 19696 } 19697 } 19698 selectCtrl.refreshViewValue(); 19699 }); 19700 } 19701 } 19702 19703 function SelectMenuController($scope, $attrs, $element) { 19704 var self = this; 19705 self.isMultiple = angular.isDefined($attrs.multiple); 19706 // selected is an object with keys matching all of the selected options' hashed values 19707 self.selected = {}; 19708 // options is an object with keys matching every option's hash value, 19709 // and values matching every option's controller. 19710 self.options = {}; 19711 19712 $scope.$watchCollection(function() { 19713 return self.options; 19714 }, function() { 19715 self.ngModel.$render(); 19716 }); 19717 19718 var deregisterCollectionWatch; 19719 var defaultIsEmpty; 19720 self.setMultiple = function(isMultiple) { 19721 var ngModel = self.ngModel; 19722 defaultIsEmpty = defaultIsEmpty || ngModel.$isEmpty; 19723 19724 self.isMultiple = isMultiple; 19725 if (deregisterCollectionWatch) deregisterCollectionWatch(); 19726 19727 if (self.isMultiple) { 19728 ngModel.$validators['md-multiple'] = validateArray; 19729 ngModel.$render = renderMultiple; 19730 19731 // watchCollection on the model because by default ngModel only watches the model's 19732 // reference. This allowed the developer to also push and pop from their array. 19733 $scope.$watchCollection(self.modelBinding, function(value) { 19734 if (validateArray(value)) renderMultiple(value); 19735 self.ngModel.$setPristine(); 19736 }); 19737 19738 ngModel.$isEmpty = function(value) { 19739 return !value || value.length === 0; 19740 }; 19741 } else { 19742 delete ngModel.$validators['md-multiple']; 19743 ngModel.$render = renderSingular; 19744 } 19745 19746 function validateArray(modelValue, viewValue) { 19747 // If a value is truthy but not an array, reject it. 19748 // If value is undefined/falsy, accept that it's an empty array. 19749 return angular.isArray(modelValue || viewValue || []); 19750 } 19751 }; 19752 19753 var searchStr = ''; 19754 var clearSearchTimeout, optNodes, optText; 19755 var CLEAR_SEARCH_AFTER = 300; 19756 19757 self.optNodeForKeyboardSearch = function(e) { 19758 clearSearchTimeout && clearTimeout(clearSearchTimeout); 19759 clearSearchTimeout = setTimeout(function() { 19760 clearSearchTimeout = undefined; 19761 searchStr = ''; 19762 optText = undefined; 19763 optNodes = undefined; 19764 }, CLEAR_SEARCH_AFTER); 19765
19766 // Support 1-9 on numpad 19767 var keyCode = e.keyCode - ($mdConstant.isNumPadKey(e) ? 48 : 0); 19768 19769 searchStr += String.fromCharCode(keyCode); 19770 var search = new RegExp('^' + searchStr, 'i'); 19771 if (!optNodes) { 19772 optNodes = $element.find('md-option'); 19773 optText = new Array(optNodes.length); 19774 angular.forEach(optNodes, function(el, i) { 19775 optText[i] = el.textContent.trim(); 19776 }); 19777 } 19778 for (var i = 0; i < optText.length; ++i) { 19779 if (search.test(optText[i])) { 19780 return optNodes[i]; 19781 } 19782 } 19783 }; 19784 19785 self.init = function(ngModel, binding) { 19786 self.ngModel = ngModel; 19787 self.modelBinding = binding; 19788 19789 // Setup a more robust version of isEmpty to ensure value is a valid option 19790 self.ngModel.$isEmpty = function($viewValue) { 19791 // We have to transform the viewValue into the hashKey, because otherwise the 19792 // OptionCtrl may not exist. Developers may have specified a trackBy function. 19793 return !self.options[self.hashGetter($viewValue)]; 19794 }; 19795 19796 // Allow users to provide `ng-model="foo" ng-model-options="{trackBy: 'foo.id'}"` so 19797 // that we can properly compare objects set on the model to the available options 19798 var trackByOption = $mdUtil.getModelOption(ngModel, 'trackBy'); 19799 19800 if (trackByOption) { 19801 var trackByLocals = {}; 19802 var trackByParsed = $parse(trackByOption); 19803 self.hashGetter = function(value, valueScope) { 19804 trackByLocals.$value = value; 19805 return trackByParsed(valueScope || $scope, trackByLocals); 19806 }; 19807 // If the user doesn't provide a trackBy, we automatically generate an id for every 19808 // value passed in 19809 } else { 19810 self.hashGetter = function getHashValue(value) { 19811 if (angular.isObject(value)) { 19812 return 'object_' + (value.$$mdSelectId || (value.$$mdSelectId = ++selectNextId)); 19813 } 19814 return value; 19815 }; 19816 } 19817 self.setMultiple(self.isMultiple); 19818 }; 19819 19820 self.selectedLabels = function(opts) { 19821 opts = opts || {}; 19822 var mode = opts.mode || 'html'; 19823 var selectedOptionEls = $mdUtil.nodesToArray($element[0].querySelectorAll('md-option[selected]')); 19824 if (selectedOptionEls.length) { 19825 var mapFn; 19826 19827 if (mode == 'html') { 19828 // Map the given element to its innerHTML string. If the element has a child ripple 19829 // container remove it from the HTML string, before returning the string. 19830 mapFn = function(el) { 19831 // If we do not have a `value` or `ng-value`, assume it is an empty option which clears the select 19832 if (el.hasAttribute('md-option-empty')) { 19833 return ''; 19834 } 19835 19836 var html = el.innerHTML; 19837 19838 // Remove the ripple container from the selected option, copying it would cause a CSP violation. 19839 var rippleContainer = el.querySelector('.md-ripple-container'); 19840 if (rippleContainer) { 19841 html = html.replace(rippleContainer.outerHTML, ''); 19842 } 19843 19844 // Remove the checkbox container, because it will cause the label to wrap inside of the placeholder. 19845 // It should be not displayed inside of the label element. 19846 var checkboxContainer = el.querySelector('.md-container'); 19847 if (checkboxContainer) { 19848 html = html.replace(checkboxContainer.outerHTML, ''); 19849 } 19850 19851 return html; 19852 }; 19853 } else if (mode == 'aria') { 19854 mapFn = function(el) { return el.hasAttribute('aria-label') ? el.getAttribute('aria-label') : el.textContent; }; 19855 } 19856 19857 // Ensure there are no duplicates; see https://github.com/angular/material/issues/9442 19858 return $mdUtil.uniq(selectedOptionEls.map(mapFn)).join(', '); 19859 } else { 19860 return ''; 19861 } 19862 }; 19863 19864 self.select = function(hashKey, hashedValue) { 19865 var option = self.options[hashKey]; 19866 option && option.setSelected(true); 19867 self.selected[hashKey] = hashedValue; 19868 }; 19869 self.deselect = function(hashKey) { 19870 var option = self.options[hashKey]; 19871 option && option.setSelected(false); 19872 delete self.selected[hashKey]; 19873 }; 19874 19875 self.addOption = function(hashKey, optionCtrl) { 19876 if (angular.isDefined(self.options[hashKey])) { 19877 throw new Error('Duplicate md-option values are not allowed in a select. ' + 19878 'Duplicate value "' + optionCtrl.value + '" found.'); 19879 } 19880 19881 self.options[hashKey] = optionCtrl; 19882 19883 // If this option's value was already in our ngModel, go ahead and select it. 19884 if (angular.isDefined(self.selected[hashKey])) { 19885 self.select(hashKey, optionCtrl.value); 19886 19887 // When the current $modelValue of the ngModel Controller is using the same hash as 19888 // the current option, which will be added, then we can be sure, that the validation 19889 // of the option has occurred before the option was added properly. 19890 // This means, that we have to manually trigger a new validation of the current option. 19891 if (angular.isDefined(self.ngModel.$modelValue) && self.hashGetter(self.ngModel.$modelValue) === hashKey) { 19892 self.ngModel.$validate(); 19893 } 19894 19895 self.refreshViewValue(); 19896 } 19897 }; 19898 self.removeOption = function(hashKey) { 19899 delete self.options[hashKey]; 19900 // Don't deselect an option when it's removed - the user's ngModel should be allowed 19901 // to have values that do not match a currently available option. 19902 }; 19903 19904 self.refreshViewValue = function() { 19905 var values = []; 19906 var option; 19907 for (var hashKey in self.selected) { 19908 // If this hashKey has an associated option, push that option's value to the model. 19909 if ((option = self.options[hashKey])) { 19910 values.push(option.value); 19911 } else { 19912 // Otherwise, the given hashKey has no associated option, and we got it 19913 // from an ngModel value at an earlier time. Push the unhashed value of 19914 // this hashKey to the model. 19915 // This allows the developer to put a value in the model that doesn't yet have 19916 // an associated option. 19917 values.push(self.selected[hashKey]); 19918 } 19919 } 19920 var usingTrackBy = $mdUtil.getModelOption(self.ngModel, 'trackBy'); 19921 19922 var newVal = self.isMultiple ? values : values[0]; 19923 var prevVal = self.ngModel.$modelValue; 19924 19925 if (usingTrackBy ? !angular.equals(prevVal, newVal) : (prevVal + '') !== newVal) { 19926 self.ngModel.$setViewValue(newVal); 19927 self.ngModel.$render(); 19928 } 19929 }; 19930 19931 function renderMultiple() { 19932 var newSelectedValues = self.ngModel.$modelValue || self.ngModel.$viewValue || []; 19933 if (!angular.isArray(newSelectedValues)) return; 19934 19935 var oldSelected = Object.keys(self.selected); 19936 19937 var newSelectedHashes = newSelectedValues.map(self.hashGetter); 19938 var deselected = oldSelected.filter(function(hash) { 19939 return newSelectedHashes.indexOf(hash) === -1; 19940 }); 19941 19942 deselected.forEach(self.deselect);
vendor: 14,004 bytes, lines 19943-20420
19943 newSelectedHashes.forEach(function(hashKey, i) { 19944 self.select(hashKey, newSelectedValues[i]); 19945 }); 19946 } 19947 19948 function renderSingular() { 19949 var value = self.ngModel.$viewValue || self.ngModel.$modelValue; 19950 Object.keys(self.selected).forEach(self.deselect); 19951 self.select(self.hashGetter(value), value); 19952 } 19953 } 19954 19955} 19956 19957function OptionDirective($mdButtonInkRipple, $mdUtil, $mdTheming) { 19958 19959 OptionController.$inject = ["$element"]; 19960 return { 19961 restrict: 'E', 19962 require: ['mdOption', '^^mdSelectMenu'], 19963 controller: OptionController, 19964 compile: compile 19965 }; 19966 19967 function compile(element, attr) { 19968 // Manual transclusion to avoid the extra inner <span> that ng-transclude generates 19969 element.append(angular.element('<div class="md-text">').append(element.contents())); 19970 19971 element.attr('tabindex', attr.tabindex || '0'); 19972 19973 if (!hasDefinedValue(attr)) { 19974 element.attr('md-option-empty', ''); 19975 } 19976 19977 return postLink; 19978 } 19979 19980 function hasDefinedValue(attr) { 19981 var value = attr.value; 19982 var ngValue = attr.ngValue; 19983 19984 return value || ngValue; 19985 } 19986 19987 function postLink(scope, element, attr, ctrls) { 19988 var optionCtrl = ctrls[0]; 19989 var selectCtrl = ctrls[1]; 19990 19991 $mdTheming(element); 19992 19993 if (selectCtrl.isMultiple) { 19994 element.addClass('md-checkbox-enabled'); 19995 element.prepend(CHECKBOX_SELECTION_INDICATOR.clone()); 19996 } 19997 19998 if (angular.isDefined(attr.ngValue)) { 19999 scope.$watch(attr.ngValue, setOptionValue); 20000 } else if (angular.isDefined(attr.value)) { 20001 setOptionValue(attr.value); 20002 } else { 20003 scope.$watch(function() { 20004 return element.text().trim(); 20005 }, setOptionValue); 20006 } 20007 20008 attr.$observe('disabled', function(disabled) { 20009 if (disabled) { 20010 element.attr('tabindex', '-1'); 20011 } else { 20012 element.attr('tabindex', '0'); 20013 } 20014 }); 20015 20016 scope.$$postDigest(function() { 20017 attr.$observe('selected', function(selected) { 20018 if (!angular.isDefined(selected)) return; 20019 if (typeof selected == 'string') selected = true; 20020 if (selected) { 20021 if (!selectCtrl.isMultiple) { 20022 selectCtrl.deselect(Object.keys(selectCtrl.selected)[0]); 20023 } 20024 selectCtrl.select(optionCtrl.hashKey, optionCtrl.value); 20025 } else { 20026 selectCtrl.deselect(optionCtrl.hashKey); 20027 } 20028 selectCtrl.refreshViewValue(); 20029 }); 20030 }); 20031 20032 $mdButtonInkRipple.attach(scope, element); 20033 configureAria(); 20034 20035 function setOptionValue(newValue, oldValue, prevAttempt) { 20036 if (!selectCtrl.hashGetter) { 20037 if (!prevAttempt) { 20038 scope.$$postDigest(function() { 20039 setOptionValue(newValue, oldValue, true); 20040 }); 20041 } 20042 return; 20043 } 20044 var oldHashKey = selectCtrl.hashGetter(oldValue, scope); 20045 var newHashKey = selectCtrl.hashGetter(newValue, scope); 20046 20047 optionCtrl.hashKey = newHashKey; 20048 optionCtrl.value = newValue; 20049 20050 selectCtrl.removeOption(oldHashKey, optionCtrl); 20051 selectCtrl.addOption(newHashKey, optionCtrl); 20052 } 20053 20054 scope.$on('$destroy', function() { 20055 selectCtrl.removeOption(optionCtrl.hashKey, optionCtrl); 20056 }); 20057 20058 function configureAria() { 20059 var ariaAttrs = { 20060 'role': 'option', 20061 'aria-selected': 'false' 20062 }; 20063 20064 if (!element[0].hasAttribute('id')) { 20065 ariaAttrs.id = 'select_option_' + $mdUtil.nextUid(); 20066 } 20067 element.attr(ariaAttrs); 20068 } 20069 } 20070 20071 function OptionController($element) { 20072 this.selected = false; 20073 this.setSelected = function(isSelected) { 20074 if (isSelected && !this.selected) { 20075 $element.attr({ 20076 'selected': 'selected', 20077 'aria-selected': 'true' 20078 }); 20079 } else if (!isSelected && this.selected) { 20080 $element.removeAttr('selected'); 20081 $element.attr('aria-selected', 'false'); 20082 } 20083 this.selected = isSelected; 20084 }; 20085 } 20086 20087} 20088 20089function OptgroupDirective() { 20090 return { 20091 restrict: 'E', 20092 compile: compile 20093 }; 20094 function compile(el, attrs) { 20095 // If we have a select header element, we don't want to add the normal label 20096 // header. 20097 if (!hasSelectHeader()) { 20098 setupLabelElement(); 20099 } 20100 20101 function hasSelectHeader() { 20102 return el.parent().find('md-select-header').length; 20103 } 20104 20105 function setupLabelElement() { 20106 var labelElement = el.find('label'); 20107 if (!labelElement.length) { 20108 labelElement = angular.element('<label>'); 20109 el.prepend(labelElement); 20110 } 20111 labelElement.addClass('md-container-ignore'); 20112 labelElement.attr('aria-hidden', 'true'); 20113 if (attrs.label) labelElement.text(attrs.label); 20114 } 20115 } 20116} 20117 20118function SelectHeaderDirective() { 20119 return { 20120 restrict: 'E', 20121 }; 20122} 20123 20124function SelectProvider($$interimElementProvider) { 20125 selectDefaultOptions.$inject = ["$mdSelect", "$mdConstant", "$mdUtil", "$window", "$q", "$$rAF", "$animateCss", "$animate", "$document"]; 20126 return $$interimElementProvider('$mdSelect') 20127 .setDefaults({ 20128 methods: ['target'], 20129 options: selectDefaultOptions 20130 }); 20131 20132 /* @ngInject */ 20133 function selectDefaultOptions($mdSelect, $mdConstant, $mdUtil, $window, $q, $$rAF, $animateCss, $animate, $document) { 20134 var ERROR_TARGET_EXPECTED = "$mdSelect.show() expected a target element in options.target but got '{0}'!"; 20135 var animator = $mdUtil.dom.animator; 20136 var keyCodes = $mdConstant.KEY_CODE; 20137 20138 return { 20139 parent: 'body', 20140 themable: true, 20141 onShow: onShow, 20142 onRemove: onRemove, 20143 hasBackdrop: true, 20144 disableParentScroll: true 20145 }; 20146 20147 /** 20148 * Interim-element onRemove logic.... 20149 */ 20150 function onRemove(scope, element, opts) { 20151 var animationRunner = null; 20152 var destroyListener = scope.$on('$destroy', function() { 20153 // Listen for the case where the element was destroyed while there was an 20154 // ongoing close animation. If this happens, we need to end the animation 20155 // manually. 20156 animationRunner.end(); 20157 }); 20158 20159 opts = opts || { }; 20160 opts.cleanupInteraction(); 20161 opts.cleanupResizing(); 20162 opts.hideBackdrop(); 20163 20164 // For navigation $destroy events, do a quick, non-animated removal, 20165 // but for normal closes (from clicks, etc) animate the removal 20166 return (opts.$destroy === true) ? cleanElement() : animateRemoval().then(cleanElement); 20167 20168 /** 20169 * For normal closes (eg clicks), animate the removal. 20170 * For forced closes (like $destroy events from navigation), 20171 * skip the animations 20172 */ 20173 function animateRemoval() { 20174 animationRunner = $animateCss(element, {addClass: 'md-leave'}); 20175 return animationRunner.start(); 20176 } 20177 20178 /** 20179 * Restore the element to a closed state 20180 */ 20181 function cleanElement() { 20182 destroyListener(); 20183 20184 element 20185 .removeClass('md-active') 20186 .attr('aria-hidden', 'true') 20187 .css('display', 'none'); 20188 element.parent().find('md-select-value').removeAttr('aria-hidden'); 20189 20190 announceClosed(opts); 20191 20192 if (!opts.$destroy && opts.restoreFocus) { 20193 opts.target.focus(); 20194 } 20195 } 20196 20197 } 20198 20199 /** 20200 * Interim-element onShow logic.... 20201 */ 20202 function onShow(scope, element, opts) { 20203 20204 watchAsyncLoad(); 20205 sanitizeAndConfigure(scope, opts); 20206 20207 opts.hideBackdrop = showBackdrop(scope, element, opts); 20208 20209 return showDropDown(scope, element, opts) 20210 .then(function(response) { 20211 element.attr('aria-hidden', 'false'); 20212 opts.alreadyOpen = true; 20213 opts.cleanupInteraction = activateInteraction(); 20214 opts.cleanupResizing = activateResizing(); 20215 autoFocus(opts.focusedNode); 20216 20217 return response; 20218 }, opts.hideBackdrop); 20219 20220 // ************************************ 20221 // Closure Functions 20222 // ************************************ 20223 20224 /** 20225 * Attach the select DOM element(s) and animate to the correct positions 20226 * and scalings... 20227 */ 20228 function showDropDown(scope, element, opts) { 20229 if (opts.parent !== element.parent()) { 20230 element.parent().attr('aria-owns', element.attr('id')); 20231 } 20232 element.parent().find('md-select-value').attr('aria-hidden', 'true'); 20233 20234 opts.parent.append(element); 20235 20236 return $q(function(resolve, reject) { 20237 20238 try { 20239 20240 $animateCss(element, {removeClass: 'md-leave', duration: 0}) 20241 .start() 20242 .then(positionAndFocusMenu) 20243 .then(resolve); 20244 20245 } catch (e) { 20246 reject(e); 20247 } 20248 20249 }); 20250 } 20251 20252 /** 20253 * Initialize container and dropDown menu positions/scale, then animate 20254 * to show. 20255 */ 20256 function positionAndFocusMenu() { 20257 return $q(function(resolve) { 20258 if (opts.isRemoved) return $q.reject(false); 20259 20260 var info = calculateMenuPositions(scope, element, opts); 20261 20262 info.container.element.css(animator.toCss(info.container.styles)); 20263 info.dropDown.element.css(animator.toCss(info.dropDown.styles)); 20264 20265 $$rAF(function() { 20266 element.addClass('md-active'); 20267 info.dropDown.element.css(animator.toCss({transform: ''})); 20268 20269 resolve(); 20270 }); 20271 20272 }); 20273 } 20274 20275 /** 20276 * Show modal backdrop element... 20277 */ 20278 function showBackdrop(scope, element, options) { 20279 20280 // If we are not within a dialog... 20281 if (options.disableParentScroll && !$mdUtil.getClosest(options.target, 'MD-DIALOG')) { 20282 // !! DO this before creating the backdrop; since disableScrollAround() 20283 // configures the scroll offset; which is used by mdBackDrop postLink() 20284 options.restoreScroll = $mdUtil.disableScrollAround(options.element, options.parent); 20285 } else { 20286 options.disableParentScroll = false; 20287 } 20288 20289 if (options.hasBackdrop) { 20290 // Override duration to immediately show invisible backdrop 20291 options.backdrop = $mdUtil.createBackdrop(scope, "md-select-backdrop md-click-catcher"); 20292 $animate.enter(options.backdrop, $document[0].body, null, {duration: 0}); 20293 } 20294 20295 /** 20296 * Hide modal backdrop element... 20297 */ 20298 return function hideBackdrop() { 20299 if (options.backdrop) options.backdrop.remove(); 20300 if (options.disableParentScroll) options.restoreScroll(); 20301 20302 delete options.restoreScroll; 20303 }; 20304 } 20305 20306 /** 20307 * 20308 */ 20309 function autoFocus(focusedNode) { 20310 if (focusedNode && !focusedNode.hasAttribute('disabled')) { 20311 focusedNode.focus(); 20312 } 20313 } 20314 20315 /** 20316 * Check for valid opts and set some sane defaults 20317 */ 20318 function sanitizeAndConfigure(scope, options) { 20319 var selectEl = element.find('md-select-menu'); 20320 20321 if (!options.target) { 20322 throw new Error($mdUtil.supplant(ERROR_TARGET_EXPECTED, [options.target])); 20323 } 20324 20325 angular.extend(options, { 20326 isRemoved: false, 20327 target: angular.element(options.target), //make sure it's not a naked dom node 20328 parent: angular.element(options.parent), 20329 selectEl: selectEl, 20330 contentEl: element.find('md-content'), 20331 optionNodes: selectEl[0].getElementsByTagName('md-option') 20332 }); 20333 } 20334 20335 /** 20336 * Configure various resize listeners for screen changes 20337 */ 20338 function activateResizing() { 20339 var debouncedOnResize = (function(scope, target, options) { 20340 20341 return function() { 20342 if (options.isRemoved) return; 20343 20344 var updates = calculateMenuPositions(scope, target, options); 20345 var container = updates.container; 20346 var dropDown = updates.dropDown; 20347 20348 container.element.css(animator.toCss(container.styles)); 20349 dropDown.element.css(animator.toCss(dropDown.styles)); 20350 }; 20351 20352 })(scope, element, opts); 20353 20354 var window = angular.element($window); 20355 window.on('resize', debouncedOnResize); 20356 window.on('orientationchange', debouncedOnResize); 20357 20358 // Publish deactivation closure... 20359 return function deactivateResizing() { 20360 20361 // Disable resizing handlers 20362 window.off('resize', debouncedOnResize); 20363 window.off('orientationchange', debouncedOnResize); 20364 }; 20365 } 20366 20367 /** 20368 * If asynchronously loading, watch and update internal 20369 * '$$loadingAsyncDone' flag 20370 */ 20371 function watchAsyncLoad() { 20372 if (opts.loadingAsync && !opts.isRemoved) { 20373 scope.$$loadingAsyncDone = false; 20374 20375 $q.when(opts.loadingAsync) 20376 .then(function() { 20377 scope.$$loadingAsyncDone = true; 20378 delete opts.loadingAsync; 20379 }).then(function() { 20380 $$rAF(positionAndFocusMenu); 20381 }); 20382 } 20383 } 20384 20385 /** 20386 * 20387 */ 20388 function activateInteraction() { 20389 if (opts.isRemoved) return; 20390 20391 var dropDown = opts.selectEl; 20392 var selectCtrl = dropDown.controller('mdSelectMenu') || {}; 20393 20394 element.addClass('md-clickable'); 20395 20396 // Close on backdrop click 20397 opts.backdrop && opts.backdrop.on('click', onBackdropClick); 20398 20399 // Escape to close 20400 // Cycling of options, and closing on enter 20401 dropDown.on('keydown', onMenuKeyDown); 20402 dropDown.on('click', checkCloseMenu); 20403 20404 return function cleanupInteraction() { 20405 opts.backdrop && opts.backdrop.off('click', onBackdropClick); 20406 dropDown.off('keydown', onMenuKeyDown); 20407 dropDown.off('click', checkCloseMenu); 20408 20409 element.removeClass('md-clickable'); 20410 opts.isRemoved = true; 20411 }; 20412 20413 // ************************************ 20414 // Closure Functions 20415 // ************************************ 20416 20417 function onBackdropClick(e) { 20418 e.preventDefault(); 20419 e.stopPropagation(); 20420 opts.restoreFocus = false;
20421 $mdUtil.nextTick($mdSelect.hide, true); 20422 } 20423 20424 function onMenuKeyDown(ev) { 20425 ev.preventDefault(); 20426 ev.stopPropagation(); 20427 20428 switch (ev.keyCode) { 20429 case keyCodes.UP_ARROW: 20430 return focusPrevOption(); 20431 case keyCodes.DOWN_ARROW: 20432 return focusNextOption(); 20433 case keyCodes.SPACE: 20434 case keyCodes.ENTER: 20435 var option = $mdUtil.getClosest(ev.target, 'md-option'); 20436 if (option) { 20437 dropDown.triggerHandler({ 20438 type: 'click', 20439 target: option 20440 }); 20441 ev.preventDefault(); 20442 } 20443 checkCloseMenu(ev); 20444 break; 20445 case keyCodes.TAB: 20446 case keyCodes.ESCAPE: 20447 ev.stopPropagation(); 20448 ev.preventDefault(); 20449 opts.restoreFocus = true; 20450 $mdUtil.nextTick($mdSelect.hide, true); 20451 break; 20452 default: 20453 if (shouldHandleKey(ev, $mdConstant)) { 20454 var optNode = dropDown.controller('mdSelectMenu').optNodeForKeyboardSearch(ev); 20455 opts.focusedNode = optNode || opts.focusedNode; 20456 optNode && optNode.focus(); 20457 } 20458 } 20459 } 20460 20461 function focusOption(direction) { 20462 var optionsArray = $mdUtil.nodesToArray(opts.optionNodes); 20463 var index = optionsArray.indexOf(opts.focusedNode); 20464 20465 var newOption; 20466 20467 do { 20468 if (index === -1) { 20469 // We lost the previously focused element, reset to first option 20470 index = 0; 20471 } else if (direction === 'next' && index < optionsArray.length - 1) { 20472 index++; 20473 } else if (direction === 'prev' && index > 0) { 20474 index--; 20475 } 20476 newOption = optionsArray[index]; 20477 if (newOption.hasAttribute('disabled')) newOption = undefined; 20478 } while (!newOption && index < optionsArray.length - 1 && index > 0); 20479 20480 newOption && newOption.focus(); 20481 opts.focusedNode = newOption; 20482 } 20483 20484 function focusNextOption() { 20485 focusOption('next'); 20486 } 20487 20488 function focusPrevOption() { 20489 focusOption('prev'); 20490 } 20491 20492 function checkCloseMenu(ev) { 20493 if (ev && ( ev.type == 'click') && (ev.currentTarget != dropDown[0])) return; 20494 if ( mouseOnScrollbar() ) return; 20495 20496 var option = $mdUtil.getClosest(ev.target, 'md-option'); 20497 if (option && option.hasAttribute && !option.hasAttribute('disabled')) { 20498 ev.preventDefault(); 20499 ev.stopPropagation(); 20500 if (!selectCtrl.isMultiple) { 20501 opts.restoreFocus = true; 20502 20503 $mdUtil.nextTick(function () { 20504 $mdSelect.hide(selectCtrl.ngModel.$viewValue); 20505 }, true); 20506 } 20507 } 20508 /** 20509 * check if the mouseup event was on a scrollbar 20510 */ 20511 function mouseOnScrollbar() { 20512 var clickOnScrollbar = false; 20513 if (ev && (ev.currentTarget.children.length > 0)) { 20514 var child = ev.currentTarget.children[0]; 20515 var hasScrollbar = child.scrollHeight > child.clientHeight; 20516 if (hasScrollbar && child.children.length > 0) { 20517 var relPosX = ev.pageX - ev.currentTarget.getBoundingClientRect().left; 20518 if (relPosX > child.querySelector('md-option').offsetWidth) 20519 clickOnScrollbar = true; 20520 } 20521 } 20522 return clickOnScrollbar; 20523 } 20524 } 20525 } 20526 20527 } 20528 20529 /** 20530 * To notify listeners that the Select menu has closed, 20531 * trigger the [optional] user-defined expression 20532 */ 20533 function announceClosed(opts) { 20534 var mdSelect = opts.selectCtrl; 20535 if (mdSelect) { 20536 var menuController = opts.selectEl.controller('mdSelectMenu'); 20537 mdSelect.setLabelText(menuController ? menuController.selectedLabels() : ''); 20538 mdSelect.triggerClose(); 20539 } 20540 } 20541 20542 20543 /** 20544 * Calculate the 20545 */ 20546 function calculateMenuPositions(scope, element, opts) { 20547 var 20548 containerNode = element[0], 20549 targetNode = opts.target[0].children[0], // target the label 20550 parentNode = $document[0].body, 20551 selectNode = opts.selectEl[0],
vendor: 9,582 bytes, lines 20552-20826
20552 contentNode = opts.contentEl[0], 20553 parentRect = parentNode.getBoundingClientRect(), 20554 targetRect = targetNode.getBoundingClientRect(), 20555 shouldOpenAroundTarget = false, 20556 bounds = { 20557 left: parentRect.left + SELECT_EDGE_MARGIN, 20558 top: SELECT_EDGE_MARGIN, 20559 bottom: parentRect.height - SELECT_EDGE_MARGIN, 20560 right: parentRect.width - SELECT_EDGE_MARGIN - ($mdUtil.floatingScrollbars() ? 16 : 0) 20561 }, 20562 spaceAvailable = { 20563 top: targetRect.top - bounds.top, 20564 left: targetRect.left - bounds.left, 20565 right: bounds.right - (targetRect.left + targetRect.width), 20566 bottom: bounds.bottom - (targetRect.top + targetRect.height) 20567 }, 20568 maxWidth = parentRect.width - SELECT_EDGE_MARGIN * 2, 20569 selectedNode = selectNode.querySelector('md-option[selected]'), 20570 optionNodes = selectNode.getElementsByTagName('md-option'), 20571 optgroupNodes = selectNode.getElementsByTagName('md-optgroup'), 20572 isScrollable = calculateScrollable(element, contentNode), 20573 centeredNode; 20574 20575 var loading = isPromiseLike(opts.loadingAsync); 20576 if (!loading) { 20577 // If a selected node, center around that 20578 if (selectedNode) { 20579 centeredNode = selectedNode; 20580 // If there are option groups, center around the first option group 20581 } else if (optgroupNodes.length) { 20582 centeredNode = optgroupNodes[0]; 20583 // Otherwise - if we are not loading async - center around the first optionNode 20584 } else if (optionNodes.length) { 20585 centeredNode = optionNodes[0]; 20586 // In case there are no options, center on whatever's in there... (eg progress indicator) 20587 } else { 20588 centeredNode = contentNode.firstElementChild || contentNode; 20589 } 20590 } else { 20591 // If loading, center on progress indicator 20592 centeredNode = contentNode.firstElementChild || contentNode; 20593 } 20594 20595 if (contentNode.offsetWidth > maxWidth) { 20596 contentNode.style['max-width'] = maxWidth + 'px'; 20597 } else { 20598 contentNode.style.maxWidth = null; 20599 } 20600 if (shouldOpenAroundTarget) { 20601 contentNode.style['min-width'] = targetRect.width + 'px'; 20602 } 20603 20604 // Remove padding before we compute the position of the menu 20605 if (isScrollable) { 20606 selectNode.classList.add('md-overflow'); 20607 } 20608 20609 var focusedNode = centeredNode; 20610 if ((focusedNode.tagName || '').toUpperCase() === 'MD-OPTGROUP') { 20611 focusedNode = optionNodes[0] || contentNode.firstElementChild || contentNode; 20612 centeredNode = focusedNode; 20613 } 20614 // Cache for autoFocus() 20615 opts.focusedNode = focusedNode; 20616 20617 // Get the selectMenuRect *after* max-width is possibly set above 20618 containerNode.style.display = 'block'; 20619 var selectMenuRect = selectNode.getBoundingClientRect(); 20620 var centeredRect = getOffsetRect(centeredNode); 20621 20622 if (centeredNode) { 20623 var centeredStyle = $window.getComputedStyle(centeredNode); 20624 centeredRect.paddingLeft = parseInt(centeredStyle.paddingLeft, 10) || 0; 20625 centeredRect.paddingRight = parseInt(centeredStyle.paddingRight, 10) || 0; 20626 } 20627 20628 if (isScrollable) { 20629 var scrollBuffer = contentNode.offsetHeight / 2; 20630 contentNode.scrollTop = centeredRect.top + centeredRect.height / 2 - scrollBuffer; 20631 20632 if (spaceAvailable.top < scrollBuffer) { 20633 contentNode.scrollTop = Math.min( 20634 centeredRect.top, 20635 contentNode.scrollTop + scrollBuffer - spaceAvailable.top 20636 ); 20637 } else if (spaceAvailable.bottom < scrollBuffer) { 20638 contentNode.scrollTop = Math.max( 20639 centeredRect.top + centeredRect.height - selectMenuRect.height, 20640 contentNode.scrollTop - scrollBuffer + spaceAvailable.bottom 20641 ); 20642 } 20643 } 20644 20645 var left, top, transformOrigin, minWidth, fontSize; 20646 if (shouldOpenAroundTarget) { 20647 left = targetRect.left; 20648 top = targetRect.top + targetRect.height; 20649 transformOrigin = '50% 0'; 20650 if (top + selectMenuRect.height > bounds.bottom) { 20651 top = targetRect.top - selectMenuRect.height; 20652 transformOrigin = '50% 100%'; 20653 } 20654 } else { 20655 left = (targetRect.left + centeredRect.left - centeredRect.paddingLeft) + 2; 20656 top = Math.floor(targetRect.top + targetRect.height / 2 - centeredRect.height / 2 - 20657 centeredRect.top + contentNode.scrollTop) + 2; 20658 20659 transformOrigin = (centeredRect.left + targetRect.width / 2) + 'px ' + 20660 (centeredRect.top + centeredRect.height / 2 - contentNode.scrollTop) + 'px 0px'; 20661 20662 minWidth = Math.min(targetRect.width + centeredRect.paddingLeft + centeredRect.paddingRight, maxWidth); 20663 20664 fontSize = window.getComputedStyle(targetNode)['font-size']; 20665 } 20666 20667 // Keep left and top within the window 20668 var containerRect = containerNode.getBoundingClientRect(); 20669 var scaleX = Math.round(100 * Math.min(targetRect.width / selectMenuRect.width, 1.0)) / 100; 20670 var scaleY = Math.round(100 * Math.min(targetRect.height / selectMenuRect.height, 1.0)) / 100; 20671 20672 return { 20673 container: { 20674 element: angular.element(containerNode), 20675 styles: { 20676 left: Math.floor(clamp(bounds.left, left, bounds.right - containerRect.width)), 20677 top: Math.floor(clamp(bounds.top, top, bounds.bottom - containerRect.height)), 20678 'min-width': minWidth, 20679 'font-size': fontSize 20680 } 20681 }, 20682 dropDown: { 20683 element: angular.element(selectNode), 20684 styles: { 20685 transformOrigin: transformOrigin, 20686 transform: !opts.alreadyOpen ? $mdUtil.supplant('scale({0},{1})', [scaleX, scaleY]) : "" 20687 } 20688 } 20689 }; 20690 20691 } 20692 20693 } 20694 20695 function isPromiseLike(obj) { 20696 return obj && angular.isFunction(obj.then); 20697 } 20698 20699 function clamp(min, n, max) { 20700 return Math.max(min, Math.min(n, max)); 20701 } 20702 20703 function getOffsetRect(node) { 20704 return node ? { 20705 left: node.offsetLeft, 20706 top: node.offsetTop, 20707 width: node.offsetWidth, 20708 height: node.offsetHeight 20709 } : {left: 0, top: 0, width: 0, height: 0}; 20710 } 20711 20712 function calculateScrollable(element, contentNode) { 20713 var isScrollable = false; 20714 20715 try { 20716 var oldDisplay = element[0].style.display; 20717 20718 // Set the element's display to block so that this calculation is correct 20719 element[0].style.display = 'block'; 20720 20721 isScrollable = contentNode.scrollHeight > contentNode.offsetHeight; 20722 20723 // Reset it back afterwards 20724 element[0].style.display = oldDisplay; 20725 } finally { 20726 // Nothing to do 20727 } 20728 return isScrollable; 20729 } 20730 20731} 20732 20733function shouldHandleKey(ev, $mdConstant) { 20734 var char = String.fromCharCode(ev.keyCode); 20735 var isNonUsefulKey = (ev.keyCode <= 31); 20736 20737 return (char && char.length && !isNonUsefulKey && 20738 !$mdConstant.isMetaKey(ev) && !$mdConstant.isFnLockKey(ev) && !$mdConstant.hasModifierKey(ev)); 20739} 20740 20741})(); 20742(function(){ 20743"use strict"; 20744 20745/** 20746 * @ngdoc module 20747 * @name material.components.showHide 20748 */ 20749 20750// Add additional handlers to ng-show and ng-hide that notify directives 20751// contained within that they should recompute their size. 20752// These run in addition to AngularJS's built-in ng-hide and ng-show directives. 20753angular.module('material.components.showHide', [ 20754 'material.core' 20755]) 20756 .directive('ngShow', createDirective('ngShow', true)) 20757 .directive('ngHide', createDirective('ngHide', false)); 20758 20759 20760function createDirective(name, targetValue) { 20761 return ['$mdUtil', '$window', function($mdUtil, $window) { 20762 return { 20763 restrict: 'A', 20764 multiElement: true, 20765 link: function($scope, $element, $attr) { 20766 var unregister = $scope.$on('$md-resize-enable', function() { 20767 unregister(); 20768 20769 var node = $element[0]; 20770 var cachedTransitionStyles = node.nodeType === $window.Node.ELEMENT_NODE ? 20771 $window.getComputedStyle(node) : {}; 20772 20773 $scope.$watch($attr[name], function(value) { 20774 if (!!value === targetValue) { 20775 $mdUtil.nextTick(function() { 20776 $scope.$broadcast('$md-resize'); 20777 }); 20778 20779 var opts = { 20780 cachedTransitionStyles: cachedTransitionStyles 20781 }; 20782 20783 $mdUtil.dom.animator.waitTransitionEnd($element, opts).then(function() { 20784 $scope.$broadcast('$md-resize'); 20785 }); 20786 } 20787 }); 20788 }); 20789 } 20790 }; 20791 }]; 20792} 20793 20794})(); 20795(function(){ 20796"use strict"; 20797 20798/** 20799 * @ngdoc module 20800 * @name material.components.sidenav 20801 * 20802 * @description 20803 * A Sidenav QP component. 20804 */ 20805SidenavService.$inject = ["$mdComponentRegistry", "$mdUtil", "$q", "$log"]; 20806SidenavDirective.$inject = ["$mdMedia", "$mdUtil", "$mdConstant", "$mdTheming", "$mdInteraction", "$animate", "$compile", "$parse", "$log", "$q", "$document", "$window", "$$rAF"]; 20807SidenavController.$inject = ["$scope", "$attrs", "$mdComponentRegistry", "$q", "$interpolate"]; 20808angular 20809 .module('material.components.sidenav', [ 20810 'material.core', 20811 'material.components.backdrop' 20812 ]) 20813 .factory('$mdSidenav', SidenavService ) 20814 .directive('mdSidenav', SidenavDirective) 20815 .directive('mdSidenavFocus', SidenavFocusDirective) 20816 .controller('$mdSidenavController', SidenavController); 20817 20818 20819/** 20820 * @ngdoc service 20821 * @name $mdSidenav 20822 * @module material.components.sidenav 20823 * 20824 * @description 20825 * `$mdSidenav` makes it easy to interact with multiple sidenavs 20826 * in an app. When looking up a sidenav instance, you can either look
20827 * it up synchronously or wait for it to be initializied asynchronously. 20828 * This is done by passing the second argument to `$mdSidenav`. 20829 * 20830 * @usage 20831 * <hljs lang="js"> 20832 * // Async lookup for sidenav instance; will resolve when the instance is available 20833 * $mdSidenav(componentId, true).then(function(instance) { 20834 * $log.debug( componentId + "is now ready" ); 20835 * }); 20836 * // Sync lookup for sidenav instance; this will resolve immediately. 20837 * $mdSidenav(componentId).then(function(instance) { 20838 * $log.debug( componentId + "is now ready" ); 20839 * }); 20840 * // Async toggle the given sidenav; 20841 * // when instance is known ready and lazy lookup is not needed. 20842 * $mdSidenav(componentId) 20843 * .toggle() 20844 * .then(function(){ 20845 * $log.debug('toggled'); 20846 * }); 20847 * // Async open the given sidenav 20848 * $mdSidenav(componentId) 20849 * .open() 20850 * .then(function(){ 20851 * $log.debug('opened'); 20852 * }); 20853 * // Async close the given sidenav 20854 * $mdSidenav(componentId) 20855 * .close() 20856 * .then(function(){ 20857 * $log.debug('closed'); 20858 * }); 20859 * // Sync check to see if the specified sidenav is set to be open 20860 * $mdSidenav(componentId).isOpen(); 20861 * // Sync check to whether given sidenav is locked open 20862 * // If this is true, the sidenav will be open regardless of close() 20863 * $mdSidenav(componentId).isLockedOpen(); 20864 * // On close callback to handle close, backdrop click or escape key pressed 20865 * // Callback happens BEFORE the close action occurs. 20866 * $mdSidenav(componentId).onClose(function () { 20867 * $log.debug('closing'); 20868 * }); 20869 * </hljs> 20870 */ 20871function SidenavService($mdComponentRegistry, $mdUtil, $q, $log) { 20872 var errorMsg = "SideNav '{0}' is not available! Did you use md-component-id='{0}'?"; 20873 var service = { 20874 find : findInstance, // sync - returns proxy API 20875 waitFor : waitForInstance // async - returns promise 20876 }; 20877 20878 /** 20879 * Service API that supports three (3) usages: 20880 * $mdSidenav().find("left") // sync (must already exist) or returns undefined 20881 * $mdSidenav("left").toggle(); // sync (must already exist) or returns reject promise; 20882 * $mdSidenav("left",true).then( function(left){ // async returns instance when available
vendor: 4,857 bytes, lines 20883-21035
20883 * left.toggle(); 20884 * }); 20885 */ 20886 return function(handle, enableWait) { 20887 if ( angular.isUndefined(handle) ) return service; 20888 20889 var shouldWait = enableWait === true; 20890 var instance = service.find(handle, shouldWait); 20891 return !instance && shouldWait ? service.waitFor(handle) : 20892 !instance && angular.isUndefined(enableWait) ? addLegacyAPI(service, handle) : instance; 20893 }; 20894 20895 /** 20896 * For failed instance/handle lookups, older-clients expect an response object with noops 20897 * that include `rejected promise APIs` 20898 */ 20899 function addLegacyAPI(service, handle) { 20900 var falseFn = function() { return false; }; 20901 var rejectFn = function() { 20902 return $q.when($mdUtil.supplant(errorMsg, [handle || ""])); 20903 }; 20904 20905 return angular.extend({ 20906 isLockedOpen : falseFn, 20907 isOpen : falseFn, 20908 toggle : rejectFn, 20909 open : rejectFn, 20910 close : rejectFn, 20911 onClose : angular.noop, 20912 then : function(callback) { 20913 return waitForInstance(handle) 20914 .then(callback || angular.noop); 20915 } 20916 }, service); 20917 } 20918 /** 20919 * Synchronously lookup the controller instance for the specified sidNav instance which has been 20920 * registered with the markup `md-component-id` 20921 */ 20922 function findInstance(handle, shouldWait) { 20923 var instance = $mdComponentRegistry.get(handle); 20924 20925 if (!instance && !shouldWait) { 20926 20927 // Report missing instance 20928 $log.error( $mdUtil.supplant(errorMsg, [handle || ""]) ); 20929 20930 // The component has not registered itself... most like NOT yet created 20931 // return null to indicate that the Sidenav is not in the DOM 20932 return undefined; 20933 } 20934 return instance; 20935 } 20936 20937 /** 20938 * Asynchronously wait for the component instantiation, 20939 * Deferred lookup of component instance using $component registry 20940 */ 20941 function waitForInstance(handle) { 20942 return $mdComponentRegistry.when(handle).catch($log.error); 20943 } 20944} 20945/** 20946 * @ngdoc directive 20947 * @name mdSidenavFocus 20948 * @module material.components.sidenav 20949 * 20950 * @restrict A 20951 * 20952 * @description 20953 * `mdSidenavFocus` provides a way to specify the focused element when a sidenav opens. 20954 * This is completely optional, as the sidenav itself is focused by default. 20955 * 20956 * @usage 20957 * <hljs lang="html"> 20958 * <md-sidenav> 20959 * <form> 20960 * <md-input-container> 20961 * <label for="testInput">Label</label> 20962 * <input id="testInput" type="text" md-sidenav-focus> 20963 * </md-input-container> 20964 * </form> 20965 * </md-sidenav> 20966 * </hljs> 20967 **/ 20968function SidenavFocusDirective() { 20969 return { 20970 restrict: 'A', 20971 require: '^mdSidenav', 20972 link: function(scope, element, attr, sidenavCtrl) { 20973 // @see $mdUtil.findFocusTarget(...) 20974 } 20975 }; 20976} 20977/** 20978 * @ngdoc directive 20979 * @name mdSidenav 20980 * @module material.components.sidenav 20981 * @restrict E 20982 * 20983 * @description 20984 * 20985 * A Sidenav component that can be opened and closed programatically. 20986 * 20987 * By default, upon opening it will slide out on top of the main content area. 20988 * 20989 * For keyboard and screen reader accessibility, focus is sent to the sidenav wrapper by default. 20990 * It can be overridden with the `md-autofocus` directive on the child element you want focused. 20991 * 20992 * @usage 20993 * <hljs lang="html"> 20994 * <div layout="row" ng-controller="MyController"> 20995 * <md-sidenav md-component-id="left" class="md-sidenav-left"> 20996 * Left Nav! 20997 * </md-sidenav> 20998 * 20999 * <md-content> 21000 * Center Content 21001 * <md-button ng-click="openLeftMenu()"> 21002 * Open Left Menu 21003 * </md-button> 21004 * </md-content> 21005 * 21006 * <md-sidenav md-component-id="right" 21007 * md-is-locked-open="$mdMedia('min-width: 333px')" 21008 * class="md-sidenav-right"> 21009 * <form> 21010 * <md-input-container> 21011 * <label for="testInput">Test input</label> 21012 * <input id="testInput" type="text" 21013 * ng-model="data" md-autofocus> 21014 * </md-input-container> 21015 * </form> 21016 * </md-sidenav> 21017 * </div> 21018 * </hljs> 21019 * 21020 * <hljs lang="js"> 21021 * var app = angular.module('myApp', ['ngMaterial']); 21022 * app.controller('MyController', function($scope, $mdSidenav) { 21023 * $scope.openLeftMenu = function() { 21024 * $mdSidenav('left').toggle(); 21025 * }; 21026 * }); 21027 * </hljs> 21028 * 21029 * @param {expression=} md-is-open A model bound to whether the sidenav is opened. 21030 * @param {boolean=} md-disable-backdrop When present in the markup, the sidenav will not show a backdrop. 21031 * @param {string=} md-component-id componentId to use with $mdSidenav service. 21032 * @param {expression=} md-is-locked-open When this expression evaluates to true, 21033 * the sidenav 'locks open': it falls into the content's flow instead 21034 * of appearing over it. This overrides the `md-is-open` attribute. 21035 * @param {string=} md-disable-scroll-target Selector, pointing to an element, whose scrolling will
21036 * be disabled when the sidenav is opened. By default this is the sidenav's direct parent. 21037 * 21038* The $mdMedia() service is exposed to the is-locked-open attribute, which 21039 * can be given a media query or one of the `sm`, `gt-sm`, `md`, `gt-md`, `lg` or `gt-lg` presets. 21040 * Examples: 21041 * 21042 * - `<md-sidenav md-is-locked-open="shouldLockOpen"></md-sidenav>` 21043 * - `<md-sidenav md-is-locked-open="$mdMedia('min-width: 1000px')"></md-sidenav>` 21044 * - `<md-sidenav md-is-locked-open="$mdMedia('sm')"></md-sidenav>` (locks open on small screens) 21045 */ 21046function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $mdInteraction, $animate, 21047 $compile, $parse, $log, $q, $document, $window, $$rAF) { 21048 return { 21049 restrict: 'E', 21050 scope: { 21051 isOpen: '=?mdIsOpen' 21052 }, 21053 controller: '$mdSidenavController', 21054 compile: function(element) { 21055 element.addClass('md-closed').attr('tabIndex', '-1'); 21056 return postLink; 21057 } 21058 }; 21059 21060 /** 21061 * Directive Post Link function... 21062 */ 21063 function postLink(scope, element, attr, sidenavCtrl) { 21064 var lastParentOverFlow; 21065 var backdrop; 21066 var disableScrollTarget = null; 21067 var triggeringInteractionType; 21068 var triggeringElement = null; 21069 var previousContainerStyles; 21070 var promise = $q.when(true); 21071 var isLockedOpenParsed = $parse(attr.mdIsLockedOpen); 21072 var ngWindow = angular.element($window);
vendor: 46,763 bytes, lines 21073-22552
21073 var isLocked = function() { 21074 return isLockedOpenParsed(scope.$parent, { 21075 $media: function(arg) { 21076 $log.warn("$media is deprecated for is-locked-open. Use $mdMedia instead."); 21077 return $mdMedia(arg); 21078 }, 21079 $mdMedia: $mdMedia 21080 }); 21081 }; 21082 21083 if (attr.mdDisableScrollTarget) { 21084 disableScrollTarget = $document[0].querySelector(attr.mdDisableScrollTarget); 21085 21086 if (disableScrollTarget) { 21087 disableScrollTarget = angular.element(disableScrollTarget); 21088 } else { 21089 $log.warn($mdUtil.supplant('mdSidenav: couldn\'t find element matching ' + 21090 'selector "{selector}". Falling back to parent.', { selector: attr.mdDisableScrollTarget })); 21091 } 21092 } 21093 21094 if (!disableScrollTarget) { 21095 disableScrollTarget = element.parent(); 21096 } 21097 21098 // Only create the backdrop if the backdrop isn't disabled. 21099 if (!attr.hasOwnProperty('mdDisableBackdrop')) { 21100 backdrop = $mdUtil.createBackdrop(scope, "md-sidenav-backdrop md-opaque ng-enter"); 21101 } 21102 21103 element.addClass('_md'); // private md component indicator for styling 21104 $mdTheming(element); 21105 21106 // The backdrop should inherit the sidenavs theme, 21107 // because the backdrop will take its parent theme by default. 21108 if ( backdrop ) $mdTheming.inherit(backdrop, element); 21109 21110 element.on('$destroy', function() { 21111 backdrop && backdrop.remove(); 21112 sidenavCtrl.destroy(); 21113 }); 21114 21115 scope.$on('$destroy', function(){ 21116 backdrop && backdrop.remove(); 21117 }); 21118 21119 scope.$watch(isLocked, updateIsLocked); 21120 scope.$watch('isOpen', updateIsOpen); 21121 21122 21123 // Publish special accessor for the Controller instance 21124 sidenavCtrl.$toggleOpen = toggleOpen; 21125 21126 /** 21127 * Toggle the DOM classes to indicate `locked` 21128 * @param isLocked 21129 */ 21130 function updateIsLocked(isLocked, oldValue) { 21131 scope.isLockedOpen = isLocked; 21132 if (isLocked === oldValue) { 21133 element.toggleClass('md-locked-open', !!isLocked); 21134 } else { 21135 $animate[isLocked ? 'addClass' : 'removeClass'](element, 'md-locked-open'); 21136 } 21137 if (backdrop) { 21138 backdrop.toggleClass('md-locked-open', !!isLocked); 21139 } 21140 } 21141 21142 /** 21143 * Toggle the SideNav view and attach/detach listeners 21144 * @param isOpen 21145 */ 21146 function updateIsOpen(isOpen) { 21147 // Support deprecated md-sidenav-focus attribute as fallback 21148 var focusEl = $mdUtil.findFocusTarget(element) || $mdUtil.findFocusTarget(element,'[md-sidenav-focus]') || element; 21149 var parent = element.parent(); 21150 21151 parent[isOpen ? 'on' : 'off']('keydown', onKeyDown); 21152 if (backdrop) backdrop[isOpen ? 'on' : 'off']('click', close); 21153 21154 var restorePositioning = updateContainerPositions(parent, isOpen); 21155 21156 if ( isOpen ) { 21157 // Capture upon opening.. 21158 triggeringElement = $document[0].activeElement; 21159 triggeringInteractionType = $mdInteraction.getLastInteractionType(); 21160 } 21161 21162 disableParentScroll(isOpen); 21163 21164 return promise = $q.all([ 21165 isOpen && backdrop ? $animate.enter(backdrop, parent) : backdrop ? 21166 $animate.leave(backdrop) : $q.when(true), 21167 $animate[isOpen ? 'removeClass' : 'addClass'](element, 'md-closed') 21168 ]).then(function() { 21169 // Perform focus when animations are ALL done... 21170 if (scope.isOpen) { 21171 $$rAF(function() { 21172 // Notifies child components that the sidenav was opened. Should wait 21173 // a frame in order to allow for the element height to be computed. 21174 ngWindow.triggerHandler('resize'); 21175 }); 21176 21177 focusEl && focusEl.focus(); 21178 } 21179 21180 // Restores the positioning on the sidenav and backdrop. 21181 restorePositioning && restorePositioning(); 21182 }); 21183 } 21184 21185 function updateContainerPositions(parent, willOpen) { 21186 var drawerEl = element[0]; 21187 var scrollTop = parent[0].scrollTop; 21188 21189 if (willOpen && scrollTop) { 21190 previousContainerStyles = { 21191 top: drawerEl.style.top, 21192 bottom: drawerEl.style.bottom, 21193 height: drawerEl.style.height 21194 }; 21195 21196 // When the parent is scrolled down, then we want to be able to show the sidenav at the current scroll 21197 // position. We're moving the sidenav down to the correct scroll position and apply the height of the 21198 // parent, to increase the performance. Using 100% as height, will impact the performance heavily. 21199 var positionStyle = { 21200 top: scrollTop + 'px', 21201 bottom: 'auto', 21202 height: parent[0].clientHeight + 'px' 21203 }; 21204 21205 // Apply the new position styles to the sidenav and backdrop. 21206 element.css(positionStyle); 21207 backdrop.css(positionStyle); 21208 } 21209 21210 // When the sidenav is closing and we have previous defined container styles, 21211 // then we return a restore function, which resets the sidenav and backdrop. 21212 if (!willOpen && previousContainerStyles) { 21213 return function() { 21214 drawerEl.style.top = previousContainerStyles.top; 21215 drawerEl.style.bottom = previousContainerStyles.bottom; 21216 drawerEl.style.height = previousContainerStyles.height; 21217 21218 backdrop[0].style.top = null; 21219 backdrop[0].style.bottom = null; 21220 backdrop[0].style.height = null; 21221 21222 previousContainerStyles = null; 21223 }; 21224 } 21225 } 21226 21227 /** 21228 * Prevent parent scrolling (when the SideNav is open) 21229 */ 21230 function disableParentScroll(disabled) { 21231 if ( disabled && !lastParentOverFlow ) { 21232 lastParentOverFlow = disableScrollTarget.css('overflow'); 21233 disableScrollTarget.css('overflow', 'hidden'); 21234 } else if (angular.isDefined(lastParentOverFlow)) { 21235 disableScrollTarget.css('overflow', lastParentOverFlow); 21236 lastParentOverFlow = undefined; 21237 } 21238 } 21239 21240 /** 21241 * Toggle the sideNav view and publish a promise to be resolved when 21242 * the view animation finishes. 21243 * 21244 * @param isOpen 21245 * @returns {*} 21246 */ 21247 function toggleOpen( isOpen ) { 21248 if (scope.isOpen == isOpen ) { 21249 21250 return $q.when(true); 21251 21252 } else { 21253 if (scope.isOpen && sidenavCtrl.onCloseCb) sidenavCtrl.onCloseCb(); 21254 21255 return $q(function(resolve){ 21256 // Toggle value to force an async `updateIsOpen()` to run 21257 scope.isOpen = isOpen; 21258 21259 $mdUtil.nextTick(function() { 21260 // When the current `updateIsOpen()` animation finishes 21261 promise.then(function(result) { 21262 21263 if ( !scope.isOpen && triggeringElement && triggeringInteractionType === 'keyboard') { 21264 // reset focus to originating element (if available) upon close 21265 triggeringElement.focus(); 21266 triggeringElement = null; 21267 } 21268 21269 resolve(result); 21270 }); 21271 }); 21272 21273 }); 21274 21275 } 21276 } 21277 21278 /** 21279 * Auto-close sideNav when the `escape` key is pressed. 21280 * @param evt 21281 */ 21282 function onKeyDown(ev) { 21283 var isEscape = (ev.keyCode === $mdConstant.KEY_CODE.ESCAPE); 21284 return isEscape ? close(ev) : $q.when(true); 21285 } 21286 21287 /** 21288 * With backdrop `clicks` or `escape` key-press, immediately 21289 * apply the CSS close transition... Then notify the controller 21290 * to close() and perform its own actions. 21291 */ 21292 function close(ev) { 21293 ev.preventDefault(); 21294 21295 return sidenavCtrl.close(); 21296 } 21297 21298 } 21299} 21300 21301/* 21302 * @private 21303 * @ngdoc controller 21304 * @name SidenavController 21305 * @module material.components.sidenav 21306 */ 21307function SidenavController($scope, $attrs, $mdComponentRegistry, $q, $interpolate) { 21308 21309 var self = this; 21310 21311 // Use Default internal method until overridden by directive postLink 21312 21313 // Synchronous getters 21314 self.isOpen = function() { return !!$scope.isOpen; }; 21315 self.isLockedOpen = function() { return !!$scope.isLockedOpen; }; 21316 21317 // Synchronous setters 21318 self.onClose = function (callback) { 21319 self.onCloseCb = callback; 21320 return self; 21321 }; 21322 21323 // Async actions 21324 self.open = function() { return self.$toggleOpen( true ); }; 21325 self.close = function() { return self.$toggleOpen( false ); }; 21326 self.toggle = function() { return self.$toggleOpen( !$scope.isOpen ); }; 21327 self.$toggleOpen = function(value) { return $q.when($scope.isOpen = value); }; 21328 21329 // Evaluate the component id. 21330 var rawId = $attrs.mdComponentId; 21331 var hasDataBinding = rawId && rawId.indexOf($interpolate.startSymbol()) > -1; 21332 var componentId = hasDataBinding ? $interpolate(rawId)($scope.$parent) : rawId; 21333 21334 // Register the component. 21335 self.destroy = $mdComponentRegistry.register(self, componentId); 21336 21337 // Watch and update the component, if the id has changed. 21338 if (hasDataBinding) { 21339 $attrs.$observe('mdComponentId', function(id) { 21340 if (id && id !== self.$$mdHandle) { 21341 self.destroy(); // `destroy` only deregisters the old component id so we can add the new one. 21342 self.destroy = $mdComponentRegistry.register(self, id); 21343 } 21344 }); 21345 } 21346} 21347 21348})(); 21349(function(){ 21350"use strict"; 21351 21352 /** 21353 * @ngdoc module 21354 * @name material.components.slider 21355 */ 21356SliderDirective.$inject = ["$$rAF", "$window", "$mdAria", "$mdUtil", "$mdConstant", "$mdTheming", "$mdGesture", "$parse", "$log", "$timeout"]; 21357 angular.module('material.components.slider', [ 21358 'material.core' 21359 ]) 21360 .directive('mdSlider', SliderDirective) 21361 .directive('mdSliderContainer', SliderContainerDirective); 21362 21363/** 21364 * @ngdoc directive 21365 * @name mdSliderContainer 21366 * @module material.components.slider 21367 * @restrict E 21368 * @description 21369 * The `<md-slider-container>` contains slider with two other elements. 21370 * 21371 * 21372 * @usage 21373 * <h4>Normal Mode</h4> 21374 * <hljs lang="html"> 21375 * </hljs> 21376 */ 21377function SliderContainerDirective() { 21378 return { 21379 controller: function () {}, 21380 compile: function (elem) { 21381 var slider = elem.find('md-slider'); 21382 21383 if (!slider) { 21384 return; 21385 } 21386 21387 var vertical = slider.attr('md-vertical'); 21388 21389 if (vertical !== undefined) { 21390 elem.attr('md-vertical', ''); 21391 } 21392 21393 if(!slider.attr('flex')) { 21394 slider.attr('flex', ''); 21395 } 21396 21397 return function postLink(scope, element, attr, ctrl) { 21398 element.addClass('_md'); // private md component indicator for styling 21399 21400 // We have to manually stop the $watch on ngDisabled because it exists 21401 // on the parent scope, and won't be automatically destroyed when 21402 // the component is destroyed. 21403 function setDisable(value) { 21404 element.children().attr('disabled', value); 21405 element.find('input').attr('disabled', value); 21406 } 21407 21408 var stopDisabledWatch = angular.noop; 21409 21410 if (attr.disabled) { 21411 setDisable(true); 21412 } 21413 else if (attr.ngDisabled) { 21414 stopDisabledWatch = scope.$watch(attr.ngDisabled, function (value) { 21415 setDisable(value); 21416 }); 21417 } 21418 21419 scope.$on('$destroy', function () { 21420 stopDisabledWatch(); 21421 }); 21422 21423 var initialMaxWidth; 21424 21425 ctrl.fitInputWidthToTextLength = function (length) { 21426 var input = element[0].querySelector('md-input-container'); 21427 21428 if (input) { 21429 var computedStyle = getComputedStyle(input); 21430 var minWidth = parseInt(computedStyle.minWidth); 21431 var padding = parseInt(computedStyle.padding) * 2; 21432 21433 initialMaxWidth = initialMaxWidth || parseInt(computedStyle.maxWidth); 21434 var newMaxWidth = Math.max(initialMaxWidth, minWidth + padding + (minWidth / 2 * length)); 21435 21436 input.style.maxWidth = newMaxWidth + 'px'; 21437 } 21438 }; 21439 }; 21440 } 21441 }; 21442} 21443 21444/** 21445 * @ngdoc directive 21446 * @name mdSlider 21447 * @module material.components.slider 21448 * @restrict E 21449 * @description 21450 * The `<md-slider>` component allows the user to choose from a range of 21451 * values. 21452 * 21453 * As per the [material design spec](http://www.google.com/design/spec/style/color.html#color-ui-color-application) 21454 * the slider is in the accent color by default. The primary color palette may be used with 21455 * the `md-primary` class. 21456 * 21457 * It has two modes: 'normal' mode, where the user slides between a wide range 21458 * of values, and 'discrete' mode, where the user slides between only a few 21459 * select values. 21460 * 21461 * To enable discrete mode, add the `md-discrete` attribute to a slider, 21462 * and use the `step` attribute to change the distance between 21463 * values the user is allowed to pick. 21464 * 21465 * @usage 21466 * <h4>Normal Mode</h4> 21467 * <hljs lang="html"> 21468 * <md-slider ng-model="myValue" min="5" max="500"> 21469 * </md-slider> 21470 * </hljs> 21471 * <h4>Discrete Mode</h4> 21472 * <hljs lang="html"> 21473 * <md-slider md-discrete ng-model="myDiscreteValue" step="10" min="10" max="130"> 21474 * </md-slider> 21475 * </hljs> 21476 * <h4>Invert Mode</h4> 21477 * <hljs lang="html"> 21478 * <md-slider md-invert ng-model="myValue" step="10" min="10" max="130"> 21479 * </md-slider> 21480 * </hljs> 21481 * 21482 * @param {boolean=} md-discrete Whether to enable discrete mode. 21483 * @param {boolean=} md-invert Whether to enable invert mode. 21484 * @param {number=} step The distance between values the user is allowed to pick. Default 1. 21485 * @param {number=} min The minimum value the user is allowed to pick. Default 0. 21486 * @param {number=} max The maximum value the user is allowed to pick. Default 100. 21487 * @param {number=} round The amount of numbers after the decimal point, maximum is 6 to prevent scientific notation. Default 3. 21488 */ 21489function SliderDirective($$rAF, $window, $mdAria, $mdUtil, $mdConstant, $mdTheming, $mdGesture, $parse, $log, $timeout) { 21490 return { 21491 scope: {}, 21492 require: ['?ngModel', '?^mdSliderContainer'], 21493 template: 21494 '<div class="md-slider-wrapper">' + 21495 '<div class="md-slider-content">' + 21496 '<div class="md-track-container">' + 21497 '<div class="md-track"></div>' + 21498 '<div class="md-track md-track-fill"></div>' + 21499 '<div class="md-track-ticks"></div>' + 21500 '</div>' + 21501 '<div class="md-thumb-container">' + 21502 '<div class="md-thumb"></div>' + 21503 '<div class="md-focus-thumb"></div>' + 21504 '<div class="md-focus-ring"></div>' + 21505 '<div class="md-sign">' + 21506 '<span class="md-thumb-text"></span>' + 21507 '</div>' + 21508 '<div class="md-disabled-thumb"></div>' + 21509 '</div>' + 21510 '</div>' + 21511 '</div>', 21512 compile: compile 21513 }; 21514 21515 // ********************************************************** 21516 // Private Methods 21517 // ********************************************************** 21518 21519 function compile (tElement, tAttrs) { 21520 var wrapper = angular.element(tElement[0].getElementsByClassName('md-slider-wrapper')); 21521 21522 var tabIndex = tAttrs.tabindex || 0; 21523 wrapper.attr('tabindex', tabIndex); 21524 21525 if (tAttrs.disabled || tAttrs.ngDisabled) wrapper.attr('tabindex', -1); 21526 21527 wrapper.attr('role', 'slider'); 21528 21529 $mdAria.expect(tElement, 'aria-label'); 21530 21531 return postLink; 21532 } 21533 21534 function postLink(scope, element, attr, ctrls) { 21535 $mdTheming(element); 21536 var ngModelCtrl = ctrls[0] || { 21537 // Mock ngModelController if it doesn't exist to give us 21538 // the minimum functionality needed 21539 $setViewValue: function(val) { 21540 this.$viewValue = val; 21541 this.$viewChangeListeners.forEach(function(cb) { cb(); }); 21542 }, 21543 $parsers: [], 21544 $formatters: [], 21545 $viewChangeListeners: [] 21546 }; 21547 21548 var containerCtrl = ctrls[1]; 21549 var container = angular.element($mdUtil.getClosest(element, '_md-slider-container', true)); 21550 var isDisabled = attr.ngDisabled ? angular.bind(null, $parse(attr.ngDisabled), scope.$parent) : function () { 21551 return element[0].hasAttribute('disabled'); 21552 }; 21553 21554 var thumb = angular.element(element[0].querySelector('.md-thumb')); 21555 var thumbText = angular.element(element[0].querySelector('.md-thumb-text')); 21556 var thumbContainer = thumb.parent(); 21557 var trackContainer = angular.element(element[0].querySelector('.md-track-container')); 21558 var activeTrack = angular.element(element[0].querySelector('.md-track-fill')); 21559 var tickContainer = angular.element(element[0].querySelector('.md-track-ticks')); 21560 var wrapper = angular.element(element[0].getElementsByClassName('md-slider-wrapper')); 21561 var content = angular.element(element[0].getElementsByClassName('md-slider-content')); 21562 var throttledRefreshDimensions = $mdUtil.throttle(refreshSliderDimensions, 5000); 21563 21564 // Default values, overridable by attrs 21565 var DEFAULT_ROUND = 3; 21566 var vertical = angular.isDefined(attr.mdVertical); 21567 var discrete = angular.isDefined(attr.mdDiscrete); 21568 var invert = angular.isDefined(attr.mdInvert); 21569 angular.isDefined(attr.min) ? attr.$observe('min', updateMin) : updateMin(0); 21570 angular.isDefined(attr.max) ? attr.$observe('max', updateMax) : updateMax(100); 21571 angular.isDefined(attr.step)? attr.$observe('step', updateStep) : updateStep(1); 21572 angular.isDefined(attr.round)? attr.$observe('round', updateRound) : updateRound(DEFAULT_ROUND); 21573 21574 // We have to manually stop the $watch on ngDisabled because it exists 21575 // on the parent scope, and won't be automatically destroyed when 21576 // the component is destroyed. 21577 var stopDisabledWatch = angular.noop; 21578 if (attr.ngDisabled) { 21579 stopDisabledWatch = scope.$parent.$watch(attr.ngDisabled, updateAriaDisabled); 21580 } 21581 21582 $mdGesture.register(wrapper, 'drag', { horizontal: !vertical }); 21583 21584 scope.mouseActive = false; 21585 21586 wrapper 21587 .on('keydown', keydownListener) 21588 .on('mousedown', mouseDownListener) 21589 .on('focus', focusListener) 21590 .on('blur', blurListener) 21591 .on('$md.pressdown', onPressDown) 21592 .on('$md.pressup', onPressUp) 21593 .on('$md.dragstart', onDragStart) 21594 .on('$md.drag', onDrag) 21595 .on('$md.dragend', onDragEnd); 21596 21597 // On resize, recalculate the slider's dimensions and re-render 21598 function updateAll() { 21599 refreshSliderDimensions(); 21600 ngModelRender(); 21601 } 21602 setTimeout(updateAll, 0); 21603 21604 var debouncedUpdateAll = $$rAF.throttle(updateAll); 21605 angular.element($window).on('resize', debouncedUpdateAll); 21606 21607 scope.$on('$destroy', function() { 21608 angular.element($window).off('resize', debouncedUpdateAll); 21609 }); 21610 21611 ngModelCtrl.$render = ngModelRender; 21612 ngModelCtrl.$viewChangeListeners.push(ngModelRender); 21613 ngModelCtrl.$formatters.push(minMaxValidator); 21614 ngModelCtrl.$formatters.push(stepValidator); 21615 21616 /** 21617 * Attributes 21618 */ 21619 var min; 21620 var max; 21621 var step; 21622 var round; 21623 function updateMin(value) { 21624 min = parseFloat(value); 21625 element.attr('aria-valuemin', value); 21626 updateAll(); 21627 } 21628 function updateMax(value) { 21629 max = parseFloat(value); 21630 element.attr('aria-valuemax', value); 21631 updateAll(); 21632 } 21633 function updateStep(value) { 21634 step = parseFloat(value); 21635 } 21636 function updateRound(value) { 21637 // Set max round digits to 6, after 6 the input uses scientific notation 21638 round = minMaxValidator(parseInt(value), 0, 6); 21639 } 21640 function updateAriaDisabled() { 21641 element.attr('aria-disabled', !!isDisabled()); 21642 } 21643 21644 // Draw the ticks with canvas. 21645 // The alternative to drawing ticks with canvas is to draw one element for each tick, 21646 // which could quickly become a performance bottleneck. 21647 var tickCanvas, tickCtx; 21648 function redrawTicks() { 21649 if (!discrete || isDisabled()) return; 21650 if ( angular.isUndefined(step) ) return; 21651 21652 if ( step <= 0 ) { 21653 var msg = 'Slider step value must be greater than zero when in discrete mode'; 21654 $log.error(msg); 21655 throw new Error(msg); 21656 } 21657 21658 var numSteps = Math.floor( (max - min) / step ); 21659 if (!tickCanvas) { 21660 tickCanvas = angular.element('<canvas>').css('position', 'absolute'); 21661 tickContainer.append(tickCanvas); 21662 21663 tickCtx = tickCanvas[0].getContext('2d'); 21664 } 21665 21666 var dimensions = getSliderDimensions(); 21667 21668 // If `dimensions` doesn't have height and width it might be the first attempt so we will refresh dimensions 21669 if (dimensions && !dimensions.height && !dimensions.width) { 21670 refreshSliderDimensions(); 21671 dimensions = sliderDimensions; 21672 } 21673 21674 tickCanvas[0].width = dimensions.width; 21675 tickCanvas[0].height = dimensions.height; 21676 21677 var distance; 21678 for (var i = 0; i <= numSteps; i++) { 21679 var trackTicksStyle = $window.getComputedStyle(tickContainer[0]); 21680 tickCtx.fillStyle = trackTicksStyle.color || 'black'; 21681 21682 distance = Math.floor((vertical ? dimensions.height : dimensions.width) * (i / numSteps)); 21683 21684 tickCtx.fillRect(vertical ? 0 : distance - 1, 21685 vertical ? distance - 1 : 0, 21686 vertical ? dimensions.width : 2, 21687 vertical ? 2 : dimensions.height); 21688 } 21689 } 21690 21691 function clearTicks() { 21692 if(tickCanvas && tickCtx) { 21693 var dimensions = getSliderDimensions(); 21694 tickCtx.clearRect(0, 0, dimensions.width, dimensions.height); 21695 } 21696 } 21697 21698 /** 21699 * Refreshing Dimensions 21700 */ 21701 var sliderDimensions = {}; 21702 refreshSliderDimensions(); 21703 function refreshSliderDimensions() { 21704 sliderDimensions = trackContainer[0].getBoundingClientRect(); 21705 } 21706 function getSliderDimensions() { 21707 throttledRefreshDimensions(); 21708 return sliderDimensions; 21709 } 21710 21711 /** 21712 * left/right/up/down arrow listener 21713 */ 21714 function keydownListener(ev) { 21715 if (isDisabled()) return; 21716 21717 var changeAmount; 21718 if (vertical ? ev.keyCode === $mdConstant.KEY_CODE.DOWN_ARROW : ev.keyCode === $mdConstant.KEY_CODE.LEFT_ARROW) { 21719 changeAmount = -step; 21720 } else if (vertical ? ev.keyCode === $mdConstant.KEY_CODE.UP_ARROW : ev.keyCode === $mdConstant.KEY_CODE.RIGHT_ARROW) { 21721 changeAmount = step; 21722 } 21723 changeAmount = invert ? -changeAmount : changeAmount; 21724 if (changeAmount) { 21725 if (ev.metaKey || ev.ctrlKey || ev.altKey) { 21726 changeAmount *= 4; 21727 } 21728 ev.preventDefault(); 21729 ev.stopPropagation(); 21730 scope.$evalAsync(function() { 21731 setModelValue(ngModelCtrl.$viewValue + changeAmount); 21732 }); 21733 } 21734 } 21735 21736 function mouseDownListener() { 21737 redrawTicks(); 21738 21739 scope.mouseActive = true; 21740 wrapper.removeClass('md-focused'); 21741 21742 $timeout(function() { 21743 scope.mouseActive = false; 21744 }, 100); 21745 } 21746 21747 function focusListener() { 21748 if (scope.mouseActive === false) { 21749 wrapper.addClass('md-focused'); 21750 } 21751 } 21752 21753 function blurListener() { 21754 wrapper.removeClass('md-focused'); 21755 element.removeClass('md-active'); 21756 clearTicks(); 21757 } 21758 21759 /** 21760 * ngModel setters and validators 21761 */ 21762 function setModelValue(value) { 21763 ngModelCtrl.$setViewValue( minMaxValidator(stepValidator(value)) ); 21764 } 21765 function ngModelRender() { 21766 if (isNaN(ngModelCtrl.$viewValue)) { 21767 ngModelCtrl.$viewValue = ngModelCtrl.$modelValue; 21768 } 21769 21770 ngModelCtrl.$viewValue = minMaxValidator(ngModelCtrl.$viewValue); 21771 21772 var percent = valueToPercent(ngModelCtrl.$viewValue); 21773 scope.modelValue = ngModelCtrl.$viewValue; 21774 element.attr('aria-valuenow', ngModelCtrl.$viewValue); 21775 setSliderPercent(percent); 21776 thumbText.text( ngModelCtrl.$viewValue ); 21777 } 21778 21779 function minMaxValidator(value, minValue, maxValue) { 21780 if (angular.isNumber(value)) { 21781 minValue = angular.isNumber(minValue) ? minValue : min; 21782 maxValue = angular.isNumber(maxValue) ? maxValue : max; 21783 21784 return Math.max(minValue, Math.min(maxValue, value)); 21785 } 21786 } 21787 21788 function stepValidator(value) { 21789 if (angular.isNumber(value)) { 21790 var formattedValue = (Math.round((value - min) / step) * step + min); 21791 formattedValue = (Math.round(formattedValue * Math.pow(10, round)) / Math.pow(10, round)); 21792 21793 if (containerCtrl && containerCtrl.fitInputWidthToTextLength){ 21794 $mdUtil.debounce(function () { 21795 containerCtrl.fitInputWidthToTextLength(formattedValue.toString().length); 21796 }, 100)(); 21797 } 21798 21799 return formattedValue; 21800 } 21801 } 21802 21803 /** 21804 * @param percent 0-1 21805 */ 21806 function setSliderPercent(percent) { 21807 21808 percent = clamp(percent); 21809 21810 var thumbPosition = (percent * 100) + '%'; 21811 var activeTrackPercent = invert ? (1 - percent) * 100 + '%' : thumbPosition; 21812 21813 if (vertical) { 21814 thumbContainer.css('bottom', thumbPosition); 21815 } 21816 else { 21817 $mdUtil.bidiProperty(thumbContainer, 'left', 'right', thumbPosition); 21818 } 21819 21820 21821 activeTrack.css(vertical ? 'height' : 'width', activeTrackPercent); 21822 21823 element.toggleClass((invert ? 'md-max' : 'md-min'), percent === 0); 21824 element.toggleClass((invert ? 'md-min' : 'md-max'), percent === 1); 21825 } 21826 21827 /** 21828 * Slide listeners 21829 */ 21830 var isDragging = false; 21831 21832 function onPressDown(ev) { 21833 if (isDisabled()) return; 21834 21835 element.addClass('md-active'); 21836 element[0].focus(); 21837 refreshSliderDimensions(); 21838 21839 var exactVal = percentToValue( positionToPercent( vertical ? ev.pointer.y : ev.pointer.x )); 21840 var closestVal = minMaxValidator( stepValidator(exactVal) ); 21841 scope.$apply(function() { 21842 setModelValue( closestVal ); 21843 setSliderPercent( valueToPercent(closestVal)); 21844 }); 21845 } 21846 function onPressUp(ev) { 21847 if (isDisabled()) return; 21848 21849 element.removeClass('md-dragging'); 21850 21851 var exactVal = percentToValue( positionToPercent( vertical ? ev.pointer.y : ev.pointer.x )); 21852 var closestVal = minMaxValidator( stepValidator(exactVal) ); 21853 scope.$apply(function() { 21854 setModelValue(closestVal); 21855 ngModelRender(); 21856 }); 21857 } 21858 function onDragStart(ev) { 21859 if (isDisabled()) return; 21860 isDragging = true; 21861 21862 ev.stopPropagation(); 21863 21864 element.addClass('md-dragging'); 21865 setSliderFromEvent(ev); 21866 } 21867 function onDrag(ev) { 21868 if (!isDragging) return; 21869 ev.stopPropagation(); 21870 setSliderFromEvent(ev); 21871 } 21872 function onDragEnd(ev) { 21873 if (!isDragging) return; 21874 ev.stopPropagation(); 21875 isDragging = false; 21876 } 21877 21878 function setSliderFromEvent(ev) { 21879 // While panning discrete, update only the 21880 // visual positioning but not the model value. 21881 if ( discrete ) adjustThumbPosition( vertical ? ev.pointer.y : ev.pointer.x ); 21882 else doSlide( vertical ? ev.pointer.y : ev.pointer.x ); 21883 } 21884 21885 /** 21886 * Slide the UI by changing the model value 21887 * @param x 21888 */ 21889 function doSlide( x ) { 21890 scope.$evalAsync( function() { 21891 setModelValue( percentToValue( positionToPercent(x) )); 21892 }); 21893 } 21894 21895 /** 21896 * Slide the UI without changing the model (while dragging/panning) 21897 * @param x 21898 */ 21899 function adjustThumbPosition( x ) { 21900 var exactVal = percentToValue( positionToPercent( x )); 21901 var closestVal = minMaxValidator( stepValidator(exactVal) ); 21902 setSliderPercent( positionToPercent(x) ); 21903 thumbText.text( closestVal ); 21904 } 21905 21906 /** 21907 * Clamps the value to be between 0 and 1. 21908 * @param {number} value The value to clamp. 21909 * @returns {number} 21910 */ 21911 function clamp(value) { 21912 return Math.max(0, Math.min(value || 0, 1)); 21913 } 21914 21915 /** 21916 * Convert position on slider to percentage value of offset from beginning... 21917 * @param position 21918 * @returns {number} 21919 */ 21920 function positionToPercent( position ) { 21921 var offset = vertical ? sliderDimensions.top : sliderDimensions.left; 21922 var size = vertical ? sliderDimensions.height : sliderDimensions.width; 21923 var calc = (position - offset) / size; 21924 21925 if (!vertical && $mdUtil.bidi() === 'rtl') { 21926 calc = 1 - calc; 21927 } 21928 21929 return Math.max(0, Math.min(1, vertical ? 1 - calc : calc)); 21930 } 21931 21932 /** 21933 * Convert percentage offset on slide to equivalent model value 21934 * @param percent 21935 * @returns {*} 21936 */ 21937 function percentToValue( percent ) { 21938 var adjustedPercent = invert ? (1 - percent) : percent; 21939 return (min + adjustedPercent * (max - min)); 21940 } 21941 21942 function valueToPercent( val ) { 21943 var percent = (val - min) / (max - min); 21944 return invert ? (1 - percent) : percent; 21945 } 21946 } 21947} 21948 21949})(); 21950(function(){ 21951"use strict"; 21952 21953/** 21954 * @ngdoc module 21955 * @name material.components.sticky 21956 * @description 21957 * Sticky effects for md 21958 * 21959 */ 21960MdSticky.$inject = ["$mdConstant", "$$rAF", "$mdUtil", "$compile"]; 21961angular 21962 .module('material.components.sticky', [ 21963 'material.core', 21964 'material.components.content' 21965 ]) 21966 .factory('$mdSticky', MdSticky); 21967 21968/** 21969 * @ngdoc service 21970 * @name $mdSticky 21971 * @module material.components.sticky 21972 * 21973 * @description 21974 * The `$mdSticky`service provides a mixin to make elements sticky. 21975 * 21976 * Whenever the current browser supports stickiness natively, the `$mdSticky` service will just 21977 * use the native browser stickiness. 21978 * 21979 * By default the `$mdSticky` service compiles the cloned element, when not specified through the `elementClone` 21980 * parameter, in the same scope as the actual element lives. 21981 * 21982 * 21983 * <h3>Notes</h3> 21984 * When using an element which is containing a compiled directive, which changed its DOM structure during compilation, 21985 * you should compile the clone yourself using the plain template.<br/><br/> 21986 * See the right usage below: 21987 * <hljs lang="js"> 21988 * angular.module('myModule') 21989 * .directive('stickySelect', function($mdSticky, $compile) { 21990 * var SELECT_TEMPLATE = 21991 * '<md-select ng-model="selected">' + 21992 * '<md-option>Option 1</md-option>' + 21993 * '</md-select>'; 21994 * 21995 * return { 21996 * restrict: 'E', 21997 * replace: true, 21998 * template: SELECT_TEMPLATE, 21999 * link: function(scope,element) { 22000 * $mdSticky(scope, element, $compile(SELECT_TEMPLATE)(scope)); 22001 * } 22002 * }; 22003 * }); 22004 * </hljs> 22005 * 22006 * @usage 22007 * <hljs lang="js"> 22008 * angular.module('myModule') 22009 * .directive('stickyText', function($mdSticky, $compile) { 22010 * return { 22011 * restrict: 'E', 22012 * template: '<span>Sticky Text</span>', 22013 * link: function(scope,element) { 22014 * $mdSticky(scope, element); 22015 * } 22016 * }; 22017 * }); 22018 * </hljs> 22019 * 22020 * @returns A `$mdSticky` function that takes three arguments: 22021 * - `scope` 22022 * - `element`: The element that will be 'sticky' 22023 * - `elementClone`: A clone of the element, that will be shown 22024 * when the user starts scrolling past the original element. 22025 * If not provided, it will use the result of `element.clone()` and compiles it in the given scope. 22026 */ 22027function MdSticky($mdConstant, $$rAF, $mdUtil, $compile) { 22028 22029 var browserStickySupport = $mdUtil.checkStickySupport(); 22030 22031 /** 22032 * Registers an element as sticky, used internally by directives to register themselves 22033 */ 22034 return function registerStickyElement(scope, element, stickyClone) { 22035 var contentCtrl = element.controller('mdContent'); 22036 if (!contentCtrl) return; 22037 22038 if (browserStickySupport) { 22039 element.css({ 22040 position: browserStickySupport, 22041 top: 0, 22042 'z-index': 2 22043 }); 22044 } else { 22045 var $$sticky = contentCtrl.$element.data('$$sticky'); 22046 if (!$$sticky) { 22047 $$sticky = setupSticky(contentCtrl); 22048 contentCtrl.$element.data('$$sticky', $$sticky); 22049 } 22050 22051 // Compile our cloned element, when cloned in this service, into the given scope. 22052 var cloneElement = stickyClone || $compile(element.clone())(scope); 22053 22054 var deregister = $$sticky.add(element, cloneElement); 22055 scope.$on('$destroy', deregister); 22056 } 22057 }; 22058 22059 function setupSticky(contentCtrl) { 22060 var contentEl = contentCtrl.$element; 22061 22062 // Refresh elements is very expensive, so we use the debounced 22063 // version when possible. 22064 var debouncedRefreshElements = $$rAF.throttle(refreshElements); 22065 22066 // setupAugmentedScrollEvents gives us `$scrollstart` and `$scroll`, 22067 // more reliable than `scroll` on android. 22068 setupAugmentedScrollEvents(contentEl); 22069 contentEl.on('$scrollstart', debouncedRefreshElements); 22070 contentEl.on('$scroll', onScroll); 22071 22072 var self; 22073 return self = { 22074 prev: null, 22075 current: null, //the currently stickied item 22076 next: null, 22077 items: [], 22078 add: add, 22079 refreshElements: refreshElements 22080 }; 22081 22082 /*************** 22083 * Public 22084 ***************/ 22085 // Add an element and its sticky clone to this content's sticky collection 22086 function add(element, stickyClone) { 22087 stickyClone.addClass('md-sticky-clone'); 22088 22089 var item = { 22090 element: element, 22091 clone: stickyClone 22092 }; 22093 self.items.push(item); 22094 22095 $mdUtil.nextTick(function() { 22096 contentEl.prepend(item.clone); 22097 }); 22098 22099 debouncedRefreshElements(); 22100 22101 return function remove() { 22102 self.items.forEach(function(item, index) { 22103 if (item.element[0] === element[0]) { 22104 self.items.splice(index, 1); 22105 item.clone.remove(); 22106 } 22107 }); 22108 debouncedRefreshElements(); 22109 }; 22110 } 22111 22112 function refreshElements() { 22113 // Sort our collection of elements by their current position in the DOM. 22114 // We need to do this because our elements' order of being added may not 22115 // be the same as their order of display. 22116 self.items.forEach(refreshPosition); 22117 self.items = self.items.sort(function(a, b) { 22118 return a.top < b.top ? -1 : 1; 22119 }); 22120 22121 // Find which item in the list should be active, 22122 // based upon the content's current scroll position 22123 var item; 22124 var currentScrollTop = contentEl.prop('scrollTop'); 22125 for (var i = self.items.length - 1; i >= 0; i--) { 22126 if (currentScrollTop > self.items[i].top) { 22127 item = self.items[i]; 22128 break; 22129 } 22130 } 22131 setCurrentItem(item); 22132 } 22133 22134 /*************** 22135 * Private 22136 ***************/ 22137 22138 // Find the `top` of an item relative to the content element, 22139 // and also the height. 22140 function refreshPosition(item) { 22141 // Find the top of an item by adding to the offsetHeight until we reach the 22142 // content element. 22143 var current = item.element[0]; 22144 item.top = 0; 22145 item.left = 0; 22146 item.right = 0; 22147 while (current && current !== contentEl[0]) { 22148 item.top += current.offsetTop; 22149 item.left += current.offsetLeft; 22150 if ( current.offsetParent ){ 22151 item.right += current.offsetParent.offsetWidth - current.offsetWidth - current.offsetLeft; //Compute offsetRight 22152 } 22153 current = current.offsetParent; 22154 } 22155 item.height = item.element.prop('offsetHeight'); 22156 22157 var defaultVal = $mdUtil.floatingScrollbars() ? '0' : undefined; 22158 $mdUtil.bidi(item.clone, 'margin-left', item.left, defaultVal); 22159 $mdUtil.bidi(item.clone, 'margin-right', defaultVal, item.right); 22160 } 22161 22162 // As we scroll, push in and select the correct sticky element. 22163 function onScroll() { 22164 var scrollTop = contentEl.prop('scrollTop'); 22165 var isScrollingDown = scrollTop > (onScroll.prevScrollTop || 0); 22166 22167 // Store the previous scroll so we know which direction we are scrolling 22168 onScroll.prevScrollTop = scrollTop; 22169 22170 // 22171 // AT TOP (not scrolling) 22172 // 22173 if (scrollTop === 0) { 22174 // If we're at the top, just clear the current item and return 22175 setCurrentItem(null); 22176 return; 22177 } 22178 22179 // 22180 // SCROLLING DOWN (going towards the next item) 22181 // 22182 if (isScrollingDown) { 22183 22184 // If we've scrolled down past the next item's position, sticky it and return 22185 if (self.next && self.next.top <= scrollTop) { 22186 setCurrentItem(self.next); 22187 return; 22188 } 22189 22190 // If the next item is close to the current one, push the current one up out of the way 22191 if (self.current && self.next && self.next.top - scrollTop <= self.next.height) { 22192 translate(self.current, scrollTop + (self.next.top - self.next.height - scrollTop)); 22193 return; 22194 } 22195 } 22196 22197 // 22198 // SCROLLING UP (not at the top & not scrolling down; must be scrolling up) 22199 // 22200 if (!isScrollingDown) { 22201 22202 // If we've scrolled up past the previous item's position, sticky it and return 22203 if (self.current && self.prev && scrollTop < self.current.top) { 22204 setCurrentItem(self.prev); 22205 return; 22206 } 22207 22208 // If the next item is close to the current one, pull the current one down into view 22209 if (self.next && self.current && (scrollTop >= (self.next.top - self.current.height))) { 22210 translate(self.current, scrollTop + (self.next.top - scrollTop - self.current.height)); 22211 return; 22212 } 22213 } 22214 22215 // 22216 // Otherwise, just move the current item to the proper place (scrolling up or down) 22217 // 22218 if (self.current) { 22219 translate(self.current, scrollTop); 22220 } 22221 } 22222 22223 function setCurrentItem(item) { 22224 if (self.current === item) return; 22225 // Deactivate currently active item 22226 if (self.current) { 22227 translate(self.current, null); 22228 setStickyState(self.current, null); 22229 } 22230 22231 // Activate new item if given 22232 if (item) { 22233 setStickyState(item, 'active'); 22234 } 22235 22236 self.current = item; 22237 var index = self.items.indexOf(item); 22238 // If index === -1, index + 1 = 0. It works out. 22239 self.next = self.items[index + 1]; 22240 self.prev = self.items[index - 1]; 22241 setStickyState(self.next, 'next'); 22242 setStickyState(self.prev, 'prev'); 22243 } 22244 22245 function setStickyState(item, state) { 22246 if (!item || item.state === state) return; 22247 if (item.state) { 22248 item.clone.attr('sticky-prev-state', item.state); 22249 item.element.attr('sticky-prev-state', item.state); 22250 } 22251 item.clone.attr('sticky-state', state); 22252 item.element.attr('sticky-state', state); 22253 item.state = state; 22254 } 22255 22256 function translate(item, amount) { 22257 if (!item) return; 22258 if (amount === null || amount === undefined) { 22259 if (item.translateY) { 22260 item.translateY = null; 22261 item.clone.css($mdConstant.CSS.TRANSFORM, ''); 22262 } 22263 } else { 22264 item.translateY = amount; 22265 22266 $mdUtil.bidi( item.clone, $mdConstant.CSS.TRANSFORM, 22267 'translate3d(' + item.left + 'px,' + amount + 'px,0)', 22268 'translateY(' + amount + 'px)' 22269 ); 22270 } 22271 } 22272 } 22273 22274 22275 // Android 4.4 don't accurately give scroll events. 22276 // To fix this problem, we setup a fake scroll event. We say: 22277 // > If a scroll or touchmove event has happened in the last DELAY milliseconds, 22278 // then send a `$scroll` event every animationFrame. 22279 // Additionally, we add $scrollstart and $scrollend events. 22280 function setupAugmentedScrollEvents(element) { 22281 var SCROLL_END_DELAY = 200; 22282 var isScrolling; 22283 var lastScrollTime; 22284 element.on('scroll touchmove', function() { 22285 if (!isScrolling) { 22286 isScrolling = true; 22287 $$rAF.throttle(loopScrollEvent); 22288 element.triggerHandler('$scrollstart'); 22289 } 22290 element.triggerHandler('$scroll'); 22291 lastScrollTime = +$mdUtil.now(); 22292 }); 22293 22294 function loopScrollEvent() { 22295 if (+$mdUtil.now() - lastScrollTime > SCROLL_END_DELAY) { 22296 isScrolling = false; 22297 element.triggerHandler('$scrollend'); 22298 } else { 22299 element.triggerHandler('$scroll'); 22300 $$rAF.throttle(loopScrollEvent); 22301 } 22302 } 22303 } 22304 22305} 22306 22307})(); 22308(function(){ 22309"use strict"; 22310 22311/** 22312 * @ngdoc module 22313 * @name material.components.subheader 22314 * @description 22315 * SubHeader module 22316 * 22317 * Subheaders are special list tiles that delineate distinct sections of a 22318 * list or grid list and are typically related to the current filtering or 22319 * sorting criteria. Subheader tiles are either displayed inline with tiles or 22320 * can be associated with content, for example, in an adjacent column. 22321 * 22322 * Upon scrolling, subheaders remain pinned to the top of the screen and remain 22323 * pinned until pushed on or off screen by the next subheader. @see [Material 22324 * Design Specifications](https://www.google.com/design/spec/components/subheaders.html) 22325 * 22326 * > To improve the visual grouping of content, use the system color for your subheaders. 22327 * 22328 */ 22329MdSubheaderDirective.$inject = ["$mdSticky", "$compile", "$mdTheming", "$mdUtil", "$mdAria"]; 22330angular 22331 .module('material.components.subheader', [ 22332 'material.core', 22333 'material.components.sticky' 22334 ]) 22335 .directive('mdSubheader', MdSubheaderDirective); 22336 22337/** 22338 * @ngdoc directive 22339 * @name mdSubheader 22340 * @module material.components.subheader 22341 * 22342 * @restrict E 22343 * 22344 * @description 22345 * The `md-subheader` directive creates a sticky subheader for a section. 22346 * 22347 * Developers are able to disable the stickiness of the subheader by using the following markup 22348 * 22349 * <hljs lang="html"> 22350 * <md-subheader class="md-no-sticky">Not Sticky</md-subheader> 22351 * </hljs> 22352 * 22353 * ### Notes 22354 * - The `md-subheader` directive uses the <a ng-href="api/service/$mdSticky">$mdSticky</a> service 22355 * to make the subheader sticky. 22356 * 22357 * > Whenever the current browser doesn't support stickiness natively, the subheader 22358 * will be compiled twice to create a sticky clone of the subheader. 22359 * 22360 * @usage 22361 * <hljs lang="html"> 22362 * <md-subheader>Online Friends</md-subheader> 22363 * </hljs> 22364 */ 22365 22366function MdSubheaderDirective($mdSticky, $compile, $mdTheming, $mdUtil, $mdAria) { 22367 return { 22368 restrict: 'E', 22369 replace: true, 22370 transclude: true, 22371 template: ( 22372 '<div class="md-subheader _md">' + 22373 ' <div class="md-subheader-inner">' + 22374 ' <div class="md-subheader-content"></div>' + 22375 ' </div>' + 22376 '</div>' 22377 ), 22378 link: function postLink(scope, element, attr, controllers, transclude) { 22379 $mdTheming(element); 22380 element.addClass('_md'); 22381 22382 // Remove the ngRepeat attribute from the root element, because we don't want to compile 22383 // the ngRepeat for the sticky clone again. 22384 $mdUtil.prefixer().removeAttribute(element, 'ng-repeat'); 22385 22386 var outerHTML = element[0].outerHTML; 22387 22388 function getContent(el) { 22389 return angular.element(el[0].querySelector('.md-subheader-content')); 22390 } 22391 22392 // Set the ARIA attributes on the original element since it keeps it's original place in 22393 // the DOM, whereas the clones are in reverse order. Should be done after the outerHTML, 22394 // in order to avoid having multiple element be marked as headers. 22395 attr.$set('role', 'heading'); 22396 $mdAria.expect(element, 'aria-level', '2'); 22397 22398 // Transclude the user-given contents of the subheader 22399 // the conventional way. 22400 transclude(scope, function(clone) { 22401 getContent(element).append(clone); 22402 }); 22403 22404 // Create another clone, that uses the outer and inner contents 22405 // of the element, that will be 'stickied' as the user scrolls. 22406 if (!element.hasClass('md-no-sticky')) { 22407 transclude(scope, function(clone) { 22408 // If the user adds an ng-if or ng-repeat directly to the md-subheader element, the 22409 // compiled clone below will only be a comment tag (since they replace their elements with 22410 // a comment) which cannot be properly passed to the $mdSticky; so we wrap it in our own 22411 // DIV to ensure we have something $mdSticky can use 22412 var wrapper = $compile('<div class="md-subheader-wrapper" aria-hidden="true">' + outerHTML + '</div>')(scope); 22413 22414 // Delay initialization until after any `ng-if`/`ng-repeat`/etc has finished before 22415 // attempting to create the clone 22416 $mdUtil.nextTick(function() { 22417 // Append our transcluded clone into the wrapper. 22418 // We don't have to recompile the element again, because the clone is already 22419 // compiled in it's transclusion scope. If we recompile the outerHTML of the new clone, we would lose 22420 // our ngIf's and other previous registered bindings / properties. 22421 getContent(wrapper).append(clone); 22422 }); 22423 22424 // Make the element sticky and provide the stickyClone our self, to avoid recompilation of the subheader 22425 // element. 22426 $mdSticky(scope, element, wrapper); 22427 }); 22428 } 22429 } 22430 }; 22431} 22432 22433})(); 22434(function(){ 22435"use strict"; 22436 22437/** 22438 * @ngdoc module 22439 * @name material.components.swipe 22440 * @description Swipe module! 22441 */ 22442/** 22443 * @ngdoc directive 22444 * @module material.components.swipe 22445 * @name mdSwipeLeft 22446 * 22447 * @restrict A 22448 * 22449 * @description 22450 * The md-swipe-left directive allows you to specify custom behavior when an element is swiped 22451 * left. 22452 * 22453 * @usage 22454 * <hljs lang="html"> 22455 * <div md-swipe-left="onSwipeLeft()">Swipe me left!</div> 22456 * </hljs> 22457 */ 22458/** 22459 * @ngdoc directive 22460 * @module material.components.swipe 22461 * @name mdSwipeRight 22462 * 22463 * @restrict A 22464 * 22465 * @description 22466 * The md-swipe-right directive allows you to specify custom behavior when an element is swiped 22467 * right. 22468 * 22469 * @usage 22470 * <hljs lang="html"> 22471 * <div md-swipe-right="onSwipeRight()">Swipe me right!</div> 22472 * </hljs> 22473 */ 22474/** 22475 * @ngdoc directive 22476 * @module material.components.swipe 22477 * @name mdSwipeUp 22478 * 22479 * @restrict A 22480 * 22481 * @description 22482 * The md-swipe-up directive allows you to specify custom behavior when an element is swiped 22483 * up. 22484 * 22485 * @usage 22486 * <hljs lang="html"> 22487 * <div md-swipe-up="onSwipeUp()">Swipe me up!</div> 22488 * </hljs> 22489 */ 22490/** 22491 * @ngdoc directive 22492 * @module material.components.swipe 22493 * @name mdSwipeDown 22494 * 22495 * @restrict A 22496 * 22497 * @description 22498 * The md-swipe-down directive allows you to specify custom behavior when an element is swiped 22499 * down. 22500 * 22501 * @usage 22502 * <hljs lang="html"> 22503 * <div md-swipe-down="onSwipDown()">Swipe me down!</div> 22504 * </hljs> 22505 */ 22506 22507angular.module('material.components.swipe', ['material.core']) 22508 .directive('mdSwipeLeft', getDirective('SwipeLeft')) 22509 .directive('mdSwipeRight', getDirective('SwipeRight')) 22510 .directive('mdSwipeUp', getDirective('SwipeUp')) 22511 .directive('mdSwipeDown', getDirective('SwipeDown')); 22512 22513function getDirective(name) { 22514 DirectiveFactory.$inject = ["$parse"]; 22515 var directiveName = 'md' + name; 22516 var eventName = '$md.' + name.toLowerCase(); 22517 22518 return DirectiveFactory; 22519 22520 /* @ngInject */ 22521 function DirectiveFactory($parse) { 22522 return { restrict: 'A', link: postLink }; 22523 function postLink(scope, element, attr) { 22524 var fn = $parse(attr[directiveName]); 22525 element.on(eventName, function(ev) { 22526 scope.$applyAsync(function() { fn(scope, { $event: ev }); }); 22527 }); 22528 } 22529 } 22530} 22531 22532 22533 22534})(); 22535(function(){ 22536"use strict"; 22537 22538/** 22539 * @ngdoc module 22540 * @name material.components.switch 22541 */ 22542 22543MdSwitch.$inject = ["mdCheckboxDirective", "$mdUtil", "$mdConstant", "$parse", "$$rAF", "$mdGesture", "$timeout"]; 22544angular.module('material.components.switch', [ 22545 'material.core', 22546 'material.components.checkbox' 22547]) 22548 .directive('mdSwitch', MdSwitch); 22549 22550/** 22551 * @ngdoc directive 22552 * @module material.components.switch
22553 * @name mdSwitch 22554 * @restrict E 22555 * 22556 * The switch directive is used very much like the normal [angular checkbox](https://docs.angularjs.org/api/ng/input/input%5Bcheckbox%5D). 22557 * 22558 * As per the [material design spec](http://www.google.com/design/spec/style/color.html#color-ui-color-application) 22559 * the switch is in the accent color by default. The primary color palette may be used with 22560 * the `md-primary` class. 22561 * 22562 * @param {string} ng-model Assignable angular expression to data-bind to. 22563 * @param {string=} name Property name of the form under which the control is published. 22564 * @param {expression=} ng-true-value The value to which the expression should be set when selected. 22565 * @param {expression=} ng-false-value The value to which the expression should be set when not selected. 22566 * @param {string=} ng-change AngularJS expression to be executed when input changes due to user interaction with the input element. 22567 * @param {expression=} ng-disabled En/Disable based on the expression. 22568 * @param {boolean=} md-no-ink Use of attribute indicates use of ripple ink effects. 22569 * @param {string=} aria-label Publish the button label used by screen-readers for accessibility. Defaults to the switch's text. 22570 * @param {boolean=} md-invert When set to true, the switch will be inverted. 22571 * 22572 * @usage 22573 * <hljs lang="html"> 22574 * <md-switch ng-model="isActive" aria-label="Finished?"> 22575 * Finished ? 22576 * </md-switch> 22577 * 22578 * <md-switch md-no-ink ng-model="hasInk" aria-label="No Ink Effects"> 22579 * No Ink Effects 22580 * </md-switch> 22581 * 22582 * <md-switch ng-disabled="true" ng-model="isDisabled" aria-label="Disabled"> 22583 * Disabled 22584 * </md-switch> 22585 * 22586 * </hljs> 22587 */ 22588function MdSwitch(mdCheckboxDirective, $mdUtil, $mdConstant, $parse, $$rAF, $mdGesture, $timeout) { 22589 var checkboxDirective = mdCheckboxDirective[0]; 22590 22591 return { 22592 restrict: 'E', 22593 priority: $mdConstant.BEFORE_NG_ARIA, 22594 transclude: true, 22595 template: 22596 '<div class="md-container">' + 22597 '<div class="md-bar"></div>' + 22598 '<div class="md-thumb-container">' + 22599 '<div class="md-thumb" md-ink-ripple md-ink-ripple-checkbox></div>' + 22600 '</div>'+ 22601 '</div>' + 22602 '<div ng-transclude class="md-label"></div>', 22603 require: ['^?mdInputContainer', '?ngModel', '?^form'], 22604 compile: mdSwitchCompile 22605 }; 22606 22607 function mdSwitchCompile(element, attr) { 22608 var checkboxLink = checkboxDirective.compile(element, attr).post; 22609 // No transition on initial load. 22610 element.addClass('md-dragging'); 22611 22612 return function (scope, element, attr, ctrls) { 22613 var containerCtrl = ctrls[0]; 22614 var ngModel = ctrls[1] || $mdUtil.fakeNgModel(); 22615 var formCtrl = ctrls[2]; 22616 22617 var disabledGetter = null; 22618 if (attr.disabled != null) { 22619 disabledGetter = function() { return true; }; 22620 } else if (attr.ngDisabled) { 22621 disabledGetter = $parse(attr.ngDisabled); 22622 } 22623 22624 var thumbContainer = angular.element(element[0].querySelector('.md-thumb-container')); 22625 var switchContainer = angular.element(element[0].querySelector('.md-container')); 22626 var labelContainer = angular.element(element[0].querySelector('.md-label')); 22627 22628 // no transition on initial load 22629 $$rAF(function() { 22630 element.removeClass('md-dragging'); 22631 }); 22632 22633 checkboxLink(scope, element, attr, ctrls); 22634 22635 if (disabledGetter) { 22636 scope.$watch(disabledGetter, function(isDisabled) { 22637 element.attr('tabindex', isDisabled ? -1 : 0); 22638 }); 22639 } 22640 22641 attr.$observe('mdInvert', function(newValue) { 22642 var isInverted = $mdUtil.parseAttributeBoolean(newValue); 22643 22644 isInverted ? element.prepend(labelContainer) : element.prepend(switchContainer); 22645 22646 // Toggle a CSS class to update the margin. 22647 element.toggleClass('md-inverted', isInverted); 22648 }); 22649 22650 // These events are triggered by setup drag 22651 $mdGesture.register(switchContainer, 'drag'); 22652 switchContainer 22653 .on('$md.dragstart', onDragStart) 22654 .on('$md.drag', onDrag) 22655 .on('$md.dragend', onDragEnd); 22656 22657 var drag; 22658 function onDragStart(ev) { 22659 // Don't go if the switch is disabled. 22660 if (disabledGetter && disabledGetter(scope)) return; 22661 ev.stopPropagation(); 22662 22663 element.addClass('md-dragging'); 22664 drag = {width: thumbContainer.prop('offsetWidth')}; 22665 } 22666 22667 function onDrag(ev) { 22668 if (!drag) return; 22669 ev.stopPropagation(); 22670 ev.srcEvent && ev.srcEvent.preventDefault(); 22671 22672 var percent = ev.pointer.distanceX / drag.width; 22673 22674 //if checked, start from right. else, start from left 22675 var translate = ngModel.$viewValue ? 1 + percent : percent; 22676 // Make sure the switch stays inside its bounds, 0-1% 22677 translate = Math.max(0, Math.min(1, translate)); 22678 22679 thumbContainer.css($mdConstant.CSS.TRANSFORM, 'translate3d(' + (100*translate) + '%,0,0)'); 22680 drag.translate = translate; 22681 } 22682 22683 function onDragEnd(ev) { 22684 if (!drag) return; 22685 ev.stopPropagation(); 22686 22687 element.removeClass('md-dragging'); 22688 thumbContainer.css($mdConstant.CSS.TRANSFORM, ''); 22689 22690 // We changed if there is no distance (this is a click a click), 22691 // or if the drag distance is >50% of the total. 22692 var isChanged = ngModel.$viewValue ? drag.translate < 0.5 : drag.translate > 0.5; 22693 if (isChanged) { 22694 applyModelValue(!ngModel.$viewValue); 22695 } 22696 drag = null; 22697 22698 // Wait for incoming mouse click 22699 scope.skipToggle = true; 22700 $timeout(function() { 22701 scope.skipToggle = false;
vendor: 7,444 bytes, lines 22702-22972
22702 }, 1); 22703 } 22704 22705 function applyModelValue(newValue) { 22706 scope.$apply(function() { 22707 ngModel.$setViewValue(newValue); 22708 ngModel.$render(); 22709 }); 22710 } 22711 22712 }; 22713 } 22714 22715 22716} 22717 22718})(); 22719(function(){ 22720"use strict"; 22721 22722/** 22723 * @ngdoc module 22724 * @name material.components.tabs 22725 * @description 22726 * 22727 * Tabs, created with the `<md-tabs>` directive provide *tabbed* navigation with different styles. 22728 * The Tabs component consists of clickable tabs that are aligned horizontally side-by-side. 22729 * 22730 * Features include support for: 22731 * 22732 * - static or dynamic tabs, 22733 * - responsive designs, 22734 * - accessibility support (ARIA), 22735 * - tab pagination, 22736 * - external or internal tab content, 22737 * - focus indicators and arrow-key navigations, 22738 * - programmatic lookup and access to tab controllers, and 22739 * - dynamic transitions through different tab contents. 22740 * 22741 */ 22742/* 22743 * @see js folder for tabs implementation 22744 */ 22745angular.module('material.components.tabs', [ 22746 'material.core', 22747 'material.components.icon' 22748]); 22749 22750})(); 22751(function(){ 22752"use strict"; 22753 22754angular 22755.module('material.components.tabs') 22756.service('MdTabsPaginationService', MdTabsPaginationService); 22757 22758/** 22759 * @private 22760 * @module material.components.tabs 22761 * @name MdTabsPaginationService 22762 * @description Provides many standalone functions to ease in pagination calculations. 22763 * 22764 * Most functions accept the elements and the current offset. 22765 * 22766 * The `elements` parameter is typically the value returned from the `getElements()` function of the 22767 * tabsController. 22768 * 22769 * The `offset` parameter is always positive regardless of LTR or RTL (we simply make the LTR one 22770 * negative when we apply our transform). This is typically the `ctrl.leftOffset` variable in the 22771 * tabsController. 22772 * 22773 * @returns MdTabsPaginationService 22774 * @constructor 22775 */ 22776function MdTabsPaginationService() { 22777 return { 22778 decreasePageOffset: decreasePageOffset, 22779 increasePageOffset: increasePageOffset, 22780 getTabOffsets: getTabOffsets, 22781 getTotalTabsWidth: getTotalTabsWidth 22782 }; 22783 22784 /** 22785 * Returns the offset for the next decreasing page. 22786 * 22787 * @param elements 22788 * @param currentOffset 22789 * @returns {number} 22790 */ 22791 function decreasePageOffset(elements, currentOffset) { 22792 var canvas = elements.canvas, 22793 tabOffsets = getTabOffsets(elements), 22794 i, firstVisibleTabOffset; 22795 22796 // Find the first fully visible tab in offset range 22797 for (i = 0; i < tabOffsets.length; i++) { 22798 if (tabOffsets[i] >= currentOffset) { 22799 firstVisibleTabOffset = tabOffsets[i]; 22800 break; 22801 } 22802 } 22803 22804 // Return (the first visible tab offset - the tabs container width) without going negative 22805 return Math.max(0, firstVisibleTabOffset - canvas.clientWidth); 22806 } 22807 22808 /** 22809 * Returns the offset for the next increasing page. 22810 * 22811 * @param elements 22812 * @param currentOffset 22813 * @returns {number} 22814 */ 22815 function increasePageOffset(elements, currentOffset) { 22816 var canvas = elements.canvas, 22817 maxOffset = getTotalTabsWidth(elements) - canvas.clientWidth, 22818 tabOffsets = getTabOffsets(elements), 22819 i, firstHiddenTabOffset; 22820 22821 // Find the first partially (or fully) invisible tab 22822 for (i = 0; i < tabOffsets.length, tabOffsets[i] <= currentOffset + canvas.clientWidth; i++) { 22823 firstHiddenTabOffset = tabOffsets[i]; 22824 } 22825 22826 // Return the offset of the first hidden tab, or the maximum offset (whichever is smaller) 22827 return Math.min(maxOffset, firstHiddenTabOffset); 22828 } 22829 22830 /** 22831 * Returns the offsets of all of the tabs based on their widths. 22832 * 22833 * @param elements 22834 * @returns {number[]} 22835 */ 22836 function getTabOffsets(elements) { 22837 var i, tab, currentOffset = 0, offsets = []; 22838 22839 for (i = 0; i < elements.tabs.length; i++) { 22840 tab = elements.tabs[i]; 22841 offsets.push(currentOffset); 22842 currentOffset += tab.offsetWidth; 22843 } 22844 22845 return offsets; 22846 } 22847 22848 /** 22849 * Sum the width of all tabs. 22850 * 22851 * @param elements 22852 * @returns {number} 22853 */ 22854 function getTotalTabsWidth(elements) { 22855 var sum = 0, i, tab; 22856 22857 for (i = 0; i < elements.tabs.length; i++) { 22858 tab = elements.tabs[i]; 22859 sum += tab.offsetWidth; 22860 } 22861 22862 return sum; 22863 } 22864 22865} 22866 22867})(); 22868(function(){ 22869"use strict"; 22870 22871/** 22872 * @ngdoc module 22873 * @name material.components.toast 22874 * @description 22875 * Toast 22876 */ 22877MdToastDirective.$inject = ["$mdToast"]; 22878MdToastProvider.$inject = ["$$interimElementProvider"]; 22879angular.module('material.components.toast', [ 22880 'material.core', 22881 'material.components.button' 22882]) 22883 .directive('mdToast', MdToastDirective) 22884 .provider('$mdToast', MdToastProvider); 22885 22886/* @ngInject */ 22887function MdToastDirective($mdToast) { 22888 return { 22889 restrict: 'E', 22890 link: function postLink(scope, element) { 22891 element.addClass('_md'); // private md component indicator for styling 22892 22893 // When navigation force destroys an interimElement, then 22894 // listen and $destroy() that interim instance... 22895 scope.$on('$destroy', function() { 22896 $mdToast.destroy(); 22897 }); 22898 } 22899 }; 22900} 22901 22902/** 22903 * @ngdoc service 22904 * @name $mdToast 22905 * @module material.components.toast 22906 * 22907 * @description 22908 * `$mdToast` is a service to build a toast notification on any position 22909 * on the screen with an optional duration, and provides a simple promise API. 22910 * 22911 * The toast will be always positioned at the `bottom`, when the screen size is 22912 * between `600px` and `959px` (`sm` breakpoint) 22913 * 22914 * ## Restrictions on custom toasts 22915 * - The toast's template must have an outer `<md-toast>` element. 22916 * - For a toast action, use element with class `md-action`. 22917 * - Add the class `md-capsule` for curved corners. 22918 * 22919 * ### Custom Presets 22920 * Developers are also able to create their own preset, which can be easily used without repeating 22921 * their options each time. 22922 * 22923 * <hljs lang="js"> 22924 * $mdToastProvider.addPreset('testPreset', { 22925 * options: function() { 22926 * return { 22927 * template: 22928 * '<md-toast>' + 22929 * '<div class="md-toast-content">' + 22930 * 'This is a custom preset' + 22931 * '</div>' + 22932 * '</md-toast>', 22933 * controllerAs: 'toast', 22934 * bindToController: true 22935 * }; 22936 * } 22937 * }); 22938 * </hljs> 22939 * 22940 * After you created your preset at config phase, you can easily access it. 22941 * 22942 * <hljs lang="js"> 22943 * $mdToast.show( 22944 * $mdToast.testPreset() 22945 * ); 22946 * </hljs> 22947 * 22948 * ## Parent container notes 22949 * 22950 * The toast is positioned using absolute positioning relative to its first non-static parent 22951 * container. Thus, if the requested parent container uses static positioning, we will temporarily 22952 * set its positioning to `relative` while the toast is visible and reset it when the toast is 22953 * hidden. 22954 * 22955 * Because of this, it is usually best to ensure that the parent container has a fixed height and 22956 * prevents scrolling by setting the `overflow: hidden;` style. Since the position is based off of 22957 * the parent's height, the toast may be mispositioned if you allow the parent to scroll. 22958 * 22959 * You can, however, have a scrollable element inside of the container; just make sure the 22960 * container itself does not scroll. 22961 * 22962 * <hljs lang="html"> 22963 * <div layout-fill id="toast-container"> 22964 * <md-content> 22965 * I can have lots of content and scroll! 22966 * </md-content> 22967 * </div> 22968 * </hljs> 22969 * 22970 * @usage 22971 * <hljs lang="html"> 22972 * <div ng-controller="MyController">
22973 * <md-button ng-click="openToast()"> 22974 * Open a Toast! 22975 * </md-button> 22976 * </div> 22977 * </hljs> 22978 * 22979 * <hljs lang="js"> 22980 * var app = angular.module('app', ['ngMaterial']); 22981 * app.controller('MyController', function($scope, $mdToast) { 22982 * $scope.openToast = function($event) { 22983 * $mdToast.show($mdToast.simple().textContent('Hello!')); 22984 * // Could also do $mdToast.showSimple('Hello'); 22985 * }; 22986 * }); 22987 * </hljs> 22988 */ 22989 22990/** 22991 * @ngdoc method 22992 * @name $mdToast#showSimple 22993 * 22994 * @param {string} message The message to display inside the toast 22995 * @description 22996 * Convenience method which builds and shows a simple toast. 22997 * 22998 * @returns {promise} A promise that can be resolved with `$mdToast.hide()` or 22999 * rejected with `$mdToast.cancel()`. 23000 * 23001 */ 23002 23003 /** 23004 * @ngdoc method 23005 * @name $mdToast#simple 23006 * 23007 * @description 23008 * Builds a preconfigured toast. 23009 * 23010 * @returns {obj} a `$mdToastPreset` with the following chainable configuration methods. 23011 * 23012 * _**Note:** These configuration methods are provided in addition to the methods provided by 23013 * the `build()` and `show()` methods below._ 23014 * 23015 * <table class="md-api-table methods"> 23016 * <thead> 23017 * <tr> 23018 * <th>Method</th> 23019 * <th>Description</th> 23020 * </tr> 23021 * </thead> 23022 * <tbody> 23023 * <tr> 23024 * <td>`.textContent(string)`</td> 23025 * <td>Sets the toast content to the specified string</td> 23026 * </tr> 23027 * <tr> 23028 * <td>`.action(string)`</td> 23029 * <td> 23030 * Adds an action button. <br/> 23031 * If clicked, the promise (returned from `show()`) 23032 * will resolve with the value `'ok'`; otherwise, it is resolved with `true` after a `hideDelay` 23033 * timeout 23034 * </td> 23035 * </tr> 23036 * <tr> 23037 * <td>`.highlightAction(boolean)`</td> 23038 * <td> 23039 * Whether or not the action button will have an additional highlight class.<br/> 23040 * By default the `accent` color will be applied to the action button. 23041 * </td> 23042 * </tr> 23043 * <tr> 23044 * <td>`.highlightClass(string)`</td> 23045 * <td> 23046 * If set, the given class will be applied to the highlighted action button.<br/> 23047 * This allows you to specify the highlight color easily. Highlight classes are `md-primary`, `md-warn` 23048 * and `md-accent` 23049 * </td> 23050 * </tr> 23051 * <tr> 23052 * <td>`.capsule(boolean)`</td> 23053 * <td>Whether or not to add the `md-capsule` class to the toast to provide rounded corners</td> 23054 * </tr> 23055 * <tr> 23056 * <td>`.theme(string)`</td> 23057 * <td>Sets the theme on the toast to the requested theme. Default is `$mdThemingProvider`'s default.</td> 23058 * </tr> 23059 * <tr> 23060 * <td>`.toastClass(string)`</td> 23061 * <td>Sets a class on the toast element</td> 23062 * </tr> 23063 * </tbody> 23064 * </table> 23065 * 23066 */ 23067 23068/** 23069 * @ngdoc method 23070 * @name $mdToast#updateTextContent 23071 * 23072 * @description 23073 * Updates the content of an existing toast. Useful for updating things like counts, etc. 23074 * 23075 */ 23076 23077 /** 23078 * @ngdoc method 23079 * @name $mdToast#build 23080 * 23081 * @description 23082 * Creates a custom `$mdToastPreset` that you can configure. 23083 * 23084 * @returns {obj} a `$mdToastPreset` with the chainable configuration methods for shows' options (see below). 23085 */ 23086 23087 /** 23088 * @ngdoc method 23089 * @name $mdToast#show 23090 * 23091 * @description Shows the toast. 23092 * 23093 * @param {object} optionsOrPreset Either provide an `$mdToastPreset` returned from `simple()` 23094 * and `build()`, or an options object with the following properties: 23095 * 23096 * - `templateUrl` - `{string=}`: The url of an html template file that will 23097 * be used as the content of the toast. Restrictions: the template must 23098 * have an outer `md-toast` element. 23099 * - `template` - `{string=}`: Same as templateUrl, except this is an actual 23100 * template string. 23101 * - `autoWrap` - `{boolean=}`: Whether or not to automatically wrap the template content with a 23102 * `<div class="md-toast-content">` if one is not provided. Defaults to true. Can be disabled if you provide a 23103 * custom toast directive. 23104 * - `scope` - `{object=}`: the scope to link the template / controller to. If none is specified, it will create a new child scope. 23105 * This scope will be destroyed when the toast is removed unless `preserveScope` is set to true. 23106 * - `preserveScope` - `{boolean=}`: whether to preserve the scope when the element is removed. Default is false 23107 * - `hideDelay` - `{number=}`: How many milliseconds the toast should stay 23108 * active before automatically closing. Set to 0 or false to have the toast stay open until 23109 * closed manually. Default: 3000. 23110 * - `position` - `{string=}`: Sets the position of the toast. <br/> 23111 * Available: any combination of `'bottom'`, `'left'`, `'top'`, `'right'`, `'end'` and `'start'`. 23112 * The properties `'end'` and `'start'` are dynamic and can be used for RT
23112L support.<br/> 23113 * Default combination: `'bottom left'`. 23114 * - `toastClass` - `{string=}`: A class to set on the toast element. 23115 * - `controller` - `{string=}`: The controller to associate with this toast. 23116 * The controller will be injected the local `$mdToast.hide( )`, which is a function 23117 * used to hide the toast. 23118 * - `locals` - `{string=}`: An object containing key/value pairs. The keys will 23119 * be used as names of values to inject into the controller. For example, 23120 * `locals: {three: 3}` would inject `three` into the controller with the value 23121 * of 3. 23122 * - `bindToController` - `bool`: bind the locals to the controller, instead of passing them in. 23123 * - `resolve` - `{object=}`: Similar to locals, except it takes promises as values 23124 * and the toast will not open until the promises resolve. 23125 * - `controllerAs` - `{string=}`: An alias to assign the controller to on the scope. 23126 * - `parent` - `{element=}`: The element to append the toast to. Defaults to appending 23127 * to the root element of the application. 23128 * 23129 * @returns {promise} A promise that can be resolved with `$mdToast.hide()` or 23130 * rejected with `$mdToast.cancel()`. `$mdToast.hide()` will resolve either with a Boolean 23131 * value == 'true' or the value passed as an argument to `$mdToast.hide()`. 23132 * And `$mdToast.cancel()` will resolve the promise with a Boolean value == 'false' 23133 */ 23134 23135/** 23136 * @ngdoc method 23137 * @name $mdToast#hide 23138 * 23139 * @description 23140 * Hide an existing toast and resolve the promise returned from `$mdToast.show()`. 23141 * 23142 * @param {*=} response An argument for the resolved promise. 23143 * 23144 * @returns {promise} a promise that is called when the existing element is removed from the DOM. 23145 * The promise is resolved with either a Boolean value == 'true' or the value passed as the 23146 * argument to `.hide()`. 23147 * 23148 */ 23149 23150/** 23151 * @ngdoc method 23152 * @name $mdToast#cancel 23153 * 23154 * @description 23155 * `DEPRECATED` - The promise returned from opening a toast is used only to notify about the closing of the toast. 23156 * As such, there isn't any reason to also allow that promise to be rejected, 23157 * since it's not clear what the difference between resolve and reject would be. 23158 * 23159 * Hide the existing toast and reject the promise returned from 23160 * `$mdToast.show()`. 23161 * 23162 * @param {*=} response An argument for the rejected promise. 23163 * 23164 * @returns {promise} a promise that is called when the existing element is removed from the DOM 23165 * The promise is resolved with a Boolean value == 'false'. 23166 * 23167 */ 23168 23169function MdToastProvider($$interimElementProvider) { 23170 // Differentiate promise resolves: hide timeout (value == true) and hide action clicks (value == ok). 23171 MdToastController.$inject = ["$mdToast", "$scope"]; 23172 toastDefaultOptions.$inject = ["$animate", "$mdToast", "$mdUtil", "$mdMedia"]; 23173 var ACTION_RESOLVE = 'ok'; 23174 23175 var activeToastContent; 23176 var $mdToast = $$interimElementProvider('$mdToast') 23177 .setDefaults({ 23178 methods: ['position', 'hideDelay', 'capsule', 'parent', 'position', 'toastClass'], 23179 options: toastDefaultOptions 23180 }) 23181 .addPreset('simple', { 23182 argOption: 'textContent', 23183 methods: ['textContent', 'content', 'action', 'highlightAction', 'highlightClass', 'theme', 'parent' ], 23184 options: /* @ngInject */ ["$mdToast", "$mdTheming", function($mdToast, $mdTheming) { 23185 return { 23186 template: 23187 '<md-toast md-theme="{{ toast.theme }}" ng-class="{\'md-capsule\': toast.capsule}">' + 23188 ' <div class="md-toast-content">' + 23189 ' <span class="md-toast-text" role="alert" aria-relevant="all" aria-atomic="true">' + 23190 ' {{ toast.content }}' + 23191 ' </span>' + 23192 ' <md-button class="md-action" ng-if="toast.action" ng-click="toast.resolve()" ' + 23193 ' ng-class="highlightClasses">' + 23194 ' {{ toast.action }}' + 23195 ' </md-button>' + 23196 ' </div>' + 23197 '</md-toast>', 23198 controller: MdToastController, 23199 theme: $mdTheming.defaultTheme(), 23200 controllerAs: 'toast', 23201 bindToController: true 23202 }; 23203 }] 23204 }) 23205 .addMethod('updateTextContent', updateTextContent) 23206 .addMethod('updateContent', updateTextContent); 23207 23208 function updateTextContent(newContent) { 23209 activeToastContent = newContent; 23210 } 23211 23212 return $mdToast; 23213 23214 /** 23215 * Controller for the Toast interim elements. 23216 * @ngInject 23217 */ 23218 function MdToastController($mdToast, $scope) { 23219 // For compatibility with AngularJS 1.6+, we should always use the $onInit hook in 23220 // interimElements. The $mdCompiler simulates the $onInit hook for all versions. 23221 this.$onInit = function() { 23222 var self = this; 23223 23224 if (self.highlightAction) { 23225 $scope.highlightClasses = [ 23226 'md-highlight', 23227 self.highlightClass 23228 ] 23229 } 23230 23231 $scope.$watch(function() { return activeToastContent; }, function() { 23232 self.content = activeToastContent; 23233 }); 23234 23235 this.resolve = function() { 23236 $mdToast.hide( ACTION_RESOLVE ); 23237 }; 23238 } 23239 } 23240 23241 /* @ngInject */ 23242 function toastDefaultOptions($animate, $mdToast, $mdUtil, $mdMedia) { 23243 var SWIPE_EVENTS = '$md.swipeleft $md.swiperight $md.swipeup $md.swipedown'; 23244 return { 23245 onShow: onShow, 23246 onRemove: onRemove, 23247 toastClass: '', 23248 position: 'bottom left', 23249 themable: true, 23250 hideDelay: 3000, 23251 autoWrap: true, 23252 transformTemplate: function(template, options) { 23253 var shouldAddWrapper = options.autoWrap && template && !/md-toast-content/g.test(template); 23254 23255 if (shouldAddWrapper) { 23256 // Root element of template will be <md-toast>. We need to wrap all of its content inside of 23257 // of <div class="md-toast-content">. All templates provided here should be static, developer-controlled 23258 // content (meaning we're not attempting to guard against XSS). 23259 var templateRoot = document.createElement('md-template'); 23260 templateRoot.innerHTML = template; 23261 23262 // Iterate through all root children, to detect possible md-toast directives. 23263 for (var i = 0; i < templateRoot.children.length; i++) { 23264 if (templateRoot.children[i].nodeName === 'MD-TOAST') { 23265 var wrapper = angular.element('<div class="md-toast-content">'); 23266 23267 // Wrap the children of the `md-toast` directive in jqLite, to be able to append multiple 23268 // nodes with the same execution. 23269 wrapper.append(angular.element(templateRoot.children[i].childNodes)); 23270 23271 // Append the new wrapped element to the `md-toast` directive. 23272 templateRoot.children[i].appendChild(wrapper[0]); 23273 } 23274 } 23275 23276 // We have to return the innerHTMl, because we do not want to have the `md-template` element to be 23277 // the root element of our interimElement. 23278 return templateRoot.innerHTML; 23279 } 23280 23281 return template || ''; 23282 } 23283 }; 23284 23285 function onShow(scope, element, options) { 23286 activeToastContent = options.textContent || options.content; // support deprecated #content method 23287 23288 var isSmScreen = !$mdMedia('gt-sm'); 23289 23290 element = $mdUtil.extractElementByName(element, 'md-toast', true); 23291 options.element = element; 23292 23293 options.onSwipe = function(ev, gesture) { 23294 //Add the relevant swipe class to the element so it can animate correctly 23295 var swipe = ev.type.replace('$md.',''); 23296 var direction = swipe.replace('swipe', ''); 23297 23298 // If the swipe direction is down/up but the toast came from top/bottom don't fade away 23299 // Unless the screen is small, then the toast always on bottom 23300 if ((direction === 'down' && options.position.indexOf('top') != -1 && !isSmScreen) || 23301 (direction === 'up' && (options.position.indexOf('bottom') != -1 || isSmScreen)
23301)) { 23302 return; 23303 } 23304 23305 if ((direction === 'left' || direction === 'right') && isSmScreen) { 23306 return; 23307 } 23308 23309 element.addClass('md-' + swipe); 23310 $mdUtil.nextTick($mdToast.cancel); 23311 }; 23312 options.openClass = toastOpenClass(options.position); 23313 23314 element.addClass(options.toastClass); 23315 23316 // 'top left' -> 'md-top md-left' 23317 options.parent.addClass(options.openClass); 23318 23319 // static is the default position 23320 if ($mdUtil.hasComputedStyle(options.parent, 'position', 'static')) { 23321 options.parent.css('position', 'relative'); 23322 } 23323 23324 element.on(SWIPE_EVENTS, options.onSwipe); 23325 element.addClass(isSmScreen ? 'md-bottom' : options.position.split(' ').map(function(pos) { 23326 return 'md-' + pos; 23327 }).join(' ')); 23328 23329 if (options.parent) options.parent.addClass('md-toast-animating'); 23330 return $animate.enter(element, options.parent).then(function() { 23331 if (options.parent) options.parent.removeClass('md-toast-animating'); 23332 }); 23333 } 23334 23335 function onRemove(scope, element, options) { 23336 element.off(SWIPE_EVENTS, options.onSwipe); 23337 if (options.parent) options.parent.addClass('md-toast-animating'); 23338 if (options.openClass) options.parent.removeClass(options.openClass); 23339 23340 return ((options.$destroy == true) ? element.remove() : $animate.leave(element)) 23341 .then(function () { 23342 if (options.parent) options.parent.removeClass('md-toast-animating'); 23343 if ($mdUtil.hasComputedStyle(options.parent, 'position', 'static')) { 23344 options.parent.css('position', ''); 23345 } 23346 }); 23347 } 23348 23349 function toastOpenClass(position) { 23350 // For mobile, always open full-width on bottom 23351 if (!$mdMedia('gt-xs')) { 23352 return 'md-toast-open-bottom'; 23353 } 23354 23355 return 'md-toast-open-' + 23356 (position.indexOf('top') > -1 ? 'top' : 'bottom'); 23357 } 23358 } 23359 23360} 23361 23362})(); 23363(function(){ 23364"use strict"; 23365 23366/** 23367 * @ngdoc module 23368 * @name material.components.toolbar 23369 */ 23370mdToolbarDirective.$inject = ["$$rAF", "$mdConstant", "$mdUtil", "$mdTheming", "$animate"]; 23371angular.module('material.components.toolbar', [ 23372 'material.core', 23373 'material.components.content' 23374]) 23375 .directive('mdToolbar', mdToolbarDirective); 23376 23377/** 23378 * @ngdoc directive 23379 * @name mdToolbar 23380 * @module material.components.toolbar 23381 * @restrict E 23382 * @description 23383 * `md-toolbar` is used to place a toolbar in your app. 23384 * 23385 * Toolbars are usually used above a content area to display the title of the 23386 * current page, and show relevant action buttons for that page. 23387 * 23388 * You can change the height of the toolbar by adding either the 23389 * `md-medium-tall` or `md-tall` class to the toolbar. 23390 * 23391 * @usage 23392 * <hljs lang="html"> 23393 * <div layout="column" layout-fill> 23394 * <md-toolbar> 23395 * 23396 * <div class="md-toolbar-tools"> 23397 * <h2 md-truncate flex>My App's Title</h2> 23398 * 23399 * <md-button> 23400 * Right Bar Button 23401 * </md-button> 23402 * </div> 23403 * 23404 * </md-toolbar> 23405 * <md-content> 23406 * Hello! 23407 * </md-content> 23408 * </div> 23409 * </hljs> 23410 * 23411 * <i><b>Note:</b> The code above shows usage with the `md-truncate` component which provides an 23412 * ellipsis if the title is longer than the width of the Toolbar.</i> 23413 * 23414 * ## CSS & Styles 23415 * 23416 * The `<md-toolbar>` provides a few custom CSS classes that you may use to enhance the 23417 * functionality of your toolbar. 23418 * 23419 * <div> 23420 * <docs-css-api-table> 23421 * 23422 * <docs-css-selector code="md-toolbar .md-toolbar-tools"> 23423 * The `md-toolbar-tools` class provides quite a bit of automatic styling for your toolbar 23424 * buttons and text. When applied, it will center the buttons and text vertically for you. 23425 * </docs-css-selector> 23426 * 23427 * </docs-css-api-table> 23428 * </div> 23429 * 23430 * ### Private Classes 23431 * 23432 * Currently, the only private class is the `md-toolbar-transitions` class. All other classes are 23433 * considered public. 23434 * 23435 * @param {boolean=} md-scroll-shrink Whether the header should shrink away as 23436 * the user scrolls down, and reveal itself as the user scrolls up. 23437 * 23438 * _**Note (1):** for scrollShrink to work, the toolbar must be a sibling of a 23439 * `md-content` element, placed before it. See the scroll shrink demo._ 23440 * 23441 * _**Note (2):** The `md-scroll-shrink` attribute is only parsed on component 23442 * initialization, it does not watch for scope changes._ 23443 * 23444 * 23445 * @param {number=} md-shrink-speed-factor How much to change the speed of the toolbar's 23446 * shrinking by. For example, if 0.25 is given then the toolbar will shrink 23447 * at one fourth the rate at which the user scrolls down. Default 0.5.
vendor: 6,872 bytes, lines 23448-23667
23448 * 23449 */ 23450 23451function mdToolbarDirective($$rAF, $mdConstant, $mdUtil, $mdTheming, $animate) { 23452 var translateY = angular.bind(null, $mdUtil.supplant, 'translate3d(0,{0}px,0)'); 23453 23454 return { 23455 template: '', 23456 restrict: 'E', 23457 23458 link: function(scope, element, attr) { 23459 23460 element.addClass('_md'); // private md component indicator for styling 23461 $mdTheming(element); 23462 23463 $mdUtil.nextTick(function () { 23464 element.addClass('_md-toolbar-transitions'); // adding toolbar transitions after digest 23465 }, false); 23466 23467 if (angular.isDefined(attr.mdScrollShrink)) { 23468 setupScrollShrink(); 23469 } 23470 23471 function setupScrollShrink() { 23472 23473 var toolbarHeight; 23474 var contentElement; 23475 var disableScrollShrink = angular.noop; 23476 23477 // Current "y" position of scroll 23478 // Store the last scroll top position 23479 var y = 0; 23480 var prevScrollTop = 0; 23481 var shrinkSpeedFactor = attr.mdShrinkSpeedFactor || 0.5; 23482 23483 var debouncedContentScroll = $$rAF.throttle(onContentScroll); 23484 var debouncedUpdateHeight = $mdUtil.debounce(updateToolbarHeight, 5 * 1000); 23485 23486 // Wait for $mdContentLoaded event from mdContent directive. 23487 // If the mdContent element is a sibling of our toolbar, hook it up 23488 // to scroll events. 23489 23490 scope.$on('$mdContentLoaded', onMdContentLoad); 23491 23492 // If the toolbar is used inside an ng-if statement, we may miss the 23493 // $mdContentLoaded event, so we attempt to fake it if we have a 23494 // md-content close enough. 23495 23496 attr.$observe('mdScrollShrink', onChangeScrollShrink); 23497 23498 // If the toolbar has ngShow or ngHide we need to update height immediately as it changed 23499 // and not wait for $mdUtil.debounce to happen 23500 23501 if (attr.ngShow) { scope.$watch(attr.ngShow, updateToolbarHeight); } 23502 if (attr.ngHide) { scope.$watch(attr.ngHide, updateToolbarHeight); } 23503 23504 // If the scope is destroyed (which could happen with ng-if), make sure 23505 // to disable scroll shrinking again 23506 23507 scope.$on('$destroy', disableScrollShrink); 23508 23509 /** 23510 * 23511 */ 23512 function onChangeScrollShrink(shrinkWithScroll) { 23513 var closestContent = element.parent().find('md-content'); 23514 23515 // If we have a content element, fake the call; this might still fail 23516 // if the content element isn't a sibling of the toolbar 23517 23518 if (!contentElement && closestContent.length) { 23519 onMdContentLoad(null, closestContent); 23520 } 23521 23522 // Evaluate the expression 23523 shrinkWithScroll = scope.$eval(shrinkWithScroll); 23524 23525 // Disable only if the attribute's expression evaluates to false 23526 if (shrinkWithScroll === false) { 23527 disableScrollShrink(); 23528 } else { 23529 disableScrollShrink = enableScrollShrink(); 23530 } 23531 } 23532 23533 /** 23534 * 23535 */ 23536 function onMdContentLoad($event, newContentEl) { 23537 // Toolbar and content must be siblings 23538 if (newContentEl && element.parent()[0] === newContentEl.parent()[0]) { 23539 // unhook old content event listener if exists 23540 if (contentElement) { 23541 contentElement.off('scroll', debouncedContentScroll); 23542 } 23543 23544 contentElement = newContentEl; 23545 disableScrollShrink = enableScrollShrink(); 23546 } 23547 } 23548 23549 /** 23550 * 23551 */ 23552 function onContentScroll(e) { 23553 var scrollTop = e ? e.target.scrollTop : prevScrollTop; 23554 23555 debouncedUpdateHeight(); 23556 23557 y = Math.min( 23558 toolbarHeight / shrinkSpeedFactor, 23559 Math.max(0, y + scrollTop - prevScrollTop) 23560 ); 23561 23562 element.css($mdConstant.CSS.TRANSFORM, translateY([-y * shrinkSpeedFactor])); 23563 contentElement.css($mdConstant.CSS.TRANSFORM, translateY([(toolbarHeight - y) * shrinkSpeedFactor])); 23564 23565 prevScrollTop = scrollTop; 23566 23567 $mdUtil.nextTick(function() { 23568 var hasWhiteFrame = element.hasClass('md-whiteframe-z1'); 23569 23570 if (hasWhiteFrame && !y) { 23571 $animate.removeClass(element, 'md-whiteframe-z1'); 23572 } else if (!hasWhiteFrame && y) { 23573 $animate.addClass(element, 'md-whiteframe-z1'); 23574 } 23575 }); 23576 23577 } 23578 23579 /** 23580 * 23581 */ 23582 function enableScrollShrink() { 23583 if (!contentElement) return angular.noop; // no md-content 23584 23585 contentElement.on('scroll', debouncedContentScroll); 23586 contentElement.attr('scroll-shrink', 'true'); 23587 23588 $mdUtil.nextTick(updateToolbarHeight, false); 23589 23590 return function disableScrollShrink() { 23591 contentElement.off('scroll', debouncedContentScroll); 23592 contentElement.attr('scroll-shrink', 'false'); 23593 23594 updateToolbarHeight(); 23595 }; 23596 } 23597 23598 /** 23599 * 23600 */ 23601 function updateToolbarHeight() { 23602 toolbarHeight = element.prop('offsetHeight'); 23603 // Add a negative margin-top the size of the toolbar to the content el. 23604 // The content will start transformed down the toolbarHeight amount, 23605 // so everything looks normal. 23606 // 23607 // As the user scrolls down, the content will be transformed up slowly 23608 // to put the content underneath where the toolbar was. 23609 var margin = (-toolbarHeight * shrinkSpeedFactor) + 'px'; 23610 23611 contentElement.css({ 23612 "margin-top": margin, 23613 "margin-bottom": margin 23614 }); 23615 23616 onContentScroll(); 23617 } 23618 23619 } 23620 23621 } 23622 }; 23623 23624} 23625 23626})(); 23627(function(){ 23628"use strict"; 23629 23630/** 23631 * @ngdoc module 23632 * @name material.components.tooltip 23633 */ 23634MdTooltipDirective.$inject = ["$timeout", "$window", "$$rAF", "$document", "$interpolate", "$mdUtil", "$mdPanel", "$$mdTooltipRegistry"]; 23635angular 23636 .module('material.components.tooltip', [ 23637 'material.core', 23638 'material.components.panel' 23639 ]) 23640 .directive('mdTooltip', MdTooltipDirective) 23641 .service('$$mdTooltipRegistry', MdTooltipRegistry); 23642 23643 23644/** 23645 * @ngdoc directive 23646 * @name mdTooltip 23647 * @module material.components.tooltip 23648 * @description 23649 * Tooltips are used to describe elements that are interactive and primarily 23650 * graphical (not textual). 23651 * 23652 * Place a `<md-tooltip>` as a child of the element it describes. 23653 * 23654 * A tooltip will activate when the user hovers over, focuses, or touches the 23655 * parent element. 23656 * 23657 * @usage 23658 * <hljs lang="html"> 23659 * <md-button class="md-fab md-accent" aria-label="Play"> 23660 * <md-tooltip>Play Music</md-tooltip> 23661 * <md-icon md-svg-src="img/icons/ic_play_arrow_24px.svg"></md-icon> 23662 * </md-button> 23663 * </hljs> 23664 * 23665 * @param {number=} md-z-index The visual level that the tooltip will appear 23666 * in comparison with the rest of the elements of the application. 23667 * @param {expression=}
23667 md-visible Boolean bound to whether the tooltip is 23668 * currently visible. 23669 * @param {number=} md-delay How many milliseconds to wait to show the tooltip 23670 * after the user hovers over, focuses, or touches the parent element. 23671 * Defaults to 0ms on non-touch devices and 75ms on touch. 23672 * @param {boolean=} md-autohide If present or provided with a boolean value, 23673 * the tooltip will hide on mouse leave, regardless of focus. 23674 * @param {string=} md-direction The direction that the tooltip is shown, 23675 * relative to the parent element. Supports top, right, bottom, and left. 23676 * Defaults to bottom. 23677 */ 23678function MdTooltipDirective($timeout, $window, $$rAF, $document, $interpolate, 23679 $mdUtil, $mdPanel, $$mdTooltipRegistry) { 23680 23681 var ENTER_EVENTS = 'focus touchstart mouseenter'; 23682 var LEAVE_EVENTS = 'blur touchcancel mouseleave'; 23683 var TOOLTIP_DEFAULT_Z_INDEX = 100; 23684 var TOOLTIP_DEFAULT_SHOW_DELAY = 0; 23685 var TOOLTIP_DEFAULT_DIRECTION = 'bottom'; 23686 var TOOLTIP_DIRECTIONS = { 23687 top: { x: $mdPanel.xPosition.CENTER, y: $mdPanel.yPosition.ABOVE }, 23688 right: { x: $mdPanel.xPosition.OFFSET_END, y: $mdPanel.yPosition.CENTER }, 23689 bottom: { x: $mdPanel.xPosition.CENTER, y: $mdPanel.yPosition.BELOW }, 23690 left: { x: $mdPanel.xPosition.OFFSET_START, y: $mdPanel.yPosition.CENTER } 23691 }; 23692 23693 return { 23694 restrict: 'E', 23695 priority: 210, // Before ngAria 23696 scope: { 23697 mdZIndex: '=?mdZIndex', 23698 mdDelay: '=?mdDelay', 23699 mdVisible: '=?mdVisible', 23700 mdAutohide: '=?mdAutohide', 23701 mdDirection: '@?mdDirection' // Do not expect expressions. 23702 }, 23703 link: linkFunc 23704 }; 23705 23706 function linkFunc(scope, element, attr) { 23707 // Set constants. 23708 var tooltipId = 'md-tooltip-' + $mdUtil.nextUid(); 23709 var parent = $mdUtil.getParentWithPointerEvents(element); 23710 var debouncedOnResize = $$rAF.throttle(updatePosition); 23711 var mouseActive = false; 23712 var origin, position, panelPosition, panelRef, autohide, showTimeout, 23713 elementFocusedOnWindowBlur = null; 23714 23715 // Set defaults 23716 setDefaults(); 23717 23718 // Set parent aria-label. 23719 addAriaLabel(); 23720 23721 // Remove the element from its current DOM position. 23722 element.detach(); 23723 23724 updatePosition(); 23725 bindEvents(); 23726 configureWatchers(); 23727 23728 function setDefaults() { 23729 scope.mdZIndex = scope.mdZIndex || TOOLTIP_DEFAULT_Z_INDEX; 23730 scope.mdDelay = scope.mdDelay || TOOLTIP_DEFAULT_SHOW_DELAY; 23731 if (!TOOLTIP_DIRECTIONS[scope.mdDirection]) { 23732 scope.mdDirection = TOOLTIP_DEFAULT_DIRECTION; 23733 } 23734 } 23735 23736 function addAriaLabel(labelText) { 23737 // Only interpolate the text from the HTML element because otherwise the custom text could 23738 // be interpolated twice and cause XSS violations. 23739 var interpolatedText = labelText || $interpolate(element.text().trim())(scope.$parent); 23740 23741 // Only add the `aria-label` to the parent if there isn't already one, if there isn't an 23742 // already present `aria-labelledby`, or if the previous `aria-label` was added by the 23743 // tooltip directive. 23744 if ( 23745 (!parent.attr('aria-label') && !parent.attr('aria-labelledby')) || 23746 parent.attr('md-labeled-by-tooltip') 23747 ) { 23748 parent.attr('aria-label', interpolatedText); 23749 23750 // Set the `md-labeled-by-tooltip` attribute if it has not already been set. 23751 if (!parent.attr('md-labeled-by-tooltip')) { 23752 parent.attr('md-labeled-by-tooltip', tooltipId); 23753 } 23754 } 23755 } 23756 23757 function updatePosition() { 23758 setDefaults(); 23759 23760 // If the panel has already been created, remove the current origin 23761 // class from the panel element. 23762 if (panelRef && panelRef.panelEl) { 23763 panelRef.panelEl.removeClass(origin); 23764 } 23765 23766 // Set the panel element origin class based off of the current 23767 // mdDirection. 23768 origin = 'md-origin-' + scope.mdDirection; 23769 23770 // Create the position of the panel based off of the mdDirection. 23771 position = TOOLTIP_DIRECTIONS[scope.mdDirection]; 23772 23773 // Using the newly created position object, use the MdPanel 23774 // panelPosition API to build the panel's position. 23775 panelPosition = $mdPanel.newPanelPosition() 23776 .relativeTo(parent) 23777 .addPanelPosition(position.x, position.y); 23778 23779 // If the panel has already been created, add the new origin class to 23780 // the panel element and update it's position with the panelPosition. 23781 if (panelRef && panelRef.panelEl) { 23782 panelRef.panelEl.addClass(origin); 23783 panelRef.updatePosition(panelPosition); 23784 } 23785 } 23786 23787 function bindEvents() { 23788 // Add a mutationObserver where there is support for it and the need 23789 // for it in the form of viable host(parent[0]). 23790 if (parent[0] && 'MutationObserver' in $window) { 23791 // Use a mutationObserver to tackle #2602. 23792 var attributeObserver = new MutationObserver(function(mutations) { 23793 if (isDisabledMutation(mutations)) { 23794 $mdUtil.nextTick(function() { 23795 setVisible(false); 23796 }); 23797 } 23798 }); 23799 23800 attributeObserver.observe(parent[0], { 23801 attributes: true 23802 }); 23803 } 23804 23805 elementFocusedOnWindowBlur = false;
vendor: 28,185 bytes, lines 23806-24657
23806 23807 $$mdTooltipRegistry.register('scroll', windowScrollEventHandler, true); 23808 $$mdTooltipRegistry.register('blur', windowBlurEventHandler); 23809 $$mdTooltipRegistry.register('resize', debouncedOnResize); 23810 23811 scope.$on('$destroy', onDestroy); 23812 23813 // To avoid 'synthetic clicks', we listen to mousedown instead of 23814 // 'click'. 23815 parent.on('mousedown', mousedownEventHandler); 23816 parent.on(ENTER_EVENTS, enterEventHandler); 23817 23818 function isDisabledMutation(mutations) { 23819 mutations.some(function(mutation) { 23820 return mutation.attributeName === 'disabled' && parent[0].disabled; 23821 }); 23822 return false; 23823 } 23824 23825 function windowScrollEventHandler() { 23826 setVisible(false); 23827 } 23828 23829 function windowBlurEventHandler() { 23830 elementFocusedOnWindowBlur = document.activeElement === parent[0]; 23831 } 23832 23833 function enterEventHandler($event) { 23834 // Prevent the tooltip from showing when the window is receiving 23835 // focus. 23836 if ($event.type === 'focus' && elementFocusedOnWindowBlur) { 23837 elementFocusedOnWindowBlur = false; 23838 } else if (!scope.mdVisible) { 23839 parent.on(LEAVE_EVENTS, leaveEventHandler); 23840 setVisible(true); 23841 23842 // If the user is on a touch device, we should bind the tap away 23843 // after the 'touched' in order to prevent the tooltip being 23844 // removed immediately. 23845 if ($event.type === 'touchstart') { 23846 parent.one('touchend', function() { 23847 $mdUtil.nextTick(function() { 23848 $document.one('touchend', leaveEventHandler); 23849 }, false); 23850 }); 23851 } 23852 } 23853 } 23854 23855 function leaveEventHandler() { 23856 autohide = scope.hasOwnProperty('mdAutohide') ? 23857 scope.mdAutohide : 23858 attr.hasOwnProperty('mdAutohide'); 23859 23860 if (autohide || mouseActive || 23861 $document[0].activeElement !== parent[0]) { 23862 // When a show timeout is currently in progress, then we have 23863 // to cancel it, otherwise the tooltip will remain showing 23864 // without focus or hover. 23865 if (showTimeout) { 23866 $timeout.cancel(showTimeout); 23867 setVisible.queued = false; 23868 showTimeout = null; 23869 } 23870 23871 parent.off(LEAVE_EVENTS, leaveEventHandler); 23872 parent.triggerHandler('blur'); 23873 setVisible(false); 23874 } 23875 mouseActive = false; 23876 } 23877 23878 function mousedownEventHandler() { 23879 mouseActive = true; 23880 } 23881 23882 function onDestroy() { 23883 $$mdTooltipRegistry.deregister('scroll', windowScrollEventHandler, true); 23884 $$mdTooltipRegistry.deregister('blur', windowBlurEventHandler); 23885 $$mdTooltipRegistry.deregister('resize', debouncedOnResize); 23886 23887 parent 23888 .off(ENTER_EVENTS, enterEventHandler) 23889 .off(LEAVE_EVENTS, leaveEventHandler) 23890 .off('mousedown', mousedownEventHandler); 23891 23892 // Trigger the handler in case any of the tooltips are 23893 // still visible. 23894 leaveEventHandler(); 23895 attributeObserver && attributeObserver.disconnect(); 23896 } 23897 } 23898 23899 function configureWatchers() { 23900 if (element[0] && 'MutationObserver' in $window) { 23901 var attributeObserver = new MutationObserver(function(mutations) { 23902 mutations.forEach(function(mutation) { 23903 if (mutation.attributeName === 'md-visible' && 23904 !scope.visibleWatcher ) { 23905 scope.visibleWatcher = scope.$watch('mdVisible', 23906 onVisibleChanged); 23907 } 23908 }); 23909 }); 23910 23911 attributeObserver.observe(element[0], { 23912 attributes: true 23913 }); 23914 23915 // Build watcher only if mdVisible is being used. 23916 if (attr.hasOwnProperty('mdVisible')) { 23917 scope.visibleWatcher = scope.$watch('mdVisible', 23918 onVisibleChanged); 23919 } 23920 } else { 23921 // MutationObserver not supported 23922 scope.visibleWatcher = scope.$watch('mdVisible', onVisibleChanged); 23923 } 23924 23925 // Direction watcher 23926 scope.$watch('mdDirection', updatePosition); 23927 23928 // Clean up if the element or parent was removed via jqLite's .remove. 23929 // A couple of notes: 23930 // - In these cases the scope might not have been destroyed, which 23931 // is why we destroy it manually. An example of this can be having 23932 // `md-visible="false"` and adding tooltips while they're 23933 // invisible. If `md-visible` becomes true, at some point, you'd 23934 // usually get a lot of tooltips. 23935 // - We use `.one`, not `.on`, because this only needs to fire once. 23936 // If we were using `.on`, it would get thrown into an infinite 23937 // loop. 23938 // - This kicks off the scope's `$destroy` event which finishes the 23939 // cleanup. 23940 element.one('$destroy', onElementDestroy); 23941 parent.one('$destroy', onElementDestroy); 23942 scope.$on('$destroy', function() { 23943 setVisible(false); 23944 panelRef && panelRef.destroy(); 23945 attributeObserver && attributeObserver.disconnect(); 23946 element.remove(); 23947 }); 23948 23949 // Updates the aria-label when the element text changes. This watch 23950 // doesn't need to be set up if the element doesn't have any data 23951 // bindings. 23952 if (element.text().indexOf($interpolate.startSymbol()) > -1) { 23953 scope.$watch(function() { 23954 return element.text().trim(); 23955 }, addAriaLabel); 23956 } 23957 23958 function onElementDestroy() { 23959 scope.$destroy(); 23960 } 23961 } 23962 23963 function setVisible(value) { 23964 // Break if passed value is already in queue or there is no queue and 23965 // passed value is current in the controller. 23966 if (setVisible.queued && setVisible.value === !!value || 23967 !setVisible.queued && scope.mdVisible === !!value) { 23968 return; 23969 } 23970 setVisible.value = !!value; 23971 23972 if (!setVisible.queued) { 23973 if (value) { 23974 setVisible.queued = true; 23975 showTimeout = $timeout(function() { 23976 scope.mdVisible = setVisible.value; 23977 setVisible.queued = false; 23978 showTimeout = null; 23979 if (!scope.visibleWatcher) { 23980 onVisibleChanged(scope.mdVisible); 23981 } 23982 }, scope.mdDelay); 23983 } else { 23984 $mdUtil.nextTick(function() { 23985 scope.mdVisible = false; 23986 if (!scope.visibleWatcher) { 23987 onVisibleChanged(false); 23988 } 23989 }); 23990 } 23991 } 23992 } 23993 23994 function onVisibleChanged(isVisible) { 23995 isVisible ? showTooltip() : hideTooltip(); 23996 } 23997 23998 function showTooltip() { 23999 // Do not show the tooltip if the text is empty. 24000 if (!element[0].textContent.trim()) { 24001 throw new Error('Text for the tooltip has not been provided. ' + 24002 'Please include text within the mdTooltip element.'); 24003 } 24004 24005 if (!panelRef) { 24006 var attachTo = angular.element(document.body); 24007 var panelAnimation = $mdPanel.newPanelAnimation() 24008 .openFrom(parent) 24009 .closeTo(parent) 24010 .withAnimation({ 24011 open: 'md-show', 24012 close: 'md-hide' 24013 }); 24014 24015 var panelConfig = { 24016 id: tooltipId, 24017 attachTo: attachTo, 24018 contentElement: element, 24019 propagateContainerEvents: true, 24020 panelClass: 'md-tooltip ' + origin, 24021 animation: panelAnimation, 24022 position: panelPosition, 24023 zIndex: scope.mdZIndex, 24024 focusOnOpen: false 24025 }; 24026 24027 panelRef = $mdPanel.create(panelConfig); 24028 } 24029 24030 panelRef.open().then(function() { 24031 panelRef.panelEl.attr('role', 'tooltip'); 24032 }); 24033 } 24034 24035 function hideTooltip() { 24036 panelRef && panelRef.close(); 24037 } 24038 } 24039 24040} 24041 24042 24043/** 24044 * Service that is used to reduce the amount of listeners that are being 24045 * registered on the `window` by the tooltip component. Works by collecting 24046 * the individual event handlers and dispatching them from a global handler. 24047 * 24048 * @ngInject 24049 */ 24050function MdTooltipRegistry() { 24051 var listeners = {}; 24052 var ngWindow = angular.element(window); 24053 24054 return { 24055 register: register, 24056 deregister: deregister 24057 }; 24058 24059 /** 24060 * Global event handler that dispatches the registered handlers in the 24061 * service. 24062 * @param {!Event} event Event object passed in by the browser 24063 */ 24064 function globalEventHandler(event) { 24065 if (listeners[event.type]) { 24066 listeners[event.type].forEach(function(currentHandler) { 24067 currentHandler.call(this, event); 24068 }, this); 24069 } 24070 } 24071 24072 /** 24073 * Registers a new handler with the service. 24074 * @param {string} type Type of event to be registered. 24075 * @param {!Function} handler Event handler. 24076 * @param {boolean} useCapture Whether to use event capturing. 24077 */ 24078 function register(type, handler, useCapture) { 24079 var handlers = listeners[type] = listeners[type] || []; 24080 24081 if (!handlers.length) { 24082 useCapture ? window.addEventListener(type, globalEventHandler, true) : 24083 ngWindow.on(type, globalEventHandler); 24084 } 24085 24086 if (handlers.indexOf(handler) === -1) { 24087 handlers.push(handler); 24088 } 24089 } 24090 24091 /** 24092 * Removes an event handler from the service. 24093 * @param {string} type Type of event handler. 24094 * @param {!Function} handler The event handler itself. 24095 * @param {boolean} useCapture Whether the event handler used event capturing. 24096 */ 24097 function deregister(type, handler, useCapture) { 24098 var handlers = listeners[type]; 24099 var index = handlers ? handlers.indexOf(handler) : -1; 24100 24101 if (index > -1) { 24102 handlers.splice(index, 1); 24103 24104 if (handlers.length === 0) { 24105 useCapture ? window.removeEventListener(type, globalEventHandler, true) : 24106 ngWindow.off(type, globalEventHandler); 24107 } 24108 } 24109 } 24110} 24111 24112})(); 24113(function(){ 24114"use strict"; 24115 24116/** 24117 * @ngdoc module 24118 * @name material.components.truncate 24119 */ 24120MdTruncateController.$inject = ["$element"]; 24121angular.module('material.components.truncate', ['material.core']) 24122 .directive('mdTruncate', MdTruncateDirective); 24123 24124/** 24125 * @ngdoc directive 24126 * @name mdTruncate 24127 * @module material.components.truncate 24128 * @restrict AE 24129 * @description 24130 * 24131 * The `md-truncate` component displays a label that will automatically clip text which is wider 24132 * than the component. By default, it displays an ellipsis, but you may apply the `md-clip` CSS 24133 * class to override this default and use a standard "clipping" approach. 24134 * 24135 * <i><b>Note:</b> The `md-truncate` component does not automatically adjust it's width. You must 24136 * provide the `flex` attribute, or some other CSS-based width management. See the 24137 * <a ng-href="./demo/truncate">demos</a> for examples.</i> 24138 * 24139 * @usage 24140 * 24141 * ### As an Element 24142 * 24143 * <hljs lang="html"> 24144 * <div layout="row"> 24145 * <md-button>Back</md-button> 24146 * 24147 * <md-truncate flex>Chapter 1 - The Way of the Old West</md-truncate> 24148 * 24149 * <md-button>Forward</md-button> 24150 * </div> 24151 * </hljs> 24152 * 24153 * ### As an Attribute 24154 * 24155 * <hljs lang="html"> 24156 * <h2 md-truncate style="max-width: 100px;">Some Title With a Lot of Text</h2> 24157 * </hljs> 24158 * 24159 * ## CSS & Styles 24160 * 24161 * `<md-truncate>` provides two CSS classes that you may use to control the type of clipping. 24162 * 24163 * <i><b>Note:</b> The `md-truncate` also applies a setting of `width: 0;` when used with the `flex` 24164 * attribute to fix an issue with the flex element not shrinking properly.</i> 24165 * 24166 * <div> 24167 * <docs-css-api-table> 24168 * 24169 * <docs-css-selector code=".md-ellipsis"> 24170 * Assigns the "ellipsis" behavior (default) which will cut off mid-word and append an ellipsis 24171 * (…) to the end of the text. 24172 * </docs-css-selector> 24173 * 24174 * <docs-css-selector code=".md-clip"> 24175 * Assigns the "clipping" behavior which will simply chop off the text. This may happen 24176 * mid-word or even mid-character. 24177 * </docs-css-selector> 24178 * 24179 * </docs-css-api-table> 24180 * </div> 24181 */ 24182function MdTruncateDirective() { 24183 return { 24184 restrict: 'AE', 24185 24186 controller: MdTruncateController, 24187 controllerAs: '$ctrl', 24188 bindToController: true 24189 } 24190} 24191 24192/** 24193 * Controller for the <md-truncate> component. 24194 * 24195 * @param $element The md-truncate element. 24196 * 24197 * @constructor 24198 * @ngInject 24199 */ 24200function MdTruncateController($element) { 24201 $element.addClass('md-truncate'); 24202} 24203 24204})(); 24205(function(){ 24206"use strict"; 24207 24208/** 24209 * @ngdoc module 24210 * @name material.components.virtualRepeat 24211 */ 24212VirtualRepeatContainerController.$inject = ["$$rAF", "$mdUtil", "$mdConstant", "$parse", "$rootScope", "$window", "$scope", "$element", "$attrs"]; 24213VirtualRepeatController.$inject = ["$scope", "$element", "$attrs", "$browser", "$document", "$rootScope", "$$rAF", "$mdUtil"]; 24214VirtualRepeatDirective.$inject = ["$parse"]; 24215angular.module('material.components.virtualRepeat', [ 24216 'material.core', 24217 'material.components.showHide' 24218]) 24219.directive('mdVirtualRepeatContainer', VirtualRepeatContainerDirective) 24220.directive('mdVirtualRepeat', VirtualRepeatDirective) 24221.directive('mdForceHeight', ForceHeightDirective); 24222 24223 24224/** 24225 * @ngdoc directive 24226 * @name mdVirtualRepeatContainer 24227 * @module material.components.virtualRepeat 24228 * @restrict E 24229 * @description 24230 * `md-virtual-repeat-container` provides the scroll container for md-virtual-repeat. 24231 * 24232 * VirtualRepeat is a limited substitute for ng-repeat that renders only 24233 * enough DOM nodes to fill the container and recycling them as the user scrolls. 24234 * 24235 * Once an element is not visible anymore, the VirtualRepeat recycles it and will reuse it for 24236 * another visible item by replacing the previous dataset with the new one. 24237 * 24238 * ### Common Issues 24239 * 24240 * - When having one-time bindings inside of the view template, the VirtualRepeat will not properly 24241 * update the bindings for new items, since the view will be recycled. 24242 * 24243 * - Directives inside of a VirtualRepeat will be only compiled (linked) once, because those 24244 * are will be recycled items and used for other items. 24245 * The VirtualRepeat just updates the scope bindings. 24246 * 24247 * 24248 * ### Notes 24249 * 24250 * > The VirtualRepeat is a similar implementation to the Android 24251 * [RecyclerView](https://developer.android.com/reference/android/support/v7/widget/RecyclerView.html) 24252 * 24253 * <!-- This comment forces a break between blockquotes //--> 24254 * 24255 * > Please also review the <a ng-href="api/directive/mdVirtualRepeat">VirtualRepeat</a> 24256 * documentation for more information. 24257 * 24258 * 24259 * @usage 24260 * <hljs lang="html"> 24261 * 24262 * <md-virtual-repeat-container md-top-index="topIndex"> 24263 * <div md-virtual-repeat="i in items" md-item-size="20">Hello {{i}}!</div> 24264 * </md-virtual-repeat-container> 24265 * </hljs> 24266 * 24267 * @param {number=} md-top-index Binds the index of the item that is at the top of the scroll 24268 * container to $scope. It can both read and set the scroll position. 24269 * @param {boolean=} md-orient-horizontal Whether the container should scroll horizontally 24270 * (defaults to orientation and scrolling vertically). 24271 * @param {boolean=} md-auto-shrink When present, the container will shrink to fit 24272 * the number of items when that number is less than its original size. 24273 * @param {number=} md-auto-shrink-min Minimum number of items that md-auto-shrink 24274 * will shrink to (default: 0). 24275 */ 24276function VirtualRepeatContainerDirective() { 24277 return { 24278 controller: VirtualRepeatContainerController, 24279 template: virtualRepeatContainerTemplate, 24280 compile: function virtualRepeatContainerCompile($element, $attrs) { 24281 $element 24282 .addClass('md-virtual-repeat-container') 24283 .addClass($attrs.hasOwnProperty('mdOrientHorizontal') 24284 ? 'md-orient-horizontal' 24285 : 'md-orient-vertical'); 24286 } 24287 }; 24288} 24289 24290 24291function virtualRepeatContainerTemplate($element) { 24292 return '<div class="md-virtual-repeat-scroller" role="presentation">' + 24293 '<div class="md-virtual-repeat-sizer" role="presentation"></div>' + 24294 '<div class="md-virtual-repeat-offsetter" role="presentation">' + 24295 $element[0].innerHTML + 24296 '</div></div>'; 24297} 24298 24299/** 24300 * Number of additional elements to render above and below the visible area inside 24301 * of the virtual repeat container. A higher number results in less flicker when scrolling 24302 * very quickly in Safari, but comes with a higher rendering and dirty-checking cost. 24303 * @const {number} 24304 */ 24305var NUM_EXTRA = 3; 24306 24307/** @ngInject */ 24308function VirtualRepeatContainerController($$rAF, $mdUtil, $mdConstant, $parse, $rootScope, $window, $scope, 24309 $element, $attrs) { 24310 this.$rootScope = $rootScope; 24311 this.$scope = $scope; 24312 this.$element = $element; 24313 this.$attrs = $attrs; 24314 24315 /** @type {number} The width or height of the container */ 24316 this.size = 0; 24317 /** @type {number} The scroll width or height of the scroller */ 24318 this.scrollSize = 0; 24319 /** @type {number} The scrollLeft or scrollTop of the scroller */ 24320 this.scrollOffset = 0; 24321 /** @type {boolean} Whether the scroller is oriented horizontally */ 24322 this.horizontal = this.$attrs.hasOwnProperty('mdOrientHorizontal'); 24323 /** @type {!VirtualRepeatController} The repeater inside of this container */ 24324 this.repeater = null; 24325 /** @type {boolean} Whether auto-shrink is enabled */ 24326 this.autoShrink = this.$attrs.hasOwnProperty('mdAutoShrink'); 24327 /** @type {number} Minimum number of items to auto-shrink to */ 24328 this.autoShrinkMin = parseInt(this.$attrs.mdAutoShrinkMin, 10) || 0; 24329 /** @type {?number} Original container size when shrank */ 24330 this.originalSize = null; 24331 /** @type {number} Amount to offset the total scroll size by. */ 24332 this.offsetSize = parseInt(this.$attrs.mdOffsetSize, 10) || 0; 24333 /** @type {?string} height or width element style on the container prior to auto-shrinking. */ 24334 this.oldElementSize = null; 24335 /** @type {!number} Maximum amount of pixels allowed for a single DOM element */ 24336 this.maxElementPixels = $mdConstant.ELEMENT_MAX_PIXELS; 24337 24338 if (this.$attrs.mdTopIndex) { 24339 /** @type {function(angular.Scope): number} Binds to topIndex on AngularJS scope */ 24340 this.bindTopIndex = $parse(this.$attrs.mdTopIndex); 24341 /** @type {number} The index of the item that is at the top of the scroll container */ 24342 this.topIndex = this.bindTopIndex(this.$scope); 24343 24344 if (!angular.isDefined(this.topIndex)) { 24345 this.topIndex = 0; 24346 this.bindTopIndex.assign(this.$scope, 0); 24347 } 24348 24349 this.$scope.$watch(this.bindTopIndex, angular.bind(this, function(newIndex) { 24350 if (newIndex !== this.topIndex) { 24351 this.scrollToIndex(newIndex); 24352 } 24353 })); 24354 } else { 24355 this.topIndex = 0; 24356 } 24357 24358 this.scroller = $element[0].querySelector('.md-virtual-repeat-scroller'); 24359 this.sizer = this.scroller.querySelector('.md-virtual-repeat-sizer'); 24360 this.offsetter = this.scroller.querySelector('.md-virtual-repeat-offsetter'); 24361 24362 // After the dom stablizes, measure the initial size of the container and 24363 // make a best effort at re-measuring as it changes. 24364 var boundUpdateSize = angular.bind(this, this.updateSize); 24365 24366 $$rAF(angular.bind(this, function() { 24367 boundUpdateSize(); 24368 24369 var debouncedUpdateSize = $mdUtil.debounce(boundUpdateSize, 10, null, false); 24370 var jWindow = angular.element($window); 24371 24372 // Make one more attempt to get the size if it is 0. 24373 // This is not by any means a perfect approach, but there's really no 24374 // silver bullet here. 24375 if (!this.size) { 24376 debouncedUpdateSize(); 24377 } 24378 24379 jWindow.on('resize', debouncedUpdateSize); 24380 $scope.$on('$destroy', function() { 24381 jWindow.off('resize', debouncedUpdateSize); 24382 }); 24383 24384 $scope.$emit('$md-resize-enable'); 24385 $scope.$on('$md-resize', boundUpdateSize); 24386 })); 24387} 24388 24389 24390/** Called by the md-virtual-repeat inside of the container at startup. */ 24391VirtualRepeatContainerController.prototype.register = function(repeaterCtrl) { 24392 this.repeater = repeaterCtrl; 24393 24394 angular.element(this.scroller) 24395 .on('scroll wheel touchmove touchend', angular.bind(this, this.handleScroll_)); 24396}; 24397 24398 24399/** @return {boolean} Whether the container is configured for horizontal scrolling. */ 24400VirtualRepeatContainerController.prototype.isHorizontal = function() { 24401 return this.horizontal; 24402}; 24403 24404 24405/** @return {number} The size (width or height) of the container. */ 24406VirtualRepeatContainerController.prototype.getSize = function() { 24407 return this.size; 24408}; 24409 24410 24411/** 24412 * Resizes the container. 24413 * @private 24414 * @param {number} size The new size to set. 24415 */ 24416VirtualRepeatContainerController.prototype.setSize_ = function(size) { 24417 var dimension = this.getDimensionName_(); 24418 24419 this.size = size; 24420 this.$element[0].style[dimension] = size + 'px'; 24421}; 24422 24423 24424VirtualRepeatContainerController.prototype.unsetSize_ = function() { 24425 this.$element[0].style[this.getDimensionName_()] = this.oldElementSize; 24426 this.oldElementSize = null; 24427}; 24428 24429 24430/** Instructs the container to re-measure its size. */ 24431VirtualRepeatContainerController.prototype.updateSize = function() { 24432 // If the original size is already determined, we can skip the update. 24433 if (this.originalSize) return; 24434 24435 this.size = this.isHorizontal() 24436 ? this.$element[0].clientWidth 24437 : this.$element[0].clientHeight; 24438 24439 // Recheck the scroll position after updating the size. This resolves 24440 // problems that can result if the scroll position was measured while the 24441 // element was display: none or detached from the document. 24442 this.handleScroll_(); 24443 24444 this.repeater && this.repeater.containerUpdated(); 24445}; 24446 24447 24448/** @return {number} The container's scrollHeight or scrollWidth. */ 24449VirtualRepeatContainerController.prototype.getScrollSize = function() { 24450 return this.scrollSize; 24451}; 24452 24453 24454VirtualRepeatContainerController.prototype.getDimensionName_ = function() { 24455 return this.isHorizontal() ? 'width' : 'height'; 24456}; 24457 24458 24459/** 24460 * Sets the scroller element to the specified size. 24461 * @private 24462 * @param {number} size The new size. 24463 */ 24464VirtualRepeatContainerController.prototype.sizeScroller_ = function(size) { 24465 var dimension = this.getDimensionName_(); 24466 var crossDimension = this.isHorizontal() ? 'height' : 'width'; 24467 24468 // Clear any existing dimensions. 24469 this.sizer.innerHTML = ''; 24470 24471 // If the size falls within the browser's maximum explicit size for a single element, we can 24472 // set the size and be done. Otherwise, we have to create children that add up the the desired 24473 // size. 24474 if (size < this.maxElementPixels) { 24475 this.sizer.style[dimension] = size + 'px'; 24476 } else { 24477 this.sizer.style[dimension] = 'auto'; 24478 this.sizer.style[crossDimension] = 'auto'; 24479 24480 // Divide the total size we have to render into N max-size pieces. 24481 var numChildren = Math.floor(size / this.maxElementPixels); 24482 24483 // Element template to clone for each max-size piece. 24484 var sizerChild = document.createElement('div'); 24485 sizerChild.style[dimension] = this.maxElementPixels + 'px'; 24486 sizerChild.style[crossDimension] = '1px'; 24487 24488 for (var i = 0; i < numChildren; i++) { 24489 this.sizer.appendChild(sizerChild.cloneNode(false)); 24490 } 24491 24492 // Re-use the element template for the remainder. 24493 sizerChild.style[dimension] = (size - (numChildren * this.maxElementPixels)) + 'px'; 24494 this.sizer.appendChild(sizerChild); 24495 } 24496}; 24497 24498 24499/** 24500 * If auto-shrinking is enabled, shrinks or unshrinks as appropriate. 24501 * @private 24502 * @param {number} size The new size. 24503 */ 24504VirtualRepeatContainerController.prototype.autoShrink_ = function(size) { 24505 var shrinkSize = Math.max(size, this.autoShrinkMin * this.repeater.getItemSize()); 24506 24507 if (this.autoShrink && shrinkSize !== this.size) { 24508 if (this.oldElementSize === null) { 24509 this.oldElementSize = this.$element[0].style[this.getDimensionName_()]; 24510 } 24511 24512 var currentSize = this.originalSize || this.size; 24513 24514 if (!currentSize || shrinkSize < currentSize) { 24515 if (!this.originalSize) { 24516 this.originalSize = this.size; 24517 } 24518 24519 // Now we update the containers size, because shrinking is enabled. 24520 this.setSize_(shrinkSize); 24521 } else if (this.originalSize !== null) { 24522 // Set the size back to our initial size. 24523 this.unsetSize_(); 24524 24525 var _originalSize = this.originalSize; 24526 this.originalSize = null; 24527 24528 // We determine the repeaters size again, if the original size was zero. 24529 // The originalSize needs to be null, to be able to determine the size. 24530 if (!_originalSize) this.updateSize(); 24531 24532 // Apply the original size or the determined size back to the container, because 24533 // it has been overwritten before, in the shrink block. 24534 this.setSize_(_originalSize || this.size); 24535 } 24536 24537 this.repeater.containerUpdated(); 24538 } 24539}; 24540 24541 24542/** 24543 * Sets the scrollHeight or scrollWidth. Called by the repeater based on 24544 * its item count and item size. 24545 * @param {number} itemsSize The total size of the items. 24546 */ 24547VirtualRepeatContainerController.prototype.setScrollSize = function(itemsSize) { 24548 var size = itemsSize + this.offsetSize; 24549 if (this.scrollSize === size) return; 24550 24551 this.sizeScroller_(size); 24552 this.autoShrink_(size); 24553 this.scrollSize = size; 24554}; 24555 24556 24557/** @return {number} The container's current scroll offset. */ 24558VirtualRepeatContainerController.prototype.getScrollOffset = function() { 24559 return this.scrollOffset; 24560}; 24561 24562/** 24563 * Scrolls to a given scrollTop position. 24564 * @param {number} position 24565 */ 24566VirtualRepeatContainerController.prototype.scrollTo = function(position) { 24567 this.scroller[this.isHorizontal() ? 'scrollLeft' : 'scrollTop'] = position; 24568 this.handleScroll_(); 24569}; 24570 24571/** 24572 * Scrolls the item with the given index to the top of the scroll container. 24573 * @param {number} index 24574 */ 24575VirtualRepeatContainerController.prototype.scrollToIndex = function(index) { 24576 var itemSize = this.repeater.getItemSize(); 24577 var itemsLength = this.repeater.itemsLength; 24578 if(index > itemsLength) { 24579 index = itemsLength - 1; 24580 } 24581 this.scrollTo(itemSize * index); 24582}; 24583 24584VirtualRepeatContainerController.prototype.resetScroll = function() { 24585 this.scrollTo(0); 24586}; 24587 24588 24589VirtualRepeatContainerController.prototype.handleScroll_ = function() { 24590 var ltr = document.dir != 'rtl' && document.body.dir != 'rtl'; 24591 if(!ltr && !this.maxSize) { 24592 this.scroller.scrollLeft = this.scrollSize; 24593 this.maxSize = this.scroller.scrollLeft; 24594 } 24595 var offset = this.isHorizontal() ? 24596 (ltr?this.scroller.scrollLeft : this.maxSize - this.scroller.scrollLeft) 24597 : this.scroller.scrollTop; 24598 if (offset === this.scrollOffset || offset > this.scrollSize - this.size) return; 24599 24600 var itemSize = this.repeater.getItemSize(); 24601 if (!itemSize) return; 24602 24603 var numItems = Math.max(0, Math.floor(offset / itemSize) - NUM_EXTRA); 24604 24605 var transform = (this.isHorizontal() ? 'translateX(' : 'translateY(') + 24606 (!this.isHorizontal() || ltr ? (numItems * itemSize) : - (numItems * itemSize)) + 'px)'; 24607 24608 this.scrollOffset = offset; 24609 this.offsetter.style.webkitTransform = transform; 24610 this.offsetter.style.transform = transform; 24611 24612 if (this.bindTopIndex) { 24613 var topIndex = Math.floor(offset / itemSize); 24614 if (topIndex !== this.topIndex && topIndex < this.repeater.getItemCount()) { 24615 this.topIndex = topIndex; 24616 this.bindTopIndex.assign(this.$scope, topIndex); 24617 if (!this.$rootScope.$$phase) this.$scope.$digest(); 24618 } 24619 } 24620 24621 this.repeater.containerUpdated(); 24622}; 24623 24624 24625/** 24626 * @ngdoc directive 24627 * @name mdVirtualRepeat 24628 * @module material.components.virtualRepeat 24629 * @restrict A 24630 * @priority 1000 24631 * @description 24632 * `md-virtual-repeat` specifies an element to repeat using virtual scrolling. 24633 * 24634 * Virtual repeat is a limited substitute for ng-repeat that renders only 24635 * enough DOM nodes to fill the container and recycling them as the user scrolls. 24636 * 24637 * Arrays, but not objects are supported for iteration. 24638 * Track by, as alias, and (key, value) syntax are not supported. 24639 * 24640 * ### On-Demand Async Item Loading 24641 * 24642 * When using the `md-on-demand` attribute and loading some asynchronous data, the `getItemAtIndex` function will 24643 * mostly return nothing. 24644 * 24645 * <hljs lang="js"> 24646 * DynamicItems.prototype.getItemAtIndex = function(index) { 24647 * if (this.pages[index]) { 24648 * return this.pages[index]; 24649 * } else { 24650 * // This is an asynchronous action and does not return any value. 24651 * this.loadPage(index); 24652 * } 24653 * }; 24654 * </hljs> 24655 * 24656 * This means that the VirtualRepeat will not have any value for the given index.<br/> 24657 * After the data loading completed, the user expects the VirtualRepeat to re
24657cognize the change. 24658 * 24659 * To make sure that the VirtualRepeat properly detects any change, you need to run the operation 24660 * in another digest. 24661 * 24662 * <hljs lang="js"> 24663 * DynamicItems.prototype.loadPage = function(index) { 24664 * var self = this; 24665 * 24666 * // Trigger a new digest by using $timeout 24667 * $timeout(function() { 24668 * self.pages[index] = Data; 24669 * }); 24670 * }; 24671 * </hljs> 24672 * 24673 * > <b>Note:</b> Please also review the 24674 * <a ng-href="api/directive/mdVirtualRepeatContainer">VirtualRepeatContainer</a> documentation 24675 * for more information. 24676 * 24677 * @usage 24678 * <hljs lang="html"> 24679 * <md-virtual-repeat-container> 24680 * <div md-virtual-repeat="i in items">Hello {{i}}!</div> 24681 * </md-virtual-repeat-container> 24682 * 24683 * <md-virtual-repeat-container md-orient-horizontal> 24684 * <div md-virtual-repeat="i in items" md-item-size="20">Hello {{i}}!</div> 24685 * </md-virtual-repeat-container> 24686 * </hljs> 24687 * 24688 * @param {number=} md-item-size The height or width of the repeated elements (which must be 24689 * identical for each element). Optional. Will attempt to read the size from the dom if missing, 24690 * but still assumes that all repeated nodes have same height or width. 24691 * @param {string=} md-extra-name Evaluates to an additional name to which the current iterated item 24692 * can be assigned on the repeated scope (needed for use in `md-autocomplete`). 24693 * @param {boolean=} md-on-demand When present, treats the md-virtual-repeat argument as an object 24694 * that can fetch rows rather than an array. 24695 * 24696 * **NOTE:** This object must implement the following interface with two (2) methods: 24697 * 24698 * - `getItemAtIndex: function(index) [object]` The item at that index or null if it is not yet 24699 * loaded (it should start downloading the item in that case). 24700 * - `getLength: function() [number]` The data length to which the repeater container 24701 * should be sized. Ideally, when the count is known, this method should return it. 24702 * Otherwise, return a higher number than the currently loaded items to produce an 24703 * infinite-scroll behavior. 24704 */ 24705function VirtualRepeatDirective($parse) { 24706 return { 24707 controller: VirtualRepeatController, 24708 priority: 1000, 24709 require: ['mdVirtualRepeat', '^^mdVirtualRepeatContainer'], 24710 restrict: 'A', 24711 terminal: true, 24712 transclude: 'element', 24713 compile: function VirtualRepeatCompile($element, $attrs) { 24714 var expression = $attrs.mdVirtualRepeat; 24715 var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)\s*$/); 24716 var repeatName = match[1]; 24717 var repeatListExpression = $parse(match[2]); 24718 var extraName = $attrs.mdExtraName && $parse($attrs.mdExtraName); 24719 24720 return function VirtualRepeatLink($scope, $element, $attrs, ctrl, $transclude) { 24721 ctrl[0].link_(ctrl[1], $transclude, repeatName, repeatListExpression, extraName); 24722 }; 24723 } 24724 }; 24725} 24726 24727 24728/** @ngInject */ 24729function VirtualRepeatController($scope, $element, $attrs, $browser, $document, $rootScope, 24730 $$rAF, $mdUtil) { 24731 this.$scope = $scope; 24732 this.$element = $element; 24733 this.$attrs = $attrs; 24734 this.$browser = $browser; 24735 this.$document = $document; 24736 this.$mdUtil = $mdUtil; 24737 this.$rootScope = $rootScope; 24738 this.$$rAF = $$rAF; 24739 24740 /** @type {boolean} Whether we are in on-demand mode. */ 24741 this.onDemand = $mdUtil.parseAttributeBoolean($attrs.mdOnDemand); 24742 /** @type {!Function} Backup reference to $browser.$$checkUrlChange */ 24743 this.browserCheckUrlChange = $browser.$$checkUrlChange; 24744 /** @type {number} Most recent starting repeat index (based on scroll offset) */ 24745 this.newStartIndex = 0; 24746 /** @type {number} Most recent ending repeat index (based on scroll offset) */ 24747 this.newEndIndex = 0; 24748 /** @type {number} Most recent end visible index (based on scroll offset) */ 24749 this.newVisibleEnd = 0; 24750 /** @type {number} Previous starting repeat index (based on scroll offset) */ 24751 this.startIndex = 0; 24752 /** @type {number} Previous ending repeat index (based on scroll offset) */ 24753 this.endIndex = 0; 24754 // TODO: measure width/height of first element from dom if not provided. 24755 // getComputedStyle? 24756 /** @type {?number} Height/width of repeated elements. */ 24757 this.itemSize = $scope.$eval($attrs.mdItemSize) || null; 24758 24759 /** @type {boolean} Whether this is the first time that items are rendered. */ 24760 this.isFirstRender = true; 24761 24762 /** 24763 * @private {boolean} Whether the items in the list are already being updated. Used to prevent 24764 * nested calls to virtualRepeatUpdate_. 24765 */ 24766 this.isVirtualRepeatUpdating_ = false;
vendor: 4,741 bytes, lines 24767-24915
24767 24768 /** @type {number} Most recently seen length of items. */ 24769 this.itemsLength = 0; 24770 24771 /** 24772 * @type {!Function} Unwatch callback for item size (when md-items-size is 24773 * not specified), or angular.noop otherwise. 24774 */ 24775 this.unwatchItemSize_ = angular.noop; 24776 24777 /** 24778 * Presently rendered blocks by repeat index. 24779 * @type {Object<number, !VirtualRepeatController.Block} 24780 */ 24781 this.blocks = {}; 24782 /** @type {Array<!VirtualRepeatController.Block>} A pool of presently unused blocks. */ 24783 this.pooledBlocks = []; 24784 24785 $scope.$on('$destroy', angular.bind(this, this.cleanupBlocks_)); 24786} 24787 24788 24789/** 24790 * An object representing a repeated item. 24791 * @typedef {{element: !jqLite, new: boolean, scope: !angular.Scope}} 24792 */ 24793VirtualRepeatController.Block; 24794 24795 24796/** 24797 * Called at startup by the md-virtual-repeat postLink function. 24798 * @param {!VirtualRepeatContainerController} container The container's controller. 24799 * @param {!Function} transclude The repeated element's bound transclude function. 24800 * @param {string} repeatName The left hand side of the repeat expression, indicating 24801 * the name for each item in the array. 24802 * @param {!Function} repeatListExpression A compiled expression based on the right hand side 24803 * of the repeat expression. Points to the array to repeat over. 24804 * @param {string|undefined} extraName The optional extra repeatName. 24805 */ 24806VirtualRepeatController.prototype.link_ = 24807 function(container, transclude, repeatName, repeatListExpression, extraName) { 24808 this.container = container; 24809 this.transclude = transclude; 24810 this.repeatName = repeatName; 24811 this.rawRepeatListExpression = repeatListExpression; 24812 this.extraName = extraName; 24813 this.sized = false; 24814 24815 this.repeatListExpression = angular.bind(this, this.repeatListExpression_); 24816 24817 this.container.register(this); 24818}; 24819 24820 24821/** @private Cleans up unused blocks. */ 24822VirtualRepeatController.prototype.cleanupBlocks_ = function() { 24823 angular.forEach(this.pooledBlocks, function cleanupBlock(block) { 24824 block.element.remove(); 24825 }); 24826}; 24827 24828 24829/** @private Attempts to set itemSize by measuring a repeated element in the dom */ 24830VirtualRepeatController.prototype.readItemSize_ = function() { 24831 if (this.itemSize) { 24832 // itemSize was successfully read in a different asynchronous call. 24833 return; 24834 } 24835 24836 this.items = this.repeatListExpression(this.$scope); 24837 this.parentNode = this.$element[0].parentNode; 24838 var block = this.getBlock_(0); 24839 if (!block.element[0].parentNode) { 24840 this.parentNode.appendChild(block.element[0]); 24841 } 24842 24843 this.itemSize = block.element[0][ 24844 this.container.isHorizontal() ? 'offsetWidth' : 'offsetHeight'] || null; 24845 24846 this.blocks[0] = block; 24847 this.poolBlock_(0); 24848 24849 if (this.itemSize) { 24850 this.containerUpdated(); 24851 } 24852}; 24853 24854 24855/** 24856 * Returns the user-specified repeat list, transforming it into an array-like 24857 * object in the case of infinite scroll/dynamic load mode. 24858 * @param {!angular.Scope} The scope. 24859 * @return {!Array|!Object} An array or array-like object for iteration. 24860 */ 24861VirtualRepeatController.prototype.repeatListExpression_ = function(scope) { 24862 var repeatList = this.rawRepeatListExpression(scope); 24863 24864 if (this.onDemand && repeatList) { 24865 var virtualList = new VirtualRepeatModelArrayLike(repeatList); 24866 virtualList.$$includeIndexes(this.newStartIndex, this.newVisibleEnd); 24867 return virtualList; 24868 } else { 24869 return repeatList; 24870 } 24871}; 24872 24873 24874/** 24875 * Called by the container. Informs us that the containers scroll or size has 24876 * changed. 24877 */ 24878VirtualRepeatController.prototype.containerUpdated = function() { 24879 // If itemSize is unknown, attempt to measure it. 24880 if (!this.itemSize) { 24881 // Make sure to clean up watchers if we can (see #8178) 24882 if(this.unwatchItemSize_ && this.unwatchItemSize_ !== angular.noop){ 24883 this.unwatchItemSize_(); 24884 } 24885 this.unwatchItemSize_ = this.$scope.$watchCollection( 24886 this.repeatListExpression, 24887 angular.bind(this, function(items) { 24888 if (items && items.length) { 24889 this.readItemSize_(); 24890 } 24891 })); 24892 if (!this.$rootScope.$$phase) this.$scope.$digest(); 24893 24894 return; 24895 } else if (!this.sized) { 24896 this.items = this.repeatListExpression(this.$scope); 24897 } 24898 24899 if (!this.sized) { 24900 this.unwatchItemSize_(); 24901 this.sized = true; 24902 this.$scope.$watchCollection(this.repeatListExpression, 24903 angular.bind(this, function(items, oldItems) { 24904 if (!this.isVirtualRepeatUpdating_) { 24905 this.virtualRepeatUpdate_(items, oldItems); 24906 } 24907 })); 24908 } 24909 24910 this.updateIndexes_(); 24911 24912 if (this.newStartIndex !== this.startIndex || 24913 this.newEndIndex !== this.endIndex || 24914 this.container.getScrollOffset() > this.container.getScrollSize()) { 24915 if (this.items instanceof VirtualRepeatModelArrayLike) {
24916 this.items.$$includeIndexes(this.newStartIndex, this.newEndIndex); 24917 } 24918 this.virtualRepeatUpdate_(this.items, this.items); 24919 } 24920}; 24921 24922 24923/** 24924 * Called by the container. Returns the size of a single repeated item. 24925 * @return {?number} Size of a repeated item. 24926 */ 24927VirtualRepeatController.prototype.getItemSize = function() { 24928 return this.itemSize; 24929}; 24930 24931 24932/** 24933 * Called by the container. Returns the size of a single repeated item. 24934 * @return {?number} Size of a repeated item. 24935 */ 24936VirtualRepeatController.prototype.getItemCount = function() { 24937 return this.itemsLength; 24938}; 24939 24940 24941/** 24942 * Updates the order and visible offset of repeated blocks in response to scrolling 24943 * or items updates.
vendor: 4,559 bytes, lines 24943-25096
24943 24944 * @private 24945 */ 24946VirtualRepeatController.prototype.virtualRepeatUpdate_ = function(items, oldItems) { 24947 this.isVirtualRepeatUpdating_ = true; 24948 24949 var itemsLength = items && items.length || 0; 24950 var lengthChanged = false; 24951 24952 // If the number of items shrank, keep the scroll position. 24953 if (this.items && itemsLength < this.items.length && this.container.getScrollOffset() !== 0) { 24954 this.items = items; 24955 var previousScrollOffset = this.container.getScrollOffset(); 24956 this.container.resetScroll(); 24957 this.container.scrollTo(previousScrollOffset); 24958 } 24959 24960 if (itemsLength !== this.itemsLength) { 24961 lengthChanged = true; 24962 this.itemsLength = itemsLength; 24963 } 24964 24965 this.items = items; 24966 if (items !== oldItems || lengthChanged) { 24967 this.updateIndexes_(); 24968 } 24969 24970 this.parentNode = this.$element[0].parentNode; 24971 24972 if (lengthChanged) { 24973 this.container.setScrollSize(itemsLength * this.itemSize); 24974 } 24975 24976 // Detach and pool any blocks that are no longer in the viewport. 24977 Object.keys(this.blocks).forEach(function(blockIndex) { 24978 var index = parseInt(blockIndex, 10); 24979 if (index < this.newStartIndex || index >= this.newEndIndex) { 24980 this.poolBlock_(index); 24981 } 24982 }, this); 24983 24984 // Add needed blocks. 24985 // For performance reasons, temporarily block browser url checks as we digest 24986 // the restored block scopes ($$checkUrlChange reads window.location to 24987 // check for changes and trigger route change, etc, which we don't need when 24988 // trying to scroll at 60fps). 24989 this.$browser.$$checkUrlChange = angular.noop; 24990 24991 var i, block, 24992 newStartBlocks = [], 24993 newEndBlocks = []; 24994 24995 // Collect blocks at the top. 24996 for (i = this.newStartIndex; i < this.newEndIndex && this.blocks[i] == null; i++) { 24997 block = this.getBlock_(i); 24998 this.updateBlock_(block, i); 24999 newStartBlocks.push(block); 25000 } 25001 25002 // Update blocks that are already rendered. 25003 for (; this.blocks[i] != null; i++) { 25004 this.updateBlock_(this.blocks[i], i); 25005 } 25006 var maxIndex = i - 1; 25007 25008 // Collect blocks at the end. 25009 for (; i < this.newEndIndex; i++) { 25010 block = this.getBlock_(i); 25011 this.updateBlock_(block, i); 25012 newEndBlocks.push(block); 25013 } 25014 25015 // Attach collected blocks to the document. 25016 if (newStartBlocks.length) { 25017 this.parentNode.insertBefore( 25018 this.domFragmentFromBlocks_(newStartBlocks), 25019 this.$element[0].nextSibling); 25020 } 25021 if (newEndBlocks.length) { 25022 this.parentNode.insertBefore( 25023 this.domFragmentFromBlocks_(newEndBlocks), 25024 this.blocks[maxIndex] && this.blocks[maxIndex].element[0].nextSibling); 25025 } 25026 25027 // Restore $$checkUrlChange. 25028 this.$browser.$$checkUrlChange = this.browserCheckUrlChange; 25029 25030 this.startIndex = this.newStartIndex; 25031 this.endIndex = this.newEndIndex; 25032 25033 if (this.isFirstRender) { 25034 this.isFirstRender = false; 25035 var firstRenderStartIndex = this.$attrs.mdStartIndex ? 25036 this.$scope.$eval(this.$attrs.mdStartIndex) : 25037 this.container.topIndex; 25038 25039 // The first call to virtualRepeatUpdate_ may not be when the virtual repeater is ready. 25040 // Introduce a slight delay so that the update happens when it is actually ready. 25041 this.$mdUtil.nextTick(function() { 25042 this.container.scrollToIndex(firstRenderStartIndex); 25043 }.bind(this)); 25044 } 25045 25046 this.isVirtualRepeatUpdating_ = false; 25047}; 25048 25049 25050/** 25051 * @param {number} index Where the block is to be in the repeated list. 25052 * @return {!VirtualRepeatController.Block} A new or pooled block to place at the specified index. 25053 * @private 25054 */ 25055VirtualRepeatController.prototype.getBlock_ = function(index) { 25056 if (this.pooledBlocks.length) { 25057 return this.pooledBlocks.pop(); 25058 } 25059 25060 var block; 25061 this.transclude(angular.bind(this, function(clone, scope) { 25062 block = { 25063 element: clone, 25064 new: true, 25065 scope: scope 25066 }; 25067 25068 this.updateScope_(scope, index); 25069 this.parentNode.appendChild(clone[0]); 25070 })); 25071 25072 return block; 25073}; 25074 25075 25076/** 25077 * Updates and if not in a digest cycle, digests the specified block's scope to the data 25078 * at the specified index. 25079 * @param {!VirtualRepeatController.Block} block The block whose scope should be updated. 25080 * @param {number} index The index to set. 25081 * @private 25082 */ 25083VirtualRepeatController.prototype.updateBlock_ = function(block, index) { 25084 this.blocks[index] = block; 25085 25086 if (!block.new && 25087 (block.scope.$index === index && block.scope[this.repeatName] === this.items[index])) { 25088 return; 25089 } 25090 block.new = false; 25091 25092 // Update and digest the block's scope. 25093 this.updateScope_(block.scope, index); 25094 25095 // Perform digest before reattaching the block. 25096 // Any resulting synchronous
vendor: 7,709 bytes, lines 25096-25348
25096dom mutations should be much faster as a result. 25097 // This might break some directives, but I'm going to try it for now. 25098 if (!this.$rootScope.$$phase) { 25099 block.scope.$digest(); 25100 } 25101}; 25102 25103 25104/** 25105 * Updates scope to the data at the specified index. 25106 * @param {!angular.Scope} scope The scope which should be updated. 25107 * @param {number} index The index to set. 25108 * @private 25109 */ 25110VirtualRepeatController.prototype.updateScope_ = function(scope, index) { 25111 scope.$index = index; 25112 scope[this.repeatName] = this.items && this.items[index]; 25113 if (this.extraName) scope[this.extraName(this.$scope)] = this.items[index]; 25114}; 25115 25116 25117/** 25118 * Pools the block at the specified index (Pulls its element out of the dom and stores it). 25119 * @param {number} index The index at which the block to pool is stored. 25120 * @private 25121 */ 25122VirtualRepeatController.prototype.poolBlock_ = function(index) { 25123 this.pooledBlocks.push(this.blocks[index]); 25124 this.parentNode.removeChild(this.blocks[index].element[0]); 25125 delete this.blocks[index]; 25126}; 25127 25128 25129/** 25130 * Produces a dom fragment containing the elements from the list of blocks. 25131 * @param {!Array<!VirtualRepeatController.Block>} blocks The blocks whose elements 25132 * should be added to the document fragment. 25133 * @return {DocumentFragment} 25134 * @private 25135 */ 25136VirtualRepeatController.prototype.domFragmentFromBlocks_ = function(blocks) { 25137 var fragment = this.$document[0].createDocumentFragment(); 25138 blocks.forEach(function(block) { 25139 fragment.appendChild(block.element[0]); 25140 }); 25141 return fragment; 25142}; 25143 25144 25145/** 25146 * Updates start and end indexes based on length of repeated items and container size. 25147 * @private 25148 */ 25149VirtualRepeatController.prototype.updateIndexes_ = function() { 25150 var itemsLength = this.items ? this.items.length : 0; 25151 var containerLength = Math.ceil(this.container.getSize() / this.itemSize); 25152 25153 this.newStartIndex = Math.max(0, Math.min( 25154 itemsLength - containerLength, 25155 Math.floor(this.container.getScrollOffset() / this.itemSize))); 25156 this.newVisibleEnd = this.newStartIndex + containerLength + NUM_EXTRA; 25157 this.newEndIndex = Math.min(itemsLength, this.newVisibleEnd); 25158 this.newStartIndex = Math.max(0, this.newStartIndex - NUM_EXTRA); 25159}; 25160 25161/** 25162 * This VirtualRepeatModelArrayLike class enforces the interface requirements 25163 * for infinite scrolling within a mdVirtualRepeatContainer. An object with this 25164 * interface must implement the following interface with two (2) methods: 25165 * 25166 * getItemAtIndex: function(index) -> item at that index or null if it is not yet 25167 * loaded (It should start downloading the item in that case). 25168 * 25169 * getLength: function() -> number The data legnth to which the repeater container 25170 * should be sized. Ideally, when the count is known, this method should return it. 25171 * Otherwise, return a higher number than the currently loaded items to produce an 25172 * infinite-scroll behavior. 25173 * 25174 * @usage 25175 * <hljs lang="html"> 25176 * <md-virtual-repeat-container md-orient-horizontal> 25177 * <div md-virtual-repeat="i in items" md-on-demand> 25178 * Hello {{i}}! 25179 * </div> 25180 * </md-virtual-repeat-container> 25181 * </hljs> 25182 * 25183 */ 25184function VirtualRepeatModelArrayLike(model) { 25185 if (!angular.isFunction(model.getItemAtIndex) || 25186 !angular.isFunction(model.getLength)) { 25187 throw Error('When md-on-demand is enabled, the Object passed to md-virtual-repeat must implement ' + 25188 'functions getItemAtIndex() and getLength() '); 25189 } 25190 25191 this.model = model; 25192} 25193 25194 25195VirtualRepeatModelArrayLike.prototype.$$includeIndexes = function(start, end) { 25196 for (var i = start; i < end; i++) { 25197 if (!this.hasOwnProperty(i)) { 25198 this[i] = this.model.getItemAtIndex(i); 25199 } 25200 } 25201 this.length = this.model.getLength(); 25202}; 25203 25204/** 25205 * @ngdoc directive 25206 * @name mdForceHeight 25207 * @module material.components.virtualRepeat 25208 * @restrict A 25209 * @description 25210 * 25211 * Force an element to have a certain px height. This is used in place of a style tag in order to 25212 * conform to the Content Security Policy regarding unsafe-inline style tags. 25213 * 25214 * @usage 25215 * <hljs lang="html"> 25216 * <div md-force-height="'100px'"></div> 25217 * </hljs> 25218 */ 25219function ForceHeightDirective($mdUtil) { 25220 return { 25221 restrict: 'A', 25222 link: function(scope, element, attrs) { 25223 var height = scope.$eval(attrs.mdForceHeight) || null; 25224 25225 if (height && element) { 25226 element[0].style.height = height; 25227 } 25228 } 25229 } 25230} 25231ForceHeightDirective.$inject = ['$mdUtil']; 25232 25233})(); 25234(function(){ 25235"use strict"; 25236 25237/** 25238 * @ngdoc module 25239 * @name material.components.whiteframe 25240 */ 25241MdWhiteframeDirective.$inject = ["$log"]; 25242angular 25243 .module('material.components.whiteframe', ['material.core']) 25244 .directive('mdWhiteframe', MdWhiteframeDirective); 25245 25246/** 25247 * @ngdoc directive 25248 * @module material.components.whiteframe 25249 * @name mdWhiteframe 25250 * 25251 * @description 25252 * The md-whiteframe directive allows you to apply an elevation shadow to an element. 25253 * 25254 * The attribute values needs to be a number between 1 and 24 or -1. 25255 * When set to -1 no style is applied. 25256 * 25257 * ### Notes 25258 * - If there is no value specified it defaults to 4dp. 25259 * - If the value is not valid it defaults to 4dp. 25260 25261 * @usage 25262 * <hljs lang="html"> 25263 * <div md-whiteframe="3"> 25264 * <span>Elevation of 3dp</span> 25265 * </div> 25266 * </hljs> 25267 * 25268 * <hljs lang="html"> 25269 * <div md-whiteframe="-1"> 25270 * <span>No elevation shadow applied</span> 25271 * </div> 25272 * </hljs> 25273 * 25274 * <hljs lang="html"> 25275 * <div ng-init="elevation = 5" md-whiteframe="{{elevation}}"> 25276 * <span>Elevation of 5dp with an interpolated value</span> 25277 * </div> 25278 * </hljs> 25279 */ 25280function MdWhiteframeDirective($log) { 25281 var DISABLE_DP = -1; 25282 var MIN_DP = 1; 25283 var MAX_DP = 24; 25284 var DEFAULT_DP = 4; 25285 25286 return { 25287 link: postLink 25288 }; 25289 25290 function postLink(scope, element, attr) { 25291 var oldClass = ''; 25292 25293 attr.$observe('mdWhiteframe', function(elevation) { 25294 elevation = parseInt(elevation, 10) || DEFAULT_DP; 25295 25296 if (elevation != DISABLE_DP && (elevation > MAX_DP || elevation < MIN_DP)) { 25297 $log.warn('md-whiteframe attribute value is invalid. It should be a number between ' + MIN_DP + ' and ' + MAX_DP, element[0]); 25298 elevation = DEFAULT_DP; 25299 } 25300 25301 var newClass = elevation == DISABLE_DP ? '' : 'md-whiteframe-' + elevation + 'dp'; 25302 attr.$updateClass(newClass, oldClass); 25303 oldClass = newClass; 25304 }); 25305 } 25306} 25307 25308 25309})(); 25310(function(){ 25311"use strict"; 25312 25313 25314MdAutocompleteCtrl.$inject = ["$scope", "$element", "$mdUtil", "$mdConstant", "$mdTheming", "$window", "$animate", "$rootElement", "$attrs", "$q", "$log", "$mdLiveAnnouncer"];angular 25315 .module('material.components.autocomplete') 25316 .controller('MdAutocompleteCtrl', MdAutocompleteCtrl); 25317 25318var ITEM_HEIGHT = 48, 25319 MAX_ITEMS = 5, 25320 MENU_PADDING = 8, 25321 INPUT_PADDING = 2; // Padding provided by `md-input-container` 25322 25323function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, $window, 25324 $animate, $rootElement, $attrs, $q, $log, $mdLiveAnnouncer) { 25325 25326 // Internal Variables. 25327 var ctrl = this, 25328 itemParts = $scope.itemsExpr.split(/ in /i), 25329 itemExpr = itemParts[ 1 ], 25330 elements = null, 25331 cache = {}, 25332 noBlur = false, 25333 selectedItemWatchers = [], 25334 hasFocus = false, 25335 fetchesInProgress = 0, 25336 enableWrapScroll = null, 25337 inputModelCtrl = null, 25338 debouncedOnResize = $mdUtil.debounce(onWindowResize); 25339 25340 // Public Exported Variables with handlers 25341 defineProperty('hidden', handleHiddenChange, true); 25342 25343 // Public Exported Variables 25344 ctrl.scope = $scope; 25345 ctrl.parent = $scope.$parent; 25346 ctrl.itemName = itemParts[ 0 ]; 25347 ctrl.matches = []; 25348 ctrl.loading = false;
25349 ctrl.hidden = true; 25350 ctrl.index = null; 25351 ctrl.id = $mdUtil.nextUid(); 25352 ctrl.isDisabled = null; 25353 ctrl.isRequired = null; 25354 ctrl.isReadonly = null; 25355 ctrl.hasNotFound = false; 25356 25357 // Public Exported Methods 25358 ctrl.keydown = keydown; 25359 ctrl.blur = blur; 25360 ctrl.focus = focus; 25361 ctrl.clear = clearValue; 25362 ctrl.select = select; 25363 ctrl.listEnter = onListEnter; 25364 ctrl.listLeave = onListLeave; 25365 ctrl.mouseUp = onMouseup; 25366 ctrl.getCurrentDisplayValue = getCurrentDisplayValue; 25367 ctrl.registerSelectedItemWatcher = registerSelectedItemWatcher; 25368 ctrl.unregisterSelectedItemWatcher = unregisterSelectedItemWatcher; 25369 ctrl.notFoundVisible = notFoundVisible; 25370 ctrl.loadingIsVisible = loadingIsVisible; 25371 ctrl.positionDropdown = positionDropdown; 25372 25373 /** 25374 * Report types to be used for the $mdLiveAnnouncer 25375 * @enum {number} Unique flag id. 25376 */ 25377 var ReportType = { 25378 Count: 1, 25379 Selected: 2 25380 }; 25381 25382 return init(); 25383 25384 //-- initialization methods 25385 25386 /** 25387 * Initialize the controller, setup watchers, gather elements 25388 */ 25389 function init () { 25390 25391 $mdUtil.initOptionalProperties($scope, $attrs, { 25392 searchText: '', 25393 selectedItem: null,
vendor: 15,269 bytes, lines 25394-25931
25394 clearButton: false 25395 }); 25396 25397 $mdTheming($element); 25398 configureWatchers(); 25399 $mdUtil.nextTick(function () { 25400 25401 gatherElements(); 25402 moveDropdown(); 25403 25404 // Forward all focus events to the input element when autofocus is enabled 25405 if ($scope.autofocus) { 25406 $element.on('focus', focusInputElement); 25407 } 25408 }); 25409 } 25410 25411 function updateModelValidators() { 25412 if (!$scope.requireMatch || !inputModelCtrl) return; 25413 25414 inputModelCtrl.$setValidity('md-require-match', !!$scope.selectedItem || !$scope.searchText); 25415 } 25416 25417 /** 25418 * Calculates the dropdown's position and applies the new styles to the menu element 25419 * @returns {*} 25420 */ 25421 function positionDropdown () { 25422 if (!elements) { 25423 return $mdUtil.nextTick(positionDropdown, false, $scope); 25424 } 25425 25426 var dropdownHeight = ($scope.dropdownItems || MAX_ITEMS) * ITEM_HEIGHT; 25427 25428 var hrect = elements.wrap.getBoundingClientRect(), 25429 vrect = elements.snap.getBoundingClientRect(), 25430 root = elements.root.getBoundingClientRect(), 25431 top = vrect.bottom - root.top, 25432 bot = root.bottom - vrect.top, 25433 left = hrect.left - root.left, 25434 width = hrect.width, 25435 offset = getVerticalOffset(), 25436 position = $scope.dropdownPosition, 25437 styles; 25438 25439 // Automatically determine dropdown placement based on available space in viewport. 25440 if (!position) { 25441 position = (top > bot && root.height - top - MENU_PADDING < dropdownHeight) ? 'top' : 'bottom'; 25442 } 25443 // Adjust the width to account for the padding provided by `md-input-container` 25444 if ($attrs.mdFloatingLabel) { 25445 left += INPUT_PADDING; 25446 width -= INPUT_PADDING * 2; 25447 } 25448 styles = { 25449 left: left + 'px', 25450 minWidth: width + 'px', 25451 maxWidth: Math.max(hrect.right - root.left, root.right - hrect.left) - MENU_PADDING + 'px' 25452 }; 25453 25454 if (position === 'top') { 25455 styles.top = 'auto'; 25456 styles.bottom = bot + 'px'; 25457 styles.maxHeight = Math.min(dropdownHeight, hrect.top - root.top - MENU_PADDING) + 'px'; 25458 } else { 25459 var bottomSpace = root.bottom - hrect.bottom - MENU_PADDING + $mdUtil.getViewportTop(); 25460 25461 styles.top = (top - offset) + 'px'; 25462 styles.bottom = 'auto'; 25463 styles.maxHeight = Math.min(dropdownHeight, bottomSpace) + 'px'; 25464 } 25465 25466 elements.$.scrollContainer.css(styles); 25467 $mdUtil.nextTick(correctHorizontalAlignment, false); 25468 25469 /** 25470 * Calculates the vertical offset for floating label examples to account for ngMessages 25471 * @returns {number} 25472 */ 25473 function getVerticalOffset () { 25474 var offset = 0; 25475 var inputContainer = $element.find('md-input-container'); 25476 if (inputContainer.length) { 25477 var input = inputContainer.find('input'); 25478 offset = inputContainer.prop('offsetHeight'); 25479 offset -= input.prop('offsetTop'); 25480 offset -= input.prop('offsetHeight'); 25481 // add in the height left up top for the floating label text 25482 offset += inputContainer.prop('offsetTop'); 25483 } 25484 return offset; 25485 } 25486 25487 /** 25488 * Makes sure that the menu doesn't go off of the screen on either side. 25489 */ 25490 function correctHorizontalAlignment () { 25491 var dropdown = elements.scrollContainer.getBoundingClientRect(), 25492 styles = {}; 25493 if (dropdown.right > root.right - MENU_PADDING) { 25494 styles.left = (hrect.right - dropdown.width) + 'px'; 25495 } 25496 elements.$.scrollContainer.css(styles); 25497 } 25498 } 25499 25500 /** 25501 * Moves the dropdown menu to the body tag in order to avoid z-index and overflow issues. 25502 */ 25503 function moveDropdown () { 25504 if (!elements.$.root.length) return; 25505 $mdTheming(elements.$.scrollContainer); 25506 elements.$.scrollContainer.detach(); 25507 elements.$.root.append(elements.$.scrollContainer); 25508 if ($animate.pin) $animate.pin(elements.$.scrollContainer, $rootElement); 25509 } 25510 25511 /** 25512 * Sends focus to the input element. 25513 */ 25514 function focusInputElement () { 25515 elements.input.focus(); 25516 } 25517 25518 /** 25519 * Sets up any watchers used by autocomplete 25520 */ 25521 function configureWatchers () { 25522 var wait = parseInt($scope.delay, 10) || 0; 25523 25524 $attrs.$observe('disabled', function (value) { ctrl.isDisabled = $mdUtil.parseAttributeBoolean(value, false); }); 25525 $attrs.$observe('required', function (value) { ctrl.isRequired = $mdUtil.parseAttributeBoolean(value, false); }); 25526 $attrs.$observe('readonly', function (value) { ctrl.isReadonly = $mdUtil.parseAttributeBoolean(value, false); }); 25527 25528 $scope.$watch('searchText', wait ? $mdUtil.debounce(handleSearchText, wait) : handleSearchText); 25529 $scope.$watch('selectedItem', selectedItemChange); 25530 25531 angular.element($window).on('resize', debouncedOnResize); 25532 25533 $scope.$on('$destroy', cleanup); 25534 } 25535 25536 /** 25537 * Removes any events or leftover elements created by this controller 25538 */ 25539 function cleanup () { 25540 if (!ctrl.hidden) { 25541 $mdUtil.enableScrolling(); 25542 } 25543 25544 angular.element($window).off('resize', debouncedOnResize); 25545 25546 if ( elements ){ 25547 var items = ['ul', 'scroller', 'scrollContainer', 'input']; 25548 angular.forEach(items, function(key){ 25549 elements.$[key].remove(); 25550 }); 25551 } 25552 } 25553 25554 /** 25555 * Event handler to be called whenever the window resizes. 25556 */ 25557 function onWindowResize() { 25558 if (!ctrl.hidden) { 25559 positionDropdown(); 25560 } 25561 } 25562 25563 /** 25564 * Gathers all of the elements needed for this controller 25565 */ 25566 function gatherElements () { 25567 25568 var snapWrap = gatherSnapWrap(); 25569 25570 elements = { 25571 main: $element[0], 25572 scrollContainer: $element[0].querySelector('.md-virtual-repeat-container'), 25573 scroller: $element[0].querySelector('.md-virtual-repeat-scroller'), 25574 ul: $element.find('ul')[0], 25575 input: $element.find('input')[0], 25576 wrap: snapWrap.wrap, 25577 snap: snapWrap.snap, 25578 root: document.body 25579 }; 25580 25581 elements.li = elements.ul.getElementsByTagName('li'); 25582 elements.$ = getAngularElements(elements); 25583 25584 inputModelCtrl = elements.$.input.controller('ngModel'); 25585 } 25586 25587 /** 25588 * Gathers the snap and wrap elements 25589 * 25590 */ 25591 function gatherSnapWrap() { 25592 var element; 25593 var value; 25594 for (element = $element; element.length; element = element.parent()) { 25595 value = element.attr('md-autocomplete-snap'); 25596 if (angular.isDefined(value)) break; 25597 } 25598 25599 if (element.length) { 25600 return { 25601 snap: element[0], 25602 wrap: (value.toLowerCase() === 'width') ? element[0] : $element.find('md-autocomplete-wrap')[0] 25603 }; 25604 } 25605 25606 var wrap = $element.find('md-autocomplete-wrap')[0]; 25607 return { 25608 snap: wrap, 25609 wrap: wrap 25610 }; 25611 } 25612 25613 /** 25614 * Gathers angular-wrapped versions of each element 25615 * @param elements 25616 * @returns {{}} 25617 */ 25618 function getAngularElements (elements) { 25619 var obj = {}; 25620 for (var key in elements) { 25621 if (elements.hasOwnProperty(key)) obj[ key ] = angular.element(elements[ key ]); 25622 } 25623 return obj; 25624 } 25625 25626 //-- event/change handlers 25627 25628 /** 25629 * Handles changes to the `hidden` property. 25630 * @param hidden 25631 * @param oldHidden 25632 */ 25633 function handleHiddenChange (hidden, oldHidden) { 25634 if (!hidden && oldHidden) { 25635 positionDropdown(); 25636 25637 // Report in polite mode, because the screenreader should finish the default description of 25638 // the input. element. 25639 reportMessages(true, ReportType.Count | ReportType.Selected); 25640 25641 if (elements) { 25642 $mdUtil.disableScrollAround(elements.ul); 25643 enableWrapScroll = disableElementScrollEvents(angular.element(elements.wrap)); 25644 } 25645 } else if (hidden && !oldHidden) { 25646 $mdUtil.enableScrolling(); 25647 25648 if (enableWrapScroll) { 25649 enableWrapScroll(); 25650 enableWrapScroll = null; 25651 } 25652 } 25653 } 25654 25655 /** 25656 * Disables scrolling for a specific element 25657 */ 25658 function disableElementScrollEvents(element) { 25659 25660 function preventDefault(e) { 25661 e.preventDefault(); 25662 } 25663 25664 element.on('wheel', preventDefault); 25665 element.on('touchmove', preventDefault); 25666 25667 return function() { 25668 element.off('wheel', preventDefault); 25669 element.off('touchmove', preventDefault); 25670 }; 25671 } 25672 25673 /** 25674 * When the user mouses over the dropdown menu, ignore blur events. 25675 */ 25676 function onListEnter () { 25677 noBlur = true; 25678 } 25679 25680 /** 25681 * When the user's mouse leaves the menu, blur events may hide the menu again. 25682 */ 25683 function onListLeave () { 25684 if (!hasFocus && !ctrl.hidden) elements.input.focus(); 25685 noBlur = false; 25686 ctrl.hidden = shouldHide(); 25687 } 25688 25689 /** 25690 * When the mouse button is released, send focus back to the input field. 25691 */ 25692 function onMouseup () { 25693 elements.input.focus(); 25694 } 25695 25696 /** 25697 * Handles changes to the selected item. 25698 * @param selectedItem 25699 * @param previousSelectedItem 25700 */ 25701 function selectedItemChange (selectedItem, previousSelectedItem) { 25702 25703 updateModelValidators(); 25704 25705 if (selectedItem) { 25706 getDisplayValue(selectedItem).then(function (val) { 25707 $scope.searchText = val; 25708 handleSelectedItemChange(selectedItem, previousSelectedItem); 25709 }); 25710 } else if (previousSelectedItem && $scope.searchText) { 25711 getDisplayValue(previousSelectedItem).then(function(displayValue) { 25712 // Clear the searchText, when the selectedItem is set to null. 25713 // Do not clear the searchText, when the searchText isn't matching with the previous 25714 // selected item. 25715 if (angular.isString($scope.searchText) 25716 && displayValue.toString().toLowerCase() === $scope.searchText.toLowerCase()) { 25717 $scope.searchText = ''; 25718 } 25719 }); 25720 } 25721 25722 if (selectedItem !== previousSelectedItem) announceItemChange(); 25723 } 25724 25725 /** 25726 * Use the user-defined expression to announce changes each time a new item is selected 25727 */ 25728 function announceItemChange () { 25729 angular.isFunction($scope.itemChange) && $scope.itemChange(getItemAsNameVal($scope.selectedItem)); 25730 } 25731 25732 /** 25733 * Use the user-defined expression to announce changes each time the search text is changed 25734 */ 25735 function announceTextChange () { 25736 angular.isFunction($scope.textChange) && $scope.textChange(); 25737 } 25738 25739 /** 25740 * Calls any external watchers listening for the selected item. Used in conjunction with 25741 * `registerSelectedItemWatcher`. 25742 * @param selectedItem 25743 * @param previousSelectedItem 25744 */ 25745 function handleSelectedItemChange (selectedItem, previousSelectedItem) { 25746 selectedItemWatchers.forEach(function (watcher) { watcher(selectedItem, previousSelectedItem); }); 25747 } 25748 25749 /** 25750 * Register a function to be called when the selected item changes. 25751 * @param cb 25752 */ 25753 function registerSelectedItemWatcher (cb) { 25754 if (selectedItemWatchers.indexOf(cb) == -1) { 25755 selectedItemWatchers.push(cb); 25756 } 25757 } 25758 25759 /** 25760 * Unregister a function previously registered for selected item changes. 25761 * @param cb 25762 */ 25763 function unregisterSelectedItemWatcher (cb) { 25764 var i = selectedItemWatchers.indexOf(cb); 25765 if (i != -1) { 25766 selectedItemWatchers.splice(i, 1); 25767 } 25768 } 25769 25770 /** 25771 * Handles changes to the searchText property. 25772 * @param searchText 25773 * @param previousSearchText 25774 */ 25775 function handleSearchText (searchText, previousSearchText) { 25776 ctrl.index = getDefaultIndex(); 25777 25778 // do nothing on init 25779 if (searchText === previousSearchText) return; 25780 25781 updateModelValidators(); 25782 25783 getDisplayValue($scope.selectedItem).then(function (val) { 25784 // clear selected item if search text no longer matches it 25785 if (searchText !== val) { 25786 $scope.selectedItem = null; 25787 25788 25789 // trigger change event if available 25790 if (searchText !== previousSearchText) announceTextChange(); 25791 25792 // cancel results if search text is not long enough 25793 if (!isMinLengthMet()) { 25794 ctrl.matches = []; 25795 25796 setLoading(false); 25797 reportMessages(false, ReportType.Count); 25798 25799 } else { 25800 handleQuery(); 25801 } 25802 } 25803 }); 25804 25805 } 25806 25807 /** 25808 * Handles input blur event, determines if the dropdown should hide. 25809 */ 25810 function blur($event) { 25811 hasFocus = false; 25812 25813 if (!noBlur) { 25814 ctrl.hidden = shouldHide(); 25815 evalAttr('ngBlur', { $event: $event }); 25816 } 25817 } 25818 25819 /** 25820 * Force blur on input element 25821 * @param forceBlur 25822 */ 25823 function doBlur(forceBlur) { 25824 if (forceBlur) { 25825 noBlur = false; 25826 hasFocus = false; 25827 } 25828 elements.input.blur(); 25829 } 25830 25831 /** 25832 * Handles input focus event, determines if the dropdown should show. 25833 */ 25834 function focus($event) { 25835 hasFocus = true; 25836 25837 if (isSearchable() && isMinLengthMet()) { 25838 handleQuery(); 25839 } 25840 25841 ctrl.hidden = shouldHide(); 25842 25843 evalAttr('ngFocus', { $event: $event }); 25844 } 25845 25846 /** 25847 * Handles keyboard input. 25848 * @param event 25849 */ 25850 function keydown (event) { 25851 switch (event.keyCode) { 25852 case $mdConstant.KEY_CODE.DOWN_ARROW: 25853 if (ctrl.loading) return; 25854 event.stopPropagation(); 25855 event.preventDefault(); 25856 ctrl.index = Math.min(ctrl.index + 1, ctrl.matches.length - 1); 25857 updateScroll(); 25858 reportMessages(false, ReportType.Selected); 25859 break; 25860 case $mdConstant.KEY_CODE.UP_ARROW: 25861 if (ctrl.loading) return; 25862 event.stopPropagation(); 25863 event.preventDefault(); 25864 ctrl.index = ctrl.index < 0 ? ctrl.matches.length - 1 : Math.max(0, ctrl.index - 1); 25865 updateScroll(); 25866 reportMessages(false, ReportType.Selected); 25867 break; 25868 case $mdConstant.KEY_CODE.TAB: 25869 // If we hit tab, assume that we've left the list so it will close 25870 onListLeave(); 25871 25872 if (ctrl.hidden || ctrl.loading || ctrl.index < 0 || ctrl.matches.length < 1) return; 25873 select(ctrl.index); 25874 break; 25875 case $mdConstant.KEY_CODE.ENTER: 25876 if (ctrl.hidden || ctrl.loading || ctrl.index < 0 || ctrl.matches.length < 1) return; 25877 if (hasSelection()) return; 25878 event.stopPropagation(); 25879 event.preventDefault(); 25880 select(ctrl.index); 25881 break; 25882 case $mdConstant.KEY_CODE.ESCAPE: 25883 event.preventDefault(); // Prevent browser from always clearing input 25884 if (!shouldProcessEscape()) return; 25885 event.stopPropagation(); 25886 25887 clearSelectedItem(); 25888 if ($scope.searchText && hasEscapeOption('clear')) { 25889 clearSearchText(); 25890 } 25891 25892 // Manually hide (needed for mdNotFound support) 25893 ctrl.hidden = true; 25894 25895 if (hasEscapeOption('blur')) { 25896 // Force the component to blur if they hit escape 25897 doBlur(true); 25898 } 25899 25900 break; 25901 default: 25902 } 25903 } 25904 25905 //-- getters 25906 25907 /** 25908 * Returns the minimum length needed to display the dropdown. 25909 * @returns {*} 25910 */ 25911 function getMinLength () { 25912 return angular.isNumber($scope.minLength) ? $scope.minLength : 1; 25913 } 25914 25915 /** 25916 * Returns the display value for an item. 25917 * @param item 25918 * @returns {*} 25919 */ 25920 function getDisplayValue (item) { 25921 return $q.when(getItemText(item) || item).then(function(itemText) { 25922 if (itemText && !angular.isString(itemText)) { 25923 $log.warn('md-autocomplete: Could not resolve display value to a string. ' + 25924 'Please check the `md-item-text` attribute.'); 25925 } 25926 25927 return itemText; 25928 }); 25929 25930 /** 25931 * Getter function to invoke user-defined expression (in the directive)
25932 * to convert your object to a single string. 25933 */ 25934 function getItemText (item) { 25935 return (item && $scope.itemText) ? $scope.itemText(getItemAsNameVal(item)) : null; 25936 } 25937 } 25938 25939 /** 25940 * Returns the locals object for compiling item templates. 25941 * @param item 25942 * @returns {{}} 25943 */ 25944 function getItemAsNameVal (item) { 25945 if (!item) return undefined; 25946 25947 var locals = {}; 25948 if (ctrl.itemName) locals[ ctrl.itemName ] = item; 25949 25950 return locals; 25951 } 25952 25953 /** 25954 * Returns the default index based on whether or not autoselect is enabled. 25955 * @returns {number} 25956 */ 25957 function getDefaultIndex () { 25958 return $scope.autoselect ? 0 : -1; 25959 } 25960 25961 /** 25962 * Sets the loading parameter and updates the hidden state. 25963 * @param value {boolean} Whether or not the component is currently loading. 25964 */ 25965 function setLoading(value) { 25966 if (ctrl.loading != value) { 25967 ctrl.loading = value; 25968 } 25969 25970 // Always refresh the hidden variable as something else might have changed 25971 ctrl.hidden = shouldHide(); 25972 } 25973 25974 /** 25975 * Determines if the menu should be hidden. 25976 * @returns {boolean} 25977 */ 25978 function shouldHide () { 25979 if (!isSearchable()) return true; // Hide when not able to query 25980 else return !shouldShow(); // Hide when the dropdown is not able to show. 25981 } 25982 25983 /** 25984 * Determines whether the autocomplete is able to query within the current state. 25985 * @returns {boolean} 25986 */ 25987 function isSearchable() { 25988 if (ctrl.loading && !hasMatches()) return false; // No query when query is in progress. 25989 else if (hasSelection()) return false; // No query if there is already a selection 25990 else if (!hasFocus) return false; // No query if the input does not have focus 25991 return true; 25992 } 25993 25994 /** 25995 * Determines if the escape keydown should be processed 25996 * @returns {boolean} 25997 */ 25998 function shouldProcessEscape() { 25999 return hasEscapeOption('blur') || !ctrl.hidden || ctrl.loading || hasEscapeOption('clear') && $scope.searchText; 26000 } 26001 26002 /** 26003 * Determines if an escape option is set 26004 * @returns {boolean} 26005 */ 26006 function hasEscapeOption(option) { 26007 return !$scope.escapeOptions || $scope.escapeOptions.toLowerCase().indexOf(option) !== -1; 26008 } 26009 26010 /** 26011 * Determines if the menu should be shown. 26012 * @returns {boolean} 26013 */ 26014 function shouldShow() { 26015 return (isMinLengthMet() && hasMatches()) || notFoundVisible(); 26016 } 26017 26018 /** 26019 * Returns true if the search text has matches. 26020 * @returns {boolean} 26021 */ 26022 function hasMatches() { 26023 return ctrl.matches.length ? true : false; 26024 } 26025 26026 /** 26027 * Returns true if the autocomplete has a valid selection. 26028 * @returns {boolean} 26029 */ 26030 function hasSelection() { 26031 return ctrl.scope.selectedItem ? true : false; 26032 } 26033 26034 /** 26035 * Returns true if the loading indicator is, or should be, visible. 26036 * @returns {boolean} 26037 */ 26038 function loadingIsVisible() { 26039 return ctrl.loading && !hasSelection(); 26040 } 26041 26042 /** 26043 * Returns the display value of the current item. 26044 * @returns {*} 26045 */ 26046 function getCurrentDisplayValue () { 26047 return getDisplayValue(ctrl.matches[ ctrl.index ]); 26048 } 26049 26050 /** 26051 * Determines if the minimum length is met by the search text. 26052 * @returns {*} 26053 */ 26054 function isMinLengthMet () { 26055 return ($scope.searchText || '').length >= getMinLength(); 26056 } 26057 26058 //-- actions 26059 26060 /** 26061 * Defines a public property with a handler and a default value. 26062 * @param key 26063 * @param handler 26064 * @param value 26065 */ 26066 function defineProperty (key, handler, value) { 26067 Object.defineProperty(ctrl, key, { 26068 get: function () { return value; }, 26069 set: function (newValue) { 26070 var oldValue = value; 26071 value = newValue; 26072 handler(newValue, oldValue); 26073 } 26074 }); 26075 } 26076 26077 /** 26078 * Selects the item at the given index. 26079 * @param index 26080 */ 26081 function select (index) { 26082 //-- force form to update state for validation 26083 $mdUtil.nextTick(function () { 26084 getDisplayValue(ctrl.matches[ index ]).then(function (val) { 26085 var ngModel = elements.$.input.controller('ngModel'); 26086 ngModel.$setViewValue(val); 26087 ngModel.$render(); 26088 }).finally(function () { 26089 $scope.selectedItem = ctrl.matches[ index ];
26090 setLoading(false); 26091 }); 26092 }, false); 26093 } 26094 26095 /** 26096 * Clears the searchText value and selected item. 26097 */ 26098 function clearValue () { 26099 clearSelectedItem(); 26100 clearSearchText(); 26101 } 26102 26103 /** 26104 * Clears the selected item 26105 */ 26106 function clearSelectedItem () { 26107 // Reset our variables 26108 ctrl.index = 0; 26109 ctrl.matches = []; 26110 } 26111 26112 /** 26113 * Clears the searchText value 26114 */ 26115 function clearSearchText () { 26116 // Set the loading to true so we don't see flashes of content. 26117 // The flashing will only occur when an async request is running. 26118 // So the loading process will stop when the results had been retrieved. 26119 setLoading(true); 26120 26121 $scope.searchText = ''; 26122 26123 // Normally, triggering the change / input event is unnecessary, because the browser detects it properly. 26124 // But some browsers are not detecting it properly, which means that we have to trigger the event. 26125 // Using the `input` is not working properly, because for example IE11 is not supporting the `input` event. 26126 // The `change` event is a good alternative and is supported by all supported browsers. 26127 var eventObj = document.createEvent('CustomEvent'); 26128 eventObj.initCustomEvent('change', true, true, { value: '' }); 26129 elements.input.dispatchEvent(eventObj); 26130 26131 // For some reason, firing the above event resets the value of $scope.searchText if 26132 // $scope.searchText has a space character at the end, so we blank it one more time and then 26133 // focus. 26134 elements.input.blur(); 26135 $scope.searchText = ''; 26136 elements.input.focus(); 26137 } 26138 26139 /** 26140 * Fetches the results for the provided search text. 26141 * @param searchText 26142 */ 26143 function fetchResults (searchText) { 26144 var items = $scope.$parent.$eval(itemExpr), 26145 term = searchText.toLowerCase(), 26146 isList = angular.isArray(items), 26147 isPromise = !!items.then; // Every promise should contain a `then` property 26148 26149 if (isList) onResultsRetrieved(items); 26150 else if (isPromise) handleAsyncResults(items); 26151 26152 function handleAsyncResults(items) { 26153 if ( !items ) return; 26154 26155 items = $q.when(items); 26156 fetchesInProgress++; 26157 setLoading(true); 26158 26159 $mdUtil.nextTick(function () { 26160 items 26161 .then(onResultsRetrieved) 26162 .finally(function(){ 26163 if (--fetchesInProgress === 0) { 26164 setLoading(false); 26165 } 26166 }); 26167 },true, $scope); 26168 } 26169 26170 function onResultsRetrieved(matches) { 26171 cache[term] = matches; 26172 26173 // Just cache the results if the request is now outdated. 26174 // The request becomes outdated, when the new searchText has changed during the result fetching. 26175 if ((searchText || '') !== ($scope.searchText || '')) { 26176 return; 26177 } 26178 26179 handleResults(matches); 26180 } 26181 } 26182 26183 26184 /** 26185 * Reports given message types to supported screenreaders. 26186 * @param {boolean} isPolite Whether the announcement should be polite. 26187 * @param {!number} types Message flags to be reported to the screenreader. 26188 */ 26189 function reportMessages(isPolite, types) { 26190 26191 var politeness = isPolite ? 'polite' : 'assertive'; 26192 var messages = []; 26193 26194 if (types & ReportType.Selected && ctrl.index !== -1) { 26195 messages.push(getCurrentDisplayValue()); 26196 } 26197 26198 if (types & ReportType.Count) { 26199 messages.push($q.resolve(getCountMessage())); 26200 } 26201 26202 $q.all(messages).then(function(data) { 26203 $mdLiveAnnouncer.announce(data.join(' '), politeness); 26204 }); 26205 26206 } 26207 26208 /** 26209 * Returns the ARIA message for how many results match the current query. 26210 * @returns {*} 26211 */ 26212 function getCountMessage () { 26213 switch (ctrl.matches.length) { 26214 case 0: 26215 return 'There are no matches available.'; 26216 case 1: 26217 return 'There is 1 match available.'; 26218 default: 26219 return 'There are ' + ctrl.matches.length + ' matches available.'; 26220 } 26221 } 26222 26223 /** 26224 * Makes sure that the focused element is within view. 26225 */ 26226 function updateScroll () { 26227 if (!elements.li[0]) return; 26228 var height = elements.li[0].offsetHeight, 26229 top = height * ctrl.index, 26230 bot = top + height, 26231 hgt = elements.scroller.clientHeight, 26232 scrollTop = elements.scroller.scrollTop; 26233 if (top < scrollTop) { 26234 scrollTo(top); 26235 } else if (bot > scrollTop + hgt) { 26236 scrollTo(bot - hgt); 26237 } 26238 } 26239 26240 function isPromiseFetching() { 26241 return fetchesInProgress !== 0; 26242 } 26243 26244 function scrollTo (offset) {
vendor: 1,762 bytes, lines 26245-26293
26245 elements.$.scrollContainer.controller('mdVirtualRepeatContainer').scrollTo(offset); 26246 } 26247 26248 function notFoundVisible () { 26249 var textLength = (ctrl.scope.searchText || '').length; 26250 26251 return ctrl.hasNotFound && !hasMatches() && (!ctrl.loading || isPromiseFetching()) && textLength >= getMinLength() && (hasFocus || noBlur) && !hasSelection(); 26252 } 26253 26254 /** 26255 * Starts the query to gather the results for the current searchText. Attempts to return cached 26256 * results first, then forwards the process to `fetchResults` if necessary. 26257 */ 26258 function handleQuery () { 26259 var searchText = $scope.searchText || ''; 26260 var term = searchText.toLowerCase(); 26261 26262 // If caching is enabled and the current searchText is stored in the cache 26263 if (!$scope.noCache && cache[term]) { 26264 // The results should be handled as same as a normal un-cached request does. 26265 handleResults(cache[term]); 26266 } else { 26267 fetchResults(searchText); 26268 } 26269 26270 ctrl.hidden = shouldHide(); 26271 } 26272 26273 /** 26274 * Handles the retrieved results by showing them in the autocompletes dropdown. 26275 * @param results Retrieved results 26276 */ 26277 function handleResults(results) { 26278 ctrl.matches = results; 26279 ctrl.hidden = shouldHide(); 26280 26281 // If loading is in progress, then we'll end the progress. This is needed for example, 26282 // when the `clear` button was clicked, because there we always show the loading process, to prevent flashing. 26283 if (ctrl.loading) setLoading(false); 26284 26285 if ($scope.selectOnMatch) selectItemOnMatch(); 26286 26287 positionDropdown(); 26288 reportMessages(true, ReportType.Count); 26289 } 26290 26291 /** 26292 * If there is only one matching item and the search text matches its display value exactly, 26293 * automatically select that item. Note: This function is only called
26293if the user uses the 26294 * `md-select-on-match` flag. 26295 */ 26296 function selectItemOnMatch () { 26297 var searchText = $scope.searchText, 26298 matches = ctrl.matches, 26299 item = matches[ 0 ]; 26300 if (matches.length === 1) getDisplayValue(item).then(function (displayValue) { 26301 var isMatching = searchText == displayValue; 26302 if ($scope.matchInsensitive && !isMatching) { 26303 isMatching = searchText.toLowerCase() == displayValue.toLowerCase(); 26304 } 26305 26306 if (isMatching) select(0); 26307 }); 26308 } 26309 26310 /** 26311 * Evaluates an attribute expression against the parent scope. 26312 * @param {String} attr Name of the attribute to be evaluated. 26313 * @param {Object?} locals Properties to be injected into the evaluation context. 26314 */ 26315 function evalAttr(attr, locals) { 26316 if ($attrs[attr]) { 26317 $scope.$parent.$eval($attrs[attr], locals || {}); 26318 } 26319 } 26320 26321} 26322 26323})(); 26324(function(){ 26325"use strict"; 26326 26327 26328MdAutocomplete.$inject = ["$$mdSvgRegistry"];angular 26329 .module('material.components.autocomplete') 26330 .directive('mdAutocomplete', MdAutocomplete); 26331 26332/** 26333 * @ngdoc directive 26334 * @name mdAutocomplete 26335 * @module material.components.autocomplete 26336 * 26337 * @description 26338 * `<md-autocomplete>` is a special input component with a drop-down of all possible matches to a 26339 * custom query. This component allows you to provide real-time suggestions as the user types 26340 * in the input area. 26341 * 26342 * To start, you will need to specify the required parameters and provide a template for your 26343 * results. The content inside `md-autocomplete` will be treated as a template. 26344 * 26345 * In more complex cases, you may want to include other content such as a message to display when 26346 * no matches were found. You can do this by wrapping your template in `md-item-template` and 26347 * adding a tag for `md-not-found`. An example of this is shown below. 26348 * 26349 * To reset the displayed value you must clear both values for `md-search-text` and `md-selected-item`. 26350 * 26351 * ### Validation 26352 * 26353 * You can use `ng-messages` to include validation the same way that you would normally validate; 26354 * however, if you want to replicate a standard input with a floating label, you will have to 26355 * do the following: 26356 * 26357 * - Make sure that your template is wrapped in `md-item-template` 26358 * - Add your `ng-messages` code inside of `md-autocomplete` 26359 * - Add your validation properties to `md-autocomplete` (ie. `required`) 26360 * - Add a `name` to `md-autocomplete` (to be used on the generated `input`) 26361 * 26362 * There is an example below of how this should look. 26363 * 26364 * ### Snapping Drop-Down 26365 * 26366 * You can cause the autocomplete drop-down to snap to an ancestor element by applying the 26367 * `md-autocomplete-snap` attribute to that element. You can also snap to the width of 26368 * the `md-autocomplete-snap` element by setting the attribute's value to `width` 26369 * (ie. `md-autocomplete-snap="width"`). 26370 * 26371 * ### Notes 26372 * 26373 * **Autocomplete Dropdown Items Rendering** 26374 * 26375 * The `md-autocomplete` uses the the <a ng-href="api/directive/mdVirtualRepeatContainer">VirtualRepeat</a> 26376 * directive for displaying the results inside of the dropdown.<br/> 26377 * 26378 * > When encountering issues regarding the item template please take a look at the 26379 * <a ng-href="api/directive/mdVirtualRepeatContainer">VirtualRepeatContainer</a> documentation. 26380 * 26381 * **Autocomplete inside of a Virtual Repeat** 26382 * 26383 * When using the `md-autocomplete` directive inside of a 26384 * <a ng-href="api/directive/mdVirtualRepeatContainer">VirtualRepeatContainer</a> the dropdown items might 26385 * not update properly, because caching of the results is enabled by default. 26386 * 26387 * The autocomplete will then show invalid dropdown items, because the VirtualRepeat only updates the 26388 * scope bindings, rather than re-creating the `md-autocomplete` and the previous cached results will be used. 26389 * 26390 * > To avoid such problems ensure that the autocomplete does not cache any results. 26391 * 26392 * <hljs lang="html"> 26393 * <md-autocomplete 26394 * md-no-cache="true" 26395 * md-selected-item="selectedItem" 26396 * md-items="item in items" 26397 * md-search-text="searchText" 26398 * md-item-text="item.display"> 26399 * <span>{{ item.display }}</span> 26400 * </md-autocomplete> 26401 * </hljs> 26402 * 26403 * 26404 * 26405 * @param {expression} md-items An expression in the format of `item in results` to iterate over 26406 * matches for your search.<br/><br/> 26407 * The `results` expression can be also a function, which returns the results synchronously 26408 * or asynchronously (per Promise) 26409 * @param {expression=} md-selected-item-change An expression to be run each time a new item is 26410 * selected 26411 * @param {expression=} md-search-text-change An expression to be run each time the search text 26412 * updates 26413 * @param {expression=} md-search-text A model to bind the search query text to 26414 * @param {object=} md-selected-item A model to bind the selected item to 26415 * @param {expression=} md-item-text An expression that will convert your object to a single str
26415ing. 26416 * @param {string=} placeholder Placeholder text that will be forwarded to the input. 26417 * @param {boolean=} md-no-cache Disables the internal caching that happens in autocomplete 26418 * @param {boolean=} ng-disabled Determines whether or not to disable the input field 26419 * @param {boolean=} md-require-match When set to true, the autocomplete will add a validator, 26420 * which will evaluate to false, when no item is currently selected. 26421 * @param {number=} md-min-length Specifies the minimum length of text before autocomplete will 26422 * make suggestions 26423 * @param {number=} md-delay Specifies the amount of time (in milliseconds) to wait before looking 26424 * for results 26425 * @param {boolean=} md-clear-button Whether the clear button for the autocomplete input should show up or not. 26426 * @param {boolean=} md-autofocus If true, the autocomplete will be automatically focused when a `$mdDialog`, 26427 * `$mdBottomsheet` or `$mdSidenav`, which contains the autocomplete, is opening. <br/><br/> 26428 * Also the autocomplete will immediately focus the input element. 26429 * @param {boolean=} md-no-asterisk When present, asterisk will not be appended to the floating label 26430 * @param {boolean=} md-autoselect If set to true, the first item will be automatically selected 26431 * in the dropdown upon open. 26432 * @param {string=} md-menu-class This will be applied to the dropdown menu for styling 26433 * @param {string=} md-floating-label This will add a floating label to autocomplete and wrap it in 26434 * `md-input-container` 26435 * @param {string=} md-input-name The name attribute given to the input element to be used with 26436 * FormController 26437 * @param {string=} md-select-on-focus When present the inputs text will be automatically selected 26438 * on focus. 26439 * @param {string=} md-input-id An ID to be added to the input element 26440 * @param {number=} md-input-minlength The minimum length for the input's value for validation 26441 * @param {number=} md-input-maxlength The maximum length for the input's value for validation 26442 * @param {boolean=} md-select-on-match When set, autocomplete will automatically select exact 26443 * the item if the search text is an exact match. <br/><br/> 26444 * Exact match means that there is only one match showing up. 26445 * @param {boolean=} md-match-case-insensitive When set and using `md-select-on-match`, autocomplete 26446 * will select on case-insensitive match 26447 * @param {string=} md-escape-options Override escape key logic. Default is `blur clear`.<br/> 26448 * Options: `blur | clear`, `none` 26449 * @param {string=} md-dropdown-items Specifies the maximum amount of items to be shown in 26450 * the dropdown.<br/><br/> 26451 * When the dropdown doesn't fit into the viewport, the dropdown will shrink 26452 * as less as possible. 26453 * @param {string=} md-dropdown-position Overrides the default dropdown position. Options: `top`, `bottom`. 26454 * @param {string=} ng-trim If set to false, the search text will be not trimmed automatically. 26455 * Defaults to true. 26456 * @param {string=} ng-pattern Adds the pattern validator to the ngModel of the search text. 26457 * [ngPattern Directive](https://docs.angularjs.org/api/ng/directive/ngPattern) 26458 * 26459 * @usage 26460 * ### Basic Example 26461 * <hljs lang="html"> 26462 * <md-autocomplete 26463 * md-selected-item="selectedItem" 26464 * md-search-text="searchText" 26465 * md-items="item in getMatches(searchText)" 26466 * md-item-text="item.display"> 26467 * <span md-highlight-text="searchText">{{item.display}}</span> 26468 * </md-autocomplete> 26469 * </hljs> 26470 * 26471 * ### Example with "not found" message 26472 * <hljs lang="html"> 26473 * <md-autocomplete 26474 * md-selected-item="selectedItem" 26475 * md-search-text="searchText" 26476 * md-items="item in getMatches(searchText)" 26477 * md-item-text="item.display"> 26478 * <md-item-template> 26479 * <span md-highlight-text="searchText">{{item.display}}</span> 26480 * </md-item-template> 26481 * <md-not-found> 26482 * No matches found. 26483 * </md-not-found> 26484 * </md-autocomplete> 26485 * </hljs> 26486 * 26487 * In this example, our code utilizes `md-item-template` and `md-not-found` to specify the 26488 * different parts that make up our component. 26489 * 26490 * ### Clear button for the input 26491 * By default, for floating label autocomplete's the clear button is not showing up 26492 * ([See specs](https://material.google.com/components/text-fields.html#text-fields-auto-complete-text-field)) 26493 * 26494 * Nevertheless, developers are able to explicitly toggle the clear button for all types of autocomplete's. 26495 * 26496 * <hljs lang="html"> 26497 * <md-autocomplete ... md-clear-button="true"></md-autocomplete> 26498 * <md-autocomplete ... md-clear-button="false"></md-autocomplete> 26499 * </hljs> 26500 * 26501 * ### Example with validation 26502 * <hljs lang="html"> 26503 * <form name="autocompleteForm"> 26504 * <md-autocomplete 26505 * required 26506 * md-input-name="autocomplete" 26507 * md-selected-item="selectedItem" 26508 * md-search-text="searchText" 26509 * md-items="item in getMatches(searchText)" 26510 * md-item-text="item.display"> 26511 * <md-item-template>
26512 * <span md-highlight-text="searchText">{{item.display}}</span> 26513 * </md-item-template> 26514 * <div ng-messages="autocompleteForm.autocomplete.$error"> 26515 * <div ng-message="required">This field is required</div> 26516 * </div> 26517 * </md-autocomplete> 26518 * </form> 26519 * </hljs> 26520 * 26521 * In this example, our code utilizes `md-item-template` and `ng-messages` to specify 26522 * input validation for the field. 26523 * 26524 * ### Asynchronous Results 26525 * The autocomplete items expression also supports promises, which will resolve with the query results. 26526 * 26527 * <hljs lang="js"> 26528 * function AppController($scope, $http) { 26529 * $scope.query = function(searchText) { 26530 * return $http 26531 * .get(BACKEND_URL + '/items/' + searchText) 26532 * .then(function(data) { 26533 * // Map the response object to the data object. 26534 * return data; 26535 * }); 26536 * }; 26537 * } 26538 * </hljs> 26539 * 26540 * <hljs lang="html"> 26541 * <md-autocomplete 26542 * md-selected-item="selectedItem" 26543 * md-search-text="searchText" 26544 * md-items="item in query(searchText)"> 26545 * <md-item-template> 26546 * <span md-highlight-text="searchText">{{item}}</span> 26547 * </md-item-template> 26548 * </md-autocomplete> 26549 * </hljs> 26550 * 26551 */ 26552 26553function MdAutocomplete ($$mdSvgRegistry) { 26554 26555 return { 26556 controller: 'MdAutocompleteCtrl', 26557 controllerAs: '$mdAutocompleteCtrl', 26558 scope: { 26559 inputName: '@mdInputName', 26560 inputMinlength: '@mdInputMinlength', 26561 inputMaxlength: '@mdInputMaxlength', 26562 searchText: '=?mdSearchText', 26563 selectedItem: '=?mdSelectedItem', 26564 itemsExpr: '@mdItems', 26565 itemText: '&mdItemText', 26566 placeholder: '@placeholder', 26567 noCache: '=?mdNoCache', 26568 requireMatch: '=?mdRequireMatch', 26569 selectOnMatch: '=?mdSelectOnMatch', 26570 matchInsensitive: '=?mdMatchCaseInsensitive', 26571 itemChange: '&?mdSelectedItemChange', 26572 textChange: '&?mdSearchTextChange', 26573 minLength: '=?mdMinLength', 26574 delay: '=?mdDelay', 26575 autofocus: '=?mdAutofocus', 26576 floatingLabel: '@?mdFloatingLabel', 26577 autoselect: '=?mdAutoselect', 26578 menuClass: '@?mdMenuClass', 26579 inputId: '@?mdInputId', 26580 escapeOptions: '@?mdEscapeOptions', 26581 dropdownItems: '=?mdDropdownItems', 26582 dropdownPosition: '@?mdDropdownPosition', 26583 clearButton: '=?mdClearButton' 26584 },
vendor: 13,250 bytes, lines 26585-26965
26585 compile: function(tElement, tAttrs) { 26586 var attributes = ['md-select-on-focus', 'md-no-asterisk', 'ng-trim', 'ng-pattern']; 26587 var input = tElement.find('input'); 26588 26589 attributes.forEach(function(attribute) { 26590 var attrValue = tAttrs[tAttrs.$normalize(attribute)]; 26591 26592 if (attrValue !== null) { 26593 input.attr(attribute, attrValue); 26594 } 26595 }); 26596 26597 return function(scope, element, attrs, ctrl) { 26598 // Retrieve the state of using a md-not-found template by using our attribute, which will 26599 // be added to the element in the template function. 26600 ctrl.hasNotFound = !!element.attr('md-has-not-found'); 26601 26602 // By default the inset autocomplete should show the clear button when not explicitly overwritten. 26603 if (!angular.isDefined(attrs.mdClearButton) && !scope.floatingLabel) { 26604 scope.clearButton = true; 26605 } 26606 } 26607 }, 26608 template: function (element, attr) { 26609 var noItemsTemplate = getNoItemsTemplate(), 26610 itemTemplate = getItemTemplate(), 26611 leftover = element.html(), 26612 tabindex = attr.tabindex; 26613 26614 // Set our attribute for the link function above which runs later. 26615 // We will set an attribute, because otherwise the stored variables will be trashed when 26616 // removing the element is hidden while retrieving the template. For example when using ngIf. 26617 if (noItemsTemplate) element.attr('md-has-not-found', true); 26618 26619 // Always set our tabindex of the autocomplete directive to -1, because our input 26620 // will hold the actual tabindex. 26621 element.attr('tabindex', '-1'); 26622 26623 return '\ 26624 <md-autocomplete-wrap\ 26625 ng-class="{ \'md-whiteframe-z1\': !floatingLabel, \ 26626 \'md-menu-showing\': !$mdAutocompleteCtrl.hidden, \ 26627 \'md-show-clear-button\': !!clearButton }">\ 26628 ' + getInputElement() + '\ 26629 ' + getClearButton() + '\ 26630 <md-progress-linear\ 26631 class="' + (attr.mdFloatingLabel ? 'md-inline' : '') + '"\ 26632 ng-if="$mdAutocompleteCtrl.loadingIsVisible()"\ 26633 md-mode="indeterminate"></md-progress-linear>\ 26634 <md-virtual-repeat-container\ 26635 md-auto-shrink\ 26636 md-auto-shrink-min="1"\ 26637 ng-mouseenter="$mdAutocompleteCtrl.listEnter()"\ 26638 ng-mouseleave="$mdAutocompleteCtrl.listLeave()"\ 26639 ng-mouseup="$mdAutocompleteCtrl.mouseUp()"\ 26640 ng-hide="$mdAutocompleteCtrl.hidden"\ 26641 class="md-autocomplete-suggestions-container md-whiteframe-z1"\ 26642 ng-class="{ \'md-not-found\': $mdAutocompleteCtrl.notFoundVisible() }"\ 26643 role="presentation">\ 26644 <ul class="md-autocomplete-suggestions"\ 26645 ng-class="::menuClass"\ 26646 id="ul-{{$mdAutocompleteCtrl.id}}">\ 26647 <li md-virtual-repeat="item in $mdAutocompleteCtrl.matches"\ 26648 ng-class="{ selected: $index === $mdAutocompleteCtrl.index }"\ 26649 ng-click="$mdAutocompleteCtrl.select($index)"\ 26650 md-extra-name="$mdAutocompleteCtrl.itemName">\ 26651 ' + itemTemplate + '\ 26652 </li>' + noItemsTemplate + '\ 26653 </ul>\ 26654 </md-virtual-repeat-container>\ 26655 </md-autocomplete-wrap>'; 26656 26657 function getItemTemplate() { 26658 var templateTag = element.find('md-item-template').detach(), 26659 html = templateTag.length ? templateTag.html() : element.html(); 26660 if (!templateTag.length) element.empty(); 26661 return '<md-autocomplete-parent-scope md-autocomplete-replace>' + html + '</md-autocomplete-parent-scope>'; 26662 } 26663 26664 function getNoItemsTemplate() { 26665 var templateTag = element.find('md-not-found').detach(), 26666 template = templateTag.length ? templateTag.html() : ''; 26667 return template 26668 ? '<li ng-if="$mdAutocompleteCtrl.notFoundVisible()"\ 26669 md-autocomplete-parent-scope>' + template + '</li>' 26670 : ''; 26671 26672 } 26673 26674 function getInputElement () { 26675 if (attr.mdFloatingLabel) { 26676 return '\ 26677 <md-input-container ng-if="floatingLabel">\ 26678 <label>{{floatingLabel}}</label>\ 26679 <input type="search"\ 26680 ' + (tabindex != null ? 'tabindex="' + tabindex + '"' : '') + '\ 26681 id="{{ inputId || \'fl-input-\' + $mdAutocompleteCtrl.id }}"\ 26682 name="{{inputName}}"\ 26683 autocomplete="off"\ 26684 ng-required="$mdAutocompleteCtrl.isRequired"\ 26685 ng-readonly="$mdAutocompleteCtrl.isReadonly"\ 26686 ng-minlength="inputMinlength"\ 26687 ng-maxlength="inputMaxlength"\ 26688 ng-disabled="$mdAutocompleteCtrl.isDisabled"\ 26689 ng-model="$mdAutocompleteCtrl.scope.searchText"\ 26690 ng-model-options="{ allowInvalid: true }"\ 26691 ng-keydown="$mdAutocompleteCtrl.keydown($event)"\ 26692 ng-blur="$mdAutocompleteCtrl.blur($event)"\ 26693 ng-focus="$mdAutocompleteCtrl.focus($event)"\ 26694 aria-owns="ul-{{$mdAutocompleteCtrl.id}}"\ 26695 aria-label="{{floatingLabel}}"\ 26696 aria-autocomplete="list"\ 26697 role="combobox"\ 26698 aria-haspopup="true"\ 26699 aria-activedescendant=""\ 26700 aria-expanded="{{!$mdAutocompleteCtrl.hidden}}"/>\ 26701 <div md-autocomplete-parent-scope md-autocomplete-replace>' + leftover + '</div>\ 26702 </md-input-container>'; 26703 } else { 26704 return '\ 26705 <input type="search"\ 26706 ' + (tabindex != null ? 'tabindex="' + tabindex + '"' : '') + '\ 26707 id="{{ inputId || \'input-\' + $mdAutocompleteCtrl.id }}"\ 26708 name="{{inputName}}"\ 26709 ng-if="!floatingLabel"\ 26710 autocomplete="off"\ 26711 ng-required="$mdAutocompleteCtrl.isRequired"\ 26712 ng-disabled="$mdAutocompleteCtrl.isDisabled"\ 26713 ng-readonly="$mdAutocompleteCtrl.isReadonly"\ 26714 ng-minlength="inputMinlength"\ 26715 ng-maxlength="inputMaxlength"\ 26716 ng-model="$mdAutocompleteCtrl.scope.searchText"\ 26717 ng-keydown="$mdAutocompleteCtrl.keydown($event)"\ 26718 ng-blur="$mdAutocompleteCtrl.blur($event)"\ 26719 ng-focus="$mdAutocompleteCtrl.focus($event)"\ 26720 placeholder="{{placeholder}}"\ 26721 aria-owns="ul-{{$mdAutocompleteCtrl.id}}"\ 26722 aria-label="{{placeholder}}"\ 26723 aria-autocomplete="list"\ 26724 role="combobox"\ 26725 aria-haspopup="true"\ 26726 aria-activedescendant=""\ 26727 aria-expanded="{{!$mdAutocompleteCtrl.hidden}}"/>'; 26728 } 26729 } 26730 26731 function getClearButton() { 26732 return '' + 26733 '<button ' + 26734 'type="button" ' + 26735 'aria-label="Clear Input" ' + 26736 'tabindex="-1" ' + 26737 'ng-if="clearButton && $mdAutocompleteCtrl.scope.searchText" ' + 26738 'ng-click="$mdAutocompleteCtrl.clear($event)">' + 26739 '<md-icon md-svg-src="' + $$mdSvgRegistry.mdClose + '"></md-icon>' + 26740 '</button>'; 26741 } 26742 } 26743 }; 26744} 26745 26746})(); 26747(function(){ 26748"use strict"; 26749 26750 26751MdAutocompleteItemScopeDirective.$inject = ["$compile", "$mdUtil"];angular 26752 .module('material.components.autocomplete') 26753 .directive('mdAutocompleteParentScope', MdAutocompleteItemScopeDirective); 26754 26755function MdAutocompleteItemScopeDirective($compile, $mdUtil) { 26756 return { 26757 restrict: 'AE', 26758 compile: compile, 26759 terminal: true, 26760 transclude: 'element' 26761 }; 26762 26763 function compile(tElement, tAttr, transclude) { 26764 return function postLink(scope, element, attr) { 26765 var ctrl = scope.$mdAutocompleteCtrl; 26766 var newScope = ctrl.parent.$new(); 26767 var itemName = ctrl.itemName; 26768 26769 // Watch for changes to our scope's variables and copy them to the new scope 26770 watchVariable('$index', '$index'); 26771 watchVariable('item', itemName); 26772 26773 // Ensure that $digest calls on our scope trigger $digest on newScope. 26774 connectScopes(); 26775 26776 // Link the element against newScope. 26777 transclude(newScope, function(clone) { 26778 element.after(clone); 26779 }); 26780 26781 /** 26782 * Creates a watcher for variables that are copied from the parent scope 26783 * @param variable 26784 * @param alias 26785 */ 26786 function watchVariable(variable, alias) { 26787 newScope[alias] = scope[variable]; 26788 26789 scope.$watch(variable, function(value) { 26790 $mdUtil.nextTick(function() { 26791 newScope[alias] = value; 26792 }); 26793 }); 26794 } 26795 26796 /** 26797 * Creates watchers on scope and newScope that ensure that for any 26798 * $digest of scope, newScope is also $digested. 26799 */ 26800 function connectScopes() { 26801 var scopeDigesting = false; 26802 var newScopeDigesting = false; 26803 26804 scope.$watch(function() { 26805 if (newScopeDigesting || scopeDigesting) { 26806 return; 26807 } 26808 26809 scopeDigesting = true; 26810 scope.$$postDigest(function() { 26811 if (!newScopeDigesting) { 26812 newScope.$digest(); 26813 } 26814 26815 scopeDigesting = newScopeDigesting = false; 26816 }); 26817 }); 26818 26819 newScope.$watch(function() { 26820 newScopeDigesting = true; 26821 }); 26822 } 26823 }; 26824 } 26825} 26826})(); 26827(function(){ 26828"use strict"; 26829 26830 26831MdHighlightCtrl.$inject = ["$scope", "$element", "$attrs"];angular 26832 .module('material.components.autocomplete') 26833 .controller('MdHighlightCtrl', MdHighlightCtrl); 26834 26835function MdHighlightCtrl ($scope, $element, $attrs) { 26836 this.$scope = $scope; 26837 this.$element = $element; 26838 this.$attrs = $attrs; 26839 26840 // Cache the Regex to avoid rebuilding each time. 26841 this.regex = null; 26842} 26843 26844MdHighlightCtrl.prototype.init = function(unsafeTermFn, unsafeContentFn) { 26845 26846 this.flags = this.$attrs.mdHighlightFlags || ''; 26847 26848 this.unregisterFn = this.$scope.$watch(function($scope) { 26849 return { 26850 term: unsafeTermFn($scope), 26851 contentText: unsafeContentFn($scope) 26852 }; 26853 }.bind(this), this.onRender.bind(this), true); 26854 26855 this.$element.on('$destroy', this.unregisterFn); 26856}; 26857 26858/** 26859 * Triggered once a new change has been recognized and the highlighted 26860 * text needs to be updated. 26861 */ 26862MdHighlightCtrl.prototype.onRender = function(state, prevState) { 26863 26864 var contentText = state.contentText; 26865 26866 /* Update the regex if it's outdated, because we don't want to rebuilt it constantly. */ 26867 if (this.regex === null || state.term !== prevState.term) { 26868 this.regex = this.createRegex(state.term, this.flags); 26869 } 26870 26871 /* If a term is available apply the regex to the content */ 26872 if (state.term) { 26873 this.applyRegex(contentText); 26874 } else { 26875 this.$element.text(contentText); 26876 } 26877 26878}; 26879 26880/** 26881 * Decomposes the specified text into different tokens (whether match or not). 26882 * Breaking down the string guarantees proper XSS protection due to the native browser 26883 * escaping of unsafe text. 26884 */ 26885MdHighlightCtrl.prototype.applyRegex = function(text) { 26886 var tokens = this.resolveTokens(text); 26887 26888 this.$element.empty(); 26889 26890 tokens.forEach(function (token) { 26891 26892 if (token.isMatch) { 26893 var tokenEl = angular.element('<span class="highlight">').text(token.text); 26894 26895 this.$element.append(tokenEl); 26896 } else { 26897 this.$element.append(document.createTextNode(token)); 26898 } 26899 26900 }.bind(this)); 26901 26902}; 26903 26904 /** 26905 * Decomposes the specified text into different tokens by running the regex against the text. 26906 */ 26907MdHighlightCtrl.prototype.resolveTokens = function(string) { 26908 var tokens = []; 26909 var lastIndex = 0; 26910 26911 // Use replace here, because it supports global and single regular expressions at same time. 26912 string.replace(this.regex, function(match, index) { 26913 appendToken(lastIndex, index); 26914 26915 tokens.push({ 26916 text: match, 26917 isMatch: true 26918 }); 26919 26920 lastIndex = index + match.length; 26921 }); 26922 26923 // Append the missing text as a token. 26924 appendToken(lastIndex); 26925 26926 return tokens; 26927 26928 function appendToken(from, to) { 26929 var targetText = string.slice(from, to); 26930 targetText && tokens.push(targetText); 26931 } 26932}; 26933 26934/** Creates a regex for the specified text with the given flags. */ 26935MdHighlightCtrl.prototype.createRegex = function(term, flags) { 26936 var startFlag = '', endFlag = ''; 26937 var regexTerm = this.sanitizeRegex(term); 26938 26939 if (flags.indexOf('^') >= 0) startFlag = '^'; 26940 if (flags.indexOf('$') >= 0) endFlag = '$'; 26941 26942 return new RegExp(startFlag + regexTerm + endFlag, flags.replace(/[$\^]/g, '')); 26943}; 26944 26945/** Sanitizes a regex by removing all common RegExp identifiers */ 26946MdHighlightCtrl.prototype.sanitizeRegex = function(term) { 26947 return term && term.toString().replace(/[\\\^\$\*\+\?\.\(\)\|\{}\[\]]/g, '\\$&'); 26948}; 26949 26950})(); 26951(function(){ 26952"use strict"; 26953 26954 26955MdHighlight.$inject = ["$interpolate", "$parse"];angular 26956 .module('material.components.autocomplete') 26957 .directive('mdHighlightText', MdHighlight); 26958 26959/** 26960 * @ngdoc directive 26961 * @name mdHighlightText 26962 * @module material.components.autocomplete 26963 * 26964 * @description 26965 * The `md-highlight-text` directive allows you to specify text that should be highlighted
26965within 26966 * an element. Highlighted text will be wrapped in `<span class="highlight"></span>` which can 26967 * be styled through CSS. Please note that child elements may not be used with this directive. 26968 * 26969 * @param {string} md-highlight-text A model to be searched for 26970 * @param {string=} md-highlight-flags A list of flags (loosely based on JavaScript RexExp flags). 26971 * #### **Supported flags**: 26972 * - `g`: Find all matches within the provided text 26973 * - `i`: Ignore case when searching for matches 26974 * - `$`: Only match if the text ends with the search term 26975 * - `^`: Only match if the text begins with the search term 26976 * 26977 * @usage 26978 * <hljs lang="html"> 26979 * <input placeholder="Enter a search term..." ng-model="searchTerm" type="text" /> 26980 * <ul> 26981 * <li ng-repeat="result in results" md-highlight-text="searchTerm"> 26982 * {{result.text}} 26983 * </li> 26984 * </ul> 26985 * </hljs> 26986 */ 26987 26988function MdHighlight ($interpolate, $parse) { 26989 return { 26990 terminal: true, 26991 controller: 'MdHighlightCtrl', 26992 compile: function mdHighlightCompile(tElement, tAttr) { 26993 var termExpr = $parse(tAttr.mdHighlightText); 26994 var unsafeContentExpr = $interpolate(tElement.html()); 26995 26996 return function mdHighlightLink(scope, element, attr, ctrl) { 26997 ctrl.init(termExpr, unsafeContentExpr); 26998 }; 26999 } 27000 }; 27001} 27002 27003})(); 27004(function(){ 27005"use strict"; 27006 27007 27008MdChipCtrl.$inject = ["$scope", "$element", "$mdConstant", "$timeout", "$mdUtil"];angular 27009 .module('material.components.chips') 27010 .controller('MdChipCtrl', MdChipCtrl); 27011 27012/** 27013 * Controller for the MdChip component. Responsible for handling keyboard 27014 * events and editting the chip if needed. 27015 * 27016 * @param $scope 27017 * @param $element 27018 * @param $mdConstant 27019 * @param $timeout 27020 * @param $mdUtil 27021 * @constructor 27022 */ 27023function MdChipCtrl ($scope, $element, $mdConstant, $timeout, $mdUtil) { 27024 /** 27025 * @type {$scope} 27026 */ 27027 this.$scope = $scope; 27028 27029 /** 27030 * @type {$element} 27031 */ 27032 this.$element = $element; 27033 27034 /** 27035 * @type {$mdConstant} 27036 */ 27037 this.$mdConstant = $mdConstant; 27038 27039 /** 27040 * @type {$timeout} 27041 */ 27042 this.$timeout = $timeout; 27043 27044 /** 27045 * @type {$mdUtil} 27046 */ 27047 this.$mdUtil = $mdUtil; 27048 27049 /** 27050 * @type {boolean} 27051 */ 27052 this.isEditting = false; 27053 27054 /** 27055 * @type {MdChipsCtrl} 27056 */ 27057 this.parentController = undefined; 27058 27059 /** 27060 * @type {boolean} 27061 */ 27062 this.enableChipEdit = false; 27063} 27064 27065 27066/** 27067 * @param {MdChipsCtrl} controller 27068 */ 27069MdChipCtrl.prototype.init = function(controller) { 27070 this.parentController = controller; 27071 this.enableChipEdit = this.parentController.enableChipEdit; 27072 27073 if (this.enableChipEdit) { 27074 this.$element.on('keydown', this.chipKeyDown.bind(this)); 27075 this.$element.on('mousedown', this.chipMouseDown.bind(this)); 27076 this.getChipContent().addClass('_md-chip-content-edit-is-enabled'); 27077 } 27078}; 27079 27080 27081/** 27082 * @return {Object} 27083 */ 27084MdChipCtrl.prototype.getChipContent = function() { 27085 var chipContents = this.$element[0].getElementsByClassName('md-chip-content'); 27086 return angular.element(chipContents[0]); 27087}; 27088 27089 27090/** 27091 * @return {Object} 27092 */ 27093MdChipCtrl.prototype.getContentElement = function() { 27094 return angular.element(this.getChipContent().children()[0]); 27095}; 27096 27097 27098/** 27099 * @return {number} 27100 */ 27101MdChipCtrl.prototype.getChipIndex = function() { 27102 return parseInt(this.$element.attr('index')); 27103}; 27104 27105 27106/** 27107 * Presents an input element to edit the contents of the chip. 27108 */ 27109MdChipCtrl.prototype.goOutOfEditMode = function() { 27110 if (!this.isEditting) return; 27111 27112 this.isEditting = false; 27113 this.$element.removeClass('_md-chip-editing'); 27114 this.getChipContent()[0].contentEditable = 'false'; 27115 var chipIndex = this.getChipIndex(); 27116 27117 var content = this.getContentElement().text(); 27118 if (content) { 27119 this.parentController.updateChipContents( 27120 chipIndex, 27121 this.getContentElement().text() 27122 ); 27123 27124 this.$mdUtil.nextTick(function() { 27125 if (this.parentController.selectedChip === chipIndex) { 27126 this.parentController.focusChip(chipIndex); 27127 } 27128 }.bind(this)); 27129 } else { 27130 this.parentController.removeChipAndFocusInput(chipIndex); 27131 } 27132}; 27133 27134 27135/** 27136 * Given an HTML element. Selects contents of it. 27137 * @param node 27138 */ 27139MdChipCtrl.prototype.selectNodeContents = function(node) { 27140 var range, selection; 27141 if (document.body.createTextRange) { 27142 range = document.body.createTextRange(); 27143 range.moveToElementText(node); 27144 range.select(); 27145 } else if (window.getSelection) { 27146 selection = window.getSelection(); 27147 range = document.createRange(); 27148 range.selectNodeContents(node); 27149 selection.removeAllRanges(); 27150 selection.addRange(range); 27151 } 27152}; 27153 27154 27155/** 27156 * Presents an input element to edit the contents of the chip. 27157 */ 27158MdChipCtrl.prototype.goInEditMode = function() { 27159 this.isEditting = true; 27160 this.$element.addClass('_md-chip-editing'); 27161 this.getChipContent()[0].contentEditable = 'true'; 27162 this.getChipContent().on('blur', function() { 27163 this.goOutOfEditMode(); 27164 }.bind(this)); 27165 27166 this.selectNodeContents(this.getChipContent()[0]); 27167}; 27168 27169 27170/** 27171 * Handles the keydown event on the chip element. If enable-chip-edit attribute is 27172 * set to true, space or enter keys can trigger going into edit mode. Enter can also 27173 * trigger submitting if the chip is already being edited. 27174 * @param event 27175 */ 27176MdChipCtrl.prototype.chipKeyDown = function(event) { 27177 if (!this.isEditting && 27178 (event.keyCode === this.$mdConstant.KEY_CODE.ENTER || 27179 event.keyCode === this.$mdConstant.KEY_CODE.SPACE)) { 27180 event.preventDefault(); 27181 this.goInEditMode(); 27182 } else if (this.isEditting && 27183 event.keyCode === this.$mdConstant.KEY_CODE.ENTER) { 27184 event.preventDefault(); 27185 this.goOutOfEditMode(); 27186 } 27187}; 27188 27189 27190/** 27191 * Handles the double click event 27192 */ 27193MdChipCtrl.prototype.chipMouseDown = function() { 27194 if(this.getChipIndex() == this.parentController.selectedChip && 27195 this.enableChipEdit && 27196 !this.isEditting) { 27197 this.goInEditMode(); 27198 } 27199}; 27200 27201})(); 27202(function(){ 27203"use strict"; 27204 27205 27206MdChip.$inject = ["$mdTheming", "$mdUtil", "$compile", "$timeout"];angular 27207 .module('material.components.chips') 27208 .directive('mdChip', MdChip); 27209 27210/** 27211 * @ngdoc directive 27212 * @name mdChip 27213 * @module material.components.chips 27214 * 27215 * @description 27216 * `<md-chip>` is a component used within `<md-chips>` and is responsible for rendering individual 27217 * chips. 27218 * 27219 * 27220 * @usage 27221 * <hljs lang="html"> 27222 * <md-chip>{{$chip}}</md-chip> 27223 * </hljs> 27224 * 27225 */ 27226 27227// This hint text is hidden within a chip but used by screen readers to 27228// inform the user how they can interact with a chip. 27229var DELETE_HINT_TEMPLATE = '\ 27230 <span ng-if="!$mdChipsCtrl.readonly" class="md-visually-hidden">\ 27231 {{$mdChipsCtrl.deleteHint}}\ 27232 </span>'; 27233 27234/** 27235 * MDChip Directive Definition 27236 * 27237 * @param $mdTheming 27238 * @param $mdUtil 27239 * @ngInject 27240 */ 27241function MdChip($mdTheming, $mdUtil, $compile, $timeout) { 27242 var deleteHintTemplate = $mdUtil.processTemplate(DELETE_HINT_TEMPLATE); 27243 27244 return { 27245 restrict: 'E', 27246 require: ['^?mdChips', 'mdChip'], 27247 link: postLink, 27248 controller: 'MdChipCtrl' 27249 }; 27250 27251 function postLink(scope, element, attr, ctrls) { 27252 var chipsController = ctrls.shift(); 27253 var chipController = ctrls.shift(); 27254 var chipContentElement = angular.element(element[0].querySelector('.md-chip-content')); 27255 27256 $mdTheming(element); 27257 27258 if (chipsController) { 27259 chipController.init(chipsController); 27260 27261 // Append our delete hint to the div.md-chip-content (which does not exist at compile time) 27262 chipContentElement.append($compile(deleteHintTemplate)(scope)); 27263 27264 // When a chip is blurred, make sure to unset (or reset) the selected chip so that tabbing 27265 // through elements works properly 27266 chipContentElement.on('blur', function() { 27267 chipsController.resetSelectedChip(); 27268 chipsController.$scope.$applyAsync(); 27269 }); 27270 } 27271 27272 // Use $timeout to ensure we run AFTER the element has been added to the DOM so we can focus it. 27273 $timeout(function() { 27274 if (!chipsController) { 27275 return; 27276 } 27277 27278 if (chipsController.shouldFocusLastChip) { 27279 chipsController.focusLastChipThenInput(); 27280 } 27281 }); 27282 } 27283} 27284 27285})(); 27286(function(){ 27287"use strict"; 27288 27289 27290MdChipRemove.$inject = ["$timeout"];angular 27291 .module('material.components.chips') 27292 .directive('mdChipRemove', MdChipRemove); 27293 27294/** 27295 * @ngdoc directive 27296 * @name mdChipRemove 27297 * @restrict A 27298 * @module material.components.chips 27299 * 27300 * @description 27301 * Designates an element to be used as the delete button for a chip. <br/> 27302 * This element is passed as a child of the `md-chips` element. 27303 * 27304 * The designated button will be just appended to the chip and removes the given chip on click.<br/> 27305 * By default the button is not being styled by the `md-chips` component. 27306 * 27307 * @usage 27308 * <hljs lang="html"> 27309 * <md-chips> 27310 * <button md-chip-remove=""> 27311 * <md-icon md-svg-icon="md-close"></md-icon> 27312 * </button> 27313 * </md-chips> 27314 * </hljs> 27315 */ 27316 27317 27318/** 27319 * MdChipRemove Directive Definition. 27320 * 27321 * @param $timeout 27322 * @returns {{restrict: string, require: string[], link: Function, scope: boolean}} 27323 * @constructor 27324 */ 27325function MdChipRemove ($timeout) { 27326 return { 27327 restrict: 'A', 27328 require: '^mdChips', 27329 scope: false, 27330 link: postLink 27331 }; 27332 27333 function postLink(scope, element, attr, ctrl) { 27334 element.on('click', function(event) { 27335 scope.$apply(function() { 27336 ctrl.removeChip(scope.$$replacedScope.$index); 27337 }); 27338 }); 27339 27340 // Child elements aren't available until after a $timeout tick as they are hidden by an 27341 // `ng-if`. see http://goo.gl/zIWfuw 27342 $timeout(function() { 27343 element.attr({ tabindex: -1, 'aria-hidden': true }); 27344 element.find('button').attr('tabindex', '-1'); 27345 }); 27346 } 27347} 27348 27349})(); 27350(function(){ 27351"use strict"; 27352 27353 27354MdChipTransclude.$inject = ["$compile"];angular 27355 .module('material.components.chips') 27356 .directive('mdChipTransclude', MdChipTransclude); 27357 27358function MdChipTransclude ($compile) { 27359 return { 27360 restrict: 'EA', 27361 terminal: true, 27362 link: link, 27363 scope: false 27364 }; 27365 function link (scope, element, attr) { 27366 var ctrl = scope.$parent.$mdChipsCtrl, 27367 newScope = ctrl.parent.$new(false, ctrl.parent); 27368 newScope.$$replacedScope = scope; 27369 newScope.$chip = scope.$chip; 27370 newScope.$index = scope.$index; 27371 newScope.$mdChipsCtrl = ctrl; 27372 27373 var newHtml = ctrl.$scope.$eval(attr.mdChipTransclude); 27374 27375 element.html(newHtml); 27376 $compile(element.contents())(newScope); 27377 } 27378} 27379 27380})(); 27381(function(){ 27382"use strict"; 27383 27384/** 27385 * The default chip append delay. 27386 * 27387 * @type {number} 27388 */ 27389MdChipsCtrl.$inject = ["$scope", "$attrs", "$mdConstant", "$log", "$element", "$timeout", "$mdUtil"]; 27390var DEFAULT_CHIP_APPEND_DELAY = 300; 27391 27392angular 27393 .module('material.components.chips') 27394 .controller('MdChipsCtrl', MdChipsCtrl); 27395 27396/** 27397 * Controller for the MdChips component. Responsible for adding to and 27398 * removing from the list of chips, marking chips as selected, and binding to 27399 * the models of various input components. 27400 * 27401 * @param $scope 27402 * @param $attrs 27403 * @param $mdConstant 27404 * @param $log 27405 * @param $element 27406 * @param $timeout 27407 * @param $mdUtil 27408 * @constructor 27409 */ 27410function MdChipsCtrl ($scope, $attrs, $mdConstant, $log, $element, $timeout, $mdUtil) { 27411 /** @type {$timeout} **/ 27412 this.$timeout = $timeout; 27413 27414 /** @type {Object} */ 27415 this.$mdConstant = $mdConstant; 27416 27417 /** @type {angular.$scope} */ 27418 this.$scope = $scope; 27419 27420 /** @type {angular.$scope} */ 27421 this.parent = $scope.$parent; 27422 27423 /** @type {$mdUtil} */ 27424 this.$mdUtil = $mdUtil; 27425 27426 /** @type {$log} */ 27427 this.$log = $log; 27428 27429 /** @type {$element} */ 27430 this.$element = $element; 27431 27432 /** @type {$attrs} */ 27433 this.$attrs = $attrs; 27434 27435 /** @type {angular.NgModelController} */ 27436 this.ngModelCtrl = null; 27437 27438 /** @type {angular.NgModelController} */ 27439 this.userInputNgModelCtrl = null; 27440 27441 /** @type {MdAutocompleteCtrl} */ 27442 this.autocompleteCtrl = null; 27443 27444 /** @type {Element} */ 27445 this.userInputElement = null; 27446 27447 /** @type {Array.<Object>} */ 27448 this.items = []; 27449 27450 /** @type {number} */ 27451 this.selectedChip = -1; 27452 27453 /** @type {string} */ 27454 this.enableChipEdit = $mdUtil.parseAttributeBoolean($attrs.mdEnableChipEdit); 27455 27456 /** @type {string} */
vendor: 12,207 bytes, lines 27457-27860
27457 this.addOnBlur = $mdUtil.parseAttributeBoolean($attrs.mdAddOnBlur); 27458 27459 /** 27460 * The text to be used as the aria-label for the input. 27461 * @type {string} 27462 */ 27463 this.inputAriaLabel = 'Chips input.'; 27464 27465 /** 27466 * Hidden hint text to describe the chips container. Used to give context to screen readers when 27467 * the chips are readonly and the input cannot be selected. 27468 * 27469 * @type {string} 27470 */ 27471 this.containerHint = 'Chips container. Use arrow keys to select chips.'; 27472 27473 /** 27474 * Hidden hint text for how to delete a chip. Used to give context to screen readers. 27475 * @type {string} 27476 */ 27477 this.deleteHint = 'Press delete to remove this chip.'; 27478 27479 /** 27480 * Hidden label for the delete button. Used to give context to screen readers. 27481 * @type {string} 27482 */ 27483 this.deleteButtonLabel = 'Remove'; 27484 27485 /** 27486 * Model used by the input element. 27487 * @type {string} 27488 */ 27489 this.chipBuffer = ''; 27490 27491 /** 27492 * Whether to use the transformChip expression to transform the chip buffer 27493 * before appending it to the list. 27494 * @type {boolean} 27495 */ 27496 this.useTransformChip = false; 27497 27498 /** 27499 * Whether to use the onAdd expression to notify of chip additions. 27500 * @type {boolean} 27501 */ 27502 this.useOnAdd = false; 27503 27504 /** 27505 * Whether to use the onRemove expression to notify of chip removals. 27506 * @type {boolean} 27507 */ 27508 this.useOnRemove = false; 27509 27510 /** 27511 * The ID of the chips wrapper which is used to build unique IDs for the chips and the aria-owns 27512 * attribute. 27513 * 27514 * Defaults to '_md-chips-wrapper-' plus a unique number. 27515 * 27516 * @type {string} 27517 */ 27518 this.wrapperId = ''; 27519 27520 /** 27521 * Array of unique numbers which will be auto-generated any time the items change, and is used to 27522 * create unique IDs for the aria-owns attribute. 27523 * 27524 * @type {Array} 27525 */ 27526 this.contentIds = []; 27527 27528 /** 27529 * The index of the chip that should have it's tabindex property set to 0 so it is selectable 27530 * via the keyboard. 27531 * 27532 * @type {int} 27533 */ 27534 this.ariaTabIndex = null; 27535 27536 /** 27537 * After appending a chip, the chip will be focused for this number of milliseconds before the 27538 * input is refocused. 27539 * 27540 * **Note:** This is **required** for compatibility with certain screen readers in order for 27541 * them to properly allow keyboard access. 27542 * 27543 * @type {number} 27544 */ 27545 this.chipAppendDelay = DEFAULT_CHIP_APPEND_DELAY; 27546 27547 this.init(); 27548} 27549 27550/** 27551 * Initializes variables and sets up watchers 27552 */ 27553MdChipsCtrl.prototype.init = function() { 27554 var ctrl = this; 27555 27556 // Set the wrapper ID 27557 ctrl.wrapperId = '_md-chips-wrapper-' + ctrl.$mdUtil.nextUid(); 27558 27559 // Setup a watcher which manages the role and aria-owns attributes 27560 ctrl.$scope.$watchCollection('$mdChipsCtrl.items', function() { 27561 // Make sure our input and wrapper have the correct ARIA attributes 27562 ctrl.setupInputAria(); 27563 ctrl.setupWrapperAria(); 27564 }); 27565 27566 ctrl.$attrs.$observe('mdChipAppendDelay', function(newValue) { 27567 ctrl.chipAppendDelay = parseInt(newValue) || DEFAULT_CHIP_APPEND_DELAY; 27568 }); 27569}; 27570 27571/** 27572 * If we have an input, ensure it has the appropriate ARIA attributes. 27573 */ 27574MdChipsCtrl.prototype.setupInputAria = function() { 27575 var input = this.$element.find('input'); 27576 27577 // If we have no input, just return 27578 if (!input) { 27579 return; 27580 } 27581 27582 input.attr('role', 'textbox'); 27583 input.attr('aria-multiline', true); 27584}; 27585 27586/** 27587 * Ensure our wrapper has the appropriate ARIA attributes. 27588 */ 27589MdChipsCtrl.prototype.setupWrapperAria = function() { 27590 var ctrl = this, 27591 wrapper = this.$element.find('md-chips-wrap'); 27592 27593 if (this.items && this.items.length) { 27594 // Dynamically add the listbox role on every change because it must be removed when there are 27595 // no items. 27596 wrapper.attr('role', 'listbox'); 27597 27598 // Generate some random (but unique) IDs for each chip 27599 this.contentIds = this.items.map(function() { 27600 return ctrl.wrapperId + '-chip-' + ctrl.$mdUtil.nextUid(); 27601 }); 27602 27603 // Use the contentIDs above to generate the aria-owns attribute 27604 wrapper.attr('aria-owns', this.contentIds.join(' ')); 27605 } else { 27606 // If we have no items, then the role and aria-owns attributes MUST be removed 27607 wrapper.removeAttr('role'); 27608 wrapper.removeAttr('aria-owns'); 27609 } 27610}; 27611 27612/** 27613 * Handles the keydown event on the input element: by default <enter> appends 27614 * the buffer to the chip list, while backspace removes the last chip in the 27615 * list if the current buffer is empty. 27616 * @param event 27617 */ 27618MdChipsCtrl.prototype.inputKeydown = function(event) { 27619 var chipBuffer = this.getChipBuffer(); 27620 27621 // If we have an autocomplete, and it handled the event, we have nothing to do 27622 if (this.autocompleteCtrl && event.isDefaultPrevented && event.isDefaultPrevented()) { 27623 return; 27624 } 27625 27626 if (event.keyCode === this.$mdConstant.KEY_CODE.BACKSPACE) { 27627 // Only select and focus the previous chip, if the current caret position of the 27628 // input element is at the beginning. 27629 if (this.getCursorPosition(event.target) !== 0) { 27630 return; 27631 } 27632 27633 event.preventDefault(); 27634 event.stopPropagation(); 27635 27636 if (this.items.length) { 27637 this.selectAndFocusChipSafe(this.items.length - 1); 27638 } 27639 27640 return; 27641 } 27642 27643 // By default <enter> appends the buffer to the chip list. 27644 if (!this.separatorKeys || this.separatorKeys.length < 1) { 27645 this.separatorKeys = [this.$mdConstant.KEY_CODE.ENTER]; 27646 } 27647 27648 // Support additional separator key codes in an array of `md-separator-keys`. 27649 if (this.separatorKeys.indexOf(event.keyCode) !== -1) { 27650 if ((this.autocompleteCtrl && this.requireMatch) || !chipBuffer) return; 27651 event.preventDefault(); 27652 27653 // Only append the chip and reset the chip buffer if the max chips limit isn't reached. 27654 if (this.hasMaxChipsReached()) return; 27655 27656 this.appendChip(chipBuffer.trim()); 27657 this.resetChipBuffer(); 27658 27659 return false; 27660 } 27661}; 27662 27663/** 27664 * Returns the cursor position of the specified input element. 27665 * @param element HTMLInputElement 27666 * @returns {Number} Cursor Position of the input. 27667 */ 27668MdChipsCtrl.prototype.getCursorPosition = function(element) { 27669 /* 27670 * Figure out whether the current input for the chips buffer is valid for using 27671 * the selectionStart / end property to retrieve the cursor position. 27672 * Some browsers do not allow the use of those attributes, on different input types. 27673 */ 27674 try { 27675 if (element.selectionStart === element.selectionEnd) { 27676 return element.selectionStart; 27677 } 27678 } catch (e) { 27679 if (!element.value) { 27680 return 0; 27681 } 27682 } 27683}; 27684 27685 27686/** 27687 * Updates the content of the chip at given index 27688 * @param chipIndex 27689 * @param chipContents 27690 */ 27691MdChipsCtrl.prototype.updateChipContents = function(chipIndex, chipContents){ 27692 if(chipIndex >= 0 && chipIndex < this.items.length) { 27693 this.items[chipIndex] = chipContents; 27694 this.ngModelCtrl.$setDirty(); 27695 } 27696}; 27697 27698 27699/** 27700 * Returns true if a chip is currently being edited. False otherwise. 27701 * @return {boolean} 27702 */ 27703MdChipsCtrl.prototype.isEditingChip = function() { 27704 return !!this.$element[0].querySelector('._md-chip-editing'); 27705}; 27706 27707 27708MdChipsCtrl.prototype.isRemovable = function() { 27709 // Return false if we have static chips 27710 if (!this.ngModelCtrl) { 27711 return false; 27712 } 27713 27714 return this.readonly ? this.removable : 27715 angular.isDefined(this.removable) ? this.removable : true; 27716}; 27717 27718/** 27719 * Handles the keydown event on the chip elements: backspace removes the selected chip, arrow 27720 * keys switch which chips is active 27721 * @param event 27722 */ 27723MdChipsCtrl.prototype.chipKeydown = function (event) { 27724 if (this.getChipBuffer()) return; 27725 if (this.isEditingChip()) return; 27726 27727 switch (event.keyCode) { 27728 case this.$mdConstant.KEY_CODE.BACKSPACE: 27729 case this.$mdConstant.KEY_CODE.DELETE: 27730 if (this.selectedChip < 0) return; 27731 event.preventDefault(); 27732 // Cancel the delete action only after the event cancel. Otherwise the page will go back. 27733 if (!this.isRemovable()) return; 27734 this.removeAndSelectAdjacentChip(this.selectedChip); 27735 break; 27736 case this.$mdConstant.KEY_CODE.LEFT_ARROW: 27737 event.preventDefault(); 27738 // By default, allow selection of -1 which will focus the input; if we're readonly, don't go 27739 // below 0 27740 if (this.selectedChip < 0 || (this.readonly && this.selectedChip == 0)) { 27741 this.selectedChip = this.items.length; 27742 } 27743 if (this.items.length) this.selectAndFocusChipSafe(this.selectedChip - 1); 27744 break; 27745 case this.$mdConstant.KEY_CODE.RIGHT_ARROW: 27746 event.preventDefault(); 27747 this.selectAndFocusChipSafe(this.selectedChip + 1); 27748 break; 27749 case this.$mdConstant.KEY_CODE.ESCAPE: 27750 case this.$mdConstant.KEY_CODE.TAB: 27751 if (this.selectedChip < 0) return; 27752 event.preventDefault(); 27753 this.onFocus(); 27754 break; 27755 } 27756}; 27757 27758/** 27759 * Get the input's placeholder - uses `placeholder` when list is empty and `secondary-placeholder` 27760 * when the list is non-empty. If `secondary-placeholder` is not provided, `placeholder` is used 27761 * always. 27762 */ 27763MdChipsCtrl.prototype.getPlaceholder = function() { 27764 // Allow `secondary-placeholder` to be blank. 27765 var useSecondary = (this.items && this.items.length && 27766 (this.secondaryPlaceholder == '' || this.secondaryPlaceholder)); 27767 return useSecondary ? this.secondaryPlaceholder : this.placeholder; 27768}; 27769 27770/** 27771 * Removes chip at {@code index} and selects the adjacent chip. 27772 * @param index 27773 */ 27774MdChipsCtrl.prototype.removeAndSelectAdjacentChip = function(index) { 27775 var self = this; 27776 var selIndex = self.getAdjacentChipIndex(index); 27777 var wrap = this.$element[0].querySelector('md-chips-wrap'); 27778 var chip = this.$element[0].querySelector('md-chip[index="' + index + '"]'); 27779 27780 self.removeChip(index); 27781 27782 // The dobule-timeout is currently necessary to ensure that the DOM has finalized and the select() 27783 // will find the proper chip since the selection is index-based. 27784 // 27785 // TODO: Investigate calling from within chip $scope.$on('$destroy') to reduce/remove timeouts 27786 self.$timeout(function() { 27787 self.$timeout(function() { 27788 self.selectAndFocusChipSafe(selIndex); 27789 }); 27790 }); 27791}; 27792 27793/** 27794 * Sets the selected chip index to -1. 27795 */ 27796MdChipsCtrl.prototype.resetSelectedChip = function() { 27797 this.selectedChip = -1; 27798 this.ariaTabIndex = null; 27799}; 27800 27801/** 27802 * Gets the index of an adjacent chip to select after deletion. Adjacency is 27803 * determined as the next chip in the list, unless the target chip is the 27804 * last in the list, then it is the chip immediately preceding the target. If 27805 * there is only one item in the list, -1 is returned (select none). 27806 * The number returned is the index to select AFTER the target has been 27807 * removed. 27808 * If the current chip is not selected, then -1 is returned to select none. 27809 */ 27810MdChipsCtrl.prototype.getAdjacentChipIndex = function(index) { 27811 var len = this.items.length - 1; 27812 return (len == 0) ? -1 : 27813 (index == len) ? index -1 : index; 27814}; 27815 27816/** 27817 * Append the contents of the buffer to the chip list. This method will first 27818 * call out to the md-transform-chip method, if provided. 27819 * 27820 * @param newChip 27821 */ 27822MdChipsCtrl.prototype.appendChip = function(newChip) { 27823 this.shouldFocusLastChip = true; 27824 if (this.useTransformChip && this.transformChip) { 27825 var transformedChip = this.transformChip({'$chip': newChip}); 27826 27827 // Check to make sure the chip is defined before assigning it, otherwise, we'll just assume 27828 // they want the string version. 27829 if (angular.isDefined(transformedChip)) { 27830 newChip = transformedChip; 27831 } 27832 } 27833 27834 // If items contains an identical object to newChip, do not append 27835 if (angular.isObject(newChip)){ 27836 var identical = this.items.some(function(item){ 27837 return angular.equals(newChip, item); 27838 }); 27839 if (identical) return; 27840 } 27841 27842 // Check for a null (but not undefined), or existing chip and cancel appending 27843 if (newChip == null || this.items.indexOf(newChip) + 1) return; 27844 27845 // Append the new chip onto our list 27846 var length = this.items.push(newChip); 27847 var index = length - 1; 27848 27849 // Update model validation 27850 this.ngModelCtrl.$setDirty(); 27851 this.validateModel(); 27852 27853 // If they provide the md-on-add attribute, notify them of the chip addition 27854 if (this.useOnAdd && this.onAdd) { 27855 this.onAdd({ '$chip': newChip, '$index': index }); 27856 } 27857}; 27858 27859/** 27860 * Sets whether to use the md-transform-chip expression. This expression is
27861 * bound to scope and controller in {@code MdChipsDirective} as 27862 * {@code transformChip}. Due to the nature of directive scope bindings, the 27863 * controller cannot know on its own/from the scope whether an expression was 27864 * actually provided. 27865 */ 27866MdChipsCtrl.prototype.useTransformChipExpression = function() { 27867 this.useTransformChip = true; 27868}; 27869 27870/** 27871 * Sets whether to use the md-on-add expression. This expression is 27872 * bound to scope and controller in {@code MdChipsDirective} as 27873 * {@code onAdd}. Due to the nature of directive scope bindings, the 27874 * controller cannot know on its own/from the scope whether an expression was 27875 * actually provided. 27876 */ 27877MdChipsCtrl.prototype.useOnAddExpression = function() { 27878 this.useOnAdd = true; 27879}; 27880 27881/** 27882 * Sets whether to use the md-on-remove expression. This expression is 27883 * bound to scope and controller in {@code MdChipsDirective} as 27884 * {@code onRemove}. Due to the nature of directive scope bindings, the 27885 * controller cannot know on its own/from the scope whether an expression was 27886 * actually provided. 27887 */ 27888MdChipsCtrl.prototype.useOnRemoveExpression = function() { 27889 this.useOnRemove = true; 27890}; 27891 27892/* 27893 * Sets whether to use the md-on-select expression. This expression is 27894 * bound to scope and controller in {@code MdChipsDirective} as 27895 * {@code onSelect}. Due to the nature of directive scope bindings, the 27896 * controller cannot know on its own/from the scope whether an expression was 27897 * actually provided. 27898 */ 27899MdChipsCtrl.prototype.useOnSelectExpression = function() { 27900 this.useOnSelect = true; 27901}; 27902 27903/** 27904 * Gets the input buffer. The input buffer can be the model bound to the 27905 * default input item {@code this.chipBuffer}, the {@code selectedItem} 27906 * model of an {@code md-autocomplete}, or, through some magic, the model 27907 * bound to any inpput or text area element found within a 27908 * {@code md-input-container} element. 27909 * @return {string} 27910 */ 27911MdChipsCtrl.prototype.getChipBuffer = function() { 27912 var chipBuffer = !this.userInputElement ? this.chipBuffer : 27913 this.userInputNgModelCtrl ? this.userInputNgModelCtrl.$viewValue : 27914 this.userInputElement[0].value; 27915 27916 // Ensure that the chip buffer is always a string. For example, the input element buffer might be falsy. 27917 return angular.isString(chipBuffer) ? chipBuffer : ''; 27918}; 27919 27920/** 27921 * Resets the input buffer for either the internal input or user provided input element. 27922 */ 27923MdChipsCtrl.prototype.resetChipBuffer = function() { 27924 if (this.userInputElement) { 27925 if (this.userInputNgModelCtrl) { 27926 this.userInputNgModelCtrl.$setViewValue(''); 27927 this.userInputNgModelCtrl.$render(); 27928 } else { 27929 this.userInputElement[0].value = ''; 27930 } 27931 } else { 27932 this.chipBuffer = ''; 27933 } 27934}; 27935 27936MdChipsCtrl.prototype.hasMaxChipsReached = function() { 27937 if (angular.isString(this.maxChips)) this.maxChips = parseInt(this.maxChips, 10) || 0; 27938 27939 return this.maxChips > 0 && this.items.length >= this.maxChips; 27940}; 27941 27942/** 27943 * Updates the validity properties for the ngModel. 27944 */ 27945MdChipsCtrl.prototype.validateModel = function() { 27946 this.ngModelCtrl.$setValidity('md-max-chips', !this.hasMaxChipsReached()); 27947}; 27948 27949/** 27950 * Removes the chip at the given index. 27951 * @param index 27952 */ 27953MdChipsCtrl.prototype.removeChip = function(index) { 27954 var removed = this.items.splice(index, 1); 27955 27956 // Update model validation 27957 this.ngModelCtrl.$setDirty(); 27958 this.validateModel(); 27959 27960 if (removed && removed.length && this.useOnRemove && this.onRemove) { 27961 this.onRemove({ '$chip': removed[0], '$index': index }); 27962 } 27963}; 27964 27965MdChipsCtrl.prototype.removeChipAndFocusInput = function (index) { 27966 this.removeChip(index); 27967 27968 if (this.autocompleteCtrl) { 27969 // Always hide the autocomplete dropdown before focusing the autocomplete input. 27970 // Wait for the input to move horizontally, because the chip was removed. 27971 // This can lead to an incorrect dropdown position. 27972 this.autocompleteCtrl.hidden = true; 27973 this.$mdUtil.nextTick(this.onFocus.bind(this)); 27974 } else { 27975 this.onFocus(); 27976 } 27977 27978}; 27979/** 27980 * Selects the chip at `index`, 27981 * @param index 27982 */ 27983MdChipsCtrl.prototype.selectAndFocusChipSafe = function(index) { 27984 // If we have no chips, or are asked to select a chip before the first, just focus the input 27985 if (!this.items.length || index === -1) { 27986 return this.focusInput(); 27987 } 27988 27989 // If we are asked to select a chip greater than the number of chips... 27990 if (index >= this.items.length) { 27991 if (this.readonly) { 27992 // If we are readonly, jump back to the start (because we have no input) 27993 index = 0; 27994 } else { 27995 // If we are not readonly, we should attempt to focus the input 27996 return this.onFocus(); 27997 } 27998 } 27999 28000 index = Math.max(index, 0); 28001 index = Math.min(index, this.items.length - 1); 28002 28003 this.selectChip(index); 28004 this.focusChip(index); 28005}; 28006
vendor: 4,438 bytes, lines 28007-28169
28007MdChipsCtrl.prototype.focusLastChipThenInput = function() { 28008 var ctrl = this; 28009 28010 ctrl.shouldFocusLastChip = false; 28011 28012 ctrl.focusChip(this.items.length - 1); 28013 28014 ctrl.$timeout(function() { 28015 ctrl.focusInput(); 28016 }, ctrl.chipAppendDelay); 28017}; 28018 28019MdChipsCtrl.prototype.focusInput = function() { 28020 this.selectChip(-1); 28021 this.onFocus(); 28022}; 28023 28024/** 28025 * Marks the chip at the given index as selected. 28026 * @param index 28027 */ 28028MdChipsCtrl.prototype.selectChip = function(index) { 28029 if (index >= -1 && index <= this.items.length) { 28030 this.selectedChip = index; 28031 28032 // Fire the onSelect if provided 28033 if (this.useOnSelect && this.onSelect) { 28034 this.onSelect({'$chip': this.items[index] }); 28035 } 28036 } else { 28037 this.$log.warn('Selected Chip index out of bounds; ignoring.'); 28038 } 28039}; 28040 28041/** 28042 * Selects the chip at `index` and gives it focus. 28043 * @param index 28044 */ 28045MdChipsCtrl.prototype.selectAndFocusChip = function(index) { 28046 this.selectChip(index); 28047 if (index != -1) { 28048 this.focusChip(index); 28049 } 28050}; 28051 28052/** 28053 * Call `focus()` on the chip at `index` 28054 */ 28055MdChipsCtrl.prototype.focusChip = function(index) { 28056 var chipContent = this.$element[0].querySelector('md-chip[index="' + index + '"] .md-chip-content'); 28057 28058 this.ariaTabIndex = index; 28059 28060 chipContent.focus(); 28061}; 28062 28063/** 28064 * Configures the required interactions with the ngModel Controller. 28065 * Specifically, set {@code this.items} to the {@code NgModelCtrl#$viewVale}. 28066 * @param ngModelCtrl 28067 */ 28068MdChipsCtrl.prototype.configureNgModel = function(ngModelCtrl) { 28069 this.ngModelCtrl = ngModelCtrl; 28070 28071 var self = this; 28072 ngModelCtrl.$render = function() { 28073 // model is updated. do something. 28074 self.items = self.ngModelCtrl.$viewValue; 28075 }; 28076}; 28077 28078MdChipsCtrl.prototype.onFocus = function () { 28079 var input = this.$element[0].querySelector('input'); 28080 input && input.focus(); 28081 this.resetSelectedChip(); 28082}; 28083 28084MdChipsCtrl.prototype.onInputFocus = function () { 28085 this.inputHasFocus = true; 28086 28087 // Make sure we have the appropriate ARIA attributes 28088 this.setupInputAria(); 28089 28090 // Make sure we don't have any chips selected 28091 this.resetSelectedChip(); 28092}; 28093 28094MdChipsCtrl.prototype.onInputBlur = function () { 28095 this.inputHasFocus = false; 28096 28097 if (this.shouldAddOnBlur()) { 28098 this.appendChip(this.getChipBuffer().trim()); 28099 this.resetChipBuffer(); 28100 } 28101}; 28102 28103/** 28104 * Configure event bindings on a user-provided input element. 28105 * @param inputElement 28106 */ 28107MdChipsCtrl.prototype.configureUserInput = function(inputElement) { 28108 this.userInputElement = inputElement; 28109 28110 // Find the NgModelCtrl for the input element 28111 var ngModelCtrl = inputElement.controller('ngModel'); 28112 // `.controller` will look in the parent as well. 28113 if (ngModelCtrl != this.ngModelCtrl) { 28114 this.userInputNgModelCtrl = ngModelCtrl; 28115 } 28116 28117 var scope = this.$scope; 28118 var ctrl = this; 28119 28120 // Run all of the events using evalAsync because a focus may fire a blur in the same digest loop 28121 var scopeApplyFn = function(event, fn) { 28122 scope.$evalAsync(angular.bind(ctrl, fn, event)); 28123 }; 28124 28125 // Bind to keydown and focus events of input 28126 inputElement 28127 .attr({ tabindex: 0 }) 28128 .on('keydown', function(event) { scopeApplyFn(event, ctrl.inputKeydown) }) 28129 .on('focus', function(event) { scopeApplyFn(event, ctrl.onInputFocus) }) 28130 .on('blur', function(event) { scopeApplyFn(event, ctrl.onInputBlur) }) 28131}; 28132 28133MdChipsCtrl.prototype.configureAutocomplete = function(ctrl) { 28134 if (ctrl) { 28135 this.autocompleteCtrl = ctrl; 28136 28137 ctrl.registerSelectedItemWatcher(angular.bind(this, function (item) { 28138 if (item) { 28139 // Only append the chip and reset the chip buffer if the max chips limit isn't reached. 28140 if (this.hasMaxChipsReached()) return; 28141 28142 this.appendChip(item); 28143 this.resetChipBuffer(); 28144 } 28145 })); 28146 28147 this.$element.find('input') 28148 .on('focus',angular.bind(this, this.onInputFocus) ) 28149 .on('blur', angular.bind(this, this.onInputBlur) ); 28150 } 28151}; 28152 28153/** 28154 * Whether the current chip buffer should be added on input blur or not. 28155 * @returns {boolean} 28156 */ 28157MdChipsCtrl.prototype.shouldAddOnBlur = function() { 28158 28159 // Update the custom ngModel validators from the chips component. 28160 this.validateModel(); 28161 28162 var chipBuffer = this.getChipBuffer().trim(); 28163 var isModelValid = this.ngModelCtrl.$valid; 28164 var isAutocompleteShowing = this.autocompleteCtrl && !this.autocompleteCtrl.hidden; 28165 28166 if (this.userInputNgModelCtrl) { 28167 isModelValid = isModelValid && this.userInputNgModelCtrl.$valid; 28168 } 28169
28170 return this.addOnBlur && !this.requireMatch && chipBuffer && isModelValid && !isAutocompleteShowing; 28171}; 28172 28173MdChipsCtrl.prototype.hasFocus = function () { 28174 return this.inputHasFocus || this.selectedChip >= 0; 28175}; 28176 28177MdChipsCtrl.prototype.contentIdFor = function(index) { 28178 return this.contentIds[index]; 28179}; 28180 28181})(); 28182(function(){ 28183"use strict"; 28184 28185 28186 MdChips.$inject = ["$mdTheming", "$mdUtil", "$compile", "$log", "$timeout", "$$mdSvgRegistry"];angular 28187 .module('material.components.chips') 28188 .directive('mdChips', MdChips); 28189 28190 /** 28191 * @ngdoc directive 28192 * @name mdChips 28193 * @module material.components.chips 28194 * 28195 * @description 28196 * `<md-chips>` is an input component for building lists of strings or objects. The list items are 28197 * displayed as 'chips'. This component can make use of an `<input>` element or an 28198 * `<md-autocomplete>` element. 28199 * 28200 * ### Custom templates 28201 * A custom template may be provided to render the content of each chip. This is achieved by 28202 * specifying an `<md-chip-template>` element containing the custom content as a child of 28203 * `<md-chips>`. 28204 * 28205 * Note: Any attributes on 28206 * `<md-chip-template>` will be dropped as only the innerHTML is used for the chip template. The 28207 * variables `$chip` and `$index` are available in the scope of `<md-chip-template>`, representing 28208 * the chip object and its index in the list of chips, respectively. 28209 * To override the chip delete control, include an element (ideally a button) with the attribute 28210 * `md-chip-remove`. A click listener to remove the chip will be added automatically. The element 28211 * is also placed as a sibling to the chip content (on which there are also click listeners) to 28212 * avoid a nested ng-click situation. 28213 * 28214 * <!-- Note: We no longer want to include this in the site docs; but it should remain here for 28215 * future developers and those looking at the documentation. 28216 * 28217 * <h3> Pending Features </h3> 28218 * <ul style="padding-left:20px;"> 28219 * 28220 * <ul>Style 28221 * <li>Colours for hover, press states (ripple?).</li> 28222 * </ul> 28223 * 28224 * <ul>Validation 28225 * <li>allow a validation callback</li> 28226 * <li>highlighting style for invalid chips</li> 28227 * </ul> 28228 * 28229 * <ul>Item mutation 28230 * <li>Support ` 28231 * <md-chip-edit>` template, show/hide the edit element on tap/click? double tap/double 28232 * click? 28233 * </li> 28234 * </ul> 28235 * 28236 * <ul>Truncation and Disambiguation (?) 28237 * <li>Truncate chip text where possible, but do not truncate entries such that two are 28238 * indistinguishable.</li> 28239 * </ul> 28240 * 28241 * <ul>Drag and Drop 28242 * <li>Drag and drop chips between related `<md-chips>` elements. 28243 * </li> 28244 * </ul> 28245 * </ul> 28246 * 28247 * //--> 28248 * 28249 * Sometimes developers want to limit the amount of possible chips.<br/> 28250 * You can specify the maximum amount of chips by using the following markup. 28251 * 28252 * <hljs lang="html"> 28253 * <md-chips 28254 * ng-model="myItems" 28255 * placeholder="Add an item" 28256 * md-max-chips="5"> 28257 * </md-chips> 28258 * </hljs> 28259 * 28260 * In some cases, you have an autocomplete inside of the `md-chips`.<br/> 28261 * When the maximum amount of chips has been reached, you can also disable the autocomplete selection.<br/> 28262 * Here is an example markup. 28263 * 28264 * <hljs lang="html"> 28265 * <md-chips ng-model="myItems" md-max-chips="5"> 28266 * <md-autocomplete ng-hide="myItems.length > 5" ...></md-autocomplete> 28267 * </md-chips> 28268 * </hljs> 28269 * 28270 * ### Accessibility 28271 * 28272 * The `md-chips` component supports keyboard and screen reader users since Version 1.1.2. In 28273 * order to achieve this, we modified the chips behavior to select newly appended chips for 28274 * `300ms` before re-focusing the input and allowing the user to type. 28275 * 28276 * For most users, this delay is small enough that it will not be noticeable but allows certain 28277 * screen readers to function properly (JAWS and NVDA in particular). 28278 * 28279 * We introduced a new `md-chip-append-delay` option to allow developers to better control this 28280 * behavior. 28281 * 28282 * Please refer to the documentation of this option (below) for more information. 28283 * 28284 * @param {string=|object=} ng-model A model to which the list of items will be bound. 28285 * @param {string=} placeholder Placeholder text that will be forwarded to the input. 28286 * @param {string=} secondary-placeholder Placeholder text that will be forwarded to the input, 28287 * displayed when there is at least one item in the list 28288 * @param {boolean=} md-removable Enables or disables the deletion of chips through the 28289 * removal icon or the Delete/Backspace key. Defaults to true. 28290 * @param {boolean=} readonly Disables list manipulation (deleting or adding list items), hiding 28291 * the input and delete buttons. If no `ng-model` is provided, the chips will automatically be 28292 * marked as readonly.<br/><br/> 28293 * When `md-removable` is not defined, the `md-remove` behavior will be overwritten and disabled. 28294 * @param {string=} md-enable-chip-edit Set this to "true" to enable editing of chip contents. The user can 28295 * go into edit mode with pressing "space", "enter", or double clicking on the chip. Chip edit is only 28296 * supported for chips with basic template. 28297 * @param {number=} md-max-chips The maximum number of chips allowed to add through user input. 28298 * <br/><br/>The validation property `md-max-chips` can be used when the max chips 28299 * amount is reached. 28300 * @param {boolean=} md-add-on-blur When set to true, remaining text inside of the input will 28301 * be converted into a new chip on blur. 28302 * @param {expression} md-transform-chip An expression of form `myFunction($chip)` that when called 28303 * expects one of the following return values: 28304 * - an object representing the `$chip` input string 28305 * - `undefined` to simply add the `$chip` input string, or 28306 * - `null` to prevent the chip from being appended 28307 * @param {expression=} md-on-add An expression which will be called when a chip has been 28308 * added. 28309 * @param {expression=} md-on-remove An expression which will be called when a chip has been 28310 * removed. 28311 * @param {expression=} md-on-select An expression which will be called when a chip is selected. 28312 * @param {boolean} md-require-match If true, and the chips template contains an autocomplete, 28313 * only allow selection of pre-defined chips (i.e. you cannot add new ones). 28314 * @param {string=} input-aria-label A string read by screen readers to identify the input. 28315 * @param {string=} container-hint A string read by screen readers informing users of how to 28316 * navigate the chips. Used in readonly mode. 28317 * @param {string=} delete-hint A string read by screen readers instructing users that pressing 28318 * the delete key will remove the chip. 28319 * @param {string=} delete-button-label A label for the delete button. Also hidden and read by 28320 * screen readers. 28321 * @param {expression=} md-separator-keys An array of key codes used to separate chips. 28322 * @param {string=} md-chip-append-delay The number of milliseconds that the component will select 28323 * a newly appended chip before allowing a user to type into the input. This is **necessary** 28324 * for keyboard accessibility for screen readers. It defaults to 300ms and any number less than 28325 * 300 can cause issues with screen readers (particularly JAWS and sometimes NVDA). 28326 * 28327 * _Available since Version 1.1.2._ 28328 * 28329 * **Note:** You can safely set this to `0` in one of the following two instances: 28330 * 28331 * 1. You are targeting an iOS or Safari-only application (where users would use VoiceOver) or 28332 * only ChromeVox users. 28333 * 28334 * 2. If you have utilized the `md-separator-keys` to disable the `enter` keystroke in 28335 * favor of another one (such as `,` or `;`). 28336 * 28337 * @usage 28338 * <hljs lang="html"> 28339 * <md-chips 28340 * ng-model="myItems" 28341 * placeholder="Add an item" 28342 * readonly="isReadOnly"> 28343 * </md-chips> 28344 * </hljs> 28345 * 28346 * <h3>Validation</h3> 28347 * When using [ngMessages](https://docs.angularjs.org/api/ngMessages), you can show errors based 28348 * on our custom validators. 28349 * <hljs lang="html"> 28350 * <form name="userForm"> 28351 * <md-chips 28352 * name="fruits" 28353 * ng-model="myItems" 28354 * placeholder="Add an item" 28355 * md-max-chips="5"> 28356 * </md-chips> 28357 * <div ng-messages="userForm.fruits.$error" ng-if="userForm.$dirty"> 28358 * <div ng-message="md-max-chips">You reached the maximum amount of chips</div> 28359 * </div> 28360 * </form> 28361 * </hljs> 28362 * 28363 */ 28364 28365 var MD_CHIPS_TEMPLATE = '\ 28366 <md-chips-wrap\ 28367 id="{{$mdChipsCtrl.wrapperId}}"\ 28368 tabindex="{{$mdChipsCtrl.readonly ? 0 : -1}}"\ 28369 ng-keydown="$mdChipsCtrl.chipKeydown($event)"\ 28370 ng-class="{ \'md-focused\': $mdChipsCtrl.hasFocus(), \ 28371 \'md-readonly\': !$mdChipsCtrl.ngModelCtrl || $mdChipsCtrl.readonly,\ 28372 \'md-removable\': $mdChipsCtrl.isRemovable() }"\ 28373 aria-setsize="{{$mdChipsCtrl.items.length}}"\ 28374 class="md-chips">\
28375 <span ng-if="$mdChipsCtrl.readonly" class="md-visually-hidden">\ 28376 {{$mdChipsCtrl.containerHint}}\ 28377 </span>\ 28378 <md-chip ng-repeat="$chip in $mdChipsCtrl.items"\ 28379 index="{{$index}}"\ 28380 ng-class="{\'md-focused\': $mdChipsCtrl.selectedChip == $index, \'md-readonly\': !$mdChipsCtrl.ngModelCtrl || $mdChipsCtrl.readonly}">\ 28381 <div class="md-chip-content"\ 28382 tabindex="{{$mdChipsCtrl.ariaTabIndex == $index ? 0 : -1}}"\ 28383 id="{{$mdChipsCtrl.contentIdFor($index)}}"\ 28384 role="option"\ 28385 aria-selected="{{$mdChipsCtrl.selectedChip == $index}}" \ 28386 aria-posinset="{{$index}}"\ 28387 ng-click="!$mdChipsCtrl.readonly && $mdChipsCtrl.focusChip($index)"\ 28388 ng-focus="!$mdChipsCtrl.readonly && $mdChipsCtrl.selectChip($index)"\ 28389 md-chip-transclude="$mdChipsCtrl.chipContentsTemplate"></div>\ 28390 <div ng-if="$mdChipsCtrl.isRemovable()"\ 28391 class="md-chip-remove-container"\ 28392 tabindex="-1"\ 28393 md-chip-transclude="$mdChipsCtrl.chipRemoveTemplate"></div>\ 28394 </md-chip>\ 28395 <div class="md-chip-input-container" ng-if="!$mdChipsCtrl.readonly && $mdChipsCtrl.ngModelCtrl">\ 28396 <div md-chip-transclude="$mdChipsCtrl.chipInputTemplate"></div>\ 28397 </div>\ 28398 </md-chips-wrap>'; 28399 28400 var CHIP_INPUT_TEMPLATE = '\ 28401 <input\ 28402 class="md-input"\ 28403 tabindex="0"\ 28404 aria-label="{{$mdChipsCtrl.inputAriaLabel}}" \ 28405 placeholder="{{$mdChipsCtrl.getPlaceholder()}}"\ 28406 ng-model="$mdChipsCtrl.chipBuffer"\ 28407 ng-focus="$mdChipsCtrl.onInputFocus()"\ 28408 ng-blur="$mdChipsCtrl.onInputBlur()"\ 28409 ng-keydown="$mdChipsCtrl.inputKeydown($event)">'; 28410 28411 var CHIP_DEFAULT_TEMPLATE = '\
vendor: 12,684 bytes, lines 28412-28745
28412 <span>{{$chip}}</span>'; 28413 28414 var CHIP_REMOVE_TEMPLATE = '\ 28415 <button\ 28416 class="md-chip-remove"\ 28417 ng-if="$mdChipsCtrl.isRemovable()"\ 28418 ng-click="$mdChipsCtrl.removeChipAndFocusInput($$replacedScope.$index)"\ 28419 type="button"\ 28420 tabindex="-1">\ 28421 <md-icon md-svg-src="{{ $mdChipsCtrl.mdCloseIcon }}"></md-icon>\ 28422 <span class="md-visually-hidden">\ 28423 {{$mdChipsCtrl.deleteButtonLabel}}\ 28424 </span>\ 28425 </button>'; 28426 28427 /** 28428 * MDChips Directive Definition 28429 */ 28430 function MdChips ($mdTheming, $mdUtil, $compile, $log, $timeout, $$mdSvgRegistry) { 28431 // Run our templates through $mdUtil.processTemplate() to allow custom start/end symbols 28432 var templates = getTemplates(); 28433 28434 return { 28435 template: function(element, attrs) { 28436 // Clone the element into an attribute. By prepending the attribute 28437 // name with '$', AngularJS won't write it into the DOM. The cloned 28438 // element propagates to the link function via the attrs argument, 28439 // where various contained-elements can be consumed. 28440 attrs['$mdUserTemplate'] = element.clone(); 28441 return templates.chips; 28442 }, 28443 require: ['mdChips'], 28444 restrict: 'E', 28445 controller: 'MdChipsCtrl', 28446 controllerAs: '$mdChipsCtrl', 28447 bindToController: true, 28448 compile: compile, 28449 scope: { 28450 readonly: '=readonly', 28451 removable: '=mdRemovable', 28452 placeholder: '@', 28453 secondaryPlaceholder: '@', 28454 maxChips: '@mdMaxChips', 28455 transformChip: '&mdTransformChip', 28456 onAppend: '&mdOnAppend', 28457 onAdd: '&mdOnAdd', 28458 onRemove: '&mdOnRemove', 28459 onSelect: '&mdOnSelect', 28460 inputAriaLabel: '@', 28461 containerHint: '@', 28462 deleteHint: '@', 28463 deleteButtonLabel: '@', 28464 separatorKeys: '=?mdSeparatorKeys', 28465 requireMatch: '=?mdRequireMatch', 28466 chipAppendDelayString: '@?mdChipAppendDelay' 28467 } 28468 }; 28469 28470 /** 28471 * Builds the final template for `md-chips` and returns the postLink function. 28472 * 28473 * Building the template involves 3 key components: 28474 * static chips 28475 * chip template 28476 * input control 28477 * 28478 * If no `ng-model` is provided, only the static chip work needs to be done. 28479 * 28480 * If no user-passed `md-chip-template` exists, the default template is used. This resulting 28481 * template is appended to the chip content element. 28482 * 28483 * The remove button may be overridden by passing an element with an md-chip-remove attribute. 28484 * 28485 * If an `input` or `md-autocomplete` element is provided by the caller, it is set aside for 28486 * transclusion later. The transclusion happens in `postLink` as the parent scope is required. 28487 * If no user input is provided, a default one is appended to the input container node in the 28488 * template. 28489 * 28490 * Static Chips (i.e. `md-chip` elements passed from the caller) are gathered and set aside for 28491 * transclusion in the `postLink` function. 28492 * 28493 * 28494 * @param element 28495 * @param attr 28496 * @returns {Function} 28497 */ 28498 function compile(element, attr) { 28499 // Grab the user template from attr and reset the attribute to null. 28500 var userTemplate = attr['$mdUserTemplate']; 28501 attr['$mdUserTemplate'] = null; 28502 28503 var chipTemplate = getTemplateByQuery('md-chips>md-chip-template'); 28504 28505 var chipRemoveSelector = $mdUtil 28506 .prefixer() 28507 .buildList('md-chip-remove') 28508 .map(function(attr) { 28509 return 'md-chips>*[' + attr + ']'; 28510 }) 28511 .join(','); 28512 28513 // Set the chip remove, chip contents and chip input templates. The link function will put 28514 // them on the scope for transclusion later. 28515 var chipRemoveTemplate = getTemplateByQuery(chipRemoveSelector) || templates.remove, 28516 chipContentsTemplate = chipTemplate || templates.default, 28517 chipInputTemplate = getTemplateByQuery('md-chips>md-autocomplete') 28518 || getTemplateByQuery('md-chips>input') 28519 || templates.input, 28520 staticChips = userTemplate.find('md-chip'); 28521 28522 // Warn of malformed template. See #2545 28523 if (userTemplate[0].querySelector('md-chip-template>*[md-chip-remove]')) { 28524 $log.warn('invalid placement of md-chip-remove within md-chip-template.'); 28525 } 28526 28527 function getTemplateByQuery (query) { 28528 if (!attr.ngModel) return; 28529 var element = userTemplate[0].querySelector(query); 28530 return element && element.outerHTML; 28531 } 28532 28533 /** 28534 * Configures controller and transcludes. 28535 */ 28536 return function postLink(scope, element, attrs, controllers) { 28537 $mdUtil.initOptionalProperties(scope, attr); 28538 28539 $mdTheming(element); 28540 var mdChipsCtrl = controllers[0]; 28541 if(chipTemplate) { 28542 // Chip editing functionality assumes we are using the default chip template. 28543 mdChipsCtrl.enableChipEdit = false; 28544 } 28545 28546 mdChipsCtrl.chipContentsTemplate = chipContentsTemplate; 28547 mdChipsCtrl.chipRemoveTemplate = chipRemoveTemplate; 28548 mdChipsCtrl.chipInputTemplate = chipInputTemplate; 28549 28550 mdChipsCtrl.mdCloseIcon = $$mdSvgRegistry.mdClose; 28551 28552 element 28553 .attr({ tabindex: -1 }) 28554 .on('focus', function () { mdChipsCtrl.onFocus(); }); 28555 28556 if (attr.ngModel) { 28557 mdChipsCtrl.configureNgModel(element.controller('ngModel')); 28558 28559 // If an `md-transform-chip` attribute was set, tell the controller to use the expression 28560 // before appending chips. 28561 if (attrs.mdTransformChip) mdChipsCtrl.useTransformChipExpression(); 28562 28563 // If an `md-on-append` attribute was set, tell the controller to use the expression 28564 // when appending chips. 28565 // 28566 // DEPRECATED: Will remove in official 1.0 release 28567 if (attrs.mdOnAppend) mdChipsCtrl.useOnAppendExpression(); 28568 28569 // If an `md-on-add` attribute was set, tell the controller to use the expression 28570 // when adding chips. 28571 if (attrs.mdOnAdd) mdChipsCtrl.useOnAddExpression(); 28572 28573 // If an `md-on-remove` attribute was set, tell the controller to use the expression 28574 // when removing chips. 28575 if (attrs.mdOnRemove) mdChipsCtrl.useOnRemoveExpression(); 28576 28577 // If an `md-on-select` attribute was set, tell the controller to use the expression 28578 // when selecting chips. 28579 if (attrs.mdOnSelect) mdChipsCtrl.useOnSelectExpression(); 28580 28581 // The md-autocomplete and input elements won't be compiled until after this directive 28582 // is complete (due to their nested nature). Wait a tick before looking for them to 28583 // configure the controller. 28584 if (chipInputTemplate != templates.input) { 28585 // The autocomplete will not appear until the readonly attribute is not true (i.e. 28586 // false or undefined), so we have to watch the readonly and then on the next tick 28587 // after the chip transclusion has run, we can configure the autocomplete and user 28588 // input. 28589 scope.$watch('$mdChipsCtrl.readonly', function(readonly) { 28590 if (!readonly) { 28591 28592 $mdUtil.nextTick(function(){ 28593 28594 if (chipInputTemplate.indexOf('<md-autocomplete') === 0) { 28595 var autocompleteEl = element.find('md-autocomplete'); 28596 mdChipsCtrl.configureAutocomplete(autocompleteEl.controller('mdAutocomplete')); 28597 } 28598 28599 mdChipsCtrl.configureUserInput(element.find('input')); 28600 }); 28601 } 28602 }); 28603 } 28604 28605 // At the next tick, if we find an input, make sure it has the md-input class 28606 $mdUtil.nextTick(function() { 28607 var input = element.find('input'); 28608 28609 input && input.toggleClass('md-input', true); 28610 }); 28611 } 28612 28613 // Compile with the parent's scope and prepend any static chips to the wrapper. 28614 if (staticChips.length > 0) { 28615 var compiledStaticChips = $compile(staticChips.clone())(scope.$parent); 28616 $timeout(function() { element.find('md-chips-wrap').prepend(compiledStaticChips); }); 28617 } 28618 }; 28619 } 28620 28621 function getTemplates() { 28622 return { 28623 chips: $mdUtil.processTemplate(MD_CHIPS_TEMPLATE), 28624 input: $mdUtil.processTemplate(CHIP_INPUT_TEMPLATE), 28625 default: $mdUtil.processTemplate(CHIP_DEFAULT_TEMPLATE), 28626 remove: $mdUtil.processTemplate(CHIP_REMOVE_TEMPLATE) 28627 }; 28628 } 28629 } 28630 28631})(); 28632(function(){ 28633"use strict"; 28634 28635angular 28636 .module('material.components.chips') 28637 .controller('MdContactChipsCtrl', MdContactChipsCtrl); 28638 28639 28640 28641/** 28642 * Controller for the MdContactChips component 28643 * @constructor 28644 */ 28645function MdContactChipsCtrl () { 28646 /** @type {Object} */ 28647 this.selectedItem = null; 28648 28649 /** @type {string} */ 28650 this.searchText = ''; 28651} 28652 28653 28654MdContactChipsCtrl.prototype.queryContact = function(searchText) { 28655 return this.contactQuery({'$query': searchText}); 28656}; 28657 28658 28659MdContactChipsCtrl.prototype.itemName = function(item) { 28660 return item[this.contactName]; 28661}; 28662 28663})(); 28664(function(){ 28665"use strict"; 28666 28667 28668MdContactChips.$inject = ["$mdTheming", "$mdUtil"];angular 28669 .module('material.components.chips') 28670 .directive('mdContactChips', MdContactChips); 28671 28672/** 28673 * @ngdoc directive 28674 * @name mdContactChips 28675 * @module material.components.chips 28676 * 28677 * @description 28678 * `<md-contact-chips>` is an input component based on `md-chips` and makes use of an 28679 * `md-autocomplete` element. The component allows the caller to supply a query expression which 28680 * returns a list of possible contacts. The user can select one of these and add it to the list of 28681 * chips. 28682 * 28683 * You may also use the `md-highlight-text` directive along with its parameters to control the 28684 * appearance of the matched text inside of the contacts' autocomplete popup. 28685 * 28686 * @param {string=|object=} ng-model A model to bind the list of items to 28687 * @param {string=} placeholder Placeholder text that will be forwarded to the input. 28688 * @param {string=} secondary-placeholder Placeholder text that will be forwarded to the input, 28689 * displayed when there is at least on item in the list 28690 * @param {expression} md-contacts An expression expected to return contacts matching the search 28691 * test, `$query`. If this expression involves a promise, a loading bar is displayed while 28692 * waiting for it to resolve. 28693 * @param {string} md-contact-name The field name of the contact object representing the 28694 * contact's name. 28695 * @param {string} md-contact-email The field name of the contact object representing the 28696 * contact's email address. 28697 * @param {string} md-contact-image The field name of the contact object representing the 28698 * contact's image. 28699 * @param {number=} md-min-length Specifies the minimum length of text before autocomplete will 28700 * make suggestions 28701 * 28702 * @param {expression=} filter-selected Whether to filter selected contacts from the list of 28703 * suggestions shown in the autocomplete. 28704 * 28705 * ***Note:** This attribute has been removed but may come back.* 28706 * 28707 * 28708 * 28709 * @usage 28710 * <hljs lang="html"> 28711 * <md-contact-chips 28712 * ng-model="ctrl.contacts" 28713 * md-contacts="ctrl.querySearch($query)" 28714 * md-contact-name="name" 28715 * md-contact-image="image" 28716 * md-contact-email="email" 28717 * placeholder="To"> 28718 * </md-contact-chips> 28719 * </hljs> 28720 * 28721 */ 28722 28723 28724var MD_CONTACT_CHIPS_TEMPLATE = '\ 28725 <md-chips class="md-contact-chips"\ 28726 ng-model="$mdContactChipsCtrl.contacts"\ 28727 md-require-match="$mdContactChipsCtrl.requireMatch"\ 28728 md-chip-append-delay="{{$mdContactChipsCtrl.chipAppendDelay}}" \ 28729 md-autocomplete-snap>\ 28730 <md-autocomplete\ 28731 md-menu-class="md-contact-chips-suggestions"\ 28732 md-selected-item="$mdContactChipsCtrl.selectedItem"\ 28733 md-search-text="$mdContactChipsCtrl.searchText"\ 28734 md-items="item in $mdContactChipsCtrl.queryContact($mdContactChipsCtrl.searchText)"\ 28735 md-item-text="$mdContactChipsCtrl.itemName(item)"\ 28736 md-no-cache="true"\ 28737 md-min-length="$mdContactChipsCtrl.minLength"\ 28738 md-autoselect\ 28739 placeholder="{{$mdContactChipsCtrl.contacts.length == 0 ?\ 28740 $mdContactChipsCtrl.placeholder : $mdContactChipsCtrl.secondaryPlaceholder}}">\ 28741 <div class="md-contact-suggestion">\ 28742 <img \ 28743 ng-src="{{item[$mdContactChipsCtrl.contactImage]}}"\ 28744 alt="{{item[$mdContactChipsCtrl.contactName]}}"\ 28745 ng-if="item[$mdContactChipsCtrl.contactImage]" />\
28746 <span class="md-contact-name" md-highlight-text="$mdContactChipsCtrl.searchText"\ 28747 md-highlight-flags="{{$mdContactChipsCtrl.highlightFlags}}">\ 28748 {{item[$mdContactChipsCtrl.contactName]}}\ 28749 </span>\ 28750 <span class="md-contact-email" >{{item[$mdContactChipsCtrl.contactEmail]}}</span>\ 28751 </div>\ 28752 </md-autocomplete>\ 28753 <md-chip-template>\ 28754 <div class="md-contact-avatar">\ 28755 <img \ 28756 ng-src="{{$chip[$mdContactChipsCtrl.contactImage]}}"\ 28757 alt="{{$chip[$mdContactChipsCtrl.contactName]}}"\ 28758 ng-if="$chip[$mdContactChipsCtrl.contactImage]" />\ 28759 </div>\ 28760 <div class="md-contact-name">\ 28761 {{$chip[$mdContactChipsCtrl.contactName]}}\ 28762 </div>\ 28763 </md-chip-template>\ 28764 </md-chips>'; 28765 28766 28767/** 28768 * MDContactChips Directive Definition 28769 * 28770 * @param $mdTheming 28771 * @returns {*} 28772 * @ngInject 28773 */ 28774function MdContactChips($mdTheming, $mdUtil) { 28775 return { 28776 template: function(element, attrs) { 28777 return MD_CONTACT_CHIPS_TEMPLATE; 28778 }, 28779 restrict: 'E', 28780 controller: 'MdContactChipsCtrl', 28781 controllerAs: '$mdContactChipsCtrl', 28782 bindToController: true, 28783 compile: compile, 28784 scope: { 28785 contactQuery: '&mdContacts', 28786 placeholder: '@', 28787 secondaryPlaceholder: '@', 28788 contactName: '@mdContactName', 28789 contactImage: '@mdContactImage', 28790 contactEmail: '@mdContactEmail', 28791 contacts: '=ngModel', 28792 requireMatch: '=?mdRequireMatch', 28793 minLength: '=?mdMinLength', 28794 highlightFlags: '@?mdHighlightFlags', 28795 chipAppendDelay: '@?mdChipAppendDelay' 28796 } 28797 }; 28798 28799 function compile(element, attr) { 28800 return function postLink(scope, element, attrs, controllers) { 28801 var contactChipsController = controllers; 28802 28803 $mdUtil.initOptionalProperties(scope, attr); 28804 $mdTheming(element); 28805 28806 element.attr('tabindex', '-1'); 28807 28808 attrs.$observe('mdChipAppendDelay', function(newValue) { 28809 contactChipsController.chipAppendDelay = newValue; 28810 }); 28811 }; 28812 } 28813} 28814 28815})(); 28816(function(){ 28817"use strict"; 28818 28819(function() { 28820 'use strict'; 28821 28822 /** 28823 * @ngdoc directive 28824 * @name mdCalendar 28825 * @module material.components.datepicker 28826 * 28827 * @param {Date} ng-model The component's model. Should be a Date object. 28828 * @param {Date=} md-min-date Expression representing the minimum date. 28829 * @param {Date=} md-max-date Expression representing the maximum date. 28830 * @param {(function(Date): boolean)=} md-date-filter Function expecting a date and returning a boolean whether it can be selected or not. 28831 * 28832 * @description 28833 * `<md-calendar>` is a component that renders a calendar that can be used to select a date. 28834 * It is a part of the `<md-datepicker>` pane, however it can also be used on it's own. 28835 * 28836 * @usage 28837 * 28838 * <hljs lang="html"> 28839 * <md-calendar ng-model="birthday"></md-calendar> 28840 * </hljs> 28841 */ 28842 CalendarCtrl.$inject = ["$element", "$scope", "$$mdDateUtil", "$mdUtil", "$mdConstant", "$mdTheming", "$$rAF", "$attrs", "$mdDateLocale"]; 28843 angular.module('material.components.datepicker') 28844 .directive('mdCalendar', calendarDirective); 28845 28846 // POST RELEASE 28847 // TODO(jelbourn): Mac Cmd + left / right == Home / End 28848 // TODO(jelbourn): Refactor month element creation to use cloneNode (performance). 28849 // TODO(jelbourn): Define virtual scrolling constants (compactness) users can override. 28850 // TODO(jelbourn): Animated month transition on ng-model change (virtual-repeat) 28851 // TODO(jelbourn): Scroll snapping (virtual repeat) 28852 // TODO(jelbourn): Remove superfluous row from short months (virtual-repeat) 28853 // TODO(jelbourn): Month headers stick to top when scrolling. 28854 // TODO(jelbourn): Previous month opacity is lowered when partially scrolled out of view. 28855 // TODO(jelbourn): Support md-calendar standalone on a page (as a tabstop w/ aria-live 28856 // announcement and key handling). 28857 // Read-only calendar (not just date-picker). 28858 28859 function calendarDirective() { 28860 return { 28861 template: function(tElement, tAttr) { 28862 // TODO(crisbeto): This is a workaround that allows the calendar to work, without 28863 // a datepicker, until issue #8585 gets resolved. It can safely be removed 28864 // afterwards. This ensures that the virtual repeater scrolls to the proper place on load by 28865 // deferring the execution until the next digest. It's necessary only if the calendar is used 28866 // without a datepicker, otherwise it's already wrapped in an ngIf. 28867 var extraAttrs = tAttr.hasOwnProperty('ngIf') ? '' : 'ng-if="calendarCtrl.isInitialized"'; 28868 var template = '' + 28869 '<div ng-switch="calendarCtrl.currentView" ' + extraAttrs + '>' + 28870 '<md-calendar-year ng-switch-when="year"></md-calendar-year>' + 28871 '<md-calendar-month ng-switch-default></md-calendar-month>' + 28872 '</div>'; 28873 28874 return template; 28875 }, 28876 scope: { 28877 minDate: '=mdMinDate', 28878 maxDate: '=mdMaxDate', 28879 dateFilter: '=mdDateFilter', 28880 _currentView: '@mdCurrentView' 28881 }, 28882 require: ['ngModel', 'mdCalendar'], 28883 controller: CalendarCtrl, 28884 controllerAs: 'calendarCtrl', 28885 bindToController: true, 28886 link: function(scope, element, attrs, controllers) { 28887 var ngModelCtrl = controllers[0]; 28888 var mdCalendarCtrl = controllers[1]; 28889 mdCalendarCtrl.configureNgModel(ngModelCtrl); 28890 } 28891 }; 28892 } 28893 28894 /** 28895 * Occasionally the hideVerticalScrollbar method might read an element's 28896 * width as 0, because it hasn't been laid out yet. This value will be used 28897 * as a fallback, in order to prevent scenarios where the element's width 28898 * would otherwise have been set to 0. This value is the "usual" width of a 28899 * calendar within a floating calendar pane. 28900 */ 28901 var FALLBACK_WIDTH = 340; 28902 28903 /** Next identifier for calendar instance. */ 28904 var nextUniqueId = 0; 28905 28906 /** 28907 * Controller for the mdCalendar component. 28908 * @ngInject @constructor 28909 */ 28910 function CalendarCtrl($element, $scope, $$mdDateUtil, $mdUtil, 28911 $mdConstant, $mdTheming, $$rAF, $attrs, $mdDateLocale) { 28912 28913 $mdTheming($element); 28914 28915 /** @final {!angular.JQLite} */ 28916 this.$element = $element; 28917 28918 /** @final {!angular.Scope} */ 28919 this.$scope = $scope; 28920 28921 /** @final */ 28922 this.dateUtil = $$mdDateUtil; 28923 28924 /** @final */ 28925 this.$mdUtil = $mdUtil; 28926 28927 /** @final */ 28928 this.keyCode = $mdConstant.KEY_CODE; 28929 28930 /** @final */ 28931 this.$$rAF = $$rAF; 28932 28933 /** @final */ 28934 this.$mdDateLocale = $mdDateLocale; 28935 28936 /** @final {Date} */ 28937 this.today = this.dateUtil.createDateAtMidnight(); 28938
28939 /** @type {!angular.NgModelController} */ 28940 this.ngModelCtrl = null; 28941 28942 /** @type {String} Class applied to the selected date cell. */ 28943 this.SELECTED_DATE_CLASS = 'md-calendar-selected-date'; 28944 28945 /** @type {String} Class applied to the cell for today. */ 28946 this.TODAY_CLASS = 'md-calendar-date-today'; 28947 28948 /** @type {String} Class applied to the focused cell. */ 28949 this.FOCUSED_DATE_CLASS = 'md-focus'; 28950 28951 /** @final {number} Unique ID for this calendar instance. */ 28952 this.id = nextUniqueId++; 28953 28954 /** 28955 * The date that is currently focused or showing in the calendar. This will initially be set 28956 * to the ng-model value if set, otherwise to today. It will be updated as the user navigates 28957 * to other months. The cell corresponding to the displayDate does not necesarily always have 28958 * focus in the document (such as for cases when the user is scrolling the calendar). 28959 * @type {Date} 28960 */ 28961 this.displayDate = null; 28962 28963 /** 28964 * The selected date. Keep track of this separately from the ng-model value so that we 28965 * can know, when the ng-model value changes, what the previous value was before it's updated 28966 * in the component's UI. 28967 * 28968 * @type {Date} 28969 */ 28970 this.selectedDate = null; 28971 28972 /** 28973 * The first date that can be rendered by the calendar. The default is taken 28974 * from the mdDateLocale provider and is limited by the mdMinDate. 28975 * @type {Date} 28976 */ 28977 this.firstRenderableDate = null; 28978 28979 /** 28980 * The last date that can be rendered by the calendar. The default comes 28981 * from the mdDateLocale provider and is limited by the maxDate. 28982 * @type {Date} 28983 */ 28984 this.lastRenderableDate = null; 28985 28986 /** 28987 * Used to toggle initialize the root element in the next digest. 28988 * @type {Boolean} 28989 */ 28990 this.isInitialized = false; 28991 28992 /** 28993 * Cache for the width of the element without a scrollbar. Used to hide the scrollbar later on 28994 * and to avoid extra reflows when switching between views. 28995 * @type {Number} 28996 */ 28997 this.width = 0; 28998 28999 /** 29000 * Caches the width of the scrollbar in order to be used when hiding it and to avoid extra reflows. 29001 * @type {Number} 29002 */ 29003 this.scrollbarWidth = 0; 29004 29005 // Unless the user specifies so, the calendar should not be a tab stop. 29006 // This is necessary because ngAria might add a tabindex to anything with an ng-model 29007 // (based on whether or not the user has turned that particular feature on/off). 29008 if (!$attrs.tabindex) { 29009 $element.attr('tabindex', '-1'); 29010 } 29011 29012 var boundKeyHandler = angular.bind(this, this.handleKeyEvent); 29013 29014 29015 29016 // If use the md-calendar directly in the body without datepicker, 29017 // handleKeyEvent will disable other inputs on the page. 29018 // So only apply the handleKeyEvent on the body when the md-calendar inside datepicker, 29019 // otherwise apply on the calendar element only. 29020 29021 var handleKeyElement; 29022 if ($element.parent().hasClass('md-datepicker-calendar')) { 29023 handleKeyElement = angular.element(document.body); 29024 } else { 29025 handleKeyElement = $element; 29026 } 29027 29028 // Bind the keydown handler to the body, in order to handle cases where the focused 29029 // element gets removed from the DOM and stops propagating click events. 29030 handleKeyElement.on('keydown', boundKeyHandler); 29031 29032 $scope.$on('$destroy', function() { 29033 handleKeyElement.off('keydown', boundKeyHandler); 29034 }); 29035 29036 // For AngularJS 1.4 and older, where there are no lifecycle hooks but bindings are pre-assigned, 29037 // manually call the $onInit hook. 29038 if (angular.version.major === 1 && angular.version.minor <= 4) { 29039 this.$onInit(); 29040 } 29041 29042 } 29043 29044 /** 29045 * AngularJS Lifecycle hook for newer AngularJS versions. 29046 * Bindings are not guaranteed to have been assigned in the controller, but they are in the $onInit hook. 29047 */ 29048 CalendarCtrl.prototype.$onInit = function() { 29049 29050 /** 29051 * The currently visible calendar view. Note the prefix on the scope value, 29052 * which is necessary, because the datepicker seems to reset the real one value if the 29053 * calendar is open, but the value on the datepicker's scope is empty. 29054 * @type {String} 29055 */ 29056 this.currentView = this._currentView || 'month'; 29057 29058 var dateLocale = this.$mdDateLocale; 29059 29060 if (this.minDate && this.minDate >
29060 dateLocale.firstRenderableDate) { 29061 this.firstRenderableDate = this.minDate; 29062 } else { 29063 this.firstRenderableDate = dateLocale.firstRenderableDate; 29064 } 29065 29066 if (this.maxDate && this.maxDate < dateLocale.lastRenderableDate) { 29067 this.lastRenderableDate = this.maxDate; 29068 } else { 29069 this.lastRenderableDate = dateLocale.lastRenderableDate; 29070 } 29071 }; 29072 29073 /** 29074 * Sets up the controller's reference to ngModelController. 29075 * @param {!angular.NgModelController} ngModelCtrl 29076 */ 29077 CalendarCtrl.prototype.configureNgModel = function(ngModelCtrl) { 29078 var self = this; 29079 29080 self.ngModelCtrl = ngModelCtrl; 29081 29082 self.$mdUtil.nextTick(function() { 29083 self.isInitialized = true; 29084 }); 29085 29086 ngModelCtrl.$render = function() { 29087 var value = this.$viewValue; 29088 29089 // Notify the child scopes of any changes. 29090 self.$scope.$broadcast('md-calendar-parent-changed', value); 29091 29092 // Set up the selectedDate if it hasn't been already. 29093 if (!self.selectedDate) { 29094 self.selectedDate = value; 29095 } 29096 29097 // Also set up the displayDate. 29098 if (!self.displayDate) { 29099 self.displayDate = self.selectedDate || self.today; 29100 } 29101 }; 29102 }; 29103 29104 /** 29105 * Sets the ng-model value for the calendar and emits a change event. 29106 * @param {Date} date 29107 */ 29108 CalendarCtrl.prototype.setNgModelValue = function(date) { 29109 var value = this.dateUtil.createDateAtMidnight(date); 29110 this.focus(value); 29111 this.$scope.$emit('md-calendar-change', value); 29112 this.ngModelCtrl.$setViewValue(value); 29113 this.ngModelCtrl.$render(); 29114 return value; 29115 }; 29116 29117 /** 29118 * Sets the current view that should be visible in the calendar 29119 * @param {string} newView View name to be set. 29120 * @param {number|Date} time Date object or a timestamp for the new display date. 29121 */ 29122 CalendarCtrl.prototype.setCurrentView = function(newView, time) { 29123 var self = this; 29124 29125 self.$mdUtil.nextTick(function() { 29126 self.currentView = newView; 29127 29128 if (time) { 29129 self.displayDate = angular.isDate(time) ? time : new Date(time); 29130 } 29131 }); 29132 }; 29133 29134 /** 29135 * Focus the cell corresponding to the given date. 29136 * @param {Date} date The date to be focused. 29137 */ 29138 CalendarCtrl.prototype.focus = function(date) { 29139 if (this.dateUtil.isValidDate(date)) { 29140 var previousFocus = this.$element[0].querySelector('.md-focus'); 29141 if (previousFocus) { 29142 previousFocus.classList.remove(this.FOCUSED_DATE_CLASS); 29143 } 29144 29145 var cellId = this.getDateId(date, this.currentView); 29146 var cell = document.getElementById(cellId); 29147 if (cell) { 29148 cell.classList.add(this.FOCUSED_DATE_CLASS); 29149 cell.focus(); 29150 this.displayDate = date; 29151 } 29152 } else { 29153 var rootElement = this.$element[0].querySelector('[ng-switch]'); 29154 29155 if (rootElement) { 29156 rootElement.focus(); 29157 } 29158 } 29159 }; 29160 29161 /** 29162 * Normalizes the key event into an action name. The action will be broadcast 29163 * to the child controllers. 29164 * @param {KeyboardEvent} event 29165 * @returns {String} The action that should be taken, or null if the key 29166 * does not match a calendar shortcut. 29167 */ 29168 CalendarCtrl.prototype.getActionFromKeyEvent = function(event) { 29169 var keyCode = this.keyCode; 29170 29171 switch (event.which) { 29172 case keyCode.ENTER: return 'select'; 29173 29174 case keyCode.RIGHT_ARROW: return 'move-right'; 29175 case keyCode.LEFT_ARROW: return 'move-left'; 29176
vendor: 41,975 bytes, lines 29177-30368
29177 // TODO(crisbeto): Might want to reconsider using metaKey, because it maps 29178 // to the "Windows" key on PC, which opens the start menu or resizes the browser. 29179 case keyCode.DOWN_ARROW: return event.metaKey ? 'move-page-down' : 'move-row-down'; 29180 case keyCode.UP_ARROW: return event.metaKey ? 'move-page-up' : 'move-row-up'; 29181 29182 case keyCode.PAGE_DOWN: return 'move-page-down'; 29183 case keyCode.PAGE_UP: return 'move-page-up'; 29184 29185 case keyCode.HOME: return 'start'; 29186 case keyCode.END: return 'end'; 29187 29188 default: return null; 29189 } 29190 }; 29191 29192 /** 29193 * Handles a key event in the calendar with the appropriate action. The action will either 29194 * be to select the focused date or to navigate to focus a new date. 29195 * @param {KeyboardEvent} event 29196 */ 29197 CalendarCtrl.prototype.handleKeyEvent = function(event) { 29198 var self = this; 29199 29200 this.$scope.$apply(function() { 29201 // Capture escape and emit back up so that a wrapping component 29202 // (such as a date-picker) can decide to close. 29203 if (event.which == self.keyCode.ESCAPE || event.which == self.keyCode.TAB) { 29204 self.$scope.$emit('md-calendar-close'); 29205 29206 if (event.which == self.keyCode.TAB) { 29207 event.preventDefault(); 29208 } 29209 29210 return; 29211 } 29212 29213 // Broadcast the action that any child controllers should take. 29214 var action = self.getActionFromKeyEvent(event); 29215 if (action) { 29216 event.preventDefault(); 29217 event.stopPropagation(); 29218 self.$scope.$broadcast('md-calendar-parent-action', action); 29219 } 29220 }); 29221 }; 29222 29223 /** 29224 * Hides the vertical scrollbar on the calendar scroller of a child controller by 29225 * setting the width on the calendar scroller and the `overflow: hidden` wrapper 29226 * around the scroller, and then setting a padding-right on the scroller equal 29227 * to the width of the browser's scrollbar. 29228 * 29229 * This will cause a reflow. 29230 * 29231 * @param {object} childCtrl The child controller whose scrollbar should be hidden. 29232 */ 29233 CalendarCtrl.prototype.hideVerticalScrollbar = function(childCtrl) { 29234 var self = this; 29235 var element = childCtrl.$element[0]; 29236 var scrollMask = element.querySelector('.md-calendar-scroll-mask'); 29237 29238 if (self.width > 0) { 29239 setWidth(); 29240 } else { 29241 self.$$rAF(function() { 29242 var scroller = childCtrl.calendarScroller; 29243 29244 self.scrollbarWidth = scroller.offsetWidth - scroller.clientWidth; 29245 self.width = element.querySelector('table').offsetWidth; 29246 setWidth(); 29247 }); 29248 } 29249 29250 function setWidth() { 29251 var width = self.width || FALLBACK_WIDTH; 29252 var scrollbarWidth = self.scrollbarWidth; 29253 var scroller = childCtrl.calendarScroller; 29254 29255 scrollMask.style.width = width + 'px'; 29256 scroller.style.width = (width + scrollbarWidth) + 'px'; 29257 scroller.style.paddingRight = scrollbarWidth + 'px'; 29258 } 29259 }; 29260 29261 /** 29262 * Gets an identifier for a date unique to the calendar instance for internal 29263 * purposes. Not to be displayed. 29264 * @param {Date} date The date for which the id is being generated 29265 * @param {string} namespace Namespace for the id. (month, year etc.) 29266 * @returns {string} 29267 */ 29268 CalendarCtrl.prototype.getDateId = function(date, namespace) { 29269 if (!namespace) { 29270 throw new Error('A namespace for the date id has to be specified.'); 29271 } 29272 29273 return [ 29274 'md', 29275 this.id, 29276 namespace, 29277 date.getFullYear(), 29278 date.getMonth(), 29279 date.getDate() 29280 ].join('-'); 29281 }; 29282 29283 /** 29284 * Util to trigger an extra digest on a parent scope, in order to to ensure that 29285 * any child virtual repeaters have updated. This is necessary, because the virtual 29286 * repeater doesn't update the $index the first time around since the content isn't 29287 * in place yet. The case, in which this is an issue, is when the repeater has less 29288 * than a page of content (e.g. a month or year view has a min or max date). 29289 */ 29290 CalendarCtrl.prototype.updateVirtualRepeat = function() { 29291 var scope = this.$scope; 29292 var virtualRepeatResizeListener = scope.$on('$md-resize-enable', function() { 29293 if (!scope.$$phase) { 29294 scope.$apply(); 29295 } 29296 29297 virtualRepeatResizeListener(); 29298 }); 29299 }; 29300})(); 29301 29302})(); 29303(function(){ 29304"use strict"; 29305 29306(function() { 29307 'use strict'; 29308 29309 CalendarMonthCtrl.$inject = ["$element", "$scope", "$animate", "$q", "$$mdDateUtil", "$mdDateLocale"]; 29310 angular.module('material.components.datepicker') 29311 .directive('mdCalendarMonth', calendarDirective); 29312 29313 /** 29314 * Height of one calendar month tbody. This must be made known to the virtual-repeat and is 29315 * subsequently used for scrolling to specific months. 29316 */ 29317 var TBODY_HEIGHT = 265; 29318 29319 /** 29320 * Height of a calendar month with a single row. This is needed to calculate the offset for 29321 * rendering an extra month in virtual-repeat that only contains one row. 29322 */ 29323 var TBODY_SINGLE_ROW_HEIGHT = 45; 29324 29325 /** Private directive that represents a list of months inside the calendar. */ 29326 function calendarDirective() { 29327 return { 29328 template: 29329 '<table aria-hidden="true" class="md-calendar-day-header"><thead></thead></table>' + 29330 '<div class="md-calendar-scroll-mask">' + 29331 '<md-virtual-repeat-container class="md-calendar-scroll-container" ' + 29332 'md-offset-size="' + (TBODY_SINGLE_ROW_HEIGHT - TBODY_HEIGHT) + '">' + 29333 '<table role="grid" tabindex="0" class="md-calendar" aria-readonly="true">' + 29334 '<tbody ' + 29335 'md-calendar-month-body ' + 29336 'role="rowgroup" ' + 29337 'md-virtual-repeat="i in monthCtrl.items" ' + 29338 'md-month-offset="$index" ' + 29339 'class="md-calendar-month" ' + 29340 'md-start-index="monthCtrl.getSelectedMonthIndex()" ' + 29341 'md-item-size="' + TBODY_HEIGHT + '">' + 29342 29343 // The <tr> ensures that the <tbody> will always have the 29344 // proper height, even if it's empty. If it's content is 29345 // compiled, the <tr> will be overwritten. 29346 '<tr aria-hidden="true" md-force-height="\'' + TBODY_HEIGHT + 'px\'"></tr>' + 29347 '</tbody>' + 29348 '</table>' + 29349 '</md-virtual-repeat-container>' + 29350 '</div>', 29351 require: ['^^mdCalendar', 'mdCalendarMonth'], 29352 controller: CalendarMonthCtrl, 29353 controllerAs: 'monthCtrl', 29354 bindToController: true, 29355 link: function(scope, element, attrs, controllers) { 29356 var calendarCtrl = controllers[0]; 29357 var monthCtrl = controllers[1]; 29358 monthCtrl.initialize(calendarCtrl); 29359 } 29360 }; 29361 } 29362 29363 /** 29364 * Controller for the calendar month component. 29365 * @ngInject @constructor 29366 */ 29367 function CalendarMonthCtrl($element, $scope, $animate, $q, 29368 $$mdDateUtil, $mdDateLocale) { 29369 29370 /** @final {!angular.JQLite} */ 29371 this.$element = $element; 29372 29373 /** @final {!angular.Scope} */ 29374 this.$scope = $scope; 29375 29376 /** @final {!angular.$animate} */ 29377 this.$animate = $animate; 29378 29379 /** @final {!angular.$q} */ 29380 this.$q = $q; 29381 29382 /** @final */ 29383 this.dateUtil = $$mdDateUtil; 29384 29385 /** @final */ 29386 this.dateLocale = $mdDateLocale; 29387 29388 /** @final {HTMLElement} */ 29389 this.calendarScroller = $element[0].querySelector('.md-virtual-repeat-scroller'); 29390 29391 /** @type {boolean} */ 29392 this.isInitialized = false; 29393 29394 /** @type {boolean} */ 29395 this.isMonthTransitionInProgress = false; 29396 29397 var self = this; 29398 29399 /** 29400 * Handles a click event on a date cell. 29401 * Created here so that every cell can use the same function instance. 29402 * @this {HTMLTableCellElement} The cell that was clicked. 29403 */ 29404 this.cellClickHandler = function() { 29405 var timestamp = $$mdDateUtil.getTimestampFromNode(this); 29406 self.$scope.$apply(function() { 29407 self.calendarCtrl.setNgModelValue(timestamp); 29408 }); 29409 }; 29410 29411 /** 29412 * Handles click events on the month headers. Switches 29413 * the calendar to the year view. 29414 * @this {HTMLTableCellElement} The cell that was clicked. 29415 */ 29416 this.headerClickHandler = function() { 29417 self.calendarCtrl.setCurrentView('year', $$mdDateUtil.getTimestampFromNode(this)); 29418 }; 29419 } 29420 29421 /*** Initialization ***/ 29422 29423 /** 29424 * Initialize the controller by saving a reference to the calendar and 29425 * setting up the object that will be iterated by the virtual repeater. 29426 */ 29427 CalendarMonthCtrl.prototype.initialize = function(calendarCtrl) { 29428 /** 29429 * Dummy array-like object for virtual-repeat to iterate over. The length is the total 29430 * number of months that can be viewed. We add 2 months: one to include the current month 29431 * and one for the last dummy month. 29432 * 29433 * This is shorter than ideal because of a (potential) Firefox bug 29434 * https://bugzilla.mozilla.org/show_bug.cgi?id=1181658. 29435 */ 29436 29437 this.items = { 29438 length: this.dateUtil.getMonthDistance( 29439 calendarCtrl.firstRenderableDate, 29440 calendarCtrl.lastRenderableDate 29441 ) + 2 29442 }; 29443 29444 this.calendarCtrl = calendarCtrl; 29445 this.attachScopeListeners(); 29446 calendarCtrl.updateVirtualRepeat(); 29447 29448 // Fire the initial render, since we might have missed it the first time it fired. 29449 calendarCtrl.ngModelCtrl && calendarCtrl.ngModelCtrl.$render(); 29450 }; 29451 29452 /** 29453 * Gets the "index" of the currently selected date as it would be in the virtual-repeat. 29454 * @returns {number} 29455 */ 29456 CalendarMonthCtrl.prototype.getSelectedMonthIndex = function() { 29457 var calendarCtrl = this.calendarCtrl; 29458 29459 return this.dateUtil.getMonthDistance( 29460 calendarCtrl.firstRenderableDate, 29461 calendarCtrl.displayDate || calendarCtrl.selectedDate || calendarCtrl.today 29462 ); 29463 }; 29464 29465 /** 29466 * Change the selected date in the calendar (ngModel value has already been changed). 29467 * @param {Date} date 29468 */ 29469 CalendarMonthCtrl.prototype.changeSelectedDate = function(date) { 29470 var self = this; 29471 var calendarCtrl = self.calendarCtrl; 29472 var previousSelectedDate = calendarCtrl.selectedDate; 29473 calendarCtrl.selectedDate = date; 29474 29475 this.changeDisplayDate(date).then(function() { 29476 var selectedDateClass = calendarCtrl.SELECTED_DATE_CLASS; 29477 var namespace = 'month'; 29478 29479 // Remove the selected class from the previously selected date, if any. 29480 if (previousSelectedDate) { 29481 var prevDateCell = document.getElementById(calendarCtrl.getDateId(previousSelectedDate, namespace)); 29482 if (prevDateCell) { 29483 prevDateCell.classList.remove(selectedDateClass); 29484 prevDateCell.setAttribute('aria-selected', 'false'); 29485 } 29486 } 29487 29488 // Apply the select class to the new selected date if it is set. 29489 if (date) { 29490 var dateCell = document.getElementById(calendarCtrl.getDateId(date, namespace)); 29491 if (dateCell) { 29492 dateCell.classList.add(selectedDateClass); 29493 dateCell.setAttribute('aria-selected', 'true'); 29494 } 29495 } 29496 }); 29497 }; 29498 29499 /** 29500 * Change the date that is being shown in the calendar. If the given date is in a different 29501 * month, the displayed month will be transitioned. 29502 * @param {Date} date 29503 */ 29504 CalendarMonthCtrl.prototype.changeDisplayDate = function(date) { 29505 // Initialization is deferred until this function is called because we want to reflect 29506 // the starting value of ngModel. 29507 if (!this.isInitialized) { 29508 this.buildWeekHeader(); 29509 this.calendarCtrl.hideVerticalScrollbar(this); 29510 this.isInitialized = true; 29511 return this.$q.when(); 29512 } 29513 29514 // If trying to show an invalid date or a transition is in progress, do nothing. 29515 if (!this.dateUtil.isValidDate(date) || this.isMonthTransitionInProgress) { 29516 return this.$q.when(); 29517 } 29518 29519 this.isMonthTransitionInProgress = true; 29520 var animationPromise = this.animateDateChange(date); 29521 29522 this.calendarCtrl.displayDate = date; 29523 29524 var self = this; 29525 animationPromise.then(function() { 29526 self.isMonthTransitionInProgress = false; 29527 }); 29528 29529 return animationPromise; 29530 }; 29531 29532 /** 29533 * Animates the transition from the calendar's current month to the given month. 29534 * @param {Date} date 29535 * @returns {angular.$q.Promise} The animation promise. 29536 */ 29537 CalendarMonthCtrl.prototype.animateDateChange = function(date) { 29538 if (this.dateUtil.isValidDate(date)) { 29539 var monthDistance = this.dateUtil.getMonthDistance(this.calendarCtrl.firstRenderableDate, date); 29540 this.calendarScroller.scrollTop = monthDistance * TBODY_HEIGHT; 29541 } 29542 29543 return this.$q.when(); 29544 }; 29545 29546 /** 29547 * Builds and appends a day-of-the-week header to the calendar. 29548 * This should only need to be called once during initialization. 29549 */ 29550 CalendarMonthCtrl.prototype.buildWeekHeader = function() { 29551 var firstDayOfWeek = this.dateLocale.firstDayOfWeek; 29552 var shortDays = this.dateLocale.shortDays; 29553 29554 var row = document.createElement('tr'); 29555 for (var i = 0; i < 7; i++) { 29556 var th = document.createElement('th'); 29557 th.textContent = shortDays[(i + firstDayOfWeek) % 7]; 29558 row.appendChild(th); 29559 } 29560 29561 this.$element.find('thead').append(row); 29562 }; 29563 29564 /** 29565 * Attaches listeners for the scope events that are broadcast by the calendar. 29566 */ 29567 CalendarMonthCtrl.prototype.attachScopeListeners = function() { 29568 var self = this; 29569 29570 self.$scope.$on('md-calendar-parent-changed', function(event, value) { 29571 self.changeSelectedDate(value); 29572 }); 29573 29574 self.$scope.$on('md-calendar-parent-action', angular.bind(this, this.handleKeyEvent)); 29575 }; 29576 29577 /** 29578 * Handles the month-specific keyboard interactions. 29579 * @param {Object} event Scope event object passed by the calendar. 29580 * @param {String} action Action, corresponding to the key that was pressed. 29581 */ 29582 CalendarMonthCtrl.prototype.handleKeyEvent = function(event, action) { 29583 var calendarCtrl = this.calendarCtrl; 29584 var displayDate = calendarCtrl.displayDate; 29585 29586 if (action === 'select') { 29587 calendarCtrl.setNgModelValue(displayDate); 29588 } else { 29589 var date = null; 29590 var dateUtil = this.dateUtil; 29591 29592 switch (action) { 29593 case 'move-right': date = dateUtil.incrementDays(displayDate, 1); break; 29594 case 'move-left': date = dateUtil.incrementDays(displayDate, -1); break; 29595 29596 case 'move-page-down': date = dateUtil.incrementMonths(displayDate, 1); break; 29597 case 'move-page-up': date = dateUtil.incrementMonths(displayDate, -1); break; 29598 29599 case 'move-row-down': date = dateUtil.incrementDays(displayDate, 7); break; 29600 case 'move-row-up': date = dateUtil.incrementDays(displayDate, -7); break; 29601 29602 case 'start': date = dateUtil.getFirstDateOfMonth(displayDate); break; 29603 case 'end': date = dateUtil.getLastDateOfMonth(displayDate); break; 29604 } 29605 29606 if (date) { 29607 date = this.dateUtil.clampDate(date, calendarCtrl.minDate, calendarCtrl.maxDate); 29608 29609 this.changeDisplayDate(date).then(function() { 29610 calendarCtrl.focus(date); 29611 }); 29612 } 29613 } 29614 }; 29615})(); 29616 29617})(); 29618(function(){ 29619"use strict"; 29620 29621(function() { 29622 'use strict'; 29623 29624 mdCalendarMonthBodyDirective.$inject = ["$compile", "$$mdSvgRegistry"]; 29625 CalendarMonthBodyCtrl.$inject = ["$element", "$$mdDateUtil", "$mdDateLocale"]; 29626 angular.module('material.components.datepicker') 29627 .directive('mdCalendarMonthBody', mdCalendarMonthBodyDirective); 29628 29629 /** 29630 * Private directive consumed by md-calendar-month. Having this directive lets the calender use 29631 * md-virtual-repeat and also cleanly separates the month DOM construction functions from 29632 * the rest of the calendar controller logic. 29633 * @ngInject 29634 */ 29635 function mdCalendarMonthBodyDirective($compile, $$mdSvgRegistry) { 29636 var ARROW_ICON = $compile('<md-icon md-svg-src="' + 29637 $$mdSvgRegistry.mdTabsArrow + '"></md-icon>')({})[0]; 29638 29639 return { 29640 require: ['^^mdCalendar', '^^mdCalendarMonth', 'mdCalendarMonthBody'], 29641 scope: { offset: '=mdMonthOffset' }, 29642 controller: CalendarMonthBodyCtrl, 29643 controllerAs: 'mdMonthBodyCtrl', 29644 bindToController: true, 29645 link: function(scope, element, attrs, controllers) { 29646 var calendarCtrl = controllers[0]; 29647 var monthCtrl = controllers[1]; 29648 var monthBodyCtrl = controllers[2]; 29649 29650 monthBodyCtrl.calendarCtrl = calendarCtrl; 29651 monthBodyCtrl.monthCtrl = monthCtrl; 29652 monthBodyCtrl.arrowIcon = ARROW_ICON.cloneNode(true); 29653 29654 // The virtual-repeat re-uses the same DOM elements, so there are only a limited number 29655 // of repeated items that are linked, and then those elements have their bindings updated. 29656 // Since the months are not generated by bindings, we simply regenerate the entire thing 29657 // when the binding (offset) changes. 29658 scope.$watch(function() { return monthBodyCtrl.offset; }, function(offset) { 29659 if (angular.isNumber(offset)) { 29660 monthBodyCtrl.generateContent(); 29661 } 29662 }); 29663 } 29664 }; 29665 } 29666 29667 /** 29668 * Controller for a single calendar month. 29669 * @ngInject @constructor 29670 */ 29671 function CalendarMonthBodyCtrl($element, $$mdDateUtil, $mdDateLocale) { 29672 /** @final {!angular.JQLite} */ 29673 this.$element = $element; 29674 29675 /** @final */ 29676 this.dateUtil = $$mdDateUtil; 29677 29678 /** @final */ 29679 this.dateLocale = $mdDateLocale; 29680 29681 /** @type {Object} Reference to the month view. */ 29682 this.monthCtrl = null; 29683 29684 /** @type {Object} Reference to the calendar. */ 29685 this.calendarCtrl = null; 29686 29687 /** 29688 * Number of months from the start of the month "items" that the currently rendered month 29689 * occurs. Set via angular data binding. 29690 * @type {number} 29691 */ 29692 this.offset = null; 29693 29694 /** 29695 * Date cell to focus after appending the month to the document. 29696 * @type {HTMLElement} 29697 */ 29698 this.focusAfterAppend = null; 29699 } 29700 29701 /** Generate and append the content for this month to the directive element. */ 29702 CalendarMonthBodyCtrl.prototype.generateContent = function() { 29703 var date = this.dateUtil.incrementMonths(this.calendarCtrl.firstRenderableDate, this.offset); 29704 29705 this.$element 29706 .empty() 29707 .append(this.buildCalendarForMonth(date)); 29708 29709 if (this.focusAfterAppend) { 29710 this.focusAfterAppend.classList.add(this.calendarCtrl.FOCUSED_DATE_CLASS); 29711 this.focusAfterAppend.focus(); 29712 this.focusAfterAppend = null; 29713 } 29714 }; 29715 29716 /** 29717 * Creates a single cell to contain a date in the calendar with all appropriate 29718 * attributes and classes added. If a date is given, the cell content will be set 29719 * based on the date. 29720 * @param {Date=} opt_date 29721 * @returns {HTMLElement} 29722 */ 29723 CalendarMonthBodyCtrl.prototype.buildDateCell = function(opt_date) { 29724 var monthCtrl = this.monthCtrl; 29725 var calendarCtrl = this.calendarCtrl; 29726 29727 // TODO(jelbourn): cloneNode is likely a faster way of doing this. 29728 var cell = document.createElement('td'); 29729 cell.tabIndex = -1; 29730 cell.classList.add('md-calendar-date'); 29731 cell.setAttribute('role', 'gridcell'); 29732 29733 if (opt_date) { 29734 cell.setAttribute('tabindex', '-1'); 29735 cell.setAttribute('aria-label', this.dateLocale.longDateFormatter(opt_date)); 29736 cell.id = calendarCtrl.getDateId(opt_date, 'month'); 29737 29738 // Use `data-timestamp` attribute because IE10 does not support the `dataset` property. 29739 cell.setAttribute('data-timestamp', opt_date.getTime()); 29740 29741 // TODO(jelourn): Doing these comparisons for class addition during generation might be slow. 29742 // It may be better to finish the construction and then query the node and add the class. 29743 if (this.dateUtil.isSameDay(opt_date, calendarCtrl.today)) { 29744 cell.classList.add(calendarCtrl.TODAY_CLASS); 29745 } 29746 29747 if (this.dateUtil.isValidDate(calendarCtrl.selectedDate) && 29748 this.dateUtil.isSameDay(opt_date, calendarCtrl.selectedDate)) { 29749 cell.classList.add(calendarCtrl.SELECTED_DATE_CLASS); 29750 cell.setAttribute('aria-selected', 'true'); 29751 } 29752 29753 var cellText = this.dateLocale.dates[opt_date.getDate()]; 29754 29755 if (this.isDateEnabled(opt_date)) { 29756 // Add a indicator for select, hover, and focus states. 29757 var selectionIndicator = document.createElement('span'); 29758 selectionIndicator.classList.add('md-calendar-date-selection-indicator'); 29759 selectionIndicator.textContent = cellText; 29760 cell.appendChild(selectionIndicator); 29761 cell.addEventListener('click', monthCtrl.cellClickHandler); 29762 29763 if (calendarCtrl.displayDate && this.dateUtil.isSameDay(opt_date, calendarCtrl.displayDate)) { 29764 this.focusAfterAppend = cell; 29765 } 29766 } else { 29767 cell.classList.add('md-calendar-date-disabled'); 29768 cell.textContent = cellText; 29769 } 29770 } 29771 29772 return cell; 29773 }; 29774 29775 /** 29776 * Check whether date is in range and enabled 29777 * @param {Date=} opt_date 29778 * @return {boolean} Whether the date is enabled. 29779 */ 29780 CalendarMonthBodyCtrl.prototype.isDateEnabled = function(opt_date) { 29781 return this.dateUtil.isDateWithinRange(opt_date, 29782 this.calendarCtrl.minDate, this.calendarCtrl.maxDate) && 29783 (!angular.isFunction(this.calendarCtrl.dateFilter) 29784 || this.calendarCtrl.dateFilter(opt_date)); 29785 }; 29786 29787 /** 29788 * Builds a `tr` element for the calendar grid. 29789 * @param rowNumber The week number within the month. 29790 * @returns {HTMLElement} 29791 */ 29792 CalendarMonthBodyCtrl.prototype.buildDateRow = function(rowNumber) { 29793 var row = document.createElement('tr'); 29794 row.setAttribute('role', 'row'); 29795 29796 // Because of an NVDA bug (with Firefox), the row needs an aria-label in order 29797 // to prevent the entire row being read aloud when the user moves between rows. 29798 // See http://community.nvda-project.org/ticket/4643. 29799 row.setAttribute('aria-label', this.dateLocale.weekNumberFormatter(rowNumber)); 29800 29801 return row; 29802 }; 29803 29804 /** 29805 * Builds the <tbody> content for the given date's month. 29806 * @param {Date=} opt_dateInMonth 29807 * @returns {DocumentFragment} A document fragment containing the <tr> elements. 29808 */ 29809 CalendarMonthBodyCtrl.prototype.buildCalendarForMonth = function(opt_dateInMonth) { 29810 var date = this.dateUtil.isValidDate(opt_dateInMonth) ? opt_dateInMonth : new Date(); 29811 29812 var firstDayOfMonth = this.dateUtil.getFirstDateOfMonth(date); 29813 var firstDayOfTheWeek = this.getLocaleDay_(firstDayOfMonth); 29814 var numberOfDaysInMonth = this.dateUtil.getNumberOfDaysInMonth(date); 29815 29816 // Store rows for the month in a document fragment so that we can append them all at once. 29817 var monthBody = document.createDocumentFragment(); 29818 29819 var rowNumber = 1; 29820 var row = this.buildDateRow(rowNumber); 29821 monthBody.appendChild(row); 29822 29823 // If this is the final month in the list of items, only the first week should render, 29824 // so we should return immediately after the first row is complete and has been 29825 // attached to the body. 29826 var isFinalMonth = this.offset === this.monthCtrl.items.length - 1; 29827 29828 // Add a label for the month. If the month starts on a Sun/Mon/Tues, the month label 29829 // goes on a row above the first of the month. Otherwise, the month label takes up the first 29830 // two cells of the first row. 29831 var blankCellOffset = 0; 29832 var monthLabelCell = document.createElement('td'); 29833 var monthLabelCellContent = document.createElement('span'); 29834 29835 monthLabelCellContent.textContent = this.dateLocale.monthHeaderFormatter(date); 29836 monthLabelCell.appendChild(monthLabelCellContent); 29837 monthLabelCell.classList.add('md-calendar-month-label'); 29838 // If the entire month is after the max date, render the label as a disabled state. 29839 if (this.calendarCtrl.maxDate && firstDayOfMonth > this.calendarCtrl.maxDate) { 29840 monthLabelCell.classList.add('md-calendar-month-label-disabled'); 29841 } else { 29842 monthLabelCell.addEventListener('click', this.monthCtrl.headerClickHandler); 29843 monthLabelCell.setAttribute('data-timestamp', firstDayOfMonth.getTime()); 29844 monthLabelCell.setAttribute('aria-label', this.dateLocale.monthFormatter(date)); 29845 monthLabelCell.appendChild(this.arrowIcon.cloneNode(true)); 29846 } 29847 29848 if (firstDayOfTheWeek <= 2) { 29849 monthLabelCell.setAttribute('colspan', '7'); 29850 29851 var monthLabelRow = this.buildDateRow(); 29852 monthLabelRow.appendChild(monthLabelCell); 29853 monthBody.insertBefore(monthLabelRow, row); 29854 29855 if (isFinalMonth) { 29856 return monthBody; 29857 } 29858 } else { 29859 blankCellOffset = 3; 29860 monthLabelCell.setAttribute('colspan', '3'); 29861 row.appendChild(monthLabelCell); 29862 } 29863 29864 // Add a blank cell for each day of the week that occurs before the first of the month. 29865 // For example, if the first day of the month is a Tuesday, add blank cells for Sun and Mon. 29866 // The blankCellOffset is needed in cases where the first N cells are used by the month label. 29867 for (var i = blankCellOffset; i < firstDayOfTheWeek; i++) { 29868 row.appendChild(this.buildDateCell()); 29869 } 29870 29871 // Add a cell for each day of the month, keeping track of the day of the week so that 29872 // we know when to start a new row. 29873 var dayOfWeek = firstDayOfTheWeek; 29874 var iterationDate = firstDayOfMonth; 29875 for (var d = 1; d <= numberOfDaysInMonth; d++) { 29876 // If we've reached the end of the week, start a new row. 29877 if (dayOfWeek === 7) { 29878 // We've finished the first row, so we're done if this is the final month. 29879 if (isFinalMonth) { 29880 return monthBody; 29881 } 29882 dayOfWeek = 0; 29883 rowNumber++; 29884 row = this.buildDateRow(rowNumber); 29885 monthBody.appendChild(row); 29886 } 29887 29888 iterationDate.setDate(d); 29889 var cell = this.buildDateCell(iterationDate); 29890 row.appendChild(cell); 29891 29892 dayOfWeek++; 29893 } 29894 29895 // Ensure that the last row of the month has 7 cells. 29896 while (row.childNodes.length < 7) { 29897 row.appendChild(this.buildDateCell()); 29898 } 29899 29900 // Ensure that all months have 6 rows. This is necessary for now because the virtual-repeat 29901 // requires that all items have exactly the same height. 29902 while (monthBody.childNodes.length < 6) { 29903 var whitespaceRow = this.buildDateRow(); 29904 for (var j = 0; j < 7; j++) { 29905 whitespaceRow.appendChild(this.buildDateCell()); 29906 } 29907 monthBody.appendChild(whitespaceRow); 29908 } 29909 29910 return monthBody; 29911 }; 29912 29913 /** 29914 * Gets the day-of-the-week index for a date for the current locale. 29915 * @private 29916 * @param {Date} date 29917 * @returns {number} The column index of the date in the calendar. 29918 */ 29919 CalendarMonthBodyCtrl.prototype.getLocaleDay_ = function(date) { 29920 return (date.getDay() + (7 - this.dateLocale.firstDayOfWeek)) % 7; 29921 }; 29922})(); 29923 29924})(); 29925(function(){ 29926"use strict"; 29927 29928(function() { 29929 'use strict'; 29930 29931 CalendarYearCtrl.$inject = ["$element", "$scope", "$animate", "$q", "$$mdDateUtil"]; 29932 angular.module('material.components.datepicker') 29933 .directive('mdCalendarYear', calendarDirective); 29934 29935 /** 29936 * Height of one calendar year tbody. This must be made known to the virtual-repeat and is 29937 * subsequently used for scrolling to specific years. 29938 */ 29939 var TBODY_HEIGHT = 88; 29940 29941 /** Private component, representing a list of years in the calendar. */ 29942 function calendarDirective() { 29943 return { 29944 template: 29945 '<div class="md-calendar-scroll-mask">' + 29946 '<md-virtual-repeat-container class="md-calendar-scroll-container">' + 29947 '<table role="grid" tabindex="0" class="md-calendar" aria-readonly="true">' + 29948 '<tbody ' + 29949 'md-calendar-year-body ' + 29950 'role="rowgroup" ' + 29951 'md-virtual-repeat="i in yearCtrl.items" ' + 29952 'md-year-offset="$index" class="md-calendar-year" ' + 29953 'md-start-index="yearCtrl.getFocusedYearIndex()" ' + 29954 'md-item-size="' + TBODY_HEIGHT + '">' + 29955 // The <tr> ensures that the <tbody> will have the proper 29956 // height, even though it may be empty. 29957 '<tr aria-hidden="true" md-force-height="\'' + TBODY_HEIGHT + 'px\'"></tr>' + 29958 '</tbody>' + 29959 '</table>' + 29960 '</md-virtual-repeat-container>' + 29961 '</div>', 29962 require: ['^^mdCalendar', 'mdCalendarYear'], 29963 controller: CalendarYearCtrl, 29964 controllerAs: 'yearCtrl', 29965 bindToController: true, 29966 link: function(scope, element, attrs, controllers) { 29967 var calendarCtrl = controllers[0]; 29968 var yearCtrl = controllers[1]; 29969 yearCtrl.initialize(calendarCtrl); 29970 } 29971 }; 29972 } 29973 29974 /** 29975 * Controller for the mdCalendar component. 29976 * @ngInject @constructor 29977 */ 29978 function CalendarYearCtrl($element, $scope, $animate, $q, $$mdDateUtil) { 29979 29980 /** @final {!angular.JQLite} */ 29981 this.$element = $element; 29982 29983 /** @final {!angular.Scope} */ 29984 this.$scope = $scope; 29985 29986 /** @final {!angular.$animate} */ 29987 this.$animate = $animate; 29988 29989 /** @final {!angular.$q} */ 29990 this.$q = $q; 29991 29992 /** @final */ 29993 this.dateUtil = $$mdDateUtil; 29994 29995 /** @final {HTMLElement} */ 29996 this.calendarScroller = $element[0].querySelector('.md-virtual-repeat-scroller'); 29997 29998 /** @type {boolean} */ 29999 this.isInitialized = false; 30000 30001 /** @type {boolean} */ 30002 this.isMonthTransitionInProgress = false; 30003 30004 var self = this; 30005 30006 /** 30007 * Handles a click event on a date cell. 30008 * Created here so that every cell can use the same function instance. 30009 * @this {HTMLTableCellElement} The cell that was clicked. 30010 */ 30011 this.cellClickHandler = function() { 30012 self.calendarCtrl.setCurrentView('month', $$mdDateUtil.getTimestampFromNode(this)); 30013 }; 30014 } 30015 30016 /** 30017 * Initialize the controller by saving a reference to the calendar and 30018 * setting up the object that will be iterated by the virtual repeater. 30019 */ 30020 CalendarYearCtrl.prototype.initialize = function(calendarCtrl) { 30021 /** 30022 * Dummy array-like object for virtual-repeat to iterate over. The length is the total 30023 * number of years that can be viewed. We add 1 extra in order to include the current year. 30024 */ 30025 this.items = { 30026 length: this.dateUtil.getYearDistance( 30027 calendarCtrl.firstRenderableDate, 30028 calendarCtrl.lastRenderableDate 30029 ) + 1 30030 }; 30031 30032 this.calendarCtrl = calendarCtrl; 30033 this.attachScopeListeners(); 30034 calendarCtrl.updateVirtualRepeat(); 30035 30036 // Fire the initial render, since we might have missed it the first time it fired. 30037 calendarCtrl.ngModelCtrl && calendarCtrl.ngModelCtrl.$render(); 30038 }; 30039 30040 /** 30041 * Gets the "index" of the currently selected date as it would be in the virtual-repeat. 30042 * @returns {number} 30043 */ 30044 CalendarYearCtrl.prototype.getFocusedYearIndex = function() { 30045 var calendarCtrl = this.calendarCtrl; 30046 30047 return this.dateUtil.getYearDistance( 30048 calendarCtrl.firstRenderableDate, 30049 calendarCtrl.displayDate || calendarCtrl.selectedDate || calendarCtrl.today 30050 ); 30051 }; 30052 30053 /** 30054 * Change the date that is highlighted in the calendar. 30055 * @param {Date} date 30056 */ 30057 CalendarYearCtrl.prototype.changeDate = function(date) { 30058 // Initialization is deferred until this function is called because we want to reflect 30059 // the starting value of ngModel. 30060 if (!this.isInitialized) { 30061 this.calendarCtrl.hideVerticalScrollbar(this); 30062 this.isInitialized = true; 30063 return this.$q.when(); 30064 } else if (this.dateUtil.isValidDate(date) && !this.isMonthTransitionInProgress) { 30065 var self = this; 30066 var animationPromise = this.animateDateChange(date); 30067 30068 self.isMonthTransitionInProgress = true; 30069 self.calendarCtrl.displayDate = date; 30070 30071 return animationPromise.then(function() { 30072 self.isMonthTransitionInProgress = false; 30073 }); 30074 } 30075 }; 30076 30077 /** 30078 * Animates the transition from the calendar's current month to the given month. 30079 * @param {Date} date 30080 * @returns {angular.$q.Promise} The animation promise. 30081 */ 30082 CalendarYearCtrl.prototype.animateDateChange = function(date) { 30083 if (this.dateUtil.isValidDate(date)) { 30084 var monthDistance = this.dateUtil.getYearDistance(this.calendarCtrl.firstRenderableDate, date); 30085 this.calendarScroller.scrollTop = monthDistance * TBODY_HEIGHT; 30086 } 30087 30088 return this.$q.when(); 30089 }; 30090 30091 /** 30092 * Handles the year-view-specific keyboard interactions. 30093 * @param {Object} event Scope event object passed by the calendar. 30094 * @param {String} action Action, corresponding to the key that was pressed. 30095 */ 30096 CalendarYearCtrl.prototype.handleKeyEvent = function(event, action) { 30097 var calendarCtrl = this.calendarCtrl; 30098 var displayDate = calendarCtrl.displayDate; 30099 30100 if (action === 'select') { 30101 this.changeDate(displayDate).then(function() { 30102 calendarCtrl.setCurrentView('month', displayDate); 30103 calendarCtrl.focus(displayDate); 30104 }); 30105 } else { 30106 var date = null; 30107 var dateUtil = this.dateUtil; 30108 30109 switch (action) { 30110 case 'move-right': date = dateUtil.incrementMonths(displayDate, 1); break; 30111 case 'move-left': date = dateUtil.incrementMonths(displayDate, -1); break; 30112 30113 case 'move-row-down': date = dateUtil.incrementMonths(displayDate, 6); break; 30114 case 'move-row-up': date = dateUtil.incrementMonths(displayDate, -6); break; 30115 } 30116 30117 if (date) { 30118 var min = calendarCtrl.minDate ? dateUtil.getFirstDateOfMonth(calendarCtrl.minDate) : null; 30119 var max = calendarCtrl.maxDate ? dateUtil.getFirstDateOfMonth(calendarCtrl.maxDate) : null; 30120 date = dateUtil.getFirstDateOfMonth(this.dateUtil.clampDate(date, min, max)); 30121 30122 this.changeDate(date).then(function() { 30123 calendarCtrl.focus(date); 30124 }); 30125 } 30126 } 30127 }; 30128 30129 /** 30130 * Attaches listeners for the scope events that are broadcast by the calendar. 30131 */ 30132 CalendarYearCtrl.prototype.attachScopeListeners = function() { 30133 var self = this; 30134 30135 self.$scope.$on('md-calendar-parent-changed', function(event, value) { 30136 self.changeDate(value); 30137 }); 30138 30139 self.$scope.$on('md-calendar-parent-action', angular.bind(self, self.handleKeyEvent)); 30140 }; 30141})(); 30142 30143})(); 30144(function(){ 30145"use strict"; 30146 30147(function() { 30148 'use strict'; 30149 30150 CalendarYearBodyCtrl.$inject = ["$element", "$$mdDateUtil", "$mdDateLocale"]; 30151 angular.module('material.components.datepicker') 30152 .directive('mdCalendarYearBody', mdCalendarYearDirective); 30153 30154 /** 30155 * Private component, consumed by the md-calendar-year, which separates the DOM construction logic 30156 * and allows for the year view to use md-virtual-repeat. 30157 */ 30158 function mdCalendarYearDirective() { 30159 return { 30160 require: ['^^mdCalendar', '^^mdCalendarYear', 'mdCalendarYearBody'], 30161 scope: { offset: '=mdYearOffset' }, 30162 controller: CalendarYearBodyCtrl, 30163 controllerAs: 'mdYearBodyCtrl', 30164 bindToController: true, 30165 link: function(scope, element, attrs, controllers) { 30166 var calendarCtrl = controllers[0]; 30167 var yearCtrl = controllers[1]; 30168 var yearBodyCtrl = controllers[2]; 30169 30170 yearBodyCtrl.calendarCtrl = calendarCtrl; 30171 yearBodyCtrl.yearCtrl = yearCtrl; 30172 30173 scope.$watch(function() { return yearBodyCtrl.offset; }, function(offset) { 30174 if (angular.isNumber(offset)) { 30175 yearBodyCtrl.generateContent(); 30176 } 30177 }); 30178 } 30179 }; 30180 } 30181 30182 /** 30183 * Controller for a single year. 30184 * @ngInject @constructor 30185 */ 30186 function CalendarYearBodyCtrl($element, $$mdDateUtil, $mdDateLocale) { 30187 /** @final {!angular.JQLite} */ 30188 this.$element = $element; 30189 30190 /** @final */ 30191 this.dateUtil = $$mdDateUtil; 30192 30193 /** @final */ 30194 this.dateLocale = $mdDateLocale; 30195 30196 /** @type {Object} Reference to the calendar. */ 30197 this.calendarCtrl = null; 30198 30199 /** @type {Object} Reference to the year view. */ 30200 this.yearCtrl = null; 30201 30202 /** 30203 * Number of months from the start of the month "items" that the currently rendered month 30204 * occurs. Set via angular data binding. 30205 * @type {number} 30206 */ 30207 this.offset = null; 30208 30209 /** 30210 * Date cell to focus after appending the month to the document. 30211 * @type {HTMLElement} 30212 */ 30213 this.focusAfterAppend = null; 30214 } 30215 30216 /** Generate and append the content for this year to the directive element. */ 30217 CalendarYearBodyCtrl.prototype.generateContent = function() { 30218 var date = this.dateUtil.incrementYears(this.calendarCtrl.firstRenderableDate, this.offset); 30219 30220 this.$element 30221 .empty() 30222 .append(this.buildCalendarForYear(date)); 30223 30224 if (this.focusAfterAppend) { 30225 this.focusAfterAppend.classList.add(this.calendarCtrl.FOCUSED_DATE_CLASS); 30226 this.focusAfterAppend.focus(); 30227 this.focusAfterAppend = null; 30228 } 30229 }; 30230 30231 /** 30232 * Creates a single cell to contain a year in the calendar. 30233 * @param {number} opt_year Four-digit year. 30234 * @param {number} opt_month Zero-indexed month. 30235 * @returns {HTMLElement} 30236 */ 30237 CalendarYearBodyCtrl.prototype.buildMonthCell = function(year, month) { 30238 var calendarCtrl = this.calendarCtrl; 30239 var yearCtrl = this.yearCtrl; 30240 var cell = this.buildBlankCell(); 30241 30242 // Represent this month/year as a date. 30243 var firstOfMonth = new Date(year, month, 1); 30244 cell.setAttribute('aria-label', this.dateLocale.monthFormatter(firstOfMonth)); 30245 cell.id = calendarCtrl.getDateId(firstOfMonth, 'year'); 30246 30247 // Use `data-timestamp` attribute because IE10 does not support the `dataset` property. 30248 cell.setAttribute('data-timestamp', firstOfMonth.getTime()); 30249 30250 if (this.dateUtil.isSameMonthAndYear(firstOfMonth, calendarCtrl.today)) { 30251 cell.classList.add(calendarCtrl.TODAY_CLASS); 30252 } 30253 30254 if (this.dateUtil.isValidDate(calendarCtrl.selectedDate) && 30255 this.dateUtil.isSameMonthAndYear(firstOfMonth, calendarCtrl.selectedDate)) { 30256 cell.classList.add(calendarCtrl.SELECTED_DATE_CLASS); 30257 cell.setAttribute('aria-selected', 'true'); 30258 } 30259 30260 var cellText = this.dateLocale.shortMonths[month]; 30261 30262 if (this.dateUtil.isMonthWithinRange(firstOfMonth, 30263 calendarCtrl.minDate, calendarCtrl.maxDate)) { 30264 var selectionIndicator = document.createElement('span'); 30265 selectionIndicator.classList.add('md-calendar-date-selection-indicator'); 30266 selectionIndicator.textContent = cellText; 30267 cell.appendChild(selectionIndicator); 30268 cell.addEventListener('click', yearCtrl.cellClickHandler); 30269 30270 if (calendarCtrl.displayDate && this.dateUtil.isSameMonthAndYear(firstOfMonth, calendarCtrl.displayDate)) { 30271 this.focusAfterAppend = cell; 30272 } 30273 } else { 30274 cell.classList.add('md-calendar-date-disabled'); 30275 cell.textContent = cellText; 30276 } 30277 30278 return cell; 30279 }; 30280 30281 /** 30282 * Builds a blank cell. 30283 * @return {HTMLTableCellElement} 30284 */ 30285 CalendarYearBodyCtrl.prototype.buildBlankCell = function() { 30286 var cell = document.createElement('td'); 30287 cell.tabIndex = -1; 30288 cell.classList.add('md-calendar-date'); 30289 cell.setAttribute('role', 'gridcell'); 30290 30291 cell.setAttribute('tabindex', '-1'); 30292 return cell; 30293 }; 30294 30295 /** 30296 * Builds the <tbody> content for the given year. 30297 * @param {Date} date Date for which the content should be built. 30298 * @returns {DocumentFragment} A document fragment containing the months within the year. 30299 */ 30300 CalendarYearBodyCtrl.prototype.buildCalendarForYear = function(date) { 30301 // Store rows for the month in a document fragment so that we can append them all at once. 30302 var year = date.getFullYear(); 30303 var yearBody = document.createDocumentFragment(); 30304 30305 var monthCell, i; 30306 // First row contains label and Jan-Jun. 30307 var firstRow = document.createElement('tr'); 30308 var labelCell = document.createElement('td'); 30309 labelCell.className = 'md-calendar-month-label'; 30310 labelCell.textContent = year; 30311 firstRow.appendChild(labelCell); 30312 30313 for (i = 0; i < 6; i++) { 30314 firstRow.appendChild(this.buildMonthCell(year, i)); 30315 } 30316 yearBody.appendChild(firstRow); 30317 30318 // Second row contains a blank cell and Jul-Dec. 30319 var secondRow = document.createElement('tr'); 30320 secondRow.appendChild(this.buildBlankCell()); 30321 for (i = 6; i < 12; i++) { 30322 secondRow.appendChild(this.buildMonthCell(year, i)); 30323 } 30324 yearBody.appendChild(secondRow); 30325 30326 return yearBody; 30327 }; 30328})(); 30329 30330})(); 30331(function(){ 30332"use strict"; 30333 30334(function() { 30335 'use strict'; 30336 30337 /** 30338 * @ngdoc service 30339 * @name $mdDateLocaleProvider 30340 * @module material.components.datepicker 30341 * 30342 * @description 30343 * The `$mdDateLocaleProvider` is the provider that creates the `$mdDateLocale` service. 30344 * This provider that allows the user to specify messages, formatters, and parsers for date 30345 * internationalization. The `$mdDateLocale` service itself is consumed by AngularJS Material 30346 * components that deal with dates. 30347 * 30348 * @property {(Array<string>)=} months Array of month names (in order). 30349 * @property {(Array<string>)=} shortMonths Array of abbreviated month names. 30350 * @property {(Array<string>)=} days Array of the days of the week (in order). 30351 * @property {(Array<string>)=} shortDays Array of abbreviated days of the week. 30352 * @property {(Array<string>)=} dates Array of dates of the month. Only necessary for locales 30353 * using a numeral system other than [1, 2, 3...]. 30354 * @property {(Array<string>)=} firstDayOfWeek The first day of the week. Sunday = 0, Monday = 1, 30355 * etc. 30356 * @property {(function(string): Date)=} parseDate Function to parse a date object from a string. 30357 * @property {(function(Date, string): string)=} formatDate Function to format a date object to a 30358 * string. The datepicker directive also provides the time zone, if it was specified. 30359 * @property {(function(Date): string)=} monthHeaderFormatter Function that returns the label for 30360 * a month given a date. 30361 * @property {(function(Date): string)=} monthFormatter Function that returns the full name of a month 30362 * for a given date. 30363 * @property {(function(number): string)=} weekNumberFormatter Function that returns a label for 30364 * a week given the week number. 30365 * @property {(string)=} msgCalendar Translation of the label "Calendar" for the current locale. 30366 * @property {(string)=} msgOpenCalendar Translation of the button label "Open calendar" for the 30367 * current locale. 30368 * @property {Date=}
30368 firstRenderableDate The date from which the datepicker calendar will begin 30369 * rendering. Note that this will be ignored if a minimum date is set. Defaults to January 1st 1880. 30370 * @property {Date=} lastRenderableDate The last date that will be rendered by the datepicker 30371 * calendar. Note that this will be ignored if a maximum date is set. Defaults to January 1st 2130. 30372 * 30373 * @usage 30374 * <hljs lang="js"> 30375 * myAppModule.config(function($mdDateLocaleProvider) { 30376 * 30377 * // Example of a French localization. 30378 * $mdDateLocaleProvider.months = ['janvier', 'février', 'mars', ...]; 30379 * $mdDateLocaleProvider.shortMonths = ['janv', 'févr', 'mars', ...]; 30380 * $mdDateLocaleProvider.days = ['dimanche', 'lundi', 'mardi', ...]; 30381 * $mdDateLocaleProvider.shortDays = ['Di', 'Lu', 'Ma', ...]; 30382 * 30383 * // Can change week display to start on Monday. 30384 * $mdDateLocaleProvider.firstDayOfWeek = 1; 30385 * 30386 * // Optional. 30387 * $mdDateLocaleProvider.dates = [1, 2, 3, 4, 5, 6, ...]; 30388 * 30389 * // Example uses moment.js to parse and format dates. 30390 * $mdDateLocaleProvider.parseDate = function(dateString) { 30391 * var m = moment(dateString, 'L', true); 30392 * return m.isValid() ? m.toDate() : new Date(NaN); 30393 * }; 30394 * 30395 * $mdDateLocaleProvider.formatDate = function(date) { 30396 * var m = moment(date); 30397 * return m.isValid() ? m.format('L') : ''; 30398 * }; 30399 * 30400 * $mdDateLocaleProvider.monthHeaderFormatter = function(date) { 30401 * return myShortMonths[date.getMonth()] + ' ' + date.getFullYear(); 30402 * }; 30403 * 30404 * // In addition to date display, date components also need localized messages 30405 * // for aria-labels for screen-reader users. 30406 * 30407 * $mdDateLocaleProvider.weekNumberFormatter = function(weekNumber) { 30408 * return 'Semaine ' + weekNumber; 30409 * }; 30410 * 30411 * $mdDateLocaleProvider.msgCalendar = 'Calendrier'; 30412 * $mdDateLocaleProvider.msgOpenCalendar = 'Ouvrir le calendrier'; 30413 * 30414 * // You can also set when your calendar begins and ends. 30415 * $mdDateLocaleProvider.firstRenderableDate = new Date(1776, 6, 4); 30416 * $mdDateLocaleProvider.lastRenderableDate = new Date(2012, 11, 21); 30417 * }); 30418 * </hljs> 30419 * 30420 */ 30421 angular.module('material.components.datepicker').config(["$provide", function($provide) { 30422 // TODO(jelbourn): Assert provided values are correctly formatted. Need assertions. 30423 30424 /** @constructor */ 30425 function DateLocaleProvider() { 30426 /** Array of full month names. E.g., ['January', 'Febuary', ...] */ 30427 this.months = null; 30428 30429 /** Array of abbreviated month names. E.g., ['Jan', 'Feb', ...] */ 30430 this.shortMonths = null; 30431 30432 /** Array of full day of the week names. E.g., ['Monday', 'Tuesday', ...] */ 30433 this.days = null; 30434 30435 /** Array of abbreviated dat of the week names. E.g., ['M', 'T', ...] */ 30436 this.shortDays = null; 30437 30438 /** Array of dates of a month (1 - 31). Characters might be different in some locales. */ 30439 this.dates = null; 30440 30441 /** Index of the first day of the week. 0 = Sunday, 1 = Monday, etc. */ 30442 this.firstDayOfWeek = 0; 30443 30444 /** 30445 * Function that converts the date portion of a Date to a string. 30446 * @type {(function(Date): string)} 30447 */ 30448 this.formatDate = null; 30449 30450 /** 30451 * Function that converts a date string to a Date object (the date portion) 30452 * @type {function(string): Date} 30453 */ 30454 this.parseDate = null; 30455 30456 /** 30457 * Function that formats a Date into a month header string. 30458 * @type {function(Date): string} 30459 */ 30460 this.monthHeaderFormatter = null; 30461 30462 /** 30463 * Function that formats a week number into a label for the week. 30464 * @type {function(number): string} 30465 */ 30466 this.weekNumberFormatter = null; 30467 30468 /** 30469 * Function that formats a date into a long aria-label that is read 30470 * when the focused date changes. 30471 * @type {function(Date): string} 30472 */ 30473 this.longDateFormatter = null; 30474 30475 /** 30476 * ARIA label for the calendar "dialog" used in the datepicker. 30477 * @type {string} 30478 */ 30479 this.msgCalendar = ''; 30480 30481 /** 30482 * ARIA label for the datepicker's "Open calendar" buttons. 30483 * @type {string} 30484 */ 30485 this.msgOpenCalendar = ''; 30486 } 30487 30488 /** 30489 * Factory function that returns an instance of the dateLocale service. 30490 * @ngInject 30491 * @param $locale 30492 * @returns {DateLocale} 30493 */ 30494 DateLocaleProvider.prototype.$get = function($locale, $filter) { 30495 /** 30496 * Default date-to-string formatting function. 30497 * @param {!Date} date 30498 * @param {string=} timezone 30499 * @returns {string} 30500 */ 30501 function defaultFormatDate(date, timezone) { 30502 if (!date) { 30503 return ''; 30504 } 30505 30506 // All of the dates created through ng-material *should* be set to midnight. 30507 // If we encounter a date where the localeTime shows at 11pm instead of midnight, 30508 // we have run into an issue with DST where we need to increment the hour by one: 30509 // var d = new Date(1992, 9, 8, 0, 0, 0);
vendor: 53,762 bytes, lines 30510-31914
30510 // d.toLocaleString(); // == "10/7/1992, 11:00:00 PM" 30511 var localeTime = date.toLocaleTimeString(); 30512 var formatDate = date; 30513 if (date.getHours() === 0 && 30514 (localeTime.indexOf('11:') !== -1 || localeTime.indexOf('23:') !== -1)) { 30515 formatDate = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 1, 0, 0); 30516 } 30517 30518 return $filter('date')(formatDate, 'dd/MM/yyyy', timezone); 30519 } 30520 30521 /** 30522 * Default string-to-date parsing function. 30523 * @param {string} dateString 30524 * @returns {!Date} 30525 */ 30526 function defaultParseDate(dateString) { 30527 return new Date(dateString); 30528 } 30529 30530 /** 30531 * Default function to determine whether a string makes sense to be 30532 * parsed to a Date object. 30533 * 30534 * This is very permissive and is just a basic sanity check to ensure that 30535 * things like single integers aren't able to be parsed into dates. 30536 * @param {string} dateString 30537 * @returns {boolean} 30538 */ 30539 function defaultIsDateComplete(dateString) { 30540 dateString = dateString.trim(); 30541 30542 // Looks for three chunks of content (either numbers or text) separated 30543 // by delimiters. 30544 var re = /^(([a-zA-Z]{3,}|[0-9]{1,4})([ \.,]+|[\/\-])){2}([a-zA-Z]{3,}|[0-9]{1,4})$/; 30545 return re.test(dateString); 30546 } 30547 30548 /** 30549 * Default date-to-string formatter to get a month header. 30550 * @param {!Date} date 30551 * @returns {string} 30552 */ 30553 function defaultMonthHeaderFormatter(date) { 30554 return service.shortMonths[date.getMonth()] + ' ' + date.getFullYear(); 30555 } 30556 30557 /** 30558 * Default formatter for a month. 30559 * @param {!Date} date 30560 * @returns {string} 30561 */ 30562 function defaultMonthFormatter(date) { 30563 return service.months[date.getMonth()] + ' ' + date.getFullYear(); 30564 } 30565 30566 /** 30567 * Default week number formatter. 30568 * @param number 30569 * @returns {string} 30570 */ 30571 function defaultWeekNumberFormatter(number) { 30572 return 'Week ' + number; 30573 } 30574 30575 /** 30576 * Default formatter for date cell aria-labels. 30577 * @param {!Date} date 30578 * @returns {string} 30579 */ 30580 function defaultLongDateFormatter(date) { 30581 // Example: 'Thursday June 18 2015' 30582 return [ 30583 service.days[date.getDay()], 30584 service.months[date.getMonth()], 30585 service.dates[date.getDate()], 30586 date.getFullYear() 30587 ].join(' '); 30588 } 30589 30590 // The default "short" day strings are the first character of each day, 30591 // e.g., "Monday" => "M". 30592 var defaultShortDays = $locale.DATETIME_FORMATS.SHORTDAY.map(function(day) { 30593 return day.substring(0, 1); 30594 }); 30595 30596 // The default dates are simply the numbers 1 through 31. 30597 var defaultDates = Array(32); 30598 for (var i = 1; i <= 31; i++) { 30599 defaultDates[i] = i; 30600 } 30601 30602 // Default ARIA messages are in English (US). 30603 var defaultMsgCalendar = 'Calendar'; 30604 var defaultMsgOpenCalendar = 'Open calendar'; 30605 30606 // Default start/end dates that are rendered in the calendar. 30607 var defaultFirstRenderableDate = new Date(1880, 0, 1); 30608 var defaultLastRendereableDate = new Date(defaultFirstRenderableDate.getFullYear() + 250, 0, 1); 30609 30610 var service = { 30611 months: this.months || $locale.DATETIME_FORMATS.MONTH, 30612 shortMonths: this.shortMonths || $locale.DATETIME_FORMATS.SHORTMONTH, 30613 days: this.days || $locale.DATETIME_FORMATS.DAY, 30614 shortDays: this.shortDays || defaultShortDays, 30615 dates: this.dates || defaultDates, 30616 firstDayOfWeek: this.firstDayOfWeek || 0, 30617 formatDate: this.formatDate || defaultFormatDate, 30618 parseDate: this.parseDate || defaultParseDate, 30619 isDateComplete: this.isDateComplete || defaultIsDateComplete, 30620 monthHeaderFormatter: this.monthHeaderFormatter || defaultMonthHeaderFormatter, 30621 monthFormatter: this.monthFormatter || defaultMonthFormatter, 30622 weekNumberFormatter: this.weekNumberFormatter || defaultWeekNumberFormatter, 30623 longDateFormatter: this.longDateFormatter || defaultLongDateFormatter, 30624 msgCalendar: this.msgCalendar || defaultMsgCalendar, 30625 msgOpenCalendar: this.msgOpenCalendar || defaultMsgOpenCalendar, 30626 firstRenderableDate: this.firstRenderableDate || defaultFirstRenderableDate, 30627 lastRenderableDate: this.lastRenderableDate || defaultLastRendereableDate 30628 }; 30629 30630 return service; 30631 }; 30632 DateLocaleProvider.prototype.$get.$inject = ["$locale", "$filter"]; 30633 30634 $provide.provider('$mdDateLocale', new DateLocaleProvider()); 30635 }]); 30636})(); 30637 30638})(); 30639(function(){ 30640"use strict"; 30641 30642(function() { 30643 'use strict'; 30644 30645 /** 30646 * Utility for performing date calculations to facilitate operation of the calendar and 30647 * datepicker. 30648 */ 30649 angular.module('material.components.datepicker').factory('$$mdDateUtil', function() { 30650 return { 30651 getFirstDateOfMonth: getFirstDateOfMonth, 30652 getNumberOfDaysInMonth: getNumberOfDaysInMonth, 30653 getDateInNextMonth: getDateInNextMonth, 30654 getDateInPreviousMonth: getDateInPreviousMonth, 30655 isInNextMonth: isInNextMonth, 30656 isInPreviousMonth: isInPreviousMonth, 30657 getDateMidpoint: getDateMidpoint, 30658 isSameMonthAndYear: isSameMonthAndYear, 30659 getWeekOfMonth: getWeekOfMonth, 30660 incrementDays: incrementDays, 30661 incrementMonths: incrementMonths, 30662 getLastDateOfMonth: getLastDateOfMonth, 30663 isSameDay: isSameDay, 30664 getMonthDistance: getMonthDistance, 30665 isValidDate: isValidDate, 30666 setDateTimeToMidnight: setDateTimeToMidnight, 30667 createDateAtMidnight: createDateAtMidnight, 30668 isDateWithinRange: isDateWithinRange, 30669 incrementYears: incrementYears, 30670 getYearDistance: getYearDistance, 30671 clampDate: clampDate, 30672 getTimestampFromNode: getTimestampFromNode, 30673 isMonthWithinRange: isMonthWithinRange 30674 }; 30675 30676 /** 30677 * Gets the first day of the month for the given date's month. 30678 * @param {Date} date 30679 * @returns {Date} 30680 */ 30681 function getFirstDateOfMonth(date) { 30682 return new Date(date.getFullYear(), date.getMonth(), 1); 30683 } 30684 30685 /** 30686 * Gets the number of days in the month for the given date's month. 30687 * @param date 30688 * @returns {number} 30689 */ 30690 function getNumberOfDaysInMonth(date) { 30691 return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate(); 30692 } 30693 30694 /** 30695 * Get an arbitrary date in the month after the given date's month. 30696 * @param date 30697 * @returns {Date} 30698 */ 30699 function getDateInNextMonth(date) { 30700 return new Date(date.getFullYear(), date.getMonth() + 1, 1); 30701 } 30702 30703 /** 30704 * Get an arbitrary date in the month before the given date's month. 30705 * @param date 30706 * @returns {Date} 30707 */ 30708 function getDateInPreviousMonth(date) { 30709 return new Date(date.getFullYear(), date.getMonth() - 1, 1); 30710 } 30711 30712 /** 30713 * Gets whether two dates have the same month and year. 30714 * @param {Date} d1 30715 * @param {Date} d2 30716 * @returns {boolean} 30717 */ 30718 function isSameMonthAndYear(d1, d2) { 30719 return d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth(); 30720 } 30721 30722 /** 30723 * Gets whether two dates are the same day (not not necesarily the same time). 30724 * @param {Date} d1 30725 * @param {Date} d2 30726 * @returns {boolean} 30727 */ 30728 function isSameDay(d1, d2) { 30729 return d1.getDate() == d2.getDate() && isSameMonthAndYear(d1, d2); 30730 } 30731 30732 /** 30733 * Gets whether a date is in the month immediately after some date. 30734 * @param {Date} startDate The date from which to compare. 30735 * @param {Date} endDate The date to check. 30736 * @returns {boolean} 30737 */ 30738 function isInNextMonth(startDate, endDate) { 30739 var nextMonth = getDateInNextMonth(startDate); 30740 return isSameMonthAndYear(nextMonth, endDate); 30741 } 30742 30743 /** 30744 * Gets whether a date is in the month immediately before some date. 30745 * @param {Date} startDate The date from which to compare. 30746 * @param {Date} endDate The date to check. 30747 * @returns {boolean} 30748 */ 30749 function isInPreviousMonth(startDate, endDate) { 30750 var previousMonth = getDateInPreviousMonth(startDate); 30751 return isSameMonthAndYear(endDate, previousMonth); 30752 } 30753 30754 /** 30755 * Gets the midpoint between two dates. 30756 * @param {Date} d1 30757 * @param {Date} d2 30758 * @returns {Date} 30759 */ 30760 function getDateMidpoint(d1, d2) { 30761 return createDateAtMidnight((d1.getTime() + d2.getTime()) / 2); 30762 } 30763 30764 /** 30765 * Gets the week of the month that a given date occurs in. 30766 * @param {Date} date 30767 * @returns {number} Index of the week of the month (zero-based). 30768 */ 30769 function getWeekOfMonth(date) { 30770 var firstDayOfMonth = getFirstDateOfMonth(date); 30771 return Math.floor((firstDayOfMonth.getDay() + date.getDate() - 1) / 7); 30772 } 30773 30774 /** 30775 * Gets a new date incremented by the given number of days. Number of days can be negative. 30776 * @param {Date} date 30777 * @param {number} numberOfDays 30778 * @returns {Date} 30779 */ 30780 function incrementDays(date, numberOfDays) { 30781 return new Date(date.getFullYear(), date.getMonth(), date.getDate() + numberOfDays); 30782 } 30783 30784 /** 30785 * Gets a new date incremented by the given number of months. Number of months can be negative. 30786 * If the date of the given month does not match the target month, the date will be set to the 30787 * last day of the month. 30788 * @param {Date} date 30789 * @param {number} numberOfMonths 30790 * @returns {Date} 30791 */ 30792 function incrementMonths(date, numberOfMonths) { 30793 // If the same date in the target month does not actually exist, the Date object will 30794 // automatically advance *another* month by the number of missing days. 30795 // For example, if you try to go from Jan. 30 to Feb. 30, you'll end up on March 2. 30796 // So, we check if the month overflowed and go to the last day of the target month instead. 30797 var dateInTargetMonth = new Date(date.getFullYear(), date.getMonth() + numberOfMonths, 1); 30798 var numberOfDaysInMonth = getNumberOfDaysInMonth(dateInTargetMonth); 30799 if (numberOfDaysInMonth < date.getDate()) { 30800 dateInTargetMonth.setDate(numberOfDaysInMonth); 30801 } else { 30802 dateInTargetMonth.setDate(date.getDate()); 30803 } 30804 30805 return dateInTargetMonth; 30806 } 30807 30808 /** 30809 * Get the integer distance between two months. This *only* considers the month and year 30810 * portion of the Date instances. 30811 * 30812 * @param {Date} start 30813 * @param {Date} end 30814 * @returns {number} Number of months between `start` and `end`. If `end` is before `start` 30815 * chronologically, this number will be negative. 30816 */ 30817 function getMonthDistance(start, end) { 30818 return (12 * (end.getFullYear() - start.getFullYear())) + (end.getMonth() - start.getMonth()); 30819 } 30820 30821 /** 30822 * Gets the last day of the month for the given date. 30823 * @param {Date} date 30824 * @returns {Date} 30825 */ 30826 function getLastDateOfMonth(date) { 30827 return new Date(date.getFullYear(), date.getMonth(), getNumberOfDaysInMonth(date)); 30828 } 30829 30830 /** 30831 * Checks whether a date is valid. 30832 * @param {Date} date 30833 * @return {boolean} Whether the date is a valid Date. 30834 */ 30835 function isValidDate(date) { 30836 return date && date.getTime && !isNaN(date.getTime()); 30837 } 30838 30839 /** 30840 * Sets a date's time to midnight. 30841 * @param {Date} date 30842 */ 30843 function setDateTimeToMidnight(date) { 30844 if (isValidDate(date)) { 30845 date.setHours(0, 0, 0, 0); 30846 } 30847 } 30848 30849 /** 30850 * Creates a date with the time set to midnight. 30851 * Drop-in replacement for two forms of the Date constructor: 30852 * 1. No argument for Date representing now. 30853 * 2. Single-argument value representing number of seconds since Unix Epoch 30854 * or a Date object. 30855 * @param {number|Date=} opt_value 30856 * @return {Date} New date with time set to midnight. 30857 */ 30858 function createDateAtMidnight(opt_value) { 30859 var date; 30860 if (angular.isUndefined(opt_value)) { 30861 date = new Date(); 30862 } else { 30863 date = new Date(opt_value); 30864 } 30865 setDateTimeToMidnight(date); 30866 return date; 30867 } 30868 30869 /** 30870 * Checks if a date is within a min and max range, ignoring the time component. 30871 * If minDate or maxDate are not dates, they are ignored. 30872 * @param {Date} date 30873 * @param {Date} minDate 30874 * @param {Date} maxDate 30875 */ 30876 function isDateWithinRange(date, minDate, maxDate) { 30877 var dateAtMidnight = createDateAtMidnight(date); 30878 var minDateAtMidnight = isValidDate(minDate) ? createDateAtMidnight(minDate) : null; 30879 var maxDateAtMidnight = isValidDate(maxDate) ? createDateAtMidnight(maxDate) : null; 30880 return (!minDateAtMidnight || minDateAtMidnight <= dateAtMidnight) && 30881 (!maxDateAtMidnight || maxDateAtMidnight >= dateAtMidnight); 30882 } 30883 30884 /** 30885 * Gets a new date incremented by the given number of years. Number of years can be negative. 30886 * See `incrementMonths` for notes on overflow for specific dates. 30887 * @param {Date} date 30888 * @param {number} numberOfYears 30889 * @returns {Date} 30890 */ 30891 function incrementYears(date, numberOfYears) { 30892 return incrementMonths(date, numberOfYears * 12); 30893 } 30894 30895 /** 30896 * Get the integer distance between two years. This *only* considers the year portion of the 30897 * Date instances. 30898 * 30899 * @param {Date} start 30900 * @param {Date} end 30901 * @returns {number} Number of months between `start` and `end`. If `end` is before `start` 30902 * chronologically, this number will be negative. 30903 */ 30904 function getYearDistance(start, end) { 30905 return end.getFullYear() - start.getFullYear(); 30906 } 30907 30908 /** 30909 * Clamps a date between a minimum and a maximum date. 30910 * @param {Date} date Date to be clamped 30911 * @param {Date=} minDate Minimum date 30912 * @param {Date=} maxDate Maximum date 30913 * @return {Date} 30914 */ 30915 function clampDate(date, minDate, maxDate) { 30916 var boundDate = date; 30917 if (minDate && date < minDate) { 30918 boundDate = new Date(minDate.getTime()); 30919 } 30920 if (maxDate && date > maxDate) { 30921 boundDate = new Date(maxDate.getTime()); 30922 } 30923 return boundDate; 30924 } 30925 30926 /** 30927 * Extracts and parses the timestamp from a DOM node. 30928 * @param {HTMLElement} node Node from which the timestamp will be extracted. 30929 * @return {number} Time since epoch. 30930 */ 30931 function getTimestampFromNode(node) { 30932 if (node && node.hasAttribute('data-timestamp')) { 30933 return Number(node.getAttribute('data-timestamp')); 30934 } 30935 } 30936 30937 /** 30938 * Checks if a month is within a min and max range, ignoring the date and time components. 30939 * If minDate or maxDate are not dates, they are ignored. 30940 * @param {Date} date 30941 * @param {Date} minDate 30942 * @param {Date} maxDate 30943 */ 30944 function isMonthWithinRange(date, minDate, maxDate) { 30945 var month = date.getMonth(); 30946 var year = date.getFullYear(); 30947 30948 return (!minDate || minDate.getFullYear() < year || minDate.getMonth() <= month) && 30949 (!maxDate || maxDate.getFullYear() > year || maxDate.getMonth() >= month); 30950 } 30951 }); 30952})(); 30953 30954})(); 30955(function(){ 30956"use strict"; 30957 30958(function() { 30959 'use strict'; 30960 30961 // POST RELEASE 30962 // TODO(jelbourn): Demo that uses moment.js 30963 // TODO(jelbourn): make sure this plays well with validation and ngMessages. 30964 // TODO(jelbourn): calendar pane doesn't open up outside of visible viewport. 30965 // TODO(jelbourn): forward more attributes to the internal input (required, autofocus, etc.) 30966 // TODO(jelbourn): something better for mobile (calendar panel takes up entire screen?) 30967 // TODO(jelbourn): input behavior (masking? auto-complete?) 30968 30969 30970 DatePickerCtrl.$inject = ["$scope", "$element", "$attrs", "$window", "$mdConstant", "$mdTheming", "$mdUtil", "$mdDateLocale", "$$mdDateUtil", "$$rAF", "$filter"]; 30971 datePickerDirective.$inject = ["$$mdSvgRegistry", "$mdUtil", "$mdAria", "inputDirective"]; 30972 angular.module('material.components.datepicker') 30973 .directive('mdDatepicker', datePickerDirective); 30974 30975 /** 30976 * @ngdoc directive 30977 * @name mdDatepicker 30978 * @module material.components.datepicker 30979 * 30980 * @param {Date} ng-model The component's model. Expects either a JavaScript Date object or a value that can be parsed into one (e.g. a ISO 8601 string). 30981 * @param {Object=} ng-model-options Allows tuning of the way in which `ng-model` is being updated. Also allows 30982 * for a timezone to be specified. <a href="https://docs.angularjs.org/api/ng/directive/ngModelOptions#usage">Read more at the ngModelOptions docs.</a> 30983 * @param {expression=} ng-change Expression evaluated when the model value changes. 30984 * @param {expression=} ng-focus Expression evaluated when the input is focused or the calendar is opened. 30985 * @param {expression=} ng-blur Expression evaluated when focus is removed from the input or the calendar is closed. 30986 * @param {boolean=} ng-disabled Whether the datepicker is disabled. 30987 * @param {boolean=} ng-required Whether a value is required for the datepicker. 30988 * @param {Date=} md-min-date Expression representing a min date (inclusive). 30989 * @param {Date=} md-max-date Expression representing a max date (inclusive). 30990 * @param {(function(Date): boolean)=} md-date-filter Function expecting a date and returning a boolean whether it can be selected or not. 30991 * @param {String=} md-placeholder The date input placeholder value. 30992 * @param {String=} md-open-on-focus When present, the calendar will be opened when the input is focused. 30993 * @param {Boolean=} md-is-open Expression that can be used to open the datepicker's calendar on-demand. 30994 * @param {String=} md-current-view Default open view of the calendar pane. Can be either "month" or "year". 30995 * @param {String=} md-hide-icons Determines which datepicker icons should be hidden. Note that this may cause the 30996 * datepicker to not align properly with other components. **Use at your own risk.** Possible values are: 30997 * * `"all"` - Hides all icons. 30998 * * `"calendar"` - Only hides the calendar icon. 30999 * * `"triangle"` - Only hides the triangle icon. 31000 * @param {Object=} md-date-locale Allows for the values from the `$mdDateLocaleProvider` to be 31001 * ovewritten on a per-element basis (e.g. `msgOpenCalendar` can be overwritten with 31002 * `md-date-locale="{ msgOpenCalendar: 'Open a special calendar' }"`). 31003 * 31004 * @description 31005 * `<md-datepicker>` is a component used to select a single date. 31006 * For information on how to configure internationalization for the date picker, 31007 * see `$mdDateLocaleProvider`. 31008 * 31009 * This component supports [ngMessages](https://docs.angularjs.org/api/ngMessages/directive/ngMessages). 31010 * Supported attributes are: 31011 * * `required`: whether a required date is not set. 31012 * * `mindate`: whether the selected date is before the minimum allowed date. 31013 * * `maxdate`: whether the selected date is after the maximum allowed date. 31014 * * `debounceInterval`: ms to delay input processing (since last debounce reset); default value 500ms 31015 * 31016 * @usage 31017 * <hljs lang="html"> 31018 * <md-datepicker ng-model="birthday"></md-datepicker> 31019 * </hljs> 31020 * 31021 */ 31022 31023 function datePickerDirective($$mdSvgRegistry, $mdUtil, $mdAria, inputDirective) { 31024 return { 31025 template: function(tElement, tAttrs) { 31026 // Buttons are not in the tab order because users can open the calendar via keyboard 31027 // interaction on the text input, and multiple tab stops for one component (picker) 31028 // may be confusing. 31029 var hiddenIcons = tAttrs.mdHideIcons; 31030 var ariaLabelValue = tAttrs.ariaLabel || tAttrs.mdPlaceholder; 31031 31032 var calendarButton = (hiddenIcons === 'all' || hiddenIcons === 'calendar') ? '' : 31033 '<md-button class="md-datepicker-button md-icon-button" type="button" ' + 31034 'tabindex="-1" aria-hidden="true" ' + 31035 'ng-click="ctrl.openCalendarPane($event)">' + 31036 '<md-icon class="md-datepicker-calendar-icon" aria-label="md-calendar" ' + 31037 'md-svg-src="' + $$mdSvgRegistry.mdCalendar + '"></md-icon>' + 31038 '</md-button>'; 31039 31040 var triangleButton = ''; 31041 31042 if (hiddenIcons !== 'all' && hiddenIcons !== 'triangle') { 31043 triangleButton = '' + 31044 '<md-button type="button" md-no-ink ' + 31045 'class="md-datepicker-triangle-button md-icon-button" ' + 31046 'ng-click="ctrl.openCalendarPane($event)" ' + 31047 'aria-label="{{::ctrl.locale.msgOpenCalendar}}">' + 31048 '<div class="md-datepicker-expand-triangle"></div>' + 31049 '</md-button>'; 31050 31051 tElement.addClass(HAS_TRIANGLE_ICON_CLASS); 31052 } 31053 31054 return calendarButton + 31055 '<div class="md-datepicker-input-container" ng-class="{\'md-datepicker-focused\': ctrl.isFocused}">' + 31056 '<input ' + 31057 (ariaLabelValue ? 'aria-label="' + ariaLabelValue + '" ' : '') + 31058 'class="md-datepicker-input" ' + 31059 'aria-haspopup="true" ' + 31060 'aria-expanded="{{ctrl.isCalendarOpen}}" ' + 31061 'ng-focus="ctrl.setFocused(true)" ' + 31062 'ng-blur="ctrl.setFocused(false)"> ' + 31063 triangleButton + 31064 '</div>' + 31065 31066 // This pane will be detached from here and re-attached to the document body. 31067 '<div class="md-datepicker-calendar-pane md-whiteframe-z1" id="{{::ctrl.calendarPaneId}}">' + 31068 '<div class="md-datepicker-input-mask">' + 31069 '<div class="md-datepicker-input-mask-opaque"></div>' + 31070 '</div>' + 31071 '<div class="md-datepicker-calendar">' + 31072 '<md-calendar role="dialog" aria-label="{{::ctrl.locale.msgCalendar}}" ' + 31073 'md-current-view="{{::ctrl.currentView}}"' + 31074 'md-min-date="ctrl.minDate"' + 31075 'md-max-date="ctrl.maxDate"' + 31076 'md-date-filter="ctrl.dateFilter"' + 31077 'ng-model="ctrl.date" ng-if="ctrl.isCalendarOpen">' + 31078 '</md-calendar>' + 31079 '</div>' + 31080 '</div>'; 31081 }, 31082 require: ['ngModel', 'mdDatepicker', '?^mdInputContainer', '?^form'], 31083 scope: { 31084 minDate: '=mdMinDate', 31085 maxDate: '=mdMaxDate', 31086 placeholder: '@mdPlaceholder', 31087 currentView: '@mdCurrentView', 31088 dateFilter: '=mdDateFilter', 31089 isOpen: '=?mdIsOpen', 31090 debounceInterval: '=mdDebounceInterval', 31091 dateLocale: '=mdDateLocale' 31092 }, 31093 controller: DatePickerCtrl, 31094 controllerAs: 'ctrl', 31095 bindToController: true, 31096 link: function(scope, element, attr, controllers) { 31097 var ngModelCtrl = controllers[0]; 31098 var mdDatePickerCtrl = controllers[1]; 31099 var mdInputContainer = controllers[2]; 31100 var parentForm = controllers[3]; 31101 var mdNoAsterisk = $mdUtil.parseAttributeBoolean(attr.mdNoAsterisk); 31102 31103 mdDatePickerCtrl.configureNgModel(ngModelCtrl, mdInputContainer, inputDirective); 31104 31105 if (mdInputContainer) { 31106 // We need to move the spacer after the datepicker itself, 31107 // because md-input-container adds it after the 31108 // md-datepicker-input by default. The spacer gets wrapped in a 31109 // div, because it floats and gets aligned next to the datepicker. 31110 // There are easier ways of working around this with CSS (making the 31111 // datepicker 100% wide, change the `display` etc.), however they 31112 // break the alignment with any other form controls. 31113 var spacer = element[0].querySelector('.md-errors-spacer'); 31114 31115 if (spacer) { 31116 element.after(angular.element('<div>').append(spacer)); 31117 } 31118 31119 mdInputContainer.setHasPlaceholder(attr.mdPlaceholder); 31120 mdInputContainer.input = element; 31121 mdInputContainer.element 31122 .addClass(INPUT_CONTAINER_CLASS) 31123 .toggleClass(HAS_CALENDAR_ICON_CLASS, attr.mdHideIcons !== 'calendar' && attr.mdHideIcons !== 'all'); 31124 31125 if (!mdInputContainer.label) { 31126 $mdAria.expect(element, 'aria-label', attr.mdPlaceholder); 31127 } else if(!mdNoAsterisk) { 31128 attr.$observe('required', function(value) { 31129 mdInputContainer.label.toggleClass('md-required', !!value); 31130 }); 31131 } 31132 31133 scope.$watch(mdInputContainer.isErrorGetter || function() { 31134 return ngModelCtrl.$invalid && (ngModelCtrl.$touched || (parentForm && parentForm.$submitted)); 31135 }, mdInputContainer.setInvalid); 31136 } else if (parentForm) { 31137 // If invalid, highlights the input when the parent form is submitted. 31138 var parentSubmittedWatcher = scope.$watch(function() { 31139 return parentForm.$submitted; 31140 }, function(isSubmitted) { 31141 if (isSubmitted) { 31142 mdDatePickerCtrl.updateErrorState(); 31143 parentSubmittedWatcher(); 31144 } 31145 }); 31146 } 31147 } 31148 }; 31149 } 31150 31151 /** Additional offset for the input's `size` attribute, which is updated based on its content. */ 31152 var EXTRA_INPUT_SIZE = 3; 31153 31154 /** Class applied to the container if the date is invalid. */ 31155 var INVALID_CLASS = 'md-datepicker-invalid'; 31156 31157 /** Class applied to the datepicker when it's open. */ 31158 var OPEN_CLASS = 'md-datepicker-open'; 31159 31160 /** Class applied to the md-input-container, if a datepicker is placed inside it */ 31161 var INPUT_CONTAINER_CLASS = '_md-datepicker-floating-label'; 31162 31163 /** Class to be applied when the calendar icon is enabled. */ 31164 var HAS_CALENDAR_ICON_CLASS = '_md-datepicker-has-calendar-icon'; 31165 31166 /** Class to be applied when the triangle icon is enabled. */ 31167 var HAS_TRIANGLE_ICON_CLASS = '_md-datepicker-has-triangle-icon'; 31168 31169 /** Default time in ms to debounce input event by. */ 31170 var DEFAULT_DEBOUNCE_INTERVAL = 500; 31171 31172 /** 31173 * Height of the calendar pane used to check if the pane is going outside the boundary of 31174 * the viewport. See calendar.scss for how $md-calendar-height is computed; an extra 20px is 31175 * also added to space the pane away from the exact edge of the screen. 31176 * 31177 * This is computed statically now, but can be changed to be measured if the circumstances 31178 * of calendar sizing are changed. 31179 */ 31180 var CALENDAR_PANE_HEIGHT = 368; 31181 31182 /** 31183 * Width of the calendar pane used to check if the pane is going outside the boundary of 31184 * the viewport. See calendar.scss for how $md-calendar-width is computed; an extra 20px is 31185 * also added to space the pane away from the exact edge of the screen. 31186 * 31187 * This is computed statically now, but can be changed to be measured if the circumstances 31188 * of calendar sizing are changed. 31189 */ 31190 var CALENDAR_PANE_WIDTH = 360; 31191 31192 /** Used for checking whether the current user agent is on iOS or Android. */ 31193 var IS_MOBILE_REGEX = /ipad|iphone|ipod|android/i; 31194 31195 /** 31196 * Controller for md-datepicker. 31197 * 31198 * @ngInject @constructor 31199 */ 31200 function DatePickerCtrl($scope, $element, $attrs, $window, $mdConstant, 31201 $mdTheming, $mdUtil, $mdDateLocale, $$mdDateUtil, $$rAF, $filter) { 31202 31203 /** @final */ 31204 this.$window = $window; 31205 31206 /** @final */ 31207 this.dateUtil = $$mdDateUtil; 31208 31209 /** @final */ 31210 this.$mdConstant = $mdConstant; 31211 31212 /* @final */ 31213 this.$mdUtil = $mdUtil; 31214 31215 /** @final */ 31216 this.$$rAF = $$rAF; 31217 31218 /** @final */ 31219 this.$mdDateLocale = $mdDateLocale; 31220 31221 /** 31222 * The root document element. This is used for attaching a top-level click handler to 31223 * close the calendar panel when a click outside said panel occurs. We use `documentElement` 31224 * instead of body because, when scrolling is disabled, some browsers consider the body element 31225 * to be completely off the screen and propagate events directly to the html element. 31226 * @type {!angular.JQLite} 31227 */ 31228 this.documentElement = angular.element(document.documentElement); 31229 31230 /** @type {!angular.NgModelController} */ 31231 this.ngModelCtrl = null; 31232 31233 /** @type {HTMLInputElement} */ 31234 this.inputElement = $element[0].querySelector('input'); 31235 31236 /** @final {!angular.JQLite} */ 31237 this.ngInputElement = angular.element(this.inputElement); 31238 31239 /** @type {HTMLElement} */ 31240 this.inputContainer = $element[0].querySelector('.md-datepicker-input-container'); 31241 31242 /** @type {HTMLElement} Floating calendar pane. */ 31243 this.calendarPane = $element[0].querySelector('.md-datepicker-calendar-pane'); 31244 31245 /** @type {HTMLElement} Calendar icon button. */ 31246 this.calendarButton = $element[0].querySelector('.md-datepicker-button'); 31247 31248 /** 31249 * Element covering everything but the input in the top of the floating calendar pane. 31250 * @type {!angular.JQLite} 31251 */ 31252 this.inputMask = angular.element($element[0].querySelector('.md-datepicker-input-mask-opaque')); 31253 31254 /** @final {!angular.JQLite} */ 31255 this.$element = $element; 31256 31257 /** @final {!angular.Attributes} */ 31258 this.$attrs = $attrs; 31259 31260 /** @final {!angular.Scope} */ 31261 this.$scope = $scope; 31262 31263 /** @type {Date} */ 31264 this.date = null; 31265 31266 /** @type {boolean} */ 31267 this.isFocused = false; 31268 31269 /** @type {boolean} */ 31270 this.isDisabled; 31271 this.setDisabled($element[0].disabled || angular.isString($attrs.disabled)); 31272 31273 /** @type {boolean} Whether the date-picker's calendar pane is open. */ 31274 this.isCalendarOpen = false; 31275 31276 /** @type {boolean} Whether the calendar should open when the input is focused. */ 31277 this.openOnFocus = $attrs.hasOwnProperty('mdOpenOnFocus'); 31278 31279 /** @final */ 31280 this.mdInputContainer = null; 31281 31282 /** 31283 * Element from which the calendar pane was opened. Keep track of this so that we can return 31284 * focus to it when the pane is closed. 31285 * @type {HTMLElement} 31286 */ 31287 this.calendarPaneOpenedFrom = null; 31288 31289 /** @type {String} Unique id for the calendar pane. */ 31290 this.calendarPaneId = 'md-date-pane-' + $mdUtil.nextUid(); 31291 31292 /** Pre-bound click handler is saved so that the event listener can be removed. */ 31293 this.bodyClickHandler = angular.bind(this, this.handleBodyClick); 31294 31295 /** 31296 * Name of the event that will trigger a close. Necessary to sniff the browser, because 31297 * the resize event doesn't make sense on mobile and can have a negative impact since it 31298 * triggers whenever the browser zooms in on a focused input. 31299 */ 31300 this.windowEventName = IS_MOBILE_REGEX.test( 31301 navigator.userAgent || navigator.vendor || window.opera 31302 ) ? 'orientationchange' : 'resize'; 31303 31304 /** Pre-bound close handler so that the event listener can be removed. */ 31305 this.windowEventHandler = $mdUtil.debounce(angular.bind(this, this.closeCalendarPane), 100); 31306 31307 /** Pre-bound handler for the window blur event. Allows for it to be removed later. */ 31308 this.windowBlurHandler = angular.bind(this, this.handleWindowBlur); 31309 31310 /** The built-in AngularJS date filter. */ 31311 this.ngDateFilter = $filter('date'); 31312 31313 /** @type {Number} Extra margin for the left side of the floating calendar pane. */ 31314 this.leftMargin = 20; 31315 31316 /** @type {Number} Extra margin for the top of the floating calendar. Gets determined on the first open. */ 31317 this.topMargin = null; 31318 31319 // Unless the user specifies so, the datepicker should not be a tab stop. 31320 // This is necessary because ngAria might add a tabindex to anything with an ng-model 31321 // (based on whether or not the user has turned that particular feature on/off). 31322 if ($attrs.tabindex) { 31323 this.ngInputElement.attr('tabindex', $attrs.tabindex); 31324 $attrs.$set('tabindex', null); 31325 } else { 31326 $attrs.$set('tabindex', '-1'); 31327 } 31328 31329 $attrs.$set('aria-owns', this.calendarPaneId); 31330 31331 $mdTheming($element); 31332 $mdTheming(angular.element(this.calendarPane)); 31333 31334 var self = this; 31335 31336 $scope.$on('$destroy', function() { 31337 self.detachCalendarPane(); 31338 }); 31339 31340 if ($attrs.mdIsOpen) { 31341 $scope.$watch('ctrl.isOpen', function(shouldBeOpen) { 31342 if (shouldBeOpen) { 31343 self.openCalendarPane({ 31344 target: self.inputElement 31345 }); 31346 } else { 31347 self.closeCalendarPane(); 31348 } 31349 }); 31350 } 31351 31352 // For AngularJS 1.4 and older, where there are no lifecycle hooks but bindings are pre-assigned, 31353 // manually call the $onInit hook. 31354 if (angular.version.major === 1 && angular.version.minor <= 4) { 31355 this.$onInit(); 31356 } 31357 31358 } 31359 31360 /** 31361 * AngularJS Lifecycle hook for newer AngularJS versions. 31362 * Bindings are not guaranteed to have been assigned in the controller, but they are in the $onInit hook. 31363 */ 31364 DatePickerCtrl.prototype.$onInit = function() { 31365 31366 /** 31367 * Holds locale-specific formatters, parsers, labels etc. Allows 31368 * the user to override specific ones from the $mdDateLocale provider. 31369 * @type {!Object} 31370 */ 31371 this.locale = this.dateLocale ? angular.extend({}, this.$mdDateLocale, this.dateLocale) : this.$mdDateLocale; 31372 31373 this.installPropertyInterceptors(); 31374 this.attachChangeListeners(); 31375 this.attachInteractionListeners(); 31376 }; 31377 31378 /** 31379 * Sets up the controller's reference to ngModelController and 31380 * applies AngularJS's `input[type="date"]` directive. 31381 * @param {!angular.NgModelController} ngModelCtrl Instance of the ngModel controller. 31382 * @param {Object} mdInputContainer Instance of the mdInputContainer controller. 31383 * @param {Object} inputDirective Config for AngularJS's `input` directive. 31384 */ 31385 DatePickerCtrl.prototype.configureNgModel = function(ngModelCtrl, mdInputContainer, inputDirective) { 31386 this.ngModelCtrl = ngModelCtrl; 31387 this.mdInputContainer = mdInputContainer; 31388 31389 // The input needs to be [type="date"] in order to be picked up by AngularJS. 31390 this.$attrs.$set('type', 'date'); 31391 31392 // Invoke the `input` directive link function, adding a stub for the element. 31393 // This allows us to re-use AngularJS's logic for setting the timezone via ng-model-options. 31394 // It works by calling the link function directly which then adds the proper `$parsers` and 31395 // `$formatters` to the ngModel controller. 31396 inputDirective[0].link.pre(this.$scope, { 31397 on: angular.noop, 31398 val: angular.noop, 31399 0: {} 31400 }, this.$attrs, [ngModelCtrl]); 31401 31402 var self = this; 31403 31404 // Responds to external changes to the model value. 31405 self.ngModelCtrl.$formatters.push(function(value) { 31406 var parsedValue = angular.isDefined(value) ? Date.parse(value) : null; 31407 31408 // `parsedValue` is the time since epoch if valid or `NaN` if invalid. 31409 if (!isNaN(parsedValue) && angular.isNumber(parsedValue)) { 31410 value = new Date(parsedValue); 31411 } 31412 31413 if (value && !(value instanceof Date)) { 31414 throw Error( 31415 'The ng-model for md-datepicker must be a Date instance or a value ' + 31416 'that can be parsed into a date. Currently the model is of type: ' + typeof value 31417 ); 31418 } 31419 31420 self.onExternalChange(value); 31421 31422 return value; 31423 }); 31424 31425 // Responds to external error state changes (e.g. ng-required based on another input). 31426 ngModelCtrl.$viewChangeListeners.unshift(angular.bind(this, this.updateErrorState)); 31427 31428 // Forwards any events from the input to the root element. This is necessary to get `updateOn` 31429 // working for events that don't bubble (e.g. 'blur') since AngularJS binds the handlers to 31430 // the `<md-datepicker>`. 31431 var updateOn = self.$mdUtil.getModelOption(ngModelCtrl, 'updateOn'); 31432 31433 if (updateOn) { 31434 this.ngInputElement.on( 31435 updateOn, 31436 angular.bind(this.$element, this.$element.triggerHandler, updateOn) 31437 ); 31438 } 31439 }; 31440 31441 /** 31442 * Attach event listeners for both the text input and the md-calendar. 31443 * Events are used instead of ng-model so that updates don't infinitely update the other 31444 * on a change. This should also be more performant than using a $watch. 31445 */ 31446 DatePickerCtrl.prototype.attachChangeListeners = function() { 31447 var self = this; 31448 31449 self.$scope.$on('md-calendar-change', function(event, date) { 31450 self.setModelValue(date); 31451 self.onExternalChange(date); 31452 self.closeCalendarPane(); 31453 }); 31454 31455 self.ngInputElement.on('input', angular.bind(self, self.resizeInputElement)); 31456 31457 var debounceInterval = angular.isDefined(this.debounceInterval) ? 31458 this.debounceInterval : DEFAULT_DEBOUNCE_INTERVAL; 31459 self.ngInputElement.on('input', self.$mdUtil.debounce(self.handleInputEvent, 31460 debounceInterval, self)); 31461 }; 31462 31463 /** Attach event listeners for user interaction. */ 31464 DatePickerCtrl.prototype.attachInteractionListeners = function() { 31465 var self = this; 31466 var $scope = this.$scope; 31467 var keyCodes = this.$mdConstant.KEY_CODE; 31468 31469 // Add event listener through angular so that we can triggerHandler in unit tests. 31470 self.ngInputElement.on('keydown', function(event) { 31471 if (event.altKey && event.keyCode == keyCodes.DOWN_ARROW) { 31472 self.openCalendarPane(event); 31473 $scope.$digest(); 31474 } 31475 }); 31476 31477 if (self.openOnFocus) { 31478 self.ngInputElement.on('focus', angular.bind(self, self.openCalendarPane)); 31479 angular.element(self.$window).on('blur', self.windowBlurHandler); 31480 31481 $scope.$on('$destroy', function() { 31482 angular.element(self.$window).off('blur', self.windowBlurHandler); 31483 }); 31484 } 31485 31486 $scope.$on('md-calendar-close', function() { 31487 self.closeCalendarPane(); 31488 }); 31489 }; 31490 31491 /** 31492 * Capture properties set to the date-picker and imperitively handle internal changes. 31493 * This is done to avoid setting up additional $watches. 31494 */ 31495 DatePickerCtrl.prototype.installPropertyInterceptors = function() { 31496 var self = this; 31497 31498 if (this.$attrs.ngDisabled) { 31499 // The expression is to be evaluated against the directive element's scope and not 31500 // the directive's isolate scope. 31501 var scope = this.$scope.$parent; 31502 31503 if (scope) { 31504 scope.$watch(this.$attrs.ngDisabled, function(isDisabled) { 31505 self.setDisabled(isDisabled); 31506 }); 31507 } 31508 } 31509 31510 Object.defineProperty(this, 'placeholder', { 31511 get: function() { return self.inputElement.placeholder; }, 31512 set: function(value) { self.inputElement.placeholder = value || ''; } 31513 }); 31514 }; 31515 31516 /** 31517 * Sets whether the date-picker is disabled. 31518 * @param {boolean} isDisabled 31519 */ 31520 DatePickerCtrl.prototype.setDisabled = function(isDisabled) { 31521 this.isDisabled = isDisabled; 31522 this.inputElement.disabled = isDisabled; 31523 31524 if (this.calendarButton) { 31525 this.calendarButton.disabled = isDisabled; 31526 } 31527 }; 31528 31529 /** 31530 * Sets the custom ngModel.$error flags to be consumed by ngMessages. Flags are: 31531 * - mindate: whether the selected date is before the minimum date. 31532 * - maxdate: whether the selected flag is after the maximum date. 31533 * - filtered: whether the selected date is allowed by the custom filtering function. 31534 * - valid: whether the entered text input is a valid date 31535 * 31536 * The 'required' flag is handled automatically by ngModel. 31537 * 31538 * @param {Date=} opt_date Date to check. If not given, defaults to the datepicker's model value. 31539 */ 31540 DatePickerCtrl.prototype.updateErrorState = function(opt_date) { 31541 var date = opt_date || this.date; 31542 31543 // Clear any existing errors to get rid of anything that's no longer relevant. 31544 this.clearErrorState(); 31545 31546 if (this.dateUtil.isValidDate(date)) { 31547 // Force all dates to midnight in order to ignore the time portion. 31548 date = this.dateUtil.createDateAtMidnight(date); 31549 31550 if (this.dateUtil.isValidDate(this.minDate)) { 31551 var minDate = this.dateUtil.createDateAtMidnight(this.minDate); 31552 this.ngModelCtrl.$setValidity('mindate', date >= minDate); 31553 } 31554 31555 if (this.dateUtil.isValidDate(this.maxDate)) { 31556 var maxDate = this.dateUtil.createDateAtMidnight(this.maxDate); 31557 this.ngModelCtrl.$setValidity('maxdate', date <= maxDate); 31558 } 31559 31560 if (angular.isFunction(this.dateFilter)) { 31561 this.ngModelCtrl.$setValidity('filtered', this.dateFilter(date)); 31562 } 31563 } else { 31564 // The date is seen as "not a valid date" if there is *something* set 31565 // (i.e.., not null or undefined), but that something isn't a valid date. 31566 this.ngModelCtrl.$setValidity('valid', date == null); 31567 } 31568 31569 angular.element(this.inputContainer).toggleClass(INVALID_CLASS, !this.ngModelCtrl.$valid); 31570 }; 31571 31572 /** Clears any error flags set by `updateErrorState`. */ 31573 DatePickerCtrl.prototype.clearErrorState = function() { 31574 this.inputContainer.classList.remove(INVALID_CLASS); 31575 ['mindate', 'maxdate', 'filtered', 'valid'].forEach(function(field) { 31576 this.ngModelCtrl.$setValidity(field, true); 31577 }, this); 31578 }; 31579 31580 /** Resizes the input element based on the size of its content. */ 31581 DatePickerCtrl.prototype.resizeInputElement = function() { 31582 this.inputElement.size = this.inputElement.value.length + EXTRA_INPUT_SIZE; 31583 }; 31584 31585 /** 31586 * Sets the model value if the user input is a valid date. 31587 * Adds an invalid class to the input element if not. 31588 */ 31589 DatePickerCtrl.prototype.handleInputEvent = function() { 31590 var inputString = this.inputElement.value; 31591 var parsedDate = inputString ? this.locale.parseDate(inputString) : null; 31592 this.dateUtil.setDateTimeToMidnight(parsedDate); 31593 31594 // An input string is valid if it is either empty (representing no date) 31595 // or if it parses to a valid date that the user is allowed to select. 31596 var isValidInput = inputString == '' || ( 31597 this.dateUtil.isValidDate(parsedDate) && 31598 this.locale.isDateComplete(inputString) && 31599 this.isDateEnabled(parsedDate) 31600 ); 31601 31602 // The datepicker's model is only updated when there is a valid input. 31603 if (isValidInput) { 31604 this.setModelValue(parsedDate); 31605 this.date = parsedDate; 31606 } 31607 31608 this.updateErrorState(parsedDate); 31609 }; 31610 31611 /** 31612 * Check whether date is in range and enabled 31613 * @param {Date=} opt_date 31614 * @return {boolean} Whether the date is enabled. 31615 */ 31616 DatePickerCtrl.prototype.isDateEnabled = function(opt_date) { 31617 return this.dateUtil.isDateWithinRange(opt_date, this.minDate, this.maxDate) && 31618 (!angular.isFunction(this.dateFilter) || this.dateFilter(opt_date)); 31619 }; 31620 31621 /** Position and attach the floating calendar to the document. */ 31622 DatePickerCtrl.prototype.attachCalendarPane = function() { 31623 var calendarPane = this.calendarPane; 31624 var body = document.body; 31625 31626 calendarPane.style.transform = ''; 31627 this.$element.addClass(OPEN_CLASS); 31628 this.mdInputContainer && this.mdInputContainer.element.addClass(OPEN_CLASS); 31629 angular.element(body).addClass('md-datepicker-is-showing'); 31630 31631 var elementRect = this.inputContainer.getBoundingClientRect(); 31632 var bodyRect = body.getBoundingClientRect(); 31633 31634 if (!this.topMargin || this.topMargin < 0) { 31635 this.topMargin = (this.inputMask.parent().prop('clientHeight') - this.ngInputElement.prop('clientHeight')) / 2; 31636 } 31637 31638 // Check to see if the calendar pane would go off the screen. If so, adjust position 31639 // accordingly to keep it within the viewport. 31640 var paneTop = elementRect.top - bodyRect.top - this.topMargin; 31641 var paneLeft = elementRect.left - bodyRect.left - this.leftMargin; 31642 31643 // If ng-material has disabled body scrolling (for example, if a dialog is open), 31644 // then it's possible that the already-scrolled body has a negative top/left. In this case, 31645 // we want to treat the "real" top as (0 - bodyRect.top). In a normal scrolling situation, 31646 // though, the top of the viewport should just be the body's scroll position. 31647 var viewportTop = (bodyRect.top < 0 && document.body.scrollTop == 0) ? 31648 -bodyRect.top : 31649 document.body.scrollTop; 31650 31651 var viewportLeft = (bodyRect.left < 0 && document.body.scrollLeft == 0) ? 31652 -bodyRect.left : 31653 document.body.scrollLeft; 31654 31655 var viewportBottom = viewportTop + this.$window.innerHeight; 31656 var viewportRight = viewportLeft + this.$window.innerWidth; 31657 31658 // Creates an overlay with a hole the same size as element. We remove a pixel or two 31659 // on each end to make it overlap slightly. The overlay's background is added in 31660 // the theme in the form of a box-shadow with a huge spread. 31661 this.inputMask.css({ 31662 position: 'absolute', 31663 left: this.leftMargin + 'px', 31664 top: this.topMargin + 'px', 31665 width: (elementRect.width - 1) + 'px', 31666 height: (elementRect.height - 2) + 'px' 31667 }); 31668 31669 // If the right edge of the pane would be off the screen and shifting it left by the 31670 // difference would not go past the left edge of the screen. If the calendar pane is too 31671 // big to fit on the screen at all, move it to the left of the screen and scale the entire 31672 // element down to fit. 31673 if (paneLeft + CALENDAR_PANE_WIDTH > viewportRight) { 31674 if (viewportRight - CALENDAR_PANE_WIDTH > 0) { 31675 paneLeft = viewportRight - CALENDAR_PANE_WIDTH; 31676 } else { 31677 paneLeft = viewportLeft; 31678 var scale = this.$window.innerWidth / CALENDAR_PANE_WIDTH; 31679 calendarPane.style.transform = 'scale(' + scale + ')'; 31680 } 31681 31682 calendarPane.classList.add('md-datepicker-pos-adjusted'); 31683 } 31684 31685 // If the bottom edge of the pane would be off the screen and shifting it up by the 31686 // difference would not go past the top edge of the screen. 31687 if (paneTop + CALENDAR_PANE_HEIGHT > viewportBottom && 31688 viewportBottom - CALENDAR_PANE_HEIGHT > viewportTop) { 31689 paneTop = viewportBottom - CALENDAR_PANE_HEIGHT; 31690 calendarPane.classList.add('md-datepicker-pos-adjusted'); 31691 } 31692 31693 calendarPane.style.left = paneLeft + 'px'; 31694 calendarPane.style.top = paneTop + 'px'; 31695 document.body.appendChild(calendarPane); 31696 31697 // Add CSS class after one frame to trigger open animation. 31698 this.$$rAF(function() { 31699 calendarPane.classList.add('md-pane-open'); 31700 }); 31701 }; 31702 31703 /** Detach the floating calendar pane from the document. */ 31704 DatePickerCtrl.prototype.detachCalendarPane = function() { 31705 this.$element.removeClass(OPEN_CLASS); 31706 this.mdInputContainer && this.mdInputContainer.element.removeClass(OPEN_CLASS); 31707 angular.element(document.body).removeClass('md-datepicker-is-showing'); 31708 this.calendarPane.classList.remove('md-pane-open'); 31709 this.calendarPane.classList.remove('md-datepicker-pos-adjusted'); 31710 31711 if (this.isCalendarOpen) { 31712 this.$mdUtil.enableScrolling(); 31713 } 31714 31715 if (this.calendarPane.parentNode) { 31716 // Use native DOM removal because we do not want any of the 31717 // angular state of this element to be disposed. 31718 this.calendarPane.parentNode.removeChild(this.calendarPane); 31719 } 31720 }; 31721 31722 /** 31723 * Open the floating calendar pane. 31724 * @param {Event} event 31725 */ 31726 DatePickerCtrl.prototype.openCalendarPane = function(event) { 31727 if (!this.isCalendarOpen && !this.isDisabled && !this.inputFocusedOnWindowBlur) { 31728 this.isCalendarOpen = this.isOpen = true; 31729 this.calendarPaneOpenedFrom = event.target; 31730 31731 // Because the calendar pane is attached directly to the body, it is possible that the 31732 // rest of the component (input, etc) is in a different scrolling container, such as 31733 // an md-content. This means that, if the container is scrolled, the pane would remain 31734 // stationary. To remedy this, we disable scrolling while the calendar pane is open, which 31735 // also matches the native behavior for things like `<select>` on Mac and Windows. 31736 this.$mdUtil.disableScrollAround(this.calendarPane); 31737 31738 this.attachCalendarPane(); 31739 this.focusCalendar(); 31740 this.evalAttr('ngFocus'); 31741 31742 // Attach click listener inside of a timeout because, if this open call was triggered by a 31743 // click, we don't want it to be immediately propogated up to the body and handled. 31744 var self = this; 31745 this.$mdUtil.nextTick(function() { 31746 // Use 'touchstart` in addition to click in order to work on iOS Safari, where click 31747 // events aren't propogated under most circumstances. 31748 // See http://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html 31749 self.documentElement.on('click touchstart', self.bodyClickHandler); 31750 }, false); 31751 31752 window.addEventListener(this.windowEventName, this.windowEventHandler); 31753 } 31754 }; 31755 31756 /** Close the floating calendar pane. */ 31757 DatePickerCtrl.prototype.closeCalendarPane = function() { 31758 if (this.isCalendarOpen) { 31759 var self = this; 31760 31761 self.detachCalendarPane(); 31762 self.ngModelCtrl.$setTouched(); 31763 self.evalAttr('ngBlur'); 31764 31765 self.documentElement.off('click touchstart', self.bodyClickHandler); 31766 window.removeEventListener(self.windowEventName, self.windowEventHandler); 31767 31768 self.calendarPaneOpenedFrom.focus(); 31769 self.calendarPaneOpenedFrom = null; 31770 31771 if (self.openOnFocus) { 31772 // Ensures that all focus events have fired before resetting 31773 // the calendar. Prevents the calendar from reopening immediately 31774 // in IE when md-open-on-focus is set. Also it needs to trigger 31775 // a digest, in order to prevent issues where the calendar wasn't 31776 // showing up on the next open. 31777 self.$mdUtil.nextTick(reset); 31778 } else { 31779 reset(); 31780 } 31781 } 31782 31783 function reset(){ 31784 self.isCalendarOpen = self.isOpen = false; 31785 } 31786 }; 31787 31788 /** Gets the controller instance for the calendar in the floating pane. */ 31789 DatePickerCtrl.prototype.getCalendarCtrl = function() { 31790 return angular.element(this.calendarPane.querySelector('md-calendar')).controller('mdCalendar'); 31791 }; 31792 31793 /** Focus the calendar in the floating pane. */ 31794 DatePickerCtrl.prototype.focusCalendar = function() { 31795 // Use a timeout in order to allow the calendar to be rendered, as it is gated behind an ng-if. 31796 var self = this; 31797 this.$mdUtil.nextTick(function() { 31798 self.getCalendarCtrl().focus(); 31799 }, false); 31800 }; 31801 31802 /** 31803 * Sets whether the input is currently focused. 31804 * @param {boolean} isFocused 31805 */ 31806 DatePickerCtrl.prototype.setFocused = function(isFocused) { 31807 if (!isFocused) { 31808 this.ngModelCtrl.$setTouched(); 31809 } 31810 31811 // The ng* expressions shouldn't be evaluated when mdOpenOnFocus is on, 31812 // because they also get called when the calendar is opened/closed. 31813 if (!this.openOnFocus) { 31814 this.evalAttr(isFocused ? 'ngFocus' : 'ngBlur'); 31815 } 31816 31817 this.isFocused = isFocused; 31818 }; 31819 31820 /** 31821 * Handles a click on the document body when the floating calendar pane is open. 31822 * Closes the floating calendar pane if the click is not inside of it. 31823 * @param {MouseEvent} event 31824 */ 31825 DatePickerCtrl.prototype.handleBodyClick = function(event) { 31826 if (this.isCalendarOpen) { 31827 var isInCalendar = this.$mdUtil.getClosest(event.target, 'md-calendar'); 31828 31829 if (!isInCalendar) { 31830 this.closeCalendarPane(); 31831 } 31832 31833 this.$scope.$digest(); 31834 } 31835 }; 31836 31837 /** 31838 * Handles the event when the user navigates away from the current tab. Keeps track of 31839 * whether the input was focused when the event happened, in order to prevent the calendar 31840 * from re-opening. 31841 */ 31842 DatePickerCtrl.prototype.handleWindowBlur = function() { 31843 this.inputFocusedOnWindowBlur = document.activeElement === this.inputElement; 31844 }; 31845 31846 /** 31847 * Evaluates an attribute expression against the parent scope. 31848 * @param {String} attr Name of the attribute to be evaluated. 31849 */ 31850 DatePickerCtrl.prototype.evalAttr = function(attr) { 31851 if (this.$attrs[attr]) { 31852 this.$scope.$parent.$eval(this.$attrs[attr]); 31853 } 31854 }; 31855 31856 /** 31857 * Sets the ng-model value by first converting the date object into a strng. Converting it 31858 * is necessary, in order to pass AngularJS's `input[type="date"]` validations. AngularJS turns 31859 * the value into a Date object afterwards, before setting it on the model. 31860 * @param {Date=} value Date to be set as the model value. 31861 */ 31862 DatePickerCtrl.prototype.setModelValue = function(value) { 31863 var timezone = this.$mdUtil.getModelOption(this.ngModelCtrl, 'timezone'); 31864 this.ngModelCtrl.$setViewValue(this.ngDateFilter(value, 'yyyy-MM-dd', timezone)); 31865 }; 31866 31867 /** 31868 * Updates the datepicker when a model change occurred externally. 31869 * @param {Date=} value Value that was set to the model. 31870 */ 31871 DatePickerCtrl.prototype.onExternalChange = function(value) { 31872 var timezone = this.$mdUtil.getModelOption(this.ngModelCtrl, 'timezone'); 31873 31874 this.date = value; 31875 this.inputElement.value = this.locale.formatDate(value, timezone); 31876 this.mdInputContainer && this.mdInputContainer.setHasValue(!!value); 31877 this.resizeInputElement(); 31878 this.updateErrorState(); 31879 }; 31880})(); 31881 31882})(); 31883(function(){ 31884"use strict"; 31885 31886angular 31887 .module('material.components.icon') 31888 .directive('mdIcon', ['$mdIcon', '$mdTheming', '$mdAria', '$sce', mdIconDirective]); 31889 31890/** 31891 * @ngdoc directive 31892 * @name mdIcon 31893 * @module material.components.icon 31894 * 31895 * @restrict E 31896 * 31897 * @description 31898 * The `md-icon` directive makes it easier to use vector-based icons in your app (as opposed to 31899 * raster-based icons types like PNG). The directive supports both icon fonts and SVG icons. 31900 * 31901 * Icons should be considered view-only elements that should not be used directly as buttons; instead nest a `<md-icon>` 31902 * inside a `md-button` to add hover and click features. 31903 * 31904 * ### Icon fonts 31905 * Icon fonts are a technique in which you use a font where the glyphs in the font are 31906 * your icons instead of text. Benefits include a straightforward way to bundle everything into a 31907 * single HTTP request, simple scaling, easy color changing, and more. 31908 * 31909 * `md-icon` lets you consume an icon font by letting you reference specific icons in that font 31910 * by name rather than character code. 31911 * 31912 * ### SVG 31913 * For SVGs, the problem with using `<img>` or a CSS `background-image` is that you can't take 31914 * advantage of some SVG features, such as styling specific parts of the icon
31914with CSS or SVG 31915 * animation. 31916 * 31917 * `md-icon` makes it easier to use SVG icons by *inlining* the SVG into an `<svg>` element in the 31918 * document. The most straightforward way of referencing an SVG icon is via URL, just like a 31919 * traditional `<img>`. `$mdIconProvider`, as a convenience, lets you _name_ an icon so you can 31920 * reference it by name instead of URL throughout your templates. 31921 * 31922 * Additionally, you may not want to make separate HTTP requests for every icon, so you can bundle 31923 * your SVG icons together and pre-load them with $mdIconProvider as an icon set. An icon set can 31924 * also be given a name, which acts as a namespace for individual icons, so you can reference them 31925 * like `"social:cake"`. 31926 * 31927 * When using SVGs, both external SVGs (via URLs) or sets of SVGs [from icon sets] can be 31928 * easily loaded and used. When using font-icons, developers must follow three (3) simple steps: 31929 * 31930 * <ol> 31931 * <li>Load the font library. e.g.<br/> 31932 * `<link href="https://fonts.googleapis.com/icon?family=Material+Icons" 31933 * rel="stylesheet">` 31934 * </li> 31935 * <li> 31936 * Use either (a) font-icon class names or (b) a fontset and a font ligature to render the font glyph by 31937 * using its textual name _or_ numerical character reference. Note that `material-icons` is the default fontset when 31938 * none is specified. 31939 * </li> 31940 * <li> Use any of the following templates: <br/> 31941 * <ul> 31942 * <li>`<md-icon md-font-icon="classname"></md-icon>`</li> 31943 * <li>`<md-icon md-font-set="font library classname or alias">textual_name</md-icon>`</li> 31944 * <li>`<md-icon> numerical_character_reference </md-icon>`</li> 31945 * <li>`<md-icon ng_bind="'textual_name'"></md-icon>`</li> 31946 * <li>`<md-icon ng-bind="scopeVariable"></md-icon>`</li> 31947 * </ul> 31948 * </li> 31949 * </ol> 31950 * 31951 * Full details for these steps can be found: 31952 * 31953 * <ul> 31954 * <li>http://google.github.io/material-design-icons/</li> 31955 * <li>http://google.github.io/material-design-icons/#icon-font-for-the-web</li> 31956 * </ul> 31957 * 31958 * The Material Design icon style <code>.material-icons</code> and the icon font references are published in 31959 * Material Design Icons: 31960 * 31961 * <ul> 31962 * <li>https://design.google.com/icons/</li> 31963 * <li>https://design.google.com/icons/#ic_accessibility</li> 31964 * </ul> 31965 * 31966 * ### Localization 31967 * 31968 * Because an `md-icon` element's text content is not intended to translated, it is recommended to declare the text 31969 * content for an `md-icon` element in its start tag. Instead of using the HTML text content, consider using `ng-bind` 31970 * with a scope variable or literal string. 31971 * 31972 * Examples: 31973 * 31974 * <ul> 31975 * <li>`<md-icon ng-bind="myIconVariable"></md-icon>`</li> 31976 * <li>`<md-icon ng-bind="'menu'"></md-icon>` 31977 * </ul> 31978 * 31979 * <h2 id="material_design_icons">Material Design Icons</h2> 31980 * Using the Material Design Icon-Selector, developers can easily and quickly search for a Material Design font-icon and 31981 * determine its textual name and character reference code. Click on any icon to see the slide-up information 31982 * panel with details regarding a SVG download or information on the font-icon usage. 31983 * 31984 * <a href="https://www.google.com/design/icons/#ic_accessibility" target="_blank" style="border-bottom:none;"> 31985 * <img src="https://cloud.githubusercontent.com/assets/210413/7902490/fe8dd14c-0780-11e5-98fb-c821cc6475e6.png" 31986 * aria-label="Material Design Icon-Selector" style="max-width:75%;padding-left:10%"> 31987 * </a> 31988 * 31989 * <span class="image_caption"> 31990 * Click on the image above to link to the 31991 * <a href="https://design.google.com/icons/#ic_accessibility" target="_blank">Material Design Icon-Selector</a>. 31992 * </span> 31993 * 31994 * @param {string} md-font-icon String name of CSS icon associated with the font-face will be used 31995 * to render the icon. Requires the fonts and the named CSS styles to be preloaded. 31996 * @param {string} md-font-set CSS style name associated with the font library; which will be assigned as 31997 * the class for the font-icon ligature. This value may also be an alias that is used to lookup the classname; 31998 * internally use `$mdIconProvider.fontSet(<alias>)` to determine the style name. 31999 * @param {string} md-svg-src String URL (or expression) used to load, cache, and display an 32000 * external SVG. 32001 * @param {string} md-svg-icon md-svg-icon String name used for lookup of the icon from the internal
32001cache; 32002 * interpolated strings or expressions may also be used. Specific set names can be used with 32003 * the syntax `<set name>:<icon name>`.<br/><br/> 32004 * To use icon sets, developers are required to pre-register the sets using the `$mdIconProvider` service. 32005 * @param {string=} aria-label Labels icon for accessibility. If an empty string is provided, icon 32006 * will be hidden from accessibility layer with `aria-hidden="true"`. If there's no aria-label on the icon 32007 * nor a label on the parent element, a warning will be logged to the console. 32008 * @param {string=} alt Labels icon for accessibility. If an empty string is provided, icon 32009 * will be hidden from accessibility layer with `aria-hidden="true"`. If there's no alt on the icon 32010 * nor a label on the parent element, a warning will be logged to the console. 32011 * 32012 * @usage 32013 * When using SVGs: 32014 * <hljs lang="html"> 32015 * 32016 * <!-- Icon ID; may contain optional icon set prefix; icons must registered using $mdIconProvider --> 32017 * <md-icon md-svg-icon="social:android" aria-label="android " ></md-icon> 32018 * 32019 * <!-- Icon urls; may be preloaded in templateCache --> 32020 * <md-icon md-svg-src="/android.svg" aria-label="android " ></md-icon> 32021 * <md-icon md-svg-src="{{ getAndroid() }}" aria-label="android " ></md-icon> 32022 * 32023 * </hljs> 32024 * 32025 * Use the <code>$mdIconProvider</code> to configure your application with 32026 * svg iconsets. 32027 * 32028 * <hljs lang="js"> 32029 * angular.module('appSvgIconSets', ['ngMaterial']) 32030 * .controller('DemoCtrl', function($scope) {}) 32031 * .config(function($mdIconProvider) { 32032 * $mdIconProvider 32033 * .iconSet('social', 'img/icons/sets/social-icons.svg', 24) 32034 * .defaultIconSet('img/icons/sets/core-icons.svg', 24); 32035 * }); 32036 * </hljs> 32037 * 32038 * 32039 * When using Font Icons with classnames: 32040 * <hljs lang="html"> 32041 * 32042 * <md-icon md-font-icon="android" aria-label="android" ></md-icon> 32043 * <md-icon class="icon_home" aria-label="Home" ></md-icon> 32044 * 32045 * </hljs> 32046 * 32047 * When using Material Font Icons with ligatures: 32048 * <hljs lang="html"> 32049 * <!-- 32050 * For Material Design Icons 32051 * The class '.material-icons' is auto-added if a style has NOT been specified 32052 * since `material-icons` is the default fontset. So your markup: 32053 * --> 32054 * <md-icon> face </md-icon> 32055 * <!-- becomes this at runtime: --> 32056 * <md-icon md-font-set="material-icons"> face </md-icon> 32057 * <!-- If the fontset does not support ligature names, then we need to use the ligature unicode.--> 32058 * <md-icon>  </md-icon> 32059 * <!-- The class '.material-icons' must be manually added if other styles are also specified--> 32060 * <md-icon class="material-icons md-light md-48"> face </md-icon> 32061 * </hljs> 32062 * 32063 * When using other Font-Icon libraries: 32064 * 32065 * <hljs lang="js"> 32066 * // Specify a font-icon style alias 32067 * angular.config(function($mdIconProvider) { 32068 * $mdIconProvider.fontSet('md', 'material-icons'); 32069 * }); 32070 * </hljs> 32071 * 32072 * <hljs lang="html"> 32073 * <md-icon md-font-set="md">favorite</md-icon> 32074 * </hljs> 32075 * 32076 */ 32077function mdIconDirective($mdIcon, $mdTheming, $mdAria, $sce) { 32078 32079 return { 32080 restrict: 'E', 32081 link : postLink 32082 }; 32083 32084 32085 /** 32086 * Directive postLink 32087 * Supports embedded SVGs, font-icons, & external SVGs 32088 */ 32089 function postLink(scope, element, attr) { 32090 $mdTheming(element); 32091 var lastFontIcon = attr.mdFontIcon; 32092 var lastFontSet = $mdIcon.fontSet(attr.mdFontSet); 32093 32094 prepareForFontIcon(); 32095 32096 attr.$observe('mdFontIcon', fontIconChanged); 32097 attr.$observe('mdFontSet', fontIconChanged); 32098 32099 // Keep track of the content of the svg src so we can compare against it later to see if the 32100 // attribute is static (and thus safe). 32101 var originalSvgSrc = element[0].getAttribute(attr.$attr.mdSvgSrc); 32102 32103 // If using a font-icon, then the textual name of the icon itself 32104 // provides the aria-label. 32105 32106 var attrName = attr.$normalize(attr.$attr.mdSvgIcon || attr.$attr.mdSvgSrc || ''); 32107 32108 /* Provide a default accessibility role of img */ 32109 if (!attr.role) { 32110 $mdAria.expect(element, 'role', 'img'); 32111 /* manually update attr variable */ 32112 attr.role = 'img'; 32113 } 32114 32115 /* Don't process ARIA if already valid */ 32116 if ( attr.role === "img" && !attr.ariaHidden && !$mdAria.hasAriaLabel(element) ) { 32117 var iconName; 32118 if (attr.alt) { 32119 /* Use alt text by default if available */ 32120 $mdAria.expect(element, 'aria-label', attr.alt); 32121 } else if ($mdAria.parentHasAriaLabel(element, 2)) { 32122 /* Parent has ARIA so we will assume it will describe the image */ 32123 $mdAria.expect(element, 'aria-hidden', 'true'); 32124 } else if (iconName = (attr.mdFontIcon || attr.mdSvgIcon || element.text())) { 32125 /* Use icon name as aria-label */ 32126 $mdAria.expect(element, 'aria-label', iconName); 32127 } else { 32128 /* No label found */ 32129 $mdAria.expect(element, 'aria-hidden', 'true'); 32130 } 32131 } 32132 32133 if (attrName) { 32134 // Use either pre-configured SVG or URL source, respectively. 32135 attr.$observe(attrName, function(attrVal) { 32136 element.empty(); 32137 if (attrVal) { 32138 $mdIcon(attrVal) 32139 .then(function(svg) { 32140 element.empty(); 32141 element.append(svg); 32142 }); 32143 } 32144 }); 32145 } 32146 32147 function prepareForFontIcon() { 32148 if (!attr.mdSvgIcon && !attr.mdSvgSrc) { 32149 if (attr.mdFontIcon) { 32150 element.addClass('md-font ' + attr.mdFontIcon); 32151 } 32152 32153 element.addClass(lastFontSet); 32154 } 32155 } 32156 32157 function fontIconChanged() { 32158 if (!attr.mdSvgIcon && !attr.mdSvgSrc) { 32159 if (attr.mdFontIcon) { 32160 element.removeClass(lastFontIcon); 32161 element.addClass(attr.mdFontIcon); 32162 32163 lastFontIcon = attr.mdFontIcon; 32164 } 32165 32166 var fontSet = $mdIcon.fontSet(attr.mdFontSet); 32167 32168 if (lastFontSet !== fontSet) { 32169 element.removeClass(lastFontSet); 32170 element.addClass(fontSet); 32171 32172 lastFontSet = fontSet; 32173 } 32174 } 32175 } 32176 } 32177} 32178 32179})(); 32180(function(){ 32181"use strict"; 32182 32183 32184MdIconService.$inject = ["config", "$templateRequest", "$q", "$log", "$mdUtil", "$sce"];angular 32185 .module('material.components.icon') 32186 .constant('$$mdSvgRegistry', { 32187 'mdTabsArrow': 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxnPjxwb2x5Z29uIHBvaW50cz0iMTUuNCw3LjQgMTQsNiA4LDEyIDE0LDE4IDE1LjQsMTYuNiAxMC44LDEyICIvPjwvZz48L3N2Zz4=', 32188 'mdClose': 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxnPjxwYXRoIGQ9Ik0xOSA2LjQxbC0xLjQxLTEuNDEtNS41OSA1LjU5LTUuNTktNS41OS0xLjQxIDEuNDEgNS41OSA1LjU5LTUuNTkgNS41OSAxLjQxIDEuNDEgNS41OS01LjU5IDUuNTkgNS41OSAxLjQxLTEuNDEtNS41OS01LjU5eiIvPjwvZz48L3N2Zz4=', 32189 'mdCancel': 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxnPjxwYXRoIGQ9Ik0xMiAyYy01LjUzIDAtMTAgNC40Ny0xMCAxMHM0LjQ3IDEwIDEwIDEwIDEwLTQuNDcgMTAtMTAtNC40Ny0xMC0xMC0xMHptNSAxMy41OWwtMS40MSAxLjQxLTMuNTktMy41OS0zLjU5IDMuNTktMS40MS0xLjQxIDMuNTktMy41OS0zLjU5LTMuNTkgMS40MS0xLjQxIDMuNTkgMy41OSAzLjU5LTMuNTkgMS40MSAxLjQxLTMuNTkgMy41OSAzLjU5IDMuNTl6Ii8+PC9nPjwvc3ZnPg==', 32190 'mdMenu': 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0zLDZIMjFWOEgzVjZNMywxMUgyMVYxM0gzVjExTTMsMTZIMjFWMThIM1YxNloiIC8+PC9zdmc+', 32191 'mdToggleArrow': 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgNDggNDgiPjxwYXRoIGQ9Ik0yNCAxNmwtMTIgMTIgMi44MyAyLjgzIDkuMTctOS4xNyA5LjE3IDkuMTcgMi44My0yLjgzeiIvPjxwYXRoIGQ9Ik0wIDBoNDh2NDhoLTQ4eiIgZmlsbD0ibm9uZSIvPjwvc3ZnPg==', 32192 'mdCalendar': 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTkgM2gtMVYxaC0ydjJIOFYxSDZ2Mkg1Yy0xLjExIDAtMS45OS45LT
32192EuOTkgMkwzIDE5YzAgMS4xLjg5IDIgMiAyaDE0YzEuMSAwIDItLjkgMi0yVjVjMC0xLjEtLjktMi0yLTJ6bTAgMTZINVY4aDE0djExek03IDEwaDV2NUg3eiIvPjwvc3ZnPg==', 32193 'mdChecked': 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxnPjxwYXRoIGQ9Ik05IDE2LjE3TDQuODMgMTJsLTEuNDIgMS40MUw5IDE5IDIxIDdsLTEuNDEtMS40MXoiLz48L2c+PC9zdmc+' 32194 }) 32195 .provider('$mdIcon', MdIconProvider); 32196 32197/** 32198 * @ngdoc service 32199 * @name $mdIconProvider 32200 * @module material.components.icon 32201 * 32202 * @description 32203 * `$mdIconProvider` is used only to register icon IDs with URLs. These configuration features allow 32204 * icons and icon sets to be pre-registered and associated with source URLs **before** the `<md-icon />` 32205 * directives are compiled. 32206 * 32207 * If using font-icons, the developer is responsible for loading the fonts. 32208 * 32209 * If using SVGs, loading of the actual svg files are deferred to on-demand requests and are loaded 32210 * internally by the `$mdIcon` service using the `$templateRequest` service. When an SVG is 32211 * requested by name/ID, the `$mdIcon` service searches its registry for the associated source URL; 32212 * that URL is used to on-demand load and parse the SVG dynamically. 32213 * 32214 * The `$templateRequest` service expects the icons source to be loaded over trusted URLs.<br/> 32215 * This means, when loading icons from an external URL, you have to trust the URL in the `$sceDelegateProvider`. 32216 * 32217 * <hljs lang="js"> 32218 * app.config(function($sceDelegateProvider) { 32219 * $sceDelegateProvider.resourceUrlWhitelist([ 32220 * // Adding 'self' to the whitelist, will allow requests from the current origin. 32221 * 'self', 32222 * // Using double asterisks here, will allow all URLs to load. 32223 * // We recommend to only specify the given domain you want to allow. 32224 * '**' 32225 * ]); 32226 * }); 32227 * </hljs> 32228 * 32229 * Read more about the [$sceDelegateProvider](https://docs.angularjs.org/api/ng/provider/$sceDelegateProvider). 32230 * 32231 * **Notice:** Most font-icons libraries do not support ligatures (for example `fontawesome`).<br/> 32232 * In such cases you are not able to use the icon's ligature name - Like so: 32233 * 32234 * <hljs lang="html"> 32235 * <md-icon md-font-set="fa">fa-bell</md-icon> 32236 * </hljs> 32237 * 32238 * You should instead use the given unicode, instead of the ligature name. 32239 * 32240 * <p ng-hide="true"> ##// Notice we can't use a hljs element here, because the characters will be escaped.</p> 32241 * ```html 32242 * <md-icon md-font-set="fa"></md-icon> 32243 * ``` 32244 * 32245 * All unicode ligatures are prefixed with the `&#x` string. 32246 * 32247 * @usage 32248 * <hljs lang="js"> 32249 * app.config(function($mdIconProvider) { 32250 * 32251 * // Configure URLs for icons specified by [set:]id. 32252 * 32253 * $mdIconProvider 32254 * .defaultFontSet( 'fa' ) // This sets our default fontset className. 32255 * .defaultIconSet('my/app/icons.svg') // Register a default set of SVG icons 32256 * .iconSet('social', 'my/app/social.svg') // Register a named icon set of SVGs 32257 * .icon('android', 'my/app/android.svg') // Register a specific icon (by name) 32258 * .icon('work:chair', 'my/app/chair.svg'); // Register icon in a specific set 32259 * }); 32260 * </hljs> 32261 * 32262 * SVG icons and icon sets can be easily pre-loaded and cached using either (a) a build process or (b) a runtime 32263 * **startup** process (shown below): 32264 * 32265 * <hljs lang="js"> 32266 * app.config(function($mdIconProvider) { 32267 * 32268 * // Register a default set of SVG icon definitions 32269 * $mdIconProvider.defaultIconSet('my/app/icons.svg') 32270 * 32271 * }) 32272 * .run(function($templateRequest){ 32273 * 32274 * // Pre-fetch icons sources by URL and cache in the $templateCache... 32275 * // subsequent $templateRequest calls will look there first. 32276 * 32277 * var urls = [ 'imy/app/icons.svg', 'img/icons/android.svg']; 32278 * 32279 * angular.forEach(urls, function(url) { 32280 * $templateRequest(url); 32281 * }); 32282 * 32283 * }); 32284 * 32285 * </hljs> 32286 * 32287 * > <b>Note:</b> The loaded SVG data is subsequently cached internally for future requests. 32288 * 32289 */ 32290 32291/** 32292 * @ngdoc method 32293 * @name $mdIconProvider#icon 32294 * 32295 * @description 32296 * Register a source URL for a specific icon name; the name may include optional 'icon set' name prefix. 32297 * These icons will later be retrieved from the cache using `$mdIcon( <icon name> )` 32298 * 32299 * @param {string} id Icon name/id used to register the icon 32300 * @param {string} url specifies the external location for the data file. Used internally by 32301 * `$templateRequest` to load the data or as part of the lookup in `$templateCache` if pre-loading 32302 * was configured. 32303 * @param {number=} viewBoxSize Sets the width and height the icon's viewBox.
32304 * It is ignored for icons with an existing viewBox. Default size is 24. 32305 * 32306 * @returns {obj} an `$mdIconProvider` reference; used to support method call chains for the API 32307 * 32308 * @usage 32309 * <hljs lang="js"> 32310 * app.config(function($mdIconProvider) { 32311 * 32312 * // Configure URLs for icons specified by [set:]id. 32313 * 32314 * $mdIconProvider 32315 * .icon('android', 'my/app/android.svg') // Register a specific icon (by name) 32316 * .icon('work:chair', 'my/app/chair.svg'); // Register icon in a specific set 32317 * }); 32318 * </hljs> 32319 * 32320 */ 32321/** 32322 * @ngdoc method 32323 * @name $mdIconProvider#iconSet 32324 * 32325 * @description 32326 * Register a source URL for a 'named' set of icons; group of SVG definitions where each definition 32327 * has an icon id. Individual icons can be subsequently retrieved from this cached set using 32328 * `$mdIcon(<icon set name>:<icon name>)` 32329 * 32330 * @param {string} id Icon name/id used to register the iconset 32331 * @param {string} url specifies the external location for the data file. Used internally by 32332 * `$templateRequest` to load the data or as part of the lookup in `$templateCache` if pre-loading 32333 * was configured. 32334 * @param {number=} viewBoxSize Sets the width and height of the viewBox of all icons in the set. 32335 * It is ignored for icons with an existing viewBox. All icons in the icon set should be the same size. 32336 * Default value is 24. 32337 * 32338 * @returns {obj} an `$mdIconProvider` reference; used to support method call chains for the API 32339 * 32340 * 32341 * @usage 32342 * <hljs lang="js"> 32343 * app.config(function($mdIconProvider) { 32344 * 32345 * // Configure URLs for icons specified by [set:]id. 32346 * 32347 * $mdIconProvider 32348 * .iconSet('social', 'my/app/social.svg') // Register a named icon set 32349 * }); 32350 * </hljs> 32351 * 32352 */ 32353/** 32354 * @ngdoc method 32355 * @name $mdIconProvider#defaultIconSet 32356 * 32357 * @description 32358 * Register a source URL for the default 'named' set of icons. Unless explicitly registered, 32359 * subsequent lookups of icons will failover to search this 'default' icon set. 32360 * Icon can be retrieved from this cached, default set using `$mdIcon(<name>)` 32361 * 32362 * @param {string} url specifies the external location for the data file. Used internally by 32363 * `$templateRequest` to load the data or as part of the lookup in `$templateCache` if pre-loading 32364 * was configured. 32365 * @param {number=} viewBoxSize Sets the width and height of the viewBox of all icons in the set. 32366 * It is ignored for icons with an existing viewBox. All icons in the icon set should be the same size. 32367 * Default value is 24. 32368 * 32369 * @returns {obj} an `$mdIconProvider` reference; used to support method call chains for the API 32370 * 32371 * @usage 32372 * <hljs lang="js"> 32373 * app.config(function($mdIconProvider) { 32374 * 32375 * // Configure URLs for icons specified by [set:]id. 32376 * 32377 * $mdIconProvider 32378 * .defaultIconSet( 'my/app/social.svg' ) // Register a default icon set 32379 * }); 32380 * </hljs> 32381 * 32382 */ 32383/** 32384 * @ngdoc method 32385 * @name $mdIconProvider#defaultFontSet 32386 * 32387 * @description 32388 * When using Font-Icons, AngularJS Material assumes the the Material Design icons will be used and automatically 32389 * configures the default font-set == 'material-icons'. Note that the font-set references the font-icon library 32390 * class style that should be applied to the `<md-icon>`. 32391 * 32392 * Configuring the default means that the attributes 32393 * `md-font-set="material-icons"` or `class="material-icons"` do not need to be explicitly declared on the 32394 * `<md-icon>` markup. For example: 32395 * 32396 * `<md-icon> face </md-icon>` 32397 * will render as 32398 * `<span class="material-icons"> face </span>`, and 32399 * 32400 * `<md-icon md-font-set="fa"> face </md-icon>` 32401 * will render as 32402 * `<span class="fa"> face </span>` 32403 * 32404 * @param {string} name of the font-library style that should be applied to the md-icon DOM element 32405 * 32406 * @usage 32407 * <hljs lang="js"> 32408 * app.config(function($mdIconProvider) { 32409 * $mdIconProvider.defaultFontSet( 'fa' ); 32410 * }); 32411 * </hljs> 32412 * 32413 */ 32414 32415/** 32416 * @ngdoc method 32417 * @name $mdIconProvider#fontSet 32418 * 32419 * @description 32420 * When using a font set for `<md-icon>` you must specify the correct font classname in the `md-font-set` 32421 * attribute. If the fonset className is really long, your markup may become cluttered... an easy 32422 * solution is to define an `alias` for your fontset: 32423 * 32424 * @param {string} alias of the specified fontset. 32425 * @param {string} className of the fontset. 32426 * 32427 * @usage 32428 * <hljs lang="js"> 32429 * app.config(function($mdIconProvider) { 32430 * // In this case, we set an alias for the `material-icons` fontset. 32431 * $mdIconProvider.fontSet('md', 'material-icons'); 32432 * }); 32433 * </hljs> 32434 * 32435 */ 32436 32437/** 32438 * @ngdoc method 32439 * @name $mdIconProvider#defaultViewBoxSize 32440 * 32441 * @description 32442 * While `<md-icon />` markup can also be style with sizing CSS, this method configures 32443 * the default width **and** height used for all icons; unless overridden by specific CSS. 32444 * The default sizing is (24px, 24px). 32445 * @param {number=} viewBoxSize Sets the width and height of the viewBox for an icon or an icon set. 32446 * All icons in a set should be the same size. The default value is 24. 32447 * 32448 * @returns {obj} an `$mdIconProvider` reference; used to support method call chains for the API 32449 * 32450 * @usage 32451 * <hljs lang="js"> 32452 * app.config(function($mdIconProvider) { 32453 * 32454 * // Configure URLs for icons specified by [set:]id. 32455 * 32456 * $mdIconProvider 32457 * .defaultViewBoxSize(36) // Register a default icon size (width == height) 32458 * }); 32459 * </hljs> 32460 * 32461 */ 32462 32463var config = { 32464 defaultViewBoxSize: 24, 32465 defaultFontSet: 'material-icons', 32466 fontSets: [] 32467}; 32468 32469function MdIconProvider() { 32470} 32471 32472MdIconProvider.prototype = { 32473 icon: function(id, url, viewBoxSize) { 32474 if (id.indexOf(':') == -1) id = '$default:' + id; 32475 32476 config[id] = new ConfigurationItem(url, viewBoxSize); 32477 return this; 32478 }, 32479 32480 iconSet: function(id, url, viewBoxSize) { 32481 config[id] = new ConfigurationItem(url, viewBoxSize); 32482 return this; 32483 }, 32484 32485 defaultIconSet: function(url, viewBoxSize) { 32486 var setName = '$default'; 32487 32488 if (!config[setName]) { 32489 config[setName] = new ConfigurationItem(url, viewBoxSize); 32490 } 32491 32492 config[setName].viewBoxSize = viewBoxSize || config.defaultViewBoxSize; 32493 32494 return this; 32495 }, 32496 32497 defaultViewBoxSize: function(viewBoxSize) { 32498 config.defaultViewBoxSize = viewBoxSize; 32499 return this; 32500 }, 32501 32502 /** 32503 * Register an alias name associated with a font-icon library style ; 32504 */ 32505 fontSet: function fontSet(alias, className) { 32506 config.fontSets.push({ 32507 alias: alias,
vendor: 17,625 bytes, lines 32508-33070
32508 fontSet: className || alias 32509 }); 32510 return this; 32511 }, 32512 32513 /** 32514 * Specify a default style name associated with a font-icon library 32515 * fallback to Material Icons. 32516 * 32517 */ 32518 defaultFontSet: function defaultFontSet(className) { 32519 config.defaultFontSet = !className ? '' : className; 32520 return this; 32521 }, 32522 32523 defaultIconSize: function defaultIconSize(iconSize) { 32524 config.defaultIconSize = iconSize; 32525 return this; 32526 }, 32527 32528 $get: ['$templateRequest', '$q', '$log', '$mdUtil', '$sce', function($templateRequest, $q, $log, $mdUtil, $sce) { 32529 return MdIconService(config, $templateRequest, $q, $log, $mdUtil, $sce); 32530 }] 32531}; 32532 32533/** 32534 * Configuration item stored in the Icon registry; used for lookups 32535 * to load if not already cached in the `loaded` cache 32536 */ 32537function ConfigurationItem(url, viewBoxSize) { 32538 this.url = url; 32539 this.viewBoxSize = viewBoxSize || config.defaultViewBoxSize; 32540} 32541 32542/** 32543 * @ngdoc service 32544 * @name $mdIcon 32545 * @module material.components.icon 32546 * 32547 * @description 32548 * The `$mdIcon` service is a function used to lookup SVG icons. 32549 * 32550 * @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 32551 * 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 32552 * performed. The Id may be a unique icon ID or may include an iconSet ID prefix. 32553 * 32554 * For the **id** query to work properly, this means that all id-to-URL mappings must have been previously configured 32555 * using the `$mdIconProvider`. 32556 * 32557 * @returns {angular.$q.Promise} A promise that gets resolved to a clone of the initial SVG DOM element; which was 32558 * created from the SVG markup in the SVG data file. If an error occurs (e.g. the icon cannot be found) the promise 32559 * will get rejected. 32560 * 32561 * @usage 32562 * <hljs lang="js"> 32563 * function SomeDirective($mdIcon) { 32564 * 32565 * // See if the icon has already been loaded, if not 32566 * // then lookup the icon from the registry cache, load and cache 32567 * // it for future requests. 32568 * // NOTE: ID queries require configuration with $mdIconProvider 32569 * 32570 * $mdIcon('android').then(function(iconEl) { element.append(iconEl); }); 32571 * $mdIcon('work:chair').then(function(iconEl) { element.append(iconEl); }); 32572 * 32573 * // Load and cache the external SVG using a URL 32574 * 32575 * $mdIcon('img/icons/android.svg').then(function(iconEl) { 32576 * element.append(iconEl); 32577 * }); 32578 * }; 32579 * </hljs> 32580 * 32581 * > <b>Note:</b> The `<md-icon>` directive internally uses the `$mdIcon` service to query, loaded, 32582 * and instantiate SVG DOM elements. 32583 */ 32584 32585/* @ngInject */ 32586function MdIconService(config, $templateRequest, $q, $log, $mdUtil, $sce) { 32587 var iconCache = {}; 32588 var svgCache = {}; 32589 var urlRegex = /[-\w@:%\+.~#?&//=]{2,}\.[a-z]{2,4}\b(\/[-\w@:%\+.~#?&//=]*)?/i; 32590 var dataUrlRegex = /^data:image\/svg\+xml[\s*;\w\-\=]*?(base64)?,(.*)$/i; 32591 32592 Icon.prototype = {clone: cloneSVG, prepare: prepareAndStyle}; 32593 getIcon.fontSet = findRegisteredFontSet; 32594 32595 // Publish service... 32596 return getIcon; 32597 32598 /** 32599 * Actual $mdIcon service is essentially a lookup function 32600 */ 32601 function getIcon(id) { 32602 id = id || ''; 32603 32604 // If the "id" provided is not a string, the only other valid value is a $sce trust wrapper 32605 // over a URL string. If the value is not trusted, this will intentionally throw an error 32606 // because the user is attempted to use an unsafe URL, potentially opening themselves up 32607 // to an XSS attack. 32608 if (!angular.isString(id)) { 32609 id = $sce.getTrustedUrl(id); 32610 } 32611 32612 // If already loaded and cached, use a clone of the cached icon. 32613 // Otherwise either load by URL, or lookup in the registry and then load by URL, and cache. 32614 32615 if (iconCache[id]) { 32616 return $q.when(transformClone(iconCache[id])); 32617 } 32618 32619 if (urlRegex.test(id) || dataUrlRegex.test(id)) { 32620 return loadByURL(id).then(cacheIcon(id)); 32621 } 32622 32623 if (id.indexOf(':') == -1) { 32624 id = '$default:' + id; 32625 } 32626 32627 var load = config[id] ? loadByID : loadFromIconSet; 32628 return load(id) 32629 .then(cacheIcon(id)); 32630 } 32631 32632 /** 32633 * Lookup registered fontSet style using its alias... 32634 * If not found, 32635 */ 32636 function findRegisteredFontSet(alias) { 32637 var useDefault = angular.isUndefined(alias) || !(alias && alias.length); 32638 if (useDefault) return config.defaultFontSet; 32639 32640 var result = alias; 32641 angular.forEach(config.fontSets, function(it) { 32642 if (it.alias == alias) result = it.fontSet || result; 32643 }); 32644 32645 return result; 32646 } 32647 32648 function transformClone(cacheElement) { 32649 var clone = cacheElement.clone(); 32650 var cacheSuffix = '_cache' + $mdUtil.nextUid(); 32651 32652 // We need to modify for each cached icon the id attributes. 32653 // This is needed because SVG id's are treated as normal DOM ids 32654 // and should not have a duplicated id. 32655 if (clone.id) clone.id += cacheSuffix; 32656 angular.forEach(clone.querySelectorAll('[id]'), function(item) { 32657 item.id += cacheSuffix; 32658 }); 32659 32660 return clone; 32661 } 32662 32663 /** 32664 * Prepare and cache the loaded icon for the specified `id` 32665 */ 32666 function cacheIcon(id) { 32667 32668 return function updateCache(icon) { 32669 iconCache[id] = isIcon(icon) ? icon : new Icon(icon, config[id]); 32670 32671 return iconCache[id].clone(); 32672 }; 32673 } 32674 32675 /** 32676 * Lookup the configuration in the registry, if !registered throw an error 32677 * otherwise load the icon [on-demand] using the registered URL. 32678 * 32679 */ 32680 function loadByID(id) { 32681 var iconConfig = config[id]; 32682 return loadByURL(iconConfig.url).then(function(icon) { 32683 return new Icon(icon, iconConfig); 32684 }); 32685 } 32686 32687 /** 32688 * Loads the file as XML and uses querySelector( <id> ) to find 32689 * the desired node... 32690 */ 32691 function loadFromIconSet(id) { 32692 var setName = id.substring(0, id.lastIndexOf(':')) || '$default'; 32693 var iconSetConfig = config[setName]; 32694 32695 return !iconSetConfig ? announceIdNotFound(id) : loadByURL(iconSetConfig.url).then(extractFromSet); 32696 32697 function extractFromSet(set) { 32698 var iconName = id.slice(id.lastIndexOf(':') + 1); 32699 var icon = set.querySelector('#' + iconName); 32700 return icon ? new Icon(icon, iconSetConfig) : announceIdNotFound(id); 32701 } 32702 32703 function announceIdNotFound(id) { 32704 var msg = 'icon ' + id + ' not found'; 32705 $log.warn(msg); 32706 32707 return $q.reject(msg || id); 32708 } 32709 } 32710 32711 /** 32712 * Load the icon by URL (may use the $templateCache). 32713 * Extract the data for later conversion to Icon 32714 */ 32715 function loadByURL(url) { 32716 /* Load the icon from embedded data URL. */ 32717 function loadByDataUrl(url) { 32718 var results = dataUrlRegex.exec(url); 32719 var isBase64 = /base64/i.test(url); 32720 var data = isBase64 ? window.atob(results[2]) : results[2]; 32721 32722 return $q.when(angular.element(data)[0]); 32723 } 32724 32725 /* Load the icon by URL using HTTP. */ 32726 function loadByHttpUrl(url) { 32727 return $q(function(resolve, reject) { 32728 // Catch HTTP or generic errors not related to incorrect icon IDs. 32729 var announceAndReject = function(err) { 32730 var msg = angular.isString(err) ? err : (err.message || err.data || err.statusText); 32731 $log.warn(msg); 32732 reject(err); 32733 }, 32734 extractSvg = function(response) { 32735 if (!svgCache[url]) { 32736 svgCache[url] = angular.element('<div>').append(response)[0].querySelector('svg'); 32737 } 32738 resolve(svgCache[url]); 32739 }; 32740 32741 $templateRequest(url, true).then(extractSvg, announceAndReject); 32742 }); 32743 } 32744 32745 return dataUrlRegex.test(url) 32746 ? loadByDataUrl(url) 32747 : loadByHttpUrl(url); 32748 } 32749 32750 /** 32751 * Check target signature to see if it is an Icon instance. 32752 */ 32753 function isIcon(target) { 32754 return angular.isDefined(target.element) && angular.isDefined(target.config); 32755 } 32756 32757 /** 32758 * Define the Icon class 32759 */ 32760 function Icon(el, config) { 32761 if (el && el.tagName != 'svg') { 32762 el = angular.element('<svg xmlns="http://www.w3.org/2000/svg">').append(el.cloneNode(true))[0]; 32763 } 32764 32765 // Inject the namespace if not available... 32766 if (!el.getAttribute('xmlns')) { 32767 el.setAttribute('xmlns', "http://www.w3.org/2000/svg"); 32768 } 32769 32770 this.element = el; 32771 this.config = config; 32772 this.prepare(); 32773 } 32774 32775 /** 32776 * Prepare the DOM element that will be cached in the 32777 * loaded iconCache store. 32778 */ 32779 function prepareAndStyle() { 32780 var viewBoxSize = this.config ? this.config.viewBoxSize : config.defaultViewBoxSize; 32781 angular.forEach({ 32782 'fit': '', 32783 'height': '100%', 32784 'width': '100%', 32785 'preserveAspectRatio': 'xMidYMid meet', 32786 'viewBox': this.element.getAttribute('viewBox') || ('0 0 ' + viewBoxSize + ' ' + viewBoxSize), 32787 'focusable': false // Disable IE11s default behavior to make SVGs focusable 32788 }, function(val, attr) { 32789 this.element.setAttribute(attr, val); 32790 }, this); 32791 } 32792 32793 /** 32794 * Clone the Icon DOM element. 32795 */ 32796 function cloneSVG() { 32797 // If the element or any of its children have a style attribute, then a CSP policy without 32798 // 'unsafe-inline' in the style-src directive, will result in a violation. 32799 return this.element.cloneNode(true); 32800 } 32801 32802} 32803 32804})(); 32805(function(){ 32806"use strict"; 32807 32808 32809 32810MenuController.$inject = ["$mdMenu", "$attrs", "$element", "$scope", "$mdUtil", "$timeout", "$rootScope", "$q", "$log"]; 32811angular 32812 .module('material.components.menu') 32813 .controller('mdMenuCtrl', MenuController); 32814 32815/** 32816 * @ngInject 32817 */ 32818function MenuController($mdMenu, $attrs, $element, $scope, $mdUtil, $timeout, $rootScope, $q, $log) { 32819 32820 var prefixer = $mdUtil.prefixer(); 32821 var menuContainer; 32822 var self = this; 32823 var triggerElement; 32824 32825 this.nestLevel = parseInt($attrs.mdNestLevel, 10) || 0; 32826 32827 /** 32828 * Called by our linking fn to provide access to the menu-content 32829 * element removed during link 32830 */ 32831 this.init = function init(setMenuContainer, opts) { 32832 opts = opts || {}; 32833 menuContainer = setMenuContainer; 32834 32835 // Default element for ARIA attributes has the ngClick or ngMouseenter expression 32836 triggerElement = $element[0].querySelector(prefixer.buildSelector(['ng-click', 'ng-mouseenter'])); 32837 triggerElement.setAttribute('aria-expanded', 'false'); 32838 32839 this.isInMenuBar = opts.isInMenuBar; 32840 this.nestedMenus = $mdUtil.nodesToArray(menuContainer[0].querySelectorAll('.md-nested-menu')); 32841 32842 menuContainer.on('$mdInterimElementRemove', function() { 32843 self.isOpen = false; 32844 $mdUtil.nextTick(function(){ self.onIsOpenChanged(self.isOpen);}); 32845 }); 32846 $mdUtil.nextTick(function(){ self.onIsOpenChanged(self.isOpen);}); 32847 32848 var menuContainerId = 'menu_container_' + $mdUtil.nextUid(); 32849 menuContainer.attr('id', menuContainerId); 32850 angular.element(triggerElement).attr({ 32851 'aria-owns': menuContainerId, 32852 'aria-haspopup': 'true' 32853 }); 32854 32855 $scope.$on('$destroy', angular.bind(this, function() { 32856 this.disableHoverListener(); 32857 $mdMenu.destroy(); 32858 })); 32859 32860 menuContainer.on('$destroy', function() { 32861 $mdMenu.destroy(); 32862 }); 32863 }; 32864 32865 var openMenuTimeout, menuItems, deregisterScopeListeners = []; 32866 this.enableHoverListener = function() { 32867 deregisterScopeListeners.push($rootScope.$on('$mdMenuOpen', function(event, el) { 32868 if (menuContainer[0].contains(el[0])) { 32869 self.currentlyOpenMenu = el.controller('mdMenu'); 32870 self.isAlreadyOpening = false; 32871 self.currentlyOpenMenu.registerContainerProxy(self.triggerContainerProxy.bind(self)); 32872 } 32873 })); 32874 deregisterScopeListeners.push($rootScope.$on('$mdMenuClose', function(event, el) { 32875 if (menuContainer[0].contains(el[0])) { 32876 self.currentlyOpenMenu = undefined; 32877 } 32878 })); 32879 menuItems = angular.element($mdUtil.nodesToArray(menuContainer[0].children[0].children)); 32880 menuItems.on('mouseenter', self.handleMenuItemHover); 32881 menuItems.on('mouseleave', self.handleMenuItemMouseLeave); 32882 }; 32883 32884 this.disableHoverListener = function() { 32885 while (deregisterScopeListeners.length) { 32886 deregisterScopeListeners.shift()(); 32887 } 32888 menuItems && menuItems.off('mouseenter', self.handleMenuItemHover); 32889 menuItems && menuItems.off('mouseleave', self.handleMenuItemMouseLeave); 32890 }; 32891 32892 this.handleMenuItemHover = function(event) { 32893 if (self.isAlreadyOpening) return; 32894 var nestedMenu = ( 32895 event.target.querySelector('md-menu') 32896 || $mdUtil.getClosest(event.target, 'MD-MENU') 32897 ); 32898 openMenuTimeout = $timeout(function() { 32899 if (nestedMenu) { 32900 nestedMenu = angular.element(nestedMenu).controller('mdMenu'); 32901 } 32902 32903 if (self.currentlyOpenMenu && self.currentlyOpenMenu != nestedMenu) { 32904 var closeTo = self.nestLevel + 1; 32905 self.currentlyOpenMenu.close(true, { closeTo: closeTo }); 32906 self.isAlreadyOpening = !!nestedMenu; 32907 nestedMenu && nestedMenu.open(); 32908 } else if (nestedMenu && !nestedMenu.isOpen && nestedMenu.open) { 32909 self.isAlreadyOpening = !!nestedMenu; 32910 nestedMenu && nestedMenu.open(); 32911 } 32912 }, nestedMenu ? 100 : 250); 32913 var focusableTarget = event.currentTarget.querySelector('.md-button:not([disabled])'); 32914 focusableTarget && focusableTarget.focus(); 32915 }; 32916 32917 this.handleMenuItemMouseLeave = function() { 32918 if (openMenuTimeout) { 32919 $timeout.cancel(openMenuTimeout); 32920 openMenuTimeout = undefined; 32921 } 32922 }; 32923 32924 32925 /** 32926 * Uses the $mdMenu interim element service to open the menu contents 32927 */ 32928 this.open = function openMenu(ev) { 32929 ev && ev.stopPropagation(); 32930 ev && ev.preventDefault(); 32931 if (self.isOpen) return; 32932 self.enableHoverListener(); 32933 self.isOpen = true; 32934 $mdUtil.nextTick(function(){ self.onIsOpenChanged(self.isOpen);}); 32935 triggerElement = triggerElement || (ev ? ev.target : $element[0]); 32936 triggerElement.setAttribute('aria-expanded', 'true'); 32937 $scope.$emit('$mdMenuOpen', $element); 32938 $mdMenu.show({ 32939 scope: $scope, 32940 mdMenuCtrl: self, 32941 nestLevel: self.nestLevel, 32942 element: menuContainer, 32943 target: triggerElement, 32944 preserveElement: true, 32945 parent: 'body' 32946 }).finally(function() { 32947 triggerElement.setAttribute('aria-expanded', 'false'); 32948 self.disableHoverListener(); 32949 }); 32950 }; 32951 32952 this.onIsOpenChanged = function(isOpen) { 32953 if (isOpen) { 32954 menuContainer.attr('aria-hidden', 'false'); 32955 $element[0].classList.add('md-open'); 32956 angular.forEach(self.nestedMenus, function(el) { 32957 el.classList.remove('md-open'); 32958 }); 32959 } else { 32960 menuContainer.attr('aria-hidden', 'true'); 32961 $element[0].classList.remove('md-open'); 32962 } 32963 $scope.$mdMenuIsOpen = self.isOpen; 32964 }; 32965 32966 this.focusMenuContainer = function focusMenuContainer() { 32967 var focusTarget = menuContainer[0] 32968 .querySelector(prefixer.buildSelector(['md-menu-focus-target', 'md-autofocus'])); 32969 32970 if (!focusTarget) focusTarget = menuContainer[0].querySelector('.md-button:not([disabled])'); 32971 focusTarget.focus(); 32972 }; 32973 32974 this.registerContainerProxy = function registerContainerProxy(handler) { 32975 this.containerProxy = handler; 32976 }; 32977 32978 this.triggerContainerProxy = function triggerContainerProxy(ev) { 32979 this.containerProxy && this.containerProxy(ev); 32980 }; 32981 32982 this.destroy = function() { 32983 return self.isOpen ? $mdMenu.destroy() : $q.when(false); 32984 }; 32985 32986 // Use the $mdMenu interim element service to close the menu contents 32987 this.close = function closeMenu(skipFocus, closeOpts) { 32988 if ( !self.isOpen ) return; 32989 self.isOpen = false; 32990 $mdUtil.nextTick(function(){ self.onIsOpenChanged(self.isOpen);}); 32991 32992 var eventDetails = angular.extend({}, closeOpts, { skipFocus: skipFocus }); 32993 $scope.$emit('$mdMenuClose', $element, eventDetails); 32994 $mdMenu.hide(null, closeOpts); 32995 32996 if (!skipFocus) { 32997 var el = self.restoreFocusTo || $element.find('button')[0]; 32998 if (el instanceof angular.element) el = el[0]; 32999 if (el) el.focus(); 33000 } 33001 }; 33002 33003 /** 33004 * Build a nice object out of our string attribute which specifies the 33005 * target mode for left and top positioning 33006 */ 33007 this.positionMode = function positionMode() { 33008 var attachment = ($attrs.mdPositionMode || 'target').split(' '); 33009 33010 // If attachment is a single item, duplicate it for our second value. 33011 // ie. 'target' -> 'target target' 33012 if (attachment.length == 1) { 33013 attachment.push(attachment[0]); 33014 } 33015 33016 return { 33017 left: attachment[0], 33018 top: attachment[1] 33019 }; 33020 }; 33021 33022 /** 33023 * Build a nice object out of our string attribute which specifies 33024 * the offset of top and left in pixels. 33025 */ 33026 this.offsets = function offsets() { 33027 var position = ($attrs.mdOffset || '0 0').split(' ').map(parseFloat); 33028 if (position.length == 2) { 33029 return { 33030 left: position[0], 33031 top: position[1] 33032 }; 33033 } else if (position.length == 1) { 33034 return { 33035 top: position[0], 33036 left: position[0] 33037 }; 33038 } else { 33039 throw Error('Invalid offsets specified. Please follow format <x, y> or <n>'); 33040 } 33041 }; 33042 33043 // Functionality that is exposed in the view. 33044 $scope.$mdMenu = { 33045 open: this.open, 33046 close: this.close 33047 }; 33048 33049 // Deprecated APIs 33050 $scope.$mdOpenMenu = angular.bind(this, function() { 33051 $log.warn('mdMenu: The $mdOpenMenu method is deprecated. Please use `$mdMenu.open`.'); 33052 return this.open.apply(this, arguments); 33053 }); 33054} 33055 33056})(); 33057(function(){ 33058"use strict"; 33059 33060/** 33061 * @ngdoc directive 33062 * @name mdMenu 33063 * @module material.components.menu 33064 * @restrict E 33065 * @description 33066 * 33067 * Menus are elements that open when clicked. They are useful for displaying 33068 * additional options within the context of an action. 33069 * 33070 * Every `md-menu` must specify exactly two child elements. The first element is
33070what is 33071 * left in the DOM and is used to open the menu. This element is called the trigger element. 33072 * The trigger element's scope has access to `$mdMenu.open($event)` 33073 * which it may call to open the menu. By passing $event as argument, the 33074 * corresponding event is stopped from propagating up the DOM-tree. Similarly, `$mdMenu.close()` 33075 * can be used to close the menu. 33076 * 33077 * The second element is the `md-menu-content` element which represents the 33078 * contents of the menu when it is open. Typically this will contain `md-menu-item`s, 33079 * but you can do custom content as well. 33080 * 33081 * <hljs lang="html"> 33082 * <md-menu> 33083 * <!-- Trigger element is a md-button with an icon --> 33084 * <md-button ng-click="$mdMenu.open($event)" class="md-icon-button" aria-label="Open sample menu"> 33085 * <md-icon md-svg-icon="call:phone"></md-icon> 33086 * </md-button> 33087 * <md-menu-content> 33088 * <md-menu-item><md-button ng-click="doSomething()">Do Something</md-button></md-menu-item> 33089 * </md-menu-content> 33090 * </md-menu> 33091 * </hljs> 33092 33093 * ## Sizing Menus 33094 * 33095 * The width of the menu when it is open may be specified by specifying a `width` 33096 * attribute on the `md-menu-content` element. 33097 * See the [Material Design Spec](https://material.google.com/components/menus.html#menus-simple-menus) 33098 * for more information. 33099 * 33100 * 33101 * ## Aligning Menus 33102 * 33103 * When a menu opens, it is important that the content aligns with the trigger element. 33104 * Failure to align menus can result in jarring experiences for users as content 33105 * suddenly shifts. To help with this, `md-menu` provides serveral APIs to help 33106 * with alignment. 33107 * 33108 * ### Target Mode 33109 * 33110 * By default, `md-menu` will attempt to align the `md-menu-content` by aligning 33111 * designated child elements in both the trigger and the menu content. 33112 * 33113 * To specify the alignment element in the `trigger` you can use the `md-menu-origin` 33114 * attribute on a child element. If no `md-menu-origin` is specified, the `md-menu` 33115 * will be used as the origin element. 33116 * 33117 * Similarly, the `md-menu-content` may specify a `md-menu-align-target` for a 33118 * `md-menu-item` to specify the node that it should try and align with. 33119 * 33120 * In this example code, we specify an icon to be our origin element, and an 33121 * icon in our menu content to be our alignment target. This ensures that both 33122 * icons are aligned when the menu opens. 33123 * 33124 * <hljs lang="html"> 33125 * <md-menu> 33126 * <md-button ng-click="$mdMenu.open($event)" class="md-icon-button" aria-label="Open some menu"> 33127 * <md-icon md-menu-origin md-svg-icon="call:phone"></md-icon> 33128 * </md-button> 33129 * <md-menu-content> 33130 * <md-menu-item> 33131 * <md-button ng-click="doSomething()" aria-label="Do something"> 33132 * <md-icon md-menu-align-target md-svg-icon="call:phone"></md-icon> 33133 * Do Something 33134 * </md-button> 33135 * </md-menu-item> 33136 * </md-menu-content> 33137 * </md-menu> 33138 * </hljs> 33139 * 33140 * Sometimes we want to specify alignment on the right side of an element, for example 33141 * if we have a menu on the right side a toolbar, we want to right align our menu content. 33142 * 33143 * We can specify the origin by using the `md-position-mode` attribute on both 33144 * the `x` and `y` axis. Right now only the `x-axis` has more than one option. 33145 * You may specify the default mode of `target target` or 33146 * `target-right target` to specify a right-oriented alignment target. See the 33147 * position section of the demos for more examples. 33148 * 33149 * ### Menu Offsets 33150 * 33151 * It is sometimes unavoidable to need to have a deeper level of control for 33152 * the positioning of a menu to ensure perfect alignment. `md-menu` provides 33153 * the `md-offset` attribute to allow pixel level specificty of adjusting the 33154 * exact positioning. 33155 * 33156 * This offset is provided in the format of `x y` or `n` where `n` will be used 33157 * in both the `x` and `y` axis. 33158 * 33159 * For example, to move a menu by `2px` down from the top, we can use: 33160 * <hljs lang="html"> 33161 * <md-menu md-offset="0 2"> 33162 * <!-- menu-content --> 33163 * </md-menu> 33164 * </hljs> 33165 * 33166 * ### Auto Focus 33167 * By default, when a menu opens, `md-menu` focuses the first button in the menu content. 33168 * 33169 * But sometimes you would like to focus another specific menu item instead of the first.<br/> 33170 * This can be done by applying the `md-autofocus` directive on the given element. 33171 * 33172 * <hljs lang="html"> 33173 * <md-menu-item> 33174 * <md-button md-autofocus ng-click="doSomething()"> 33175 * Auto Focus 33176 * </md-button> 33177 * </md-menu-item> 33178 * </hljs> 33179 * 33180 * 33181 * ### Preventing close 33182 * 33183 * Sometimes you would like to be able to click on a menu item without having the menu 33184 * close. To do this, AngularJS Material exposes the `md-prevent-menu-close` attribute which 33185 * can be added to a button inside a menu to stop the menu from automatically closing. 33186 * You can then close the menu either by using `$mdMenu.close()` in the template, 33187 * or programatically by injecting `$mdMenu` and calling `$mdMenu.hide()`. 33188 * 33189 * <hljs lang="html"> 33190 * <md-menu-content ng-mouseleave="$mdMenu.close()"> 33191 * <md-menu-item>
33192 * <md-button ng-click="doSomething()" aria-label="Do something" md-prevent-menu-close="md-prevent-menu-close"> 33193 * <md-icon md-menu-align-target md-svg-icon="call:phone"></md-icon> 33194 * Do Something 33195 * </md-button> 33196 * </md-menu-item> 33197 * </md-menu-content> 33198 * </hljs> 33199 * 33200 * @usage 33201 * <hljs lang="html"> 33202 * <md-menu> 33203 * <md-button ng-click="$mdMenu.open($event)" class="md-icon-button"> 33204 * <md-icon md-svg-icon="call:phone"></md-icon> 33205 * </md-button> 33206 * <md-menu-content> 33207 * <md-menu-item><md-button ng-click="doSomething()">Do Something</md-button></md-menu-item> 33208 * </md-menu-content> 33209 * </md-menu> 33210 * </hljs> 33211 * 33212 * @param {string} md-position-mode The position mode in the form of 33213 * `x`, `y`. Default value is `target`,`target`. Right now the `x` axis 33214 * also supports `target-right`. 33215 * @param {string} md-offset An offset to apply to the dropdown after positioning 33216 * `x`, `y`. Default value is `0`,`0`. 33217 * 33218 */ 33219 33220MenuDirective.$inject = ["$mdUtil"]; 33221angular 33222 .module('material.components.menu') 33223 .directive('mdMenu', MenuDirective); 33224 33225/** 33226 * @ngInject 33227 */ 33228function MenuDirective($mdUtil) { 33229 var INVALID_PREFIX = 'Invalid HTML for md-menu: '; 33230 return { 33231 restrict: 'E', 33232 require: ['mdMenu', '?^mdMenuBar'], 33233 controller: 'mdMenuCtrl', // empty function to be built by link 33234 scope: true, 33235 compile: compile 33236 }; 33237 33238 function compile(templateElement) { 33239 templateElement.addClass('md-menu'); 33240 33241 var triggerEl = templateElement.children()[0]; 33242 var contentEl = templateElement.children()[1]; 33243 33244 var prefixer = $mdUtil.prefixer(); 33245 33246 if (!prefixer.hasAttribute(triggerEl, 'ng-click')) { 33247 triggerEl = triggerEl 33248 .querySelector(prefixer.buildSelector(['ng-click', 'ng-mouseenter'])) || triggerEl; 33249 } 33250 33251 var isButtonTrigger = triggerEl.nodeName === 'MD-BUTTON' || triggerEl.nodeName === 'BUTTON'; 33252 33253 if (triggerEl && isButtonTrigger && !triggerEl.hasAttribute('type')) { 33254 triggerEl.setAttribute('type', 'button'); 33255 } 33256 33257 if (!triggerEl) { 33258 throw Error(INVALID_PREFIX + 'Expected the menu to have a trigger element.'); 33259 } 33260 33261 if (!contentEl || contentEl.nodeName !== 'MD-MENU-CONTENT') { 33262 throw Error(INVALID_PREFIX + 'Expected the menu to contain a `md-menu-content` element.'); 33263 } 33264 33265 // Default element for ARIA attributes has the ngClick or ngMouseenter expression 33266 triggerEl && triggerEl.setAttribute('aria-haspopup', 'true'); 33267 33268 var nestedMenus = templateElement[0].querySelectorAll('md-menu'); 33269 var nestingDepth = parseInt(templateElement[0].getAttribute('md-nest-level'), 10) || 0; 33270 if (nestedMenus) { 33271 angular.forEach($mdUtil.nodesToArray(nestedMenus), function(menuEl) { 33272 if (!menuEl.hasAttribute('md-position-mode')) { 33273 menuEl.setAttribute('md-position-mode', 'cascade'); 33274 } 33275 menuEl.classList.add('_md-nested-menu'); 33276 menuEl.setAttribute('md-nest-level', nestingDepth + 1); 33277 }); 33278 } 33279 return link; 33280 } 33281 33282 function link(scope, element, attr, ctrls) { 33283 var mdMenuCtrl = ctrls[0]; 33284 var isInMenuBar = !!ctrls[1]; 33285 // Move everything into a md-menu-container and pass it to the controller 33286 var menuContainer = angular.element( '<div class="_md md-open-menu-container md-whiteframe-z2"></div>'); 33287 var menuContents = element.children()[1]; 33288 33289 element.addClass('_md'); // private md component indicator for styling 33290 33291 if (!menuContents.hasAttribute('role')) { 33292 menuContents.setAttribute('role', 'menu'); 33293 } 33294 menuContainer.append(menuContents); 33295 33296 element.on('$destroy', function() { 33297 menuContainer.remove(); 33298 }); 33299 33300 element.append(menuContainer); 33301 menuContainer[0].style.display = 'none'; 33302 mdMenuCtrl.init(menuContainer, { isInMenuBar: isInMenuBar }); 33303 33304 } 33305} 33306 33307})(); 33308(function(){ 33309"use strict"; 33310 33311 33312MenuProvider.$inject = ["$$interimElementProvider"];angular 33313 .module('material.components.menu') 33314 .provider('$mdMenu', MenuProvider); 33315 33316/* 33317 * Interim element provider for the menu. 33318 * Handles behavior for a menu while it is open, including: 33319 * - handling animating the menu opening/closing 33320 * - handling key/mouse events on the menu element 33321 * - handling enabling/disabling scroll while the menu is open 33322 * - handling redrawing during resizes and orientation changes 33323 * 33324 */ 33325 33326function MenuProvider($$interimElementProvider) { 33327 menuDefaultOptions.$inject = ["$mdUtil", "$mdTheming", "$mdConstant", "$document", "$window", "$q", "$$rAF", "$animateCss", "$animate", "$log"]; 33328 var MENU_EDGE_MARGIN = 8; 33329 33330 return $$interimElementProvider('$mdMenu') 33331 .setDefaults({ 33332 methods: ['target'], 33333 options: menuDefaultOptions 33334 }); 33335 33336 /* @ngInject */ 33337 function menuDefaultOptions($mdUtil, $mdTheming, $mdConstant, $document, $window, $q, $$rAF, 33338 $animateCss, $animate, $log) { 33339 33340 var prefixer = $mdUtil.prefixer(); 33341 var animator = $mdUtil.dom.animator; 33342 33343 return { 33344 parent: 'body', 33345 onShow: onShow, 33346 onRemove: onRemove, 33347 hasBackdrop: true, 33348 disableParentScroll: true, 33349 skipCompile: true, 33350 preserveScope: true, 33351 multiple: true, 33352 themable: true 33353 }; 33354 33355 /** 33356 * Show modal backdrop element... 33357 * @returns {function(): void} A function that removes this backdrop 33358 */ 33359 function showBackdrop(scope, element, options) { 33360 if (options.nestLevel) return angular.noop; 33361 33362 // If we are not within a dialog... 33363 if (options.disableParentScroll && !$mdUtil.getClosest(options.target, 'MD-DIALOG')) { 33364 // !! DO this before creating the backdrop; since disableScrollAround() 33365 // configures the scroll offset; which is used by mdBackDrop postLink() 33366 options.restoreScroll = $mdUtil.disableScrollAround(options.element, options.parent); 33367 } else { 33368 options.disableParentScroll = false;
vendor: 26,687 bytes, lines 33369-34176
33369 } 33370 33371 if (options.hasBackdrop) { 33372 options.backdrop = $mdUtil.createBackdrop(scope, "md-menu-backdrop md-click-catcher"); 33373 33374 $animate.enter(options.backdrop, $document[0].body); 33375 } 33376 33377 /** 33378 * Hide and destroys the backdrop created by showBackdrop() 33379 */ 33380 return function hideBackdrop() { 33381 if (options.backdrop) options.backdrop.remove(); 33382 if (options.disableParentScroll) options.restoreScroll(); 33383 }; 33384 } 33385 33386 /** 33387 * Removing the menu element from the DOM and remove all associated event listeners 33388 * and backdrop 33389 */ 33390 function onRemove(scope, element, opts) { 33391 opts.cleanupInteraction(); 33392 opts.cleanupBackdrop(); 33393 opts.cleanupResizing(); 33394 opts.hideBackdrop(); 33395 33396 // Before the menu is closing remove the clickable class. 33397 element.removeClass('md-clickable'); 33398 33399 // For navigation $destroy events, do a quick, non-animated removal, 33400 // but for normal closes (from clicks, etc) animate the removal 33401 33402 return (opts.$destroy === true) ? detachAndClean() : animateRemoval().then( detachAndClean ); 33403 33404 /** 33405 * For normal closes, animate the removal. 33406 * For forced closes (like $destroy events), skip the animations 33407 */ 33408 function animateRemoval() { 33409 return $animateCss(element, {addClass: 'md-leave'}).start(); 33410 } 33411 33412 /** 33413 * Detach the element 33414 */ 33415 function detachAndClean() { 33416 element.removeClass('md-active'); 33417 detachElement(element, opts); 33418 opts.alreadyOpen = false; 33419 } 33420 33421 } 33422 33423 /** 33424 * Inserts and configures the staged Menu element into the DOM, positioning it, 33425 * and wiring up various interaction events 33426 */ 33427 function onShow(scope, element, opts) { 33428 sanitizeAndConfigure(opts); 33429 33430 if (opts.menuContentEl[0]) { 33431 // Inherit the theme from the target element. 33432 $mdTheming.inherit(opts.menuContentEl, opts.target); 33433 } else { 33434 $log.warn( 33435 '$mdMenu: Menu elements should always contain a `md-menu-content` element,' + 33436 'otherwise interactivity features will not work properly.', 33437 element 33438 ); 33439 } 33440 33441 // Register various listeners to move menu on resize/orientation change 33442 opts.cleanupResizing = startRepositioningOnResize(); 33443 opts.hideBackdrop = showBackdrop(scope, element, opts); 33444 33445 // Return the promise for when our menu is done animating in 33446 return showMenu() 33447 .then(function(response) { 33448 opts.alreadyOpen = true; 33449 opts.cleanupInteraction = activateInteraction(); 33450 opts.cleanupBackdrop = setupBackdrop(); 33451 33452 // Since the menu finished its animation, mark the menu as clickable. 33453 element.addClass('md-clickable'); 33454 33455 return response; 33456 }); 33457 33458 /** 33459 * Place the menu into the DOM and call positioning related functions 33460 */ 33461 function showMenu() { 33462 opts.parent.append(element); 33463 element[0].style.display = ''; 33464 33465 return $q(function(resolve) { 33466 var position = calculateMenuPosition(element, opts); 33467 33468 element.removeClass('md-leave'); 33469 33470 // Animate the menu scaling, and opacity [from its position origin (default == top-left)] 33471 // to normal scale. 33472 $animateCss(element, { 33473 addClass: 'md-active', 33474 from: animator.toCss(position), 33475 to: animator.toCss({transform: ''}) 33476 }) 33477 .start() 33478 .then(resolve); 33479 33480 }); 33481 } 33482 33483 /** 33484 * Check for valid opts and set some sane defaults 33485 */ 33486 function sanitizeAndConfigure() { 33487 if (!opts.target) { 33488 throw Error( 33489 '$mdMenu.show() expected a target to animate from in options.target' 33490 ); 33491 } 33492 angular.extend(opts, { 33493 alreadyOpen: false, 33494 isRemoved: false, 33495 target: angular.element(opts.target), //make sure it's not a naked dom node 33496 parent: angular.element(opts.parent), 33497 menuContentEl: angular.element(element[0].querySelector('md-menu-content')) 33498 }); 33499 } 33500 33501 /** 33502 * Configure various resize listeners for screen changes 33503 */ 33504 function startRepositioningOnResize() { 33505 33506 var repositionMenu = (function(target, options) { 33507 return $$rAF.throttle(function() { 33508 if (opts.isRemoved) return; 33509 var position = calculateMenuPosition(target, options); 33510 33511 target.css(animator.toCss(position)); 33512 }); 33513 })(element, opts); 33514 33515 $window.addEventListener('resize', repositionMenu); 33516 $window.addEventListener('orientationchange', repositionMenu); 33517 33518 return function stopRepositioningOnResize() { 33519 33520 // Disable resizing handlers 33521 $window.removeEventListener('resize', repositionMenu); 33522 $window.removeEventListener('orientationchange', repositionMenu); 33523 33524 }; 33525 } 33526 33527 /** 33528 * Sets up the backdrop and listens for click elements. 33529 * Once the backdrop will be clicked, the menu will automatically close. 33530 * @returns {!Function} Function to remove the backdrop. 33531 */ 33532 function setupBackdrop() { 33533 if (!opts.backdrop) return angular.noop; 33534 33535 opts.backdrop.on('click', onBackdropClick); 33536 33537 return function() { 33538 opts.backdrop.off('click', onBackdropClick); 33539 } 33540 } 33541 33542 /** 33543 * Function to be called whenever the backdrop is clicked. 33544 * @param {!MouseEvent} event 33545 */ 33546 function onBackdropClick(event) { 33547 event.preventDefault(); 33548 event.stopPropagation(); 33549 33550 scope.$apply(function() { 33551 opts.mdMenuCtrl.close(true, { closeAll: true }); 33552 }); 33553 } 33554 33555 /** 33556 * Activate interaction on the menu. Resolves the focus target and closes the menu on 33557 * escape or option click. 33558 * @returns {!Function} Function to deactivate the interaction listeners. 33559 */ 33560 function activateInteraction() { 33561 if (!opts.menuContentEl[0]) return angular.noop; 33562 33563 // Wire up keyboard listeners. 33564 // - Close on escape, 33565 // - focus next item on down arrow, 33566 // - focus prev item on up 33567 opts.menuContentEl.on('keydown', onMenuKeyDown); 33568 opts.menuContentEl[0].addEventListener('click', captureClickListener, true); 33569 33570 // kick off initial focus in the menu on the first enabled element 33571 var focusTarget = opts.menuContentEl[0] 33572 .querySelector(prefixer.buildSelector(['md-menu-focus-target', 'md-autofocus'])); 33573 33574 if ( !focusTarget ) { 33575 var childrenLen = opts.menuContentEl[0].children.length; 33576 for(var childIndex = 0; childIndex < childrenLen; childIndex++) { 33577 var child = opts.menuContentEl[0].children[childIndex]; 33578 focusTarget = child.querySelector('.md-button:not([disabled])'); 33579 if (focusTarget) { 33580 break; 33581 } 33582 if (child.firstElementChild && !child.firstElementChild.disabled) { 33583 focusTarget = child.firstElementChild; 33584 break; 33585 } 33586 } 33587 } 33588 33589 focusTarget && focusTarget.focus(); 33590 33591 return function cleanupInteraction() { 33592 opts.menuContentEl.off('keydown', onMenuKeyDown); 33593 opts.menuContentEl[0].removeEventListener('click', captureClickListener, true); 33594 }; 33595 33596 // ************************************ 33597 // internal functions 33598 // ************************************ 33599 33600 function onMenuKeyDown(ev) { 33601 var handled; 33602 switch (ev.keyCode) { 33603 case $mdConstant.KEY_CODE.ESCAPE: 33604 opts.mdMenuCtrl.close(false, { closeAll: true }); 33605 handled = true; 33606 break; 33607 case $mdConstant.KEY_CODE.UP_ARROW: 33608 if (!focusMenuItem(ev, opts.menuContentEl, opts, -1) && !opts.nestLevel) { 33609 opts.mdMenuCtrl.triggerContainerProxy(ev); 33610 } 33611 handled = true; 33612 break; 33613 case $mdConstant.KEY_CODE.DOWN_ARROW: 33614 if (!focusMenuItem(ev, opts.menuContentEl, opts, 1) && !opts.nestLevel) { 33615 opts.mdMenuCtrl.triggerContainerProxy(ev); 33616 } 33617 handled = true; 33618 break; 33619 case $mdConstant.KEY_CODE.LEFT_ARROW: 33620 if (opts.nestLevel) { 33621 opts.mdMenuCtrl.close(); 33622 } else { 33623 opts.mdMenuCtrl.triggerContainerProxy(ev); 33624 } 33625 handled = true; 33626 break; 33627 case $mdConstant.KEY_CODE.RIGHT_ARROW: 33628 var parentMenu = $mdUtil.getClosest(ev.target, 'MD-MENU'); 33629 if (parentMenu && parentMenu != opts.parent[0]) { 33630 ev.target.click(); 33631 } else { 33632 opts.mdMenuCtrl.triggerContainerProxy(ev); 33633 } 33634 handled = true; 33635 break; 33636 } 33637 if (handled) { 33638 ev.preventDefault(); 33639 ev.stopImmediatePropagation(); 33640 } 33641 } 33642 33643 function onBackdropClick(e) { 33644 e.preventDefault(); 33645 e.stopPropagation(); 33646 scope.$apply(function() { 33647 opts.mdMenuCtrl.close(true, { closeAll: true }); 33648 }); 33649 } 33650 33651 // Close menu on menu item click, if said menu-item is not disabled 33652 function captureClickListener(e) { 33653 var target = e.target; 33654 // Traverse up the event until we get to the menuContentEl to see if 33655 // there is an ng-click and that the ng-click is not disabled 33656 do { 33657 if (target == opts.menuContentEl[0]) return; 33658 if ((hasAnyAttribute(target, ['ng-click', 'ng-href', 'ui-sref']) || 33659 target.nodeName == 'BUTTON' || target.nodeName == 'MD-BUTTON') && !hasAnyAttribute(target, ['md-prevent-menu-close'])) { 33660 var closestMenu = $mdUtil.getClosest(target, 'MD-MENU'); 33661 if (!target.hasAttribute('disabled') && (!closestMenu || closestMenu == opts.parent[0])) { 33662 close(); 33663 } 33664 break; 33665 } 33666 } while (target = target.parentNode); 33667 33668 function close() { 33669 scope.$apply(function() { 33670 opts.mdMenuCtrl.close(true, { closeAll: true }); 33671 }); 33672 } 33673 33674 function hasAnyAttribute(target, attrs) { 33675 if (!target) return false; 33676 33677 for (var i = 0, attr; attr = attrs[i]; ++i) { 33678 if (prefixer.hasAttribute(target, attr)) { 33679 return true; 33680 } 33681 } 33682 33683 return false; 33684 } 33685 } 33686 33687 } 33688 } 33689 33690 /** 33691 * Takes a keypress event and focuses the next/previous menu 33692 * item from the emitting element 33693 * @param {event} e - The origin keypress event 33694 * @param {angular.element} menuEl - The menu element 33695 * @param {object} opts - The interim element options for the mdMenu 33696 * @param {number} direction - The direction to move in (+1 = next, -1 = prev) 33697 */ 33698 function focusMenuItem(e, menuEl, opts, direction) { 33699 var currentItem = $mdUtil.getClosest(e.target, 'MD-MENU-ITEM'); 33700 33701 var items = $mdUtil.nodesToArray(menuEl[0].children); 33702 var currentIndex = items.indexOf(currentItem); 33703 33704 // Traverse through our elements in the specified direction (+/-1) and try to 33705 // focus them until we find one that accepts focus 33706 var didFocus; 33707 for (var i = currentIndex + direction; i >= 0 && i < items.length; i = i + direction) { 33708 var focusTarget = items[i].querySelector('.md-button'); 33709 didFocus = attemptFocus(focusTarget); 33710 if (didFocus) { 33711 break; 33712 } 33713 } 33714 return didFocus; 33715 } 33716 33717 /** 33718 * Attempts to focus an element. Checks whether that element is the currently 33719 * focused element after attempting. 33720 * @param {HTMLElement} el - the element to attempt focus on 33721 * @returns {bool} - whether the element was successfully focused 33722 */ 33723 function attemptFocus(el) { 33724 if (el && el.getAttribute('tabindex') != -1) { 33725 el.focus(); 33726 return ($document[0].activeElement == el); 33727 } 33728 } 33729 33730 /** 33731 * Use browser to remove this element without triggering a $destroy event 33732 */ 33733 function detachElement(element, opts) { 33734 if (!opts.preserveElement) { 33735 if (toNode(element).parentNode === toNode(opts.parent)) { 33736 toNode(opts.parent).removeChild(toNode(element)); 33737 } 33738 } else { 33739 toNode(element).style.display = 'none'; 33740 } 33741 } 33742 33743 /** 33744 * Computes menu position and sets the style on the menu container 33745 * @param {HTMLElement} el - the menu container element 33746 * @param {object} opts - the interim element options object 33747 */ 33748 function calculateMenuPosition(el, opts) { 33749 33750 var containerNode = el[0], 33751 openMenuNode = el[0].firstElementChild, 33752 openMenuNodeRect = openMenuNode.getBoundingClientRect(), 33753 boundryNode = $document[0].body, 33754 boundryNodeRect = boundryNode.getBoundingClientRect(); 33755 33756 var menuStyle = $window.getComputedStyle(openMenuNode); 33757 33758 var originNode = opts.target[0].querySelector(prefixer.buildSelector('md-menu-origin')) || opts.target[0], 33759 originNodeRect = originNode.getBoundingClientRect(); 33760 33761 var bounds = { 33762 left: boundryNodeRect.left + MENU_EDGE_MARGIN, 33763 top: Math.max(boundryNodeRect.top, 0) + MENU_EDGE_MARGIN, 33764 bottom: Math.max(boundryNodeRect.bottom, Math.max(boundryNodeRect.top, 0) + boundryNodeRect.height) - MENU_EDGE_MARGIN, 33765 right: boundryNodeRect.right - MENU_EDGE_MARGIN 33766 }; 33767 33768 var alignTarget, alignTargetRect = { top:0, left : 0, right:0, bottom:0 }, existingOffsets = { top:0, left : 0, right:0, bottom:0 }; 33769 var positionMode = opts.mdMenuCtrl.positionMode(); 33770 33771 if (positionMode.top == 'target' || positionMode.left == 'target' || positionMode.left == 'target-right') { 33772 alignTarget = firstVisibleChild(); 33773 if ( alignTarget ) { 33774 // TODO: Allow centering on an arbitrary node, for now center on first menu-item's child 33775 alignTarget = alignTarget.firstElementChild || alignTarget; 33776 alignTarget = alignTarget.querySelector(prefixer.buildSelector('md-menu-align-target')) || alignTarget; 33777 alignTargetRect = alignTarget.getBoundingClientRect(); 33778 33779 existingOffsets = { 33780 top: parseFloat(containerNode.style.top || 0), 33781 left: parseFloat(containerNode.style.left || 0) 33782 }; 33783 } 33784 } 33785 33786 var position = {}; 33787 var transformOrigin = 'top '; 33788 33789 switch (positionMode.top) { 33790 case 'target': 33791 position.top = existingOffsets.top + originNodeRect.top - alignTargetRect.top; 33792 break; 33793 case 'cascade': 33794 position.top = originNodeRect.top - parseFloat(menuStyle.paddingTop) - originNode.style.top; 33795 break; 33796 case 'bottom': 33797 position.top = originNodeRect.top + originNodeRect.height; 33798 break; 33799 default: 33800 throw new Error('Invalid target mode "' + positionMode.top + '" specified for md-menu on Y axis.'); 33801 } 33802 33803 var rtl = ($mdUtil.bidi() == 'rtl'); 33804 33805 switch (positionMode.left) { 33806 case 'target': 33807 position.left = existingOffsets.left + originNodeRect.left - alignTargetRect.left; 33808 transformOrigin += rtl ? 'right' : 'left'; 33809 break; 33810 case 'target-left': 33811 position.left = originNodeRect.left; 33812 transformOrigin += 'left'; 33813 break; 33814 case 'target-right': 33815 position.left = originNodeRect.right - openMenuNodeRect.width + (openMenuNodeRect.right - alignTargetRect.right); 33816 transformOrigin += 'right'; 33817 break; 33818 case 'cascade': 33819 var willFitRight = rtl ? (originNodeRect.left - openMenuNodeRect.width) < bounds.left : (originNodeRect.right + openMenuNodeRect.width) < bounds.right; 33820 position.left = willFitRight ? originNodeRect.right - originNode.style.left : originNodeRect.left - originNode.style.left - openMenuNodeRect.width; 33821 transformOrigin += willFitRight ? 'left' : 'right'; 33822 break; 33823 case 'right': 33824 if (rtl) { 33825 position.left = originNodeRect.right - originNodeRect.width; 33826 transformOrigin += 'left'; 33827 } else { 33828 position.left = originNodeRect.right - openMenuNodeRect.width; 33829 transformOrigin += 'right'; 33830 } 33831 break; 33832 case 'left': 33833 if (rtl) { 33834 position.left = originNodeRect.right - openMenuNodeRect.width; 33835 transformOrigin += 'right'; 33836 } else { 33837 position.left = originNodeRect.left; 33838 transformOrigin += 'left'; 33839 } 33840 break; 33841 default: 33842 throw new Error('Invalid target mode "' + positionMode.left + '" specified for md-menu on X axis.'); 33843 } 33844 33845 var offsets = opts.mdMenuCtrl.offsets(); 33846 position.top += offsets.top; 33847 position.left += offsets.left; 33848 33849 clamp(position); 33850 33851 var scaleX = Math.round(100 * Math.min(originNodeRect.width / containerNode.offsetWidth, 1.0)) / 100; 33852 var scaleY = Math.round(100 * Math.min(originNodeRect.height / containerNode.offsetHeight, 1.0)) / 100; 33853 33854 return { 33855 top: Math.round(position.top), 33856 left: Math.round(position.left), 33857 // Animate a scale out if we aren't just repositioning 33858 transform: !opts.alreadyOpen ? $mdUtil.supplant('scale({0},{1})', [scaleX, scaleY]) : undefined, 33859 transformOrigin: transformOrigin 33860 }; 33861 33862 /** 33863 * Clamps the repositioning of the menu within the confines of 33864 * bounding element (often the screen/body) 33865 */ 33866 function clamp(pos) { 33867 pos.top = Math.max(Math.min(pos.top, bounds.bottom - containerNode.offsetHeight), bounds.top); 33868 pos.left = Math.max(Math.min(pos.left, bounds.right - containerNode.offsetWidth), bounds.left); 33869 } 33870 33871 /** 33872 * Gets the first visible child in the openMenuNode 33873 * Necessary incase menu nodes are being dynamically hidden 33874 */ 33875 function firstVisibleChild() { 33876 for (var i = 0; i < openMenuNode.children.length; ++i) { 33877 if ($window.getComputedStyle(openMenuNode.children[i]).display != 'none') { 33878 return openMenuNode.children[i]; 33879 } 33880 } 33881 } 33882 } 33883 } 33884 function toNode(el) { 33885 if (el instanceof angular.element) { 33886 el = el[0]; 33887 } 33888 return el; 33889 } 33890} 33891 33892})(); 33893(function(){ 33894"use strict"; 33895 33896 33897MenuBarController.$inject = ["$scope", "$rootScope", "$element", "$attrs", "$mdConstant", "$document", "$mdUtil", "$timeout"]; 33898angular 33899 .module('material.components.menuBar') 33900 .controller('MenuBarController', MenuBarController); 33901 33902var BOUND_MENU_METHODS = ['handleKeyDown', 'handleMenuHover', 'scheduleOpenHoveredMenu', 'cancelScheduledOpen']; 33903 33904/** 33905 * @ngInject 33906 */ 33907function MenuBarController($scope, $rootScope, $element, $attrs, $mdConstant, $document, $mdUtil, $timeout) { 33908 this.$element = $element; 33909 this.$attrs = $attrs; 33910 this.$mdConstant = $mdConstant; 33911 this.$mdUtil = $mdUtil; 33912 this.$document = $document; 33913 this.$scope = $scope; 33914 this.$rootScope = $rootScope; 33915 this.$timeout = $timeout; 33916 33917 var self = this; 33918 angular.forEach(BOUND_MENU_METHODS, function(methodName) { 33919 self[methodName] = angular.bind(self, self[methodName]); 33920 }); 33921} 33922 33923MenuBarController.prototype.init = function() { 33924 var $element = this.$element; 33925 var $mdUtil = this.$mdUtil; 33926 var $scope = this.$scope; 33927 33928 var self = this; 33929 var deregisterFns = []; 33930 $element.on('keydown', this.handleKeyDown); 33931 this.parentToolbar = $mdUtil.getClosest($element, 'MD-TOOLBAR'); 33932 33933 deregisterFns.push(this.$rootScope.$on('$mdMenuOpen', function(event, el) { 33934 if (self.getMenus().indexOf(el[0]) != -1) { 33935 $element[0].classList.add('md-open'); 33936 el[0].classList.add('md-open'); 33937 self.currentlyOpenMenu = el.controller('mdMenu'); 33938 self.currentlyOpenMenu.registerContainerProxy(self.handleKeyDown); 33939 self.enableOpenOnHover(); 33940 } 33941 })); 33942 33943 deregisterFns.push(this.$rootScope.$on('$mdMenuClose', function(event, el, opts) { 33944 var rootMenus = self.getMenus(); 33945 if (rootMenus.indexOf(el[0]) != -1) { 33946 $element[0].classList.remove('md-open'); 33947 el[0].classList.remove('md-open'); 33948 } 33949 33950 if ($element[0].contains(el[0])) { 33951 var parentMenu = el[0]; 33952 while (parentMenu && rootMenus.indexOf(parentMenu) == -1) { 33953 parentMenu = $mdUtil.getClosest(parentMenu, 'MD-MENU', true); 33954 } 33955 if (parentMenu) { 33956 if (!opts.skipFocus) parentMenu.querySelector('button:not([disabled])').focus(); 33957 self.currentlyOpenMenu = undefined; 33958 self.disableOpenOnHover(); 33959 self.setKeyboardMode(true); 33960 } 33961 } 33962 })); 33963 33964 $scope.$on('$destroy', function() { 33965 self.disableOpenOnHover(); 33966 while (deregisterFns.length) { 33967 deregisterFns.shift()(); 33968 } 33969 }); 33970 33971 33972 this.setKeyboardMode(true); 33973}; 33974 33975MenuBarController.prototype.setKeyboardMode = function(enabled) { 33976 if (enabled) this.$element[0].classList.add('md-keyboard-mode'); 33977 else this.$element[0].classList.remove('md-keyboard-mode'); 33978}; 33979 33980MenuBarController.prototype.enableOpenOnHover = function() { 33981 if (this.openOnHoverEnabled) return; 33982 33983 var self = this; 33984 33985 self.openOnHoverEnabled = true; 33986 33987 if (self.parentToolbar) { 33988 self.parentToolbar.classList.add('md-has-open-menu'); 33989 33990 // Needs to be on the next tick so it doesn't close immediately. 33991 self.$mdUtil.nextTick(function() { 33992 angular.element(self.parentToolbar).on('click', self.handleParentClick); 33993 }, false); 33994 } 33995 33996 angular 33997 .element(self.getMenus()) 33998 .on('mouseenter', self.handleMenuHover); 33999}; 34000 34001MenuBarController.prototype.handleMenuHover = function(e) { 34002 this.setKeyboardMode(false); 34003 if (this.openOnHoverEnabled) { 34004 this.scheduleOpenHoveredMenu(e); 34005 } 34006}; 34007 34008MenuBarController.prototype.disableOpenOnHover = function() { 34009 if (!this.openOnHoverEnabled) return; 34010 34011 this.openOnHoverEnabled = false; 34012 34013 if (this.parentToolbar) { 34014 this.parentToolbar.classList.remove('md-has-open-menu'); 34015 angular.element(this.parentToolbar).off('click', this.handleParentClick); 34016 } 34017 34018 angular 34019 .element(this.getMenus()) 34020 .off('mouseenter', this.handleMenuHover); 34021}; 34022 34023MenuBarController.prototype.scheduleOpenHoveredMenu = function(e) { 34024 var menuEl = angular.element(e.currentTarget); 34025 var menuCtrl = menuEl.controller('mdMenu'); 34026 this.setKeyboardMode(false); 34027 this.scheduleOpenMenu(menuCtrl); 34028}; 34029 34030MenuBarController.prototype.scheduleOpenMenu = function(menuCtrl) { 34031 var self = this; 34032 var $timeout = this.$timeout; 34033 if (menuCtrl != self.currentlyOpenMenu) { 34034 $timeout.cancel(self.pendingMenuOpen); 34035 self.pendingMenuOpen = $timeout(function() { 34036 self.pendingMenuOpen = undefined; 34037 if (self.currentlyOpenMenu) { 34038 self.currentlyOpenMenu.close(true, { closeAll: true }); 34039 } 34040 menuCtrl.open(); 34041 }, 200, false); 34042 } 34043}; 34044 34045MenuBarController.prototype.handleKeyDown = function(e) { 34046 var keyCodes = this.$mdConstant.KEY_CODE; 34047 var currentMenu = this.currentlyOpenMenu; 34048 var wasOpen = currentMenu && currentMenu.isOpen; 34049 this.setKeyboardMode(true); 34050 var handled, newMenu, newMenuCtrl; 34051 switch (e.keyCode) { 34052 case keyCodes.DOWN_ARROW: 34053 if (currentMenu) { 34054 currentMenu.focusMenuContainer(); 34055 } else { 34056 this.openFocusedMenu(); 34057 } 34058 handled = true; 34059 break; 34060 case keyCodes.UP_ARROW: 34061 currentMenu && currentMenu.close(); 34062 handled = true; 34063 break; 34064 case keyCodes.LEFT_ARROW: 34065 newMenu = this.focusMenu(-1); 34066 if (wasOpen) { 34067 newMenuCtrl = angular.element(newMenu).controller('mdMenu'); 34068 this.scheduleOpenMenu(newMenuCtrl); 34069 } 34070 handled = true; 34071 break; 34072 case keyCodes.RIGHT_ARROW: 34073 newMenu = this.focusMenu(+1); 34074 if (wasOpen) { 34075 newMenuCtrl = angular.element(newMenu).controller('mdMenu'); 34076 this.scheduleOpenMenu(newMenuCtrl); 34077 } 34078 handled = true; 34079 break; 34080 } 34081 if (handled) { 34082 e && e.preventDefault && e.preventDefault(); 34083 e && e.stopImmediatePropagation && e.stopImmediatePropagation(); 34084 } 34085}; 34086 34087MenuBarController.prototype.focusMenu = function(direction) { 34088 var menus = this.getMenus(); 34089 var focusedIndex = this.getFocusedMenuIndex(); 34090 34091 if (focusedIndex == -1) { focusedIndex = this.getOpenMenuIndex(); } 34092 34093 var changed = false; 34094 34095 if (focusedIndex == -1) { focusedIndex = 0; changed = true; } 34096 else if ( 34097 direction < 0 && focusedIndex > 0 || 34098 direction > 0 && focusedIndex < menus.length - direction 34099 ) { 34100 focusedIndex += direction; 34101 changed = true; 34102 } 34103 if (changed) { 34104 menus[focusedIndex].querySelector('button').focus(); 34105 return menus[focusedIndex]; 34106 } 34107}; 34108 34109MenuBarController.prototype.openFocusedMenu = function() { 34110 var menu = this.getFocusedMenu(); 34111 menu && angular.element(menu).controller('mdMenu').open(); 34112}; 34113 34114MenuBarController.prototype.getMenus = function() { 34115 var $element = this.$element; 34116 return this.$mdUtil.nodesToArray($element[0].children) 34117 .filter(function(el) { return el.nodeName == 'MD-MENU'; }); 34118}; 34119 34120MenuBarController.prototype.getFocusedMenu = function() { 34121 return this.getMenus()[this.getFocusedMenuIndex()]; 34122}; 34123 34124MenuBarController.prototype.getFocusedMenuIndex = function() { 34125 var $mdUtil = this.$mdUtil; 34126 var focusedEl = $mdUtil.getClosest( 34127 this.$document[0].activeElement, 34128 'MD-MENU' 34129 ); 34130 if (!focusedEl) return -1; 34131 34132 var focusedIndex = this.getMenus().indexOf(focusedEl); 34133 return focusedIndex; 34134}; 34135 34136MenuBarController.prototype.getOpenMenuIndex = function() { 34137 var menus = this.getMenus(); 34138 for (var i = 0; i < menus.length; ++i) { 34139 if (menus[i].classList.contains('md-open')) return i; 34140 } 34141 return -1; 34142}; 34143 34144MenuBarController.prototype.handleParentClick = function(event) { 34145 var openMenu = this.querySelector('md-menu.md-open'); 34146 34147 if (openMenu && !openMenu.contains(event.target)) { 34148 angular.element(openMenu).controller('mdMenu').close(true, { 34149 closeAll: true 34150 }); 34151 } 34152}; 34153 34154})(); 34155(function(){ 34156"use strict"; 34157 34158/** 34159 * @ngdoc directive 34160 * @name mdMenuBar 34161 * @module material.components.menuBar 34162 * @restrict E 34163 * @description 34164 * 34165 * Menu bars are containers that hold multiple menus. They change the behavior and appearence 34166 * of the `md-menu` directive to behave similar to an operating system provided menu. 34167 * 34168 * @usage 34169 * <hljs lang="html"> 34170 * <md-menu-bar> 34171 * <md-menu> 34172 * <button ng-click="$mdMenu.open()"> 34173 * File 34174 * </button> 34175 * <md-menu-content> 34176 * <md-menu-item>
34177 * <md-button ng-click="ctrl.sampleAction('share', $event)"> 34178 * Share... 34179 * </md-button> 34180 * </md-menu-item> 34181 * <md-menu-divider></md-menu-divider> 34182 * <md-menu-item> 34183 * <md-menu-item> 34184 * <md-menu> 34185 * <md-button ng-click="$mdMenu.open()">New</md-button> 34186 * <md-menu-content> 34187 * <md-menu-item><md-button ng-click="ctrl.sampleAction('New Document', $event)">Document</md-button></md-menu-item> 34188 * <md-menu-item><md-button ng-click="ctrl.sampleAction('New Spreadsheet', $event)">Spreadsheet</md-button></md-menu-item> 34189 * <md-menu-item><md-button ng-click="ctrl.sampleAction('New Presentation', $event)">Presentation</md-button></md-menu-item> 34190 * <md-menu-item><md-button ng-click="ctrl.sampleAction('New Form', $event)">Form</md-button></md-menu-item> 34191 * <md-menu-item><md-button ng-click="ctrl.sampleAction('New Drawing', $event)">Drawing</md-button></md-menu-item> 34192 * </md-menu-content> 34193 * </md-menu> 34194 * </md-menu-item> 34195 * </md-menu-content> 34196 * </md-menu> 34197 * </md-menu-bar> 34198 * </hljs> 34199 * 34200 * ## Menu Bar Controls 34201 * 34202 * You may place `md-menu-items` that function as controls within menu bars. 34203 * There are two modes that are exposed via the `type` attribute of the `md-menu-item`. 34204 * `type="checkbox"` will function as a boolean control for the `ng-model` attribute of the 34205 * `md-menu-item`. `type="radio"` will function like a radio button, setting the `ngModel` 34206 * to the `string` value of the `value` attribute. If you need non-string values, you can use 34207 * `ng-value` to provide an expression (this is similar to how angular's native `input[type=radio]` works. 34208 * 34209 * <hljs lang="html"> 34210 * <md-menu-bar> 34211 * <md-menu> 34212 * <button ng-click="$mdMenu.open()"> 34213 * Sample Menu 34214 * </button> 34215 * <md-menu-content> 34216 * <md-menu-item type="checkbox" ng-model="settings.allowChanges">Allow changes</md-menu-item> 34217 * <md-menu-divider></md-menu-divider> 34218 * <md-menu-item type="radio" ng-model="settings.mode" ng-value="1">Mode 1</md-menu-item> 34219 * <md-menu-item type="radio" ng-model="settings.mode" ng-value="1">Mode 2</md-menu-item> 34220 * <md-menu-item type="radio" ng-model="settings.mode" ng-value="1">Mode 3</md-menu-item> 34221 * </md-menu-content> 34222 * </md-menu> 34223 * </md-menu-bar> 34224 * </hljs> 34225 * 34226 * 34227 * ### Nesting Menus 34228 * 34229 * Menus may be nested within menu bars. This is commonly called cascading menus. 34230 * To nest a menu place the nested menu inside the content of the `md-menu-item`. 34231 * <hljs lang="html"> 34232 * <md-menu-item> 34233 * <md-menu> 34234 * <button ng-click="$mdMenu.open()">New</md-button> 34235 * <md-menu-content> 34236 * <md-menu-item><md-button ng-click="ctrl.sampleAction('New Document', $event)">Document</md-button></md-menu-item> 34237 * <md-menu-item><md-button ng-click="ctrl.sampleAction('New Spreadsheet', $event)">Spreadsheet</md-button></md-menu-item> 34238 * <md-menu-item><md-button ng-click="ctrl.sampleAction('New Presentation', $event)">Presentation</md-button></md-menu-item> 34239 * <md-menu-item><md-button ng-click="ctrl.sampleAction('New Form', $event)">Form</md-button></md-menu-item> 34240 * <md-menu-item><md-button ng-click="ctrl.sampleAction('New Drawing', $event)">Drawing</md-button></md-menu-item> 34241 * </md-menu-content> 34242 * </md-menu> 34243 * </md-menu-item> 34244 * </hljs> 34245 * 34246 */ 34247 34248MenuBarDirective.$inject = ["$mdUtil", "$mdTheming"]; 34249angular 34250 .module('material.components.menuBar') 34251 .directive('mdMenuBar', MenuBarDirective); 34252 34253/* @ngInject */ 34254function MenuBarDirective($mdUtil, $mdTheming) { 34255 return { 34256 restrict: 'E', 34257 require: 'mdMenuBar', 34258 controller: 'MenuBarController', 34259 34260 compile: function compile(templateEl, templateAttrs) { 34261 if (!templateAttrs.ariaRole) { 34262 templateEl[0].setAttribute('role', 'menubar'); 34263 } 34264 angular.forEach(templateEl[0].children, function(menuEl) { 34265 if (menuEl.nodeName == 'MD-MENU') { 34266 if (!menuEl.hasAttribute('md-position-mode')) { 34267 menuEl.setAttribute('md-position-mode', 'left bottom'); 34268 34269 // Since we're in the compile function and actual `md-buttons` are not compiled yet, 34270 // we need to query for possible `md-buttons` as well. 34271 menuEl.querySelector('button, a, md-button').setAttribute('role', 'menuitem'); 34272 } 34273 var contentEls = $mdUtil.nodesToArray(menuEl.querySelectorAll('md-menu-content')); 34274 angular.forEach(contentEls, function(contentEl) { 34275 contentEl.classList.add('md-menu-bar-menu'); 34276 contentEl.classList.add('md-dense'); 34277 if (!contentEl.hasAttribute('width')) { 34278 contentEl.setAttribute('width', 5); 34279 } 34280 }); 34281 } 34282 }); 34283 34284 // Mark the child menu items that they're inside a menu bar. This is necessary, 34285 // because mnMenuItem has special behaviour during compilation, depending on 34286 // whether it is inside a mdMenuBar. We can usually figure this out via the DOM,
vendor: 6,915 bytes, lines 34287-34529
34287 // however if a directive that uses documentFragment is applied to the child (e.g. ngRepeat), 34288 // the element won't have a parent and won't compile properly. 34289 templateEl.find('md-menu-item').addClass('md-in-menu-bar'); 34290 34291 return function postLink(scope, el, attr, ctrl) { 34292 el.addClass('_md'); // private md component indicator for styling 34293 $mdTheming(scope, el); 34294 ctrl.init(); 34295 }; 34296 } 34297 }; 34298 34299} 34300 34301})(); 34302(function(){ 34303"use strict"; 34304 34305 34306angular 34307 .module('material.components.menuBar') 34308 .directive('mdMenuDivider', MenuDividerDirective); 34309 34310 34311function MenuDividerDirective() { 34312 return { 34313 restrict: 'E', 34314 compile: function(templateEl, templateAttrs) { 34315 if (!templateAttrs.role) { 34316 templateEl[0].setAttribute('role', 'separator'); 34317 } 34318 } 34319 }; 34320} 34321 34322})(); 34323(function(){ 34324"use strict"; 34325 34326 34327MenuItemController.$inject = ["$scope", "$element", "$attrs"]; 34328angular 34329 .module('material.components.menuBar') 34330 .controller('MenuItemController', MenuItemController); 34331 34332 34333/** 34334 * @ngInject 34335 */ 34336function MenuItemController($scope, $element, $attrs) { 34337 this.$element = $element; 34338 this.$attrs = $attrs; 34339 this.$scope = $scope; 34340} 34341 34342MenuItemController.prototype.init = function(ngModel) { 34343 var $element = this.$element; 34344 var $attrs = this.$attrs; 34345 34346 this.ngModel = ngModel; 34347 if ($attrs.type == 'checkbox' || $attrs.type == 'radio') { 34348 this.mode = $attrs.type; 34349 this.iconEl = $element[0].children[0]; 34350 this.buttonEl = $element[0].children[1]; 34351 if (ngModel) { 34352 // Clear ngAria set attributes 34353 this.initClickListeners(); 34354 } 34355 } 34356}; 34357 34358// ngAria auto sets attributes on a menu-item with a ngModel. 34359// We don't want this because our content (buttons) get the focus 34360// and set their own aria attributes appropritately. Having both 34361// breaks NVDA / JAWS. This undeoes ngAria's attrs. 34362MenuItemController.prototype.clearNgAria = function() { 34363 var el = this.$element[0]; 34364 var clearAttrs = ['role', 'tabindex', 'aria-invalid', 'aria-checked']; 34365 angular.forEach(clearAttrs, function(attr) { 34366 el.removeAttribute(attr); 34367 }); 34368}; 34369 34370MenuItemController.prototype.initClickListeners = function() { 34371 var self = this; 34372 var ngModel = this.ngModel; 34373 var $scope = this.$scope; 34374 var $attrs = this.$attrs; 34375 var $element = this.$element; 34376 var mode = this.mode; 34377 34378 this.handleClick = angular.bind(this, this.handleClick); 34379 34380 var icon = this.iconEl; 34381 var button = angular.element(this.buttonEl); 34382 var handleClick = this.handleClick; 34383 34384 $attrs.$observe('disabled', setDisabled); 34385 setDisabled($attrs.disabled); 34386 34387 ngModel.$render = function render() { 34388 self.clearNgAria(); 34389 if (isSelected()) { 34390 icon.style.display = ''; 34391 button.attr('aria-checked', 'true'); 34392 } else { 34393 icon.style.display = 'none'; 34394 button.attr('aria-checked', 'false'); 34395 } 34396 }; 34397 34398 $scope.$$postDigest(ngModel.$render); 34399 34400 function isSelected() { 34401 if (mode == 'radio') { 34402 var val = $attrs.ngValue ? $scope.$eval($attrs.ngValue) : $attrs.value; 34403 return ngModel.$modelValue == val; 34404 } else { 34405 return ngModel.$modelValue; 34406 } 34407 } 34408 34409 function setDisabled(disabled) { 34410 if (disabled) { 34411 button.off('click', handleClick); 34412 } else { 34413 button.on('click', handleClick); 34414 } 34415 } 34416}; 34417 34418MenuItemController.prototype.handleClick = function(e) { 34419 var mode = this.mode; 34420 var ngModel = this.ngModel; 34421 var $attrs = this.$attrs; 34422 var newVal; 34423 if (mode == 'checkbox') { 34424 newVal = !ngModel.$modelValue; 34425 } else if (mode == 'radio') { 34426 newVal = $attrs.ngValue ? this.$scope.$eval($attrs.ngValue) : $attrs.value; 34427 } 34428 ngModel.$setViewValue(newVal); 34429 ngModel.$render(); 34430}; 34431 34432})(); 34433(function(){ 34434"use strict"; 34435 34436 34437MenuItemDirective.$inject = ["$mdUtil", "$mdConstant", "$$mdSvgRegistry"]; 34438angular 34439 .module('material.components.menuBar') 34440 .directive('mdMenuItem', MenuItemDirective); 34441 34442 /* @ngInject */ 34443function MenuItemDirective($mdUtil, $mdConstant, $$mdSvgRegistry) { 34444 return { 34445 controller: 'MenuItemController', 34446 require: ['mdMenuItem', '?ngModel'], 34447 priority: $mdConstant.BEFORE_NG_ARIA, 34448 compile: function(templateEl, templateAttrs) { 34449 var type = templateAttrs.type; 34450 var inMenuBarClass = 'md-in-menu-bar'; 34451 34452 // Note: This allows us to show the `check` icon for the md-menu-bar items. 34453 // The `md-in-menu-bar` class is set by the mdMenuBar directive. 34454 if ((type == 'checkbox' || type == 'radio') && templateEl.hasClass(inMenuBarClass)) { 34455 var text = templateEl[0].textContent; 34456 var buttonEl = angular.element('<md-button type="button"></md-button>'); 34457 var iconTemplate = '<md-icon md-svg-src="' + $$mdSvgRegistry.mdChecked + '"></md-icon>'; 34458 34459 buttonEl.html(text); 34460 buttonEl.attr('tabindex', '0'); 34461 34462 templateEl.html(''); 34463 templateEl.append(angular.element(iconTemplate)); 34464 templateEl.append(buttonEl); 34465 templateEl.addClass('md-indent').removeClass(inMenuBarClass); 34466 34467 setDefault('role', type == 'checkbox' ? 'menuitemcheckbox' : 'menuitemradio', buttonEl); 34468 moveAttrToButton('ng-disabled'); 34469 34470 } else { 34471 setDefault('role', 'menuitem', templateEl[0].querySelector('md-button, button, a')); 34472 } 34473 34474 34475 return function(scope, el, attrs, ctrls) { 34476 var ctrl = ctrls[0]; 34477 var ngModel = ctrls[1]; 34478 ctrl.init(ngModel); 34479 }; 34480 34481 function setDefault(attr, val, el) { 34482 el = el || templateEl; 34483 if (el instanceof angular.element) { 34484 el = el[0]; 34485 } 34486 if (!el.hasAttribute(attr)) { 34487 el.setAttribute(attr, val); 34488 } 34489 } 34490 34491 function moveAttrToButton(attribute) { 34492 var attributes = $mdUtil.prefixer(attribute); 34493 34494 angular.forEach(attributes, function(attr) { 34495 if (templateEl[0].hasAttribute(attr)) { 34496 var val = templateEl[0].getAttribute(attr); 34497 buttonEl[0].setAttribute(attr, val); 34498 templateEl[0].removeAttribute(attr); 34499 } 34500 }); 34501 } 34502 } 34503 }; 34504} 34505 34506})(); 34507(function(){ 34508"use strict"; 34509 34510/** 34511 * @ngdoc directive 34512 * @name mdProgressCircular 34513 * @module material.components.progressCircular 34514 * @restrict E 34515 * 34516 * @description 34517 * The circular progress directive is used to make loading content in your app as delightful and 34518 * painless as possible by minimizing the amount of visual change a user sees before they can view 34519 * and interact with content. 34520 * 34521 * For operations where the percentage of the operation completed can be determined, use a 34522 * determinate indicator. They give users a quick sense of how long an operation will take. 34523 * 34524 * For operations where the user is asked to wait a moment while something finishes up, and it’s 34525 * not necessary to expose what's happening behind the scenes and how long it will take, use an 34526 * indeterminate indicator. 34527 * 34528 * @param {string} md-mode Select from one of two modes: **'determinate'** and **'indeterminate'**. 34529 *
34530 * Note: if the `md-mode` value is set as undefined or specified as not 1 of the two (2) valid modes, then **'indeterminate'** 34531 * will be auto-applied as the mode. 34532 * 34533 * Note: if not configured, the `md-mode="indeterminate"` will be auto injected as an attribute. 34534 * If `value=""` is also specified, however, then `md-mode="determinate"` would be auto-injected instead. 34535 * @param {number=} value In determinate mode, this number represents the percentage of the 34536 * circular progress. Default: 0 34537 * @param {number=} md-diameter This specifies the diameter of the circular progress. The value 34538 * should be a pixel-size value (eg '100'). If this attribute is 34539 * not present then a default value of '50px' is assumed. 34540 * 34541 * @param {boolean=} ng-disabled Determines whether to disable the progress element. 34542 * 34543 * @usage 34544 * <hljs lang="html"> 34545 * <md-progress-circular md-mode="determinate" value="..."></md-progress-circular> 34546 * 34547 * <md-progress-circular md-mode="determinate" ng-value="..."></md-progress-circular> 34548 * 34549 * <md-progress-circular md-mode="determinate" value="..." md-diameter="100"></md-progress-circular> 34550 * 34551 * <md-progress-circular md-mode="indeterminate"></md-progress-circular> 34552 * </hljs> 34553 */ 34554 34555MdProgressCircularDirective.$inject = ["$window", "$mdProgressCircular", "$mdTheming", "$mdUtil", "$interval", "$log"]; 34556angular 34557 .module('material.components.progressCircular') 34558 .directive('mdProgressCircular', MdProgressCircularDirective); 34559 34560/* @ngInject */ 34561function MdProgressCircularDirective($window, $mdProgressCircular, $mdTheming, 34562 $mdUtil, $interval, $log) { 34563 34564 // Note that this shouldn't use use $$rAF, because it can cause an infinite loop 34565 // in any tests that call $animate.flush. 34566 var rAF = $window.requestAnimationFrame || 34567 $window.webkitRequestAnimationFrame || 34568 angular.noop; 34569 34570 var cAF = $window.cancelAnimationFrame || 34571 $window.webkitCancelAnimationFrame || 34572 $window.webkitCancelRequestAnimationFrame || 34573 angular.noop; 34574 34575 var MODE_DETERMINATE = 'determinate'; 34576 var MODE_INDETERMINATE = 'indeterminate'; 34577 var DISABLED_CLASS = '_md-progress-circular-disabled'; 34578 var INDETERMINATE_CLASS = 'md-mode-indeterminate'; 34579 34580 return { 34581 restrict: 'E', 34582 scope: { 34583 value: '@', 34584 mdDiameter: '@', 34585 mdMode: '@' 34586 }, 34587 template: 34588 '<svg xmlns="http://www.w3.org/2000/svg">' + 34589 '<path fill="none"/>' + 34590 '</svg>', 34591 compile: function(element, attrs) { 34592 element.attr({ 34593 'aria-valuemin': 0, 34594 'aria-valuemax': 100, 34595 'role': 'progressbar' 34596 }); 34597 34598 if (angular.isUndefined(attrs.mdMode)) { 34599 var mode = attrs.hasOwnProperty('value') ? MODE_DETERMINATE : MODE_INDETERMINATE; 34600 attrs.$set('mdMode', mode); 34601 } else { 34602 attrs.$set('mdMode', attrs.mdMode.trim()); 34603 } 34604 34605 return MdProgressCircularLink; 34606 } 34607 }; 34608 34609 function MdProgressCircularLink(scope, element, attrs) { 34610 var node = element[0]; 34611 var svg = angular.element(node.querySelector('svg')); 34612 var path = angular.element(node.querySelector('path')); 34613 var startIndeterminate = $mdProgressCircular.startIndeterminate; 34614 var endIndeterminate = $mdProgressCircular.endIndeterminate; 34615 var iterationCount = 0; 34616 var lastAnimationId = 0; 34617 var lastDrawFrame; 34618 var interval; 34619 34620 $mdTheming(element); 34621 element.toggleClass(DISABLED_CLASS, attrs.hasOwnProperty('disabled')); 34622 34623 // If the mode is indeterminate, it doesn't need to 34624 // wait for the next digest. It can start right away. 34625 if(scope.mdMode === MODE_INDETERMINATE){ 34626 startIndeterminateAnimation(); 34627 } 34628 34629 scope.$on('$destroy', function(){ 34630 cleanupIndeterminateAnimation(); 34631 34632 if (lastDrawFrame) { 34633 cAF(lastDrawFrame); 34634 } 34635 }); 34636 34637 scope.$watchGroup(['value', 'mdMode', function() { 34638 var isDisabled = node.disabled; 34639 34640 // Sometimes the browser doesn't return a boolean, in 34641 // which case we should check whether the attribute is 34642 // present. 34643 if (isDisabled === true || isDisabled === false){ 34644 return isDisabled; 34645 } 34646 34647 return angular.isDefined(element.attr('disabled')); 34648 }], function(newValues, oldValues) { 34649 var mode = newValues[1]; 34650 var isDisabled = newValues[2]; 34651 var wasDisabled = oldValues[2]; 34652 34653 if (isDisabled !== wasDisabled) { 34654 element.toggleClass(DISABLED_CLASS, !!isDisabled); 34655 } 34656 34657 if (isDisabled) { 34658 cleanupIndeterminateAnimation(); 34659 } else { 34660 if (mode !== MODE_DETERMINATE && mode !== MODE_INDETERMINATE) { 34661 mode = MODE_INDETERMINATE; 34662 attrs.$set('mdMode', mode); 34663 } 34664 34665 if (mode === MODE_INDETERMINATE) { 34666 startIndeterminateAnimation(); 34667 } else { 34668 var newValue = clamp(newValues[0]); 34669 34670 cleanupIndeterminateAnimation(); 34671 34672 element.attr('aria-valuenow', newValue); 34673 renderCircle(clamp(oldValues[0]), newValue); 34674 } 34675 } 34676 34677 }); 34678 34679 // This is in a separate watch in order to avoid layout, unless 34680 // the value has actually changed. 34681 scope.$watch('mdDiameter', function(newValue) { 34682 var diameter = getSize(newValue); 34683 var strokeWidth = getStroke(diameter); 34684 var value = clamp(scope.value); 34685 var transformOrigin = (diameter / 2) + 'px'; 34686 var dimensions = { 34687 width: diameter + 'px', 34688 height: diameter + 'px' 34689 }; 34690 34691 // The viewBox has to be applied via setAttribute, because it is 34692 // case-sensitive. If jQuery is included in the page, `.attr` lowercases 34693 // all attribute names. 34694 svg[0].setAttribute('viewBox', '0 0 ' + diameter + ' ' + diameter); 34695 34696 // Usually viewBox sets the dimensions for the SVG, however that doesn't 34697 // seem to be the case on IE10. 34698 // Important! The transform origin has to be set from here and it has to 34699 // be in the format of "Ypx Ypx Ypx", otherwise the rotation wobbles in 34700 // IE and Edge, because they don't account for the stroke width when 34701 // rotating. Also "center" doesn't help in this case, it has to be a 34702 // precise value. 34703 svg 34704 .css(dimensions) 34705 .css('transform-origin', transformOrigin + ' ' + transformOrigin + ' ' + transformOrigin); 34706 34707 element.css(dimensions); 34708 34709 path.attr('stroke-width', strokeWidth); 34710 path.attr('stroke-linecap', 'square'); 34711 if (scope.mdMode == MODE_INDETERMINATE) { 34712 path.attr('d', getSvgArc(diameter, strokeWidth, true)); 34713 path.attr('stroke-dasharray', (diameter - strokeWidth) * $window.Math.PI * 0.75); 34714 path.attr('stroke-dashoffset', getDashLength(diameter, strokeWidth, 1, 75)); 34715 } else { 34716 path.attr('d', getSvgArc(diameter, strokeWidth, false)); 34717 path.attr('stroke-dasharray', (diameter - strokeWidth) * $window.Math.PI); 34718 path.attr('stroke-dashoffset', getDashLength(diameter, strokeWidth, 0, 100)); 34719 renderCircle(value, value); 34720 } 34721 34722 }); 34723 34724 function renderCircle(animateFrom, animateTo, easing, duration, iterationCount, maxValue) { 34725 var id = ++lastAnimationId; 34726 var startTime = $mdUtil.now(); 34727 var changeInValue = animateTo - animateFrom; 34728 var diameter = getSize(scope.mdDiameter); 34729 var strokeWidth = getStroke(diameter); 34730 var ease = easing || $mdProgressCircular.easeFn; 34731 var animationDuration = duration || $mdProgressCircular.duration; 34732 var rotation = -90 * (iterationCount || 0); 34733 var dashLimit = maxValue || 100; 34734 34735 // No need to animate it if the values are the same 34736 if (animateTo === animateFrom) { 34737 renderFrame(animateTo); 34738 } else { 34739 lastDrawFrame = rAF(function animation() { 34740 var currentTime = $window.Math.max(0, $window.Math.min($mdUtil.now() - startTime, animationDuration)); 34741 34742 renderFrame(ease(currentTime, animateFrom, changeInValue, animationDuration)); 34743
vendor: 36,121 bytes, lines 34744-35883
34744 // Do not allow overlapping animations 34745 if (id === lastAnimationId && currentTime < animationDuration) { 34746 lastDrawFrame = rAF(animation); 34747 } 34748 }); 34749 } 34750 34751 function renderFrame(value) { 34752 path.attr('stroke-dashoffset', getDashLength(diameter, strokeWidth, value, dashLimit)); 34753 path.attr('transform','rotate(' + (rotation) + ' ' + diameter/2 + ' ' + diameter/2 + ')'); 34754 } 34755 } 34756 34757 function animateIndeterminate() { 34758 renderCircle( 34759 startIndeterminate, 34760 endIndeterminate, 34761 $mdProgressCircular.easeFnIndeterminate, 34762 $mdProgressCircular.durationIndeterminate, 34763 iterationCount, 34764 75 34765 ); 34766 34767 // The %4 technically isn't necessary, but it keeps the rotation 34768 // under 360, instead of becoming a crazy large number. 34769 iterationCount = ++iterationCount % 4; 34770 34771 } 34772 34773 function startIndeterminateAnimation() { 34774 if (!interval) { 34775 // Note that this interval isn't supposed to trigger a digest. 34776 interval = $interval( 34777 animateIndeterminate, 34778 $mdProgressCircular.durationIndeterminate, 34779 0, 34780 false 34781 ); 34782 34783 animateIndeterminate(); 34784 34785 element 34786 .addClass(INDETERMINATE_CLASS) 34787 .removeAttr('aria-valuenow'); 34788 } 34789 } 34790 34791 function cleanupIndeterminateAnimation() { 34792 if (interval) { 34793 $interval.cancel(interval); 34794 interval = null; 34795 element.removeClass(INDETERMINATE_CLASS); 34796 } 34797 } 34798 } 34799 34800 /** 34801 * Returns SVG path data for progress circle 34802 * Syntax spec: https://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands 34803 * 34804 * @param {number} diameter Diameter of the container. 34805 * @param {number} strokeWidth Stroke width to be used when drawing circle 34806 * @param {boolean} indeterminate Use if progress circle will be used for indeterminate 34807 * 34808 * @returns {string} String representation of an SVG arc. 34809 */ 34810 function getSvgArc(diameter, strokeWidth, indeterminate) { 34811 var radius = diameter / 2; 34812 var offset = strokeWidth / 2; 34813 var start = radius + ',' + offset; // ie: (25, 2.5) or 12 o'clock 34814 var end = offset + ',' + radius; // ie: (2.5, 25) or 9 o'clock 34815 var arcRadius = radius - offset; 34816 return 'M' + start 34817 + 'A' + arcRadius + ',' + arcRadius + ' 0 1 1 ' + end // 75% circle 34818 + (indeterminate ? '' : 'A' + arcRadius + ',' + arcRadius + ' 0 0 1 ' + start); // loop to start 34819 } 34820 34821 /** 34822 * Return stroke length for progress circle 34823 * 34824 * @param {number} diameter Diameter of the container. 34825 * @param {number} strokeWidth Stroke width to be used when drawing circle 34826 * @param {number} value Percentage of circle (between 0 and 100) 34827 * @param {number} limit Max percentage for circle 34828 * 34829 * @returns {number} Stroke length for progres circle 34830 */ 34831 function getDashLength(diameter, strokeWidth, value, limit) { 34832 return (diameter - strokeWidth) * $window.Math.PI * ( (3 * (limit || 100) / 100) - (value/100) ); 34833 } 34834 34835 /** 34836 * Limits a value between 0 and 100. 34837 */ 34838 function clamp(value) { 34839 return $window.Math.max(0, $window.Math.min(value || 0, 100)); 34840 } 34841 34842 /** 34843 * Determines the size of a progress circle, based on the provided 34844 * value in the following formats: `X`, `Ypx`, `Z%`. 34845 */ 34846 function getSize(value) { 34847 var defaultValue = $mdProgressCircular.progressSize; 34848 34849 if (value) { 34850 var parsed = parseFloat(value); 34851 34852 if (value.lastIndexOf('%') === value.length - 1) { 34853 parsed = (parsed / 100) * defaultValue; 34854 } 34855 34856 return parsed; 34857 } 34858 34859 return defaultValue; 34860 } 34861 34862 /** 34863 * Determines the circle's stroke width, based on 34864 * the provided diameter. 34865 */ 34866 function getStroke(diameter) { 34867 return $mdProgressCircular.strokeWidth / 100 * diameter; 34868 } 34869 34870} 34871 34872})(); 34873(function(){ 34874"use strict"; 34875 34876/** 34877 * @ngdoc service 34878 * @name $mdProgressCircular 34879 * @module material.components.progressCircular 34880 * 34881 * @description 34882 * Allows the user to specify the default options for the `progressCircular` directive. 34883 * 34884 * @property {number} progressSize Diameter of the progress circle in pixels. 34885 * @property {number} strokeWidth Width of the circle's stroke as a percentage of the circle's size. 34886 * @property {number} duration Length of the circle animation in milliseconds. 34887 * @property {function} easeFn Default easing animation function. 34888 * @property {object} easingPresets Collection of pre-defined easing functions. 34889 * 34890 * @property {number} durationIndeterminate Duration of the indeterminate animation. 34891 * @property {number} startIndeterminate Indeterminate animation start point. 34892 * @property {number} endIndeterminate Indeterminate animation end point. 34893 * @property {function} easeFnIndeterminate Easing function to be used when animating 34894 * between the indeterminate values. 34895 * 34896 * @property {(function(object): object)} configure Used to modify the default options. 34897 * 34898 * @usage 34899 * <hljs lang="js"> 34900 * myAppModule.config(function($mdProgressCircularProvider) { 34901 * 34902 * // Example of changing the default progress options. 34903 * $mdProgressCircularProvider.configure({ 34904 * progressSize: 100, 34905 * strokeWidth: 20, 34906 * duration: 800 34907 * }); 34908 * }); 34909 * </hljs> 34910 * 34911 */ 34912 34913angular 34914 .module('material.components.progressCircular') 34915 .provider("$mdProgressCircular", MdProgressCircularProvider); 34916 34917function MdProgressCircularProvider() { 34918 var progressConfig = { 34919 progressSize: 50, 34920 strokeWidth: 10, 34921 duration: 100, 34922 easeFn: linearEase, 34923 34924 durationIndeterminate: 1333, 34925 startIndeterminate: 1, 34926 endIndeterminate: 149, 34927 easeFnIndeterminate: materialEase, 34928 34929 easingPresets: { 34930 linearEase: linearEase, 34931 materialEase: materialEase 34932 } 34933 }; 34934 34935 return { 34936 configure: function(options) { 34937 progressConfig = angular.extend(progressConfig, options || {}); 34938 return progressConfig; 34939 }, 34940 $get: function() { return progressConfig; } 34941 }; 34942 34943 function linearEase(t, b, c, d) { 34944 return c * t / d + b; 34945 } 34946 34947 function materialEase(t, b, c, d) { 34948 // via http://www.timotheegroleau.com/Flash/experiments/easing_function_generator.htm 34949 // with settings of [0, 0, 1, 1] 34950 var ts = (t /= d) * t; 34951 var tc = ts * t; 34952 return b + c * (6 * tc * ts + -15 * ts * ts + 10 * tc); 34953 } 34954} 34955 34956})(); 34957(function(){ 34958"use strict"; 34959 34960/** 34961 * @ngdoc directive 34962 * @name mdTab 34963 * @module material.components.tabs 34964 * 34965 * @restrict E 34966 * 34967 * @description 34968 * The `<md-tab>` is a nested directive used within `<md-tabs>` to specify a tab with a **label** and optional *view content*. 34969 * 34970 * If the `label` attribute is not specified, then an optional `<md-tab-label>` tag can be used to specify more 34971 * complex tab header markup. If neither the **label** nor the **md-tab-label** are specified, then the nested 34972 * markup of the `<md-tab>` is used as the tab header markup. 34973 * 34974 * Please note that if you use `<md-tab-label>`, your content **MUST** be wrapped in the `<md-tab-body>` tag. This 34975 * is to define a clear separation between the tab content and the tab label. 34976 * 34977 * This container is used by the TabsController to show/hide the active tab's content view. This synchronization is 34978 * automatically managed by the internal TabsController whenever the tab selection changes. Selection changes can 34979 * be initiated via data binding changes, programmatic invocation, or user gestures. 34980 * 34981 * @param {string=} label Optional attribute to specify a simple string as the tab label 34982 * @param {boolean=} ng-disabled If present and expression evaluates to truthy, disabled tab selection. 34983 * @param {expression=} md-on-deselect Expression to be evaluated after the tab has been de-selected. 34984 * @param {expression=} md-on-select Expression to be evaluated after the tab has been selected. 34985 * @param {boolean=} md-active When true, sets the active tab. Note: There can only be one active tab at a time. 34986 * 34987 * 34988 * @usage 34989 * 34990 * <hljs lang="html"> 34991 * <md-tab label="" ng-disabled md-on-select="" md-on-deselect="" > 34992 * <h3>My Tab content</h3> 34993 * </md-tab> 34994 * 34995 * <md-tab > 34996 * <md-tab-label> 34997 * <h3>My Tab content</h3> 34998 * </md-tab-label> 34999 * <md-tab-body> 35000 * <p> 35001 * Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, 35002 * totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae 35003 * dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, 35004 * sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. 35005 * </p> 35006 * </md-tab-body> 35007 * </md-tab> 35008 * </hljs> 35009 * 35010 */ 35011angular 35012 .module('material.components.tabs') 35013 .directive('mdTab', MdTab); 35014 35015function MdTab () { 35016 return { 35017 require: '^?mdTabs', 35018 terminal: true, 35019 compile: function (element, attr) { 35020 var label = firstChild(element, 'md-tab-label'), 35021 body = firstChild(element, 'md-tab-body'); 35022 35023 if (label.length === 0) { 35024 label = angular.element('<md-tab-label></md-tab-label>'); 35025 if (attr.label) label.text(attr.label); 35026 else label.append(element.contents()); 35027 35028 if (body.length === 0) { 35029 var contents = element.contents().detach(); 35030 body = angular.element('<md-tab-body></md-tab-body>'); 35031 body.append(contents); 35032 } 35033 } 35034 35035 element.append(label); 35036 if (body.html()) element.append(body); 35037 35038 return postLink; 35039 }, 35040 scope: { 35041 active: '=?mdActive', 35042 disabled: '=?ngDisabled', 35043 select: '&?mdOnSelect', 35044 deselect: '&?mdOnDeselect' 35045 } 35046 }; 35047 35048 function postLink (scope, element, attr, ctrl) { 35049 if (!ctrl) return; 35050 var index = ctrl.getTabElementIndex(element), 35051 body = firstChild(element, 'md-tab-body').remove(), 35052 label = firstChild(element, 'md-tab-label').remove(), 35053 data = ctrl.insertTab({ 35054 scope: scope, 35055 parent: scope.$parent, 35056 index: index, 35057 element: element, 35058 template: body.html(), 35059 label: label.html() 35060 }, index); 35061 35062 scope.select = scope.select || angular.noop; 35063 scope.deselect = scope.deselect || angular.noop; 35064 35065 scope.$watch('active', function (active) { if (active) ctrl.select(data.getIndex(), true); }); 35066 scope.$watch('disabled', function () { ctrl.refreshIndex(); }); 35067 scope.$watch( 35068 function () { 35069 return ctrl.getTabElementIndex(element); 35070 }, 35071 function (newIndex) { 35072 data.index = newIndex; 35073 ctrl.updateTabOrder(); 35074 } 35075 ); 35076 scope.$on('$destroy', function () { ctrl.removeTab(data); }); 35077 } 35078 35079 function firstChild (element, tagName) { 35080 var children = element[0].children; 35081 for (var i = 0, len = children.length; i < len; i++) { 35082 var child = children[i]; 35083 if (child.tagName === tagName.toUpperCase()) return angular.element(child); 35084 } 35085 return angular.element(); 35086 } 35087} 35088 35089})(); 35090(function(){ 35091"use strict"; 35092 35093angular 35094 .module('material.components.tabs') 35095 .directive('mdTabItem', MdTabItem); 35096 35097function MdTabItem () { 35098 return { 35099 require: '^?mdTabs', 35100 link: function link (scope, element, attr, ctrl) { 35101 if (!ctrl) return; 35102 ctrl.attachRipple(scope, element); 35103 } 35104 }; 35105} 35106 35107})(); 35108(function(){ 35109"use strict"; 35110 35111angular 35112 .module('material.components.tabs') 35113 .directive('mdTabLabel', MdTabLabel); 35114 35115function MdTabLabel () { 35116 return { terminal: true }; 35117} 35118 35119 35120})(); 35121(function(){ 35122"use strict"; 35123 35124 35125MdTabScroll.$inject = ["$parse"];angular.module('material.components.tabs') 35126 .directive('mdTabScroll', MdTabScroll); 35127 35128function MdTabScroll ($parse) { 35129 return { 35130 restrict: 'A', 35131 compile: function ($element, attr) { 35132 var fn = $parse(attr.mdTabScroll, null, true); 35133 return function ngEventHandler (scope, element) { 35134 element.on('mousewheel', function (event) { 35135 scope.$apply(function () { fn(scope, { $event: event }); }); 35136 }); 35137 }; 35138 } 35139 }; 35140} 35141 35142})(); 35143(function(){ 35144"use strict"; 35145 35146 35147MdTabsController.$inject = ["$scope", "$element", "$window", "$mdConstant", "$mdTabInkRipple", "$mdUtil", "$animateCss", "$attrs", "$compile", "$mdTheming", "$mdInteraction", "MdTabsPaginationService"];angular 35148 .module('material.components.tabs') 35149 .controller('MdTabsController', MdTabsController); 35150 35151/** 35152 * @ngInject 35153 */ 35154function MdTabsController ($scope, $element, $window, $mdConstant, $mdTabInkRipple, $mdUtil, 35155 $animateCss, $attrs, $compile, $mdTheming, $mdInteraction, 35156 MdTabsPaginationService) { 35157 // define private properties 35158 var ctrl = this, 35159 locked = false, 35160 elements = getElements(), 35161 queue = [], 35162 destroyed = false, 35163 loaded = false; 35164 35165 35166 // Define public methods 35167 ctrl.$onInit = $onInit; 35168 ctrl.updatePagination = $mdUtil.debounce(updatePagination, 100); 35169 ctrl.redirectFocus = redirectFocus; 35170 ctrl.attachRipple = attachRipple; 35171 ctrl.insertTab = insertTab; 35172 ctrl.removeTab = removeTab; 35173 ctrl.select = select; 35174 ctrl.scroll = scroll; 35175 ctrl.nextPage = nextPage; 35176 ctrl.previousPage = previousPage; 35177 ctrl.keydown = keydown; 35178 ctrl.canPageForward = canPageForward; 35179 ctrl.canPageBack = canPageBack; 35180 ctrl.refreshIndex = refreshIndex; 35181 ctrl.incrementIndex = incrementIndex; 35182 ctrl.getTabElementIndex = getTabElementIndex; 35183 ctrl.updateInkBarStyles = $mdUtil.debounce(updateInkBarStyles, 100); 35184 ctrl.updateTabOrder = $mdUtil.debounce(updateTabOrder, 100); 35185 ctrl.getFocusedTabId = getFocusedTabId; 35186 35187 // For AngularJS 1.4 and older, where there are no lifecycle hooks but bindings are pre-assigned, 35188 // manually call the $onInit hook. 35189 if (angular.version.major === 1 && angular.version.minor <= 4) { 35190 this.$onInit(); 35191 } 35192 35193 /** 35194 * AngularJS Lifecycle hook for newer AngularJS versions. 35195 * Bindings are not guaranteed to have been assigned in the controller, but they are in the $onInit hook. 35196 */ 35197 function $onInit() { 35198 // Define one-way bindings 35199 defineOneWayBinding('stretchTabs', handleStretchTabs); 35200 35201 // Define public properties with change handlers 35202 defineProperty('focusIndex', handleFocusIndexChange, ctrl.selectedIndex || 0); 35203 defineProperty('offsetLeft', handleOffsetChange, 0); 35204 defineProperty('hasContent', handleHasContent, false); 35205 defineProperty('maxTabWidth', handleMaxTabWidth, getMaxTabWidth()); 35206 defineProperty('shouldPaginate', handleShouldPaginate, false); 35207 35208 // Define boolean attributes 35209 defineBooleanAttribute('noInkBar', handleInkBar); 35210 defineBooleanAttribute('dynamicHeight', handleDynamicHeight); 35211 defineBooleanAttribute('noPagination'); 35212 defineBooleanAttribute('swipeContent'); 35213 defineBooleanAttribute('noDisconnect'); 35214 defineBooleanAttribute('autoselect'); 35215 defineBooleanAttribute('noSelectClick'); 35216 defineBooleanAttribute('centerTabs', handleCenterTabs, false); 35217 defineBooleanAttribute('enableDisconnect'); 35218 35219 // Define public properties 35220 ctrl.scope = $scope; 35221 ctrl.parent = $scope.$parent; 35222 ctrl.tabs = []; 35223 ctrl.lastSelectedIndex = null; 35224 ctrl.hasFocus = false; 35225 ctrl.styleTabItemFocus = false; 35226 ctrl.shouldCenterTabs = shouldCenterTabs(); 35227 ctrl.tabContentPrefix = 'tab-content-'; 35228 35229 // Setup the tabs controller after all bindings are available. 35230 setupTabsController(); 35231 } 35232 35233 /** 35234 * Perform setup for the controller, setup events and watcher(s) 35235 */ 35236 function setupTabsController () { 35237 ctrl.selectedIndex = ctrl.selectedIndex || 0; 35238 compileTemplate(); 35239 configureWatchers(); 35240 bindEvents(); 35241 $mdTheming($element); 35242 $mdUtil.nextTick(function () { 35243 // Note that the element references need to be updated, because certain "browsers" 35244 // (IE/Edge) lose them and start throwing "Invalid calling object" errors, when we 35245 // compile the element contents down in `compileElement`. 35246 elements = getElements(); 35247 updateHeightFromContent(); 35248 adjustOffset(); 35249 updateInkBarStyles(); 35250 ctrl.tabs[ ctrl.selectedIndex ] && ctrl.tabs[ ctrl.selectedIndex ].scope.select(); 35251 loaded = true; 35252 updatePagination(); 35253 }); 35254 } 35255 35256 /** 35257 * Compiles the template provided by the user. This is passed as an attribute from the tabs 35258 * directive's template function. 35259 */ 35260 function compileTemplate () { 35261 var template = $attrs.$mdTabsTemplate, 35262 element = angular.element($element[0].querySelector('md-tab-data')); 35263 35264 element.html(template); 35265 $compile(element.contents())(ctrl.parent); 35266 delete $attrs.$mdTabsTemplate; 35267 } 35268 35269 /** 35270 * Binds events used by the tabs component. 35271 */ 35272 function bindEvents () { 35273 angular.element($window).on('resize', handleWindowResize); 35274 $scope.$on('$destroy', cleanup); 35275 } 35276 35277 /** 35278 * Configure watcher(s) used by Tabs 35279 */ 35280 function configureWatchers () { 35281 $scope.$watch('$mdTabsCtrl.selectedIndex', handleSelectedIndexChange); 35282 } 35283 35284 /** 35285 * Creates a one-way binding manually rather than relying on AngularJS's isolated scope 35286 * @param key 35287 * @param handler 35288 */ 35289 function defineOneWayBinding (key, handler) { 35290 var attr = $attrs.$normalize('md-' + key); 35291 if (handler) defineProperty(key, handler); 35292 $attrs.$observe(attr, function (newValue) { ctrl[ key ] = newValue; }); 35293 } 35294 35295 /** 35296 * Defines boolean attributes with default value set to true. (ie. md-stretch-tabs with no value 35297 * will be treated as being truthy) 35298 * @param key 35299 * @param handler 35300 */ 35301 function defineBooleanAttribute (key, handler) { 35302 var attr = $attrs.$normalize('md-' + key); 35303 if (handler) defineProperty(key, handler); 35304 if ($attrs.hasOwnProperty(attr)) updateValue($attrs[attr]); 35305 $attrs.$observe(attr, updateValue); 35306 function updateValue (newValue) { 35307 ctrl[ key ] = newValue !== 'false'; 35308 } 35309 } 35310 35311 /** 35312 * Remove any events defined by this controller 35313 */ 35314 function cleanup () { 35315 destroyed = true; 35316 angular.element($window).off('resize', handleWindowResize); 35317 } 35318 35319 // Change handlers 35320 35321 /** 35322 * Toggles stretch tabs class and updates inkbar when tab stretching changes 35323 * @param stretchTabs 35324 */ 35325 function handleStretchTabs (stretchTabs) { 35326 var elements = getElements(); 35327 angular.element(elements.wrapper).toggleClass('md-stretch-tabs', shouldStretchTabs()); 35328 updateInkBarStyles(); 35329 } 35330 35331 function handleCenterTabs (newValue) { 35332 ctrl.shouldCenterTabs = shouldCenterTabs(); 35333 } 35334 35335 function handleMaxTabWidth (newWidth, oldWidth) { 35336 if (newWidth !== oldWidth) { 35337 var elements = getElements(); 35338 35339 // Set the max width for the real tabs 35340 angular.forEach(elements.tabs, function(tab) { 35341 tab.style.maxWidth = newWidth + 'px'; 35342 }); 35343 35344 // Set the max width for the dummy tabs too 35345 angular.forEach(elements.dummies, function(tab) { 35346 tab.style.maxWidth = newWidth + 'px'; 35347 }); 35348 35349 $mdUtil.nextTick(ctrl.updateInkBarStyles); 35350 } 35351 } 35352 35353 function handleShouldPaginate (newValue, oldValue) { 35354 if (newValue !== oldValue) { 35355 ctrl.maxTabWidth = getMaxTabWidth(); 35356 ctrl.shouldCenterTabs = shouldCenterTabs(); 35357 $mdUtil.nextTick(function () { 35358 ctrl.maxTabWidth = getMaxTabWidth(); 35359 adjustOffset(ctrl.selectedIndex); 35360 }); 35361 } 35362 } 35363 35364 /** 35365 * Add/remove the `md-no-tab-content` class depending on `ctrl.hasContent` 35366 * @param hasContent 35367 */ 35368 function handleHasContent (hasContent) { 35369 $element[ hasContent ? 'removeClass' : 'addClass' ]('md-no-tab-content'); 35370 } 35371 35372 /** 35373 * Apply ctrl.offsetLeft to the paging element when it changes 35374 * @param left 35375 */ 35376 function handleOffsetChange (left) { 35377 var elements = getElements(); 35378 var newValue = ((ctrl.shouldCenterTabs || isRtl() ? '' : '-') + left + 'px'); 35379 35380 // Fix double-negative which can happen with RTL support 35381 newValue = newValue.replace('--', ''); 35382 35383 angular.element(elements.paging).css($mdConstant.CSS.TRANSFORM, 'translate3d(' + newValue + ', 0, 0)'); 35384 $scope.$broadcast('$mdTabsPaginationChanged'); 35385 } 35386 35387 /** 35388 * Update the UI whenever `ctrl.focusIndex` is updated 35389 * @param newIndex 35390 * @param oldIndex 35391 */ 35392 function handleFocusIndexChange (newIndex, oldIndex) { 35393 if (newIndex === oldIndex) return; 35394 if (!getElements().tabs[ newIndex ]) return; 35395 adjustOffset(); 35396 redirectFocus(); 35397 } 35398 35399 /** 35400 * Update the UI whenever the selected index changes. Calls user-defined select/deselect methods. 35401 * @param newValue 35402 * @param oldValue 35403 */ 35404 function handleSelectedIndexChange (newValue, oldValue) { 35405 if (newValue === oldValue) return; 35406 35407 ctrl.selectedIndex = getNearestSafeIndex(newValue); 35408 ctrl.lastSelectedIndex = oldValue; 35409 ctrl.updateInkBarStyles(); 35410 updateHeightFromContent(); 35411 adjustOffset(newValue); 35412 $scope.$broadcast('$mdTabsChanged'); 35413 ctrl.tabs[ oldValue ] && ctrl.tabs[ oldValue ].scope.deselect(); 35414 ctrl.tabs[ newValue ] && ctrl.tabs[ newValue ].scope.select(); 35415 } 35416 35417 function getTabElementIndex(tabEl){ 35418 var tabs = $element[0].getElementsByTagName('md-tab'); 35419 return Array.prototype.indexOf.call(tabs, tabEl[0]); 35420 } 35421 35422 /** 35423 * Queues up a call to `handleWindowResize` when a resize occurs while the tabs component is 35424 * hidden. 35425 */ 35426 function handleResizeWhenVisible () { 35427 // if there is already a watcher waiting for resize, do nothing 35428 if (handleResizeWhenVisible.watcher) return; 35429 // otherwise, we will abuse the $watch function to check for visible 35430 handleResizeWhenVisible.watcher = $scope.$watch(function () { 35431 // since we are checking for DOM size, we use $mdUtil.nextTick() to wait for after the DOM updates 35432 $mdUtil.nextTick(function () { 35433 // if the watcher has already run (ie. multiple digests in one cycle), do nothing 35434 if (!handleResizeWhenVisible.watcher) return; 35435 35436 if ($element.prop('offsetParent')) { 35437 handleResizeWhenVisible.watcher(); 35438 handleResizeWhenVisible.watcher = null; 35439 35440 handleWindowResize(); 35441 } 35442 }, false); 35443 }); 35444 } 35445 35446 // Event handlers / actions 35447 35448 /** 35449 * Handle user keyboard interactions 35450 * @param event 35451 */ 35452 function keydown (event) { 35453 switch (event.keyCode) { 35454 case $mdConstant.KEY_CODE.LEFT_ARROW: 35455 event.preventDefault(); 35456 incrementIndex(-1, true); 35457 break; 35458 case $mdConstant.KEY_CODE.RIGHT_ARROW: 35459 event.preventDefault(); 35460 incrementIndex(1, true); 35461 break; 35462 case $mdConstant.KEY_CODE.SPACE: 35463 case $mdConstant.KEY_CODE.ENTER: 35464 event.preventDefault(); 35465 if (!locked) select(ctrl.focusIndex); 35466 break; 35467 case $mdConstant.KEY_CODE.TAB: 35468 // On tabbing out of the tablist, reset hasFocus to reset ng-focused and 35469 // its md-focused class if the focused tab is not the active tab. 35470 if (ctrl.focusIndex !== ctrl.selectedIndex) { 35471 ctrl.focusIndex = ctrl.selectedIndex; 35472 } 35473 break; 35474 } 35475 } 35476 35477 /** 35478 * Update the selected index. Triggers a click event on the original `md-tab` element in order 35479 * to fire user-added click events if canSkipClick or `md-no-select-click` are false. 35480 * @param index 35481 * @param canSkipClick Optionally allow not firing the click event if `md-no-select-click` is also true. 35482 */ 35483 function select (index, canSkipClick) { 35484 if (!locked) ctrl.focusIndex = ctrl.selectedIndex = index; 35485 // skip the click event if noSelectClick is enabled 35486 if (canSkipClick && ctrl.noSelectClick) return; 35487 // nextTick is required to prevent errors in user-defined click events 35488 $mdUtil.nextTick(function () { 35489 ctrl.tabs[ index ].element.triggerHandler('click'); 35490 }, false); 35491 } 35492 35493 /** 35494 * When pagination is on, this makes sure the selected index is in view. 35495 * @param event 35496 */ 35497 function scroll (event) { 35498 if (!ctrl.shouldPaginate) return; 35499 event.preventDefault(); 35500 ctrl.offsetLeft = fixOffset(ctrl.offsetLeft - event.wheelDelta); 35501 } 35502 35503 /** 35504 * Slides the tabs over approximately one page forward. 35505 */ 35506 function nextPage () { 35507 if (!ctrl.canPageForward()) { return } 35508 35509 var newOffset = MdTabsPaginationService.increasePageOffset(getElements(), ctrl.offsetLeft); 35510 35511 ctrl.offsetLeft = fixOffset(newOffset); 35512 } 35513 35514 /** 35515 * Slides the tabs over approximately one page backward. 35516 */ 35517 function previousPage () { 35518 if (!ctrl.canPageBack()) { return } 35519 35520 var newOffset = MdTabsPaginationService.decreasePageOffset(getElements(), ctrl.offsetLeft); 35521 35522 // Set the new offset 35523 ctrl.offsetLeft = fixOffset(newOffset); 35524 } 35525 35526 /** 35527 * Update size calculations when the window is resized. 35528 */ 35529 function handleWindowResize () { 35530 ctrl.lastSelectedIndex = ctrl.selectedIndex; 35531 ctrl.offsetLeft = fixOffset(ctrl.offsetLeft); 35532 35533 $mdUtil.nextTick(function () { 35534 ctrl.updateInkBarStyles(); 35535 updatePagination(); 35536 }); 35537 } 35538 35539 function handleInkBar (hide) { 35540 angular.element(getElements().inkBar).toggleClass('ng-hide', hide); 35541 } 35542 35543 /** 35544 * Toggle dynamic height class when value changes 35545 * @param value 35546 */ 35547 function handleDynamicHeight (value) { 35548 $element.toggleClass('md-dynamic-height', value); 35549 } 35550 35551 /** 35552 * Remove a tab from the data and select the nearest valid tab. 35553 * @param tabData 35554 */ 35555 function removeTab (tabData) { 35556 if (destroyed) return; 35557 var selectedIndex = ctrl.selectedIndex, 35558 tab = ctrl.tabs.splice(tabData.getIndex(), 1)[ 0 ]; 35559 refreshIndex(); 35560 // when removing a tab, if the selected index did not change, we have to manually trigger the 35561 // tab select/deselect events 35562 if (ctrl.selectedIndex === selectedIndex) { 35563 tab.scope.deselect(); 35564 ctrl.tabs[ ctrl.selectedIndex ] && ctrl.tabs[ ctrl.selectedIndex ].scope.select(); 35565 } 35566 $mdUtil.nextTick(function () { 35567 updatePagination(); 35568 ctrl.offsetLeft = fixOffset(ctrl.offsetLeft); 35569 }); 35570 } 35571 35572 /** 35573 * Create an entry in the tabs array for a new tab at the specified index. 35574 * @param tabData 35575 * @param index 35576 * @returns {*} 35577 */ 35578 function insertTab (tabData, index) { 35579 var hasLoaded = loaded; 35580 var proto = { 35581 getIndex: function () { return ctrl.tabs.indexOf(tab); }, 35582 isActive: function () { return this.getIndex() === ctrl.selectedIndex; }, 35583 isLeft: function () { return this.getIndex() < ctrl.selectedIndex; }, 35584 isRight: function () { return this.getIndex() > ctrl.selectedIndex; }, 35585 shouldRender: function () { return !ctrl.noDisconnect || this.isActive(); }, 35586 hasFocus: function () { 35587 return ctrl.styleTabItemFocus 35588 && ctrl.hasFocus && this.getIndex() === ctrl.focusIndex; 35589 }, 35590 id: $mdUtil.nextUid(), 35591 hasContent: !!(tabData.template && tabData.template.trim()) 35592 }, 35593 tab = angular.extend(proto, tabData); 35594 if (angular.isDefined(index)) { 35595 ctrl.tabs.splice(index, 0, tab); 35596 } else { 35597 ctrl.tabs.push(tab); 35598 } 35599 35600 processQueue(); 35601 updateHasContent(); 35602 $mdUtil.nextTick(function () { 35603 updatePagination(); 35604 setAriaControls(tab); 35605 35606 // if autoselect is enabled, select the newly added tab 35607 if (hasLoaded && ctrl.autoselect) $mdUtil.nextTick(function () { 35608 $mdUtil.nextTick(function () { select(ctrl.tabs.indexOf(tab)); }); 35609 }); 35610 }); 35611 return tab; 35612 } 35613 35614 // Getter methods 35615 35616 /** 35617 * Gathers references to all of the DOM elements used by this controller. 35618 * @returns {{}} 35619 */ 35620 function getElements () { 35621 var elements = {}; 35622 var node = $element[0]; 35623 35624 // gather tab bar elements 35625 elements.wrapper = node.querySelector('md-tabs-wrapper'); 35626 elements.canvas = elements.wrapper.querySelector('md-tabs-canvas'); 35627 elements.paging = elements.canvas.querySelector('md-pagination-wrapper'); 35628 elements.inkBar = elements.paging.querySelector('md-ink-bar'); 35629 elements.nextButton = node.querySelector('md-next-button'); 35630 elements.prevButton = node.querySelector('md-prev-button'); 35631 35632 elements.contents = node.querySelectorAll('md-tabs-content-wrapper > md-tab-content'); 35633 elements.tabs = elements.paging.querySelectorAll('md-tab-item'); 35634 elements.dummies = elements.canvas.querySelectorAll('md-dummy-tab'); 35635 35636 return elements; 35637 } 35638 35639 /** 35640 * Determines whether or not the left pagination arrow should be enabled. 35641 * @returns {boolean} 35642 */ 35643 function canPageBack () { 35644 // This works for both LTR and RTL 35645 return ctrl.offsetLeft > 0; 35646 } 35647 35648 /** 35649 * Determines whether or not the right pagination arrow should be enabled. 35650 * @returns {*|boolean} 35651 */ 35652 function canPageForward () { 35653 var elements = getElements(); 35654 var lastTab = elements.tabs[ elements.tabs.length - 1 ]; 35655 35656 if (isRtl()) { 35657 return ctrl.offsetLeft < elements.paging.offsetWidth - elements.canvas.offsetWidth; 35658 } 35659 35660 return lastTab && lastTab.offsetLeft + lastTab.offsetWidth > elements.canvas.clientWidth + 35661 ctrl.offsetLeft; 35662 } 35663 35664 /** 35665 * Returns currently focused tab item's element ID 35666 */ 35667 function getFocusedTabId() { 35668 var focusedTab = ctrl.tabs[ctrl.focusIndex]; 35669 if (!focusedTab || !focusedTab.id) { 35670 return null; 35671 } 35672 return 'tab-item-' + focusedTab.id; 35673 } 35674 35675 /** 35676 * Determines if the UI should stretch the tabs to fill the available space. 35677 * @returns {*} 35678 */ 35679 function shouldStretchTabs () { 35680 switch (ctrl.stretchTabs) { 35681 case 'always': 35682 return true; 35683 case 'never': 35684 return false; 35685 default: 35686 return !ctrl.shouldPaginate 35687 && $window.matchMedia('(max-width: 600px)').matches; 35688 } 35689 } 35690 35691 /** 35692 * Determines if the tabs should appear centered. 35693 * @returns {string|boolean} 35694 */ 35695 function shouldCenterTabs () { 35696 return ctrl.centerTabs && !ctrl.shouldPaginate; 35697 } 35698 35699 /** 35700 * Determines if pagination is necessary to display the tabs within the available space. 35701 * @returns {boolean} 35702 */ 35703 function shouldPaginate () { 35704 if (ctrl.noPagination || !loaded) return false; 35705 var canvasWidth = $element.prop('clientWidth'); 35706 35707 angular.forEach(getElements().tabs, function (tab) { 35708 canvasWidth -= tab.offsetWidth; 35709 }); 35710 35711 return canvasWidth < 0; 35712 } 35713 35714 /** 35715 * Finds the nearest tab index that is available. This is primarily used for when the active 35716 * tab is removed. 35717 * @param newIndex 35718 * @returns {*} 35719 */ 35720 function getNearestSafeIndex (newIndex) { 35721 if (newIndex === -1) return -1; 35722 var maxOffset = Math.max(ctrl.tabs.length - newIndex, newIndex), 35723 i, tab; 35724 for (i = 0; i <= maxOffset; i++) { 35725 tab = ctrl.tabs[ newIndex + i ]; 35726 if (tab && (tab.scope.disabled !== true)) return tab.getIndex(); 35727 tab = ctrl.tabs[ newIndex - i ]; 35728 if (tab && (tab.scope.disabled !== true)) return tab.getIndex(); 35729 } 35730 return newIndex; 35731 } 35732 35733 // Utility methods 35734 35735 /** 35736 * Defines a property using a getter and setter in order to trigger a change handler without 35737 * using `$watch` to observe changes. 35738 * @param key 35739 * @param handler 35740 * @param value 35741 */ 35742 function defineProperty (key, handler, value) { 35743 Object.defineProperty(ctrl, key, { 35744 get: function () { return value; }, 35745 set: function (newValue) { 35746 var oldValue = value; 35747 value = newValue; 35748 handler && handler(newValue, oldValue); 35749 } 35750 }); 35751 } 35752 35753 /** 35754 * Updates whether or not pagination should be displayed. 35755 */ 35756 function updatePagination () { 35757 ctrl.maxTabWidth = getMaxTabWidth(); 35758 ctrl.shouldPaginate = shouldPaginate(); 35759 } 35760 35761 /** 35762 * Calculates the width of the pagination wrapper by summing the widths of the dummy tabs. 35763 * @returns {number} 35764 */ 35765 function calcPagingWidth () { 35766 return calcTabsWidth(getElements().tabs); 35767 } 35768 35769 function calcTabsWidth(tabs) { 35770 var width = 0; 35771 35772 angular.forEach(tabs, function (tab) { 35773 //-- Uses the larger value between `getBoundingClientRect().width` and `offsetWidth`. This 35774 // prevents `offsetWidth` value from being rounded down and causing wrapping issues, but 35775 // also handles scenarios where `getBoundingClientRect()` is inaccurate (ie. tabs inside 35776 // of a dialog) 35777 width += Math.max(tab.offsetWidth, tab.getBoundingClientRect().width); 35778 }); 35779 35780 return Math.ceil(width); 35781 } 35782 35783 function getMaxTabWidth () { 35784 var elements = getElements(), 35785 containerWidth = elements.canvas.clientWidth, 35786 35787 // See https://material.google.com/components/tabs.html#tabs-specs 35788 specMax = 264; 35789 35790 // Do the spec maximum, or the canvas width; whichever is *smaller* (tabs larger than the canvas 35791 // width can break the pagination) but not less than 0 35792 return Math.max(0, Math.min(containerWidth - 1, specMax)); 35793 } 35794 35795 function getMinTabWidth() { 35796 var elements = getElements(), 35797 containerWidth = elements.canvas.clientWidth, 35798 xsBreakpoint = 600, 35799 35800 // See https://material.google.com/components/tabs.html#tabs-specs 35801 specMin = containerWidth > xsBreakpoint ? 160 : 72; 35802 35803 // Do the spec minimum, or the canvas width; whichever is *smaller* (tabs larger than the canvas 35804 // width can break the pagination) but not less than 0 35805 return Math.max(0, Math.min(containerWidth - 1, specMin)); 35806 } 35807 35808 /** 35809 * Re-orders the tabs and updates the selected and focus indexes to their new positions. 35810 * This is triggered by `tabDirective.js` when the user's tabs have been re-ordered. 35811 */ 35812 function updateTabOrder () { 35813 var selectedItem = ctrl.tabs[ ctrl.selectedIndex ], 35814 focusItem = ctrl.tabs[ ctrl.focusIndex ]; 35815 ctrl.tabs = ctrl.tabs.sort(function (a, b) { 35816 return a.index - b.index; 35817 }); 35818 ctrl.selectedIndex = ctrl.tabs.indexOf(selectedItem); 35819 ctrl.focusIndex = ctrl.tabs.indexOf(focusItem); 35820 } 35821 35822 /** 35823 * This moves the selected or focus index left or right. This is used by the keydown handler. 35824 * @param inc 35825 */ 35826 function incrementIndex (inc, focus) { 35827 var newIndex, 35828 key = focus ? 'focusIndex' : 'selectedIndex', 35829 index = ctrl[ key ]; 35830 for (newIndex = index + inc; 35831 ctrl.tabs[ newIndex ] && ctrl.tabs[ newIndex ].scope.disabled; 35832 newIndex += inc) {} 35833 35834 newIndex = (index + inc + ctrl.tabs.length) % ctrl.tabs.length; 35835 35836 if (ctrl.tabs[ newIndex ]) { 35837 ctrl[ key ] = newIndex; 35838 } 35839 } 35840 35841 /** 35842 * This is used to forward focus to tab container elements. This method is necessary to avoid 35843 * animation issues when attempting to focus an item that is out of view. 35844 */ 35845 function redirectFocus () { 35846 ctrl.styleTabItemFocus = ($mdInteraction.getLastInteractionType() === 'keyboard'); 35847 getElements().tabs[ ctrl.focusIndex ].focus(); 35848 } 35849 35850 /** 35851 * Forces the pagination to move the focused tab into view. 35852 */ 35853 function adjustOffset (index) { 35854 var elements = getElements(); 35855 35856 if (!angular.isNumber(index)) index = ctrl.focusIndex; 35857 if (!elements.tabs[ index ]) return; 35858 if (ctrl.shouldCenterTabs) return; 35859 var tab = elements.tabs[ index ], 35860 left = tab.offsetLeft, 35861 right = tab.offsetWidth + left, 35862 extraOffset = 32; 35863 35864 // If we are selecting the first tab (in LTR and RTL), always set the offset to 0 35865 if (index == 0) { 35866 ctrl.offsetLeft = 0; 35867 return; 35868 } 35869 35870 if (isRtl()) { 35871 var tabWidthsBefore = calcTabsWidth(Array.prototype.slice.call(elements.tabs, 0, index)); 35872 var tabWidthsIncluding = calcTabsWidth(Array.prototype.slice.call(elements.tabs, 0, index + 1)); 35873 35874 ctrl.offsetLeft = Math.min(ctrl.offsetLeft, fixOffset(tabWidthsBefore)); 35875 ctrl.offsetLeft = Math.max(ctrl.offsetLeft, fixOffset(tabWidthsIncluding - elements.canvas.clientWidth)); 35876 } else { 35877 ctrl.offsetLeft = Math.max(ctrl.offsetLeft, fixOffset(right - elements.canvas.clientWidth + extraOffset)); 35878 ctrl.offsetLeft = Math.min(ctrl.offsetLeft, fixOffset(left)); 35879 } 35880 } 35881 35882 /** 35883 * Iterates through all queued functions and clears the queue. This is used for functions that
35884 * are called before the UI is ready, such as size calculations. 35885 */ 35886 function processQueue () { 35887 queue.forEach(function (func) { $mdUtil.nextTick(func); }); 35888 queue = []; 35889 } 35890 35891 /** 35892 * Determines if the tab content area is needed. 35893 */ 35894 function updateHasContent () { 35895 var hasContent = false; 35896 35897 for (var i = 0; i < ctrl.tabs.length; i++) { 35898 if (ctrl.tabs[i].hasContent) { 35899 hasContent = true; 35900 break; 35901 } 35902 } 35903 35904 ctrl.hasContent = hasContent; 35905 } 35906 35907 /** 35908 * Moves the indexes to their nearest valid values. 35909 */ 35910 function refreshIndex () { 35911 ctrl.selectedIndex = getNearestSafeIndex(ctrl.selectedIndex); 35912 ctrl.focusIndex = getNearestSafeIndex(ctrl.focusIndex); 35913 } 35914 35915 /** 35916 * Calculates the content height of the current tab. 35917 * @returns {*} 35918 */ 35919 function updateHeightFromContent () { 35920 if (!ctrl.dynamicHeight) return $element.css('height', ''); 35921 if (!ctrl.tabs.length) return queue.push(updateHeightFromContent); 35922 35923 var elements = getElements(); 35924 35925 var tabContent = elements.contents[ ctrl.selectedIndex ], 35926 contentHeight = tabContent ? tabContent.offsetHeight : 0, 35927 tabsHeight = elements.wrapper.offsetHeight, 35928 newHeight = contentHeight + tabsHeight, 35929 currentHeight = $element.prop('clientHeight'); 35930 35931 if (currentHeight === newHeight) return; 35932 35933 // Adjusts calculations for when the buttons are bottom-aligned since this relies on absolute 35934 // positioning. This should probably be cleaned up if a cleaner solution is possible. 35935 if ($element.attr('md-align-tabs') === 'bottom') { 35936 currentHeight -= tabsHeight; 35937 newHeight -= tabsHeight; 35938 // Need to include bottom border in these calculations 35939 if ($element.attr('md-border-bottom') !== undefined) ++currentHeight; 35940 } 35941 35942 // Lock during animation so the user can't change tabs 35943 locked = true; 35944 35945 var fromHeight = { height: currentHeight + 'px' }, 35946 toHeight = { height: newHeight + 'px' }; 35947 35948 // Set the height to the current, specific pixel height to fix a bug on iOS where the height 35949 // first animates to 0, then back to the proper height causing a visual glitch 35950 $element.css(fromHeight); 35951 35952 // Animate the height from the old to the new 35953 $animateCss($element, { 35954 from: fromHeight, 35955 to: toHeight, 35956 easing: 'cubic-bezier(0.35, 0, 0.25, 1)', 35957 duration: 0.5 35958 }).start().done(function () { 35959 // Then (to fix the same iOS issue as above), disable transitions and remove the specific 35960 // pixel height so the height can size with browser width/content changes, etc. 35961 $element.css({ 35962 transition: 'none', 35963 height: '' 35964 }); 35965 35966 // In the next tick, re-allow transitions (if we do it all at once, $element.css is "smart" 35967 // enough to batch it for us instead of doing it immediately, which undoes the original 35968 // transition: none) 35969 $mdUtil.nextTick(function() { 35970 $element.css('transition', ''); 35971 }); 35972 35973 // And unlock so tab changes can occur 35974 locked = false; 35975 }); 35976 } 35977 35978 /** 35979 * Repositions the ink bar to the selected tab. 35980 * @returns {*} 35981 */ 35982 function updateInkBarStyles () { 35983 var elements = getElements(); 35984 35985 if (!elements.tabs[ ctrl.selectedIndex ]) { 35986 angular.element(elements.inkBar).css({ left: 'auto', right: 'auto' }); 35987 return; 35988 } 35989 35990 if (!ctrl.tabs.length) return queue.push(ctrl.updateInkBarStyles); 35991 // if the element is not visible, we will not be able to calculate sizes until it is 35992 // we should treat that as a resize event rather than just updating the ink bar 35993 if (!$element.prop('offsetParent')) return handleResizeWhenVisible(); 35994 35995 var index = ctrl.selectedIndex, 35996 totalWidth = elements.paging.offsetWidth, 35997 tab = elements.tabs[ index ], 35998 left = tab.offsetLeft, 35999 right = totalWidth - left - tab.offsetWidth; 36000 36001 if (ctrl.shouldCenterTabs) { 36002 // We need to use the same calculate process as in the pagination wrapper, to avoid rounding deviations. 36003 var tabWidth = calcTabsWidth(elements.tabs); 36004 36005 if (totalWidth > tabWidth) { 36006 $mdUtil.nextTick(updateInkBarStyles, false); 36007 } 36008 } 36009 updateInkBarClassName(); 36010 angular.element(elements.inkBar).css({ left: left + 'px', right: right + 'px' }); 36011 } 36012 36013 /** 36014 * Adds left/right classes so that the ink bar will animate properly. 36015 */ 36016 function updateInkBarClassName () { 36017 var elements = getElements(); 36018 var newIndex = ctrl.selectedIndex, 36019 oldIndex = ctrl.lastSelectedIndex, 36020 ink = angular.element(elements.inkBar); 36021 if (!angular.isNumber(oldIndex)) return; 36022 ink 36023 .toggleClass('md-left', newIndex < oldIndex) 36024 .toggleClass('md-right', newIndex > oldIndex); 36025 } 36026 36027 /**
vendor: 22,548 bytes, lines 36028-36400
36028 * Takes an offset value and makes sure that it is within the min/max allowed values. 36029 * @param value 36030 * @returns {*} 36031 */ 36032 function fixOffset (value) { 36033 var elements = getElements(); 36034 36035 if (!elements.tabs.length || !ctrl.shouldPaginate) return 0; 36036 36037 var lastTab = elements.tabs[ elements.tabs.length - 1 ], 36038 totalWidth = lastTab.offsetLeft + lastTab.offsetWidth; 36039 36040 if (isRtl()) { 36041 value = Math.min(elements.paging.offsetWidth - elements.canvas.clientWidth, value); 36042 value = Math.max(0, value); 36043 } else { 36044 value = Math.max(0, value); 36045 value = Math.min(totalWidth - elements.canvas.clientWidth, value); 36046 } 36047 36048 return value; 36049 } 36050 36051 /** 36052 * Attaches a ripple to the tab item element. 36053 * @param scope 36054 * @param element 36055 */ 36056 function attachRipple (scope, element) { 36057 var elements = getElements(); 36058 var options = { colorElement: angular.element(elements.inkBar) }; 36059 $mdTabInkRipple.attach(scope, element, options); 36060 } 36061 36062 /** 36063 * Sets the `aria-controls` attribute to the elements that 36064 * correspond to the passed-in tab. 36065 * @param tab 36066 */ 36067 function setAriaControls (tab) { 36068 if (tab.hasContent) { 36069 var nodes = $element[0].querySelectorAll('[md-tab-id="' + tab.id + '"]'); 36070 angular.element(nodes).attr('aria-controls', ctrl.tabContentPrefix + tab.id); 36071 } 36072 } 36073 36074 function isRtl() { 36075 return ($mdUtil.bidi() == 'rtl'); 36076 } 36077} 36078 36079})(); 36080(function(){ 36081"use strict"; 36082 36083/** 36084 * @ngdoc directive 36085 * @name mdTabs 36086 * @module material.components.tabs 36087 * 36088 * @restrict E 36089 * 36090 * @description 36091 * The `<md-tabs>` directive serves as the container for 1..n `<md-tab>` child directives to 36092 * produces a Tabs components. In turn, the nested `<md-tab>` directive is used to specify a tab 36093 * label for the **header button** and a [optional] tab view content that will be associated with 36094 * each tab button. 36095 * 36096 * Below is the markup for its simplest usage: 36097 * 36098 * <hljs lang="html"> 36099 * <md-tabs> 36100 * <md-tab label="Tab #1"></md-tab> 36101 * <md-tab label="Tab #2"></md-tab> 36102 * <md-tab label="Tab #3"></md-tab> 36103 * </md-tabs> 36104 * </hljs> 36105 * 36106 * Tabs supports three (3) usage scenarios: 36107 * 36108 * 1. Tabs (buttons only) 36109 * 2. Tabs with internal view content 36110 * 3. Tabs with external view content 36111 * 36112 * **Tab-only** support is useful when tab buttons are used for custom navigation regardless of any 36113 * other components, content, or views. 36114 * 36115 * <i><b>Note:</b> If you are using the Tabs component for page-level navigation, please take a look 36116 * at the <a ng-href="./api/directive/mdNavBar">NavBar component</a> instead as it can handle this 36117 * case a bit more natively.</i> 36118 * 36119 * **Tabs with internal views** are the traditional usages where each tab has associated view 36120 * content and the view switching is managed internally by the Tabs component. 36121 * 36122 * **Tabs with external view content** is often useful when content associated with each tab is 36123 * independently managed and data-binding notifications announce tab selection changes. 36124 * 36125 * Additional features also include: 36126 * 36127 * * Content can include any markup. 36128 * * If a tab is disabled while active/selected, then the next tab will be auto-selected. 36129 * 36130 * ### Explanation of tab stretching 36131 * 36132 * 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. 36133 * 36134 * On mobile devices, tabs will be expanded to fill the available horizontal space. When this happens, all tabs will become the same size. 36135 * 36136 * On desktops, by default, stretching will never occur. 36137 * 36138 * This default behavior can be overridden through the `md-stretch-tabs` attribute. Here is a table showing when stretching will occur: 36139 * 36140 * `md-stretch-tabs` | mobile | desktop 36141 * ------------------|-----------|-------- 36142 * `auto` | stretched | --- 36143 * `always` | stretched | stretched 36144 * `never` | --- | --- 36145 * 36146 * @param {integer=} md-selected Index of the active/selected tab 36147 * @param {boolean=} md-no-ink If present, disables ink ripple effects. 36148 * @param {boolean=} md-no-ink-bar If present, disables the selection ink bar. 36149 * @param {string=} md-align-tabs Attribute to indicate position of tab buttons: `bottom` or `top`; default is `top` 36150 * @param {string=} md-stretch-tabs Attribute to indicate whether or not to stretch tabs: `auto`, `always`, or `never`; default is `auto` 36151 * @param {boolean=} md-dynamic-height When enabled, the tab wrapper will resize based on the contents of the selected tab 36152 * @param {boolean=} md-border-bottom If present, shows a solid `1px` border between the tabs and their content 36153 * @param {boolean=} md-center-tabs When enabled, tabs will be centered provided there is no need for pagination 36154 * @param {boolean=} md-no-pagination When enabled, pagination will remain off 36155 * @param {boolean=} md-swipe-content When enabled, swipe gestures will be enabled for the content area to jump between tabs 36156 * @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. 36157 * @param {boolean=} md-autoselect When present, any tabs added after the initial load will be automatically selected 36158 * @param {boolean=} md-no-select-click When enabled, click events will not be fired when selecting tabs 36159 * 36160 * @usage 36161 * <hljs lang="html"> 36162 * <md-tabs md-selected="selectedIndex" > 36163 * <img ng-src="img/angular.png" class="centered"> 36164 * <md-tab 36165 * ng-repeat="tab in tabs | orderBy:predicate:reversed" 36166 * md-on-select="onTabSelected(tab)" 36167 * md-on-deselect="announceDeselected(tab)" 36168 * ng-disabled="tab.disabled"> 36169 * <md-tab-label> 36170 * {{tab.title}} 36171 * <img src="img/removeTab.png" ng-click="removeTab(tab)" class="delete"> 36172 * </md-tab-label> 36173 * <md-tab-body> 36174 * {{tab.content}} 36175 * </md-tab-body> 36176 * </md-tab> 36177 * </md-tabs> 36178 * </hljs> 36179 * 36180 */ 36181MdTabs.$inject = ["$$mdSvgRegistry"]; 36182angular 36183 .module('material.components.tabs') 36184 .directive('mdTabs', MdTabs); 36185 36186function MdTabs ($$mdSvgRegistry) { 36187 return { 36188 scope: { 36189 selectedIndex: '=?mdSelected' 36190 }, 36191 template: function (element, attr) { 36192 attr.$mdTabsTemplate = element.html(); 36193 return '' + 36194 '<md-tabs-wrapper> ' + 36195 '<md-tab-data></md-tab-data> ' + 36196 '<md-prev-button ' + 36197 'tabindex="-1" ' + 36198 'role="button" ' + 36199 'aria-label="Previous Page" ' + 36200 'aria-disabled="{{!$mdTabsCtrl.canPageBack()}}" ' + 36201 'ng-class="{ \'md-disabled\': !$mdTabsCtrl.canPageBack() }" ' + 36202 'ng-if="$mdTabsCtrl.shouldPaginate" ' + 36203 'ng-click="$mdTabsCtrl.previousPage()"> ' + 36204 '<md-icon md-svg-src="'+ $$mdSvgRegistry.mdTabsArrow +'"></md-icon> ' + 36205 '</md-prev-button> ' + 36206 '<md-next-button ' + 36207 'tabindex="-1" ' + 36208 'role="button" ' + 36209 'aria-label="Next Page" ' + 36210 'aria-disabled="{{!$mdTabsCtrl.canPageForward()}}" ' + 36211 'ng-class="{ \'md-disabled\': !$mdTabsCtrl.canPageForward() }" ' + 36212 'ng-if="$mdTabsCtrl.shouldPaginate" ' + 36213 'ng-click="$mdTabsCtrl.nextPage()"> ' + 36214 '<md-icon md-svg-src="'+ $$mdSvgRegistry.mdTabsArrow +'"></md-icon> ' + 36215 '</md-next-button> ' + 36216 '<md-tabs-canvas ' + 36217 'ng-focus="$mdTabsCtrl.redirectFocus()" ' + 36218 'ng-class="{ ' + 36219 '\'md-paginated\': $mdTabsCtrl.shouldPaginate, ' + 36220 '\'md-center-tabs\': $mdTabsCtrl.shouldCenterTabs ' + 36221 '}" ' + 36222 'ng-keydown="$mdTabsCtrl.keydown($event)"> ' + 36223 '<md-pagination-wrapper ' + 36224 'ng-class="{ \'md-center-tabs\': $mdTabsCtrl.shouldCenterTabs }" ' + 36225 'md-tab-scroll="$mdTabsCtrl.scroll($event)" ' + 36226 'role="tablist"> ' + 36227 '<md-tab-item ' + 36228 'tabindex="{{ tab.isActive() ? 0 : -1 }}" ' + 36229 'class="md-tab" ' + 36230 'ng-repeat="tab in $mdTabsCtrl.tabs" ' + 36231 'role="tab" ' + 36232 'id="tab-item-{{::tab.id}}" ' + 36233 'md-tab-id="{{::tab.id}}"' + 36234 'aria-selected="{{tab.isActive()}}" ' + 36235 'aria-disabled="{{tab.scope.disabled || \'false\'}}" ' + 36236 'ng-click="$mdTabsCtrl.select(tab.getIndex())" ' + 36237 'ng-focus="$mdTabsCtrl.hasFocus = true" ' + 36238 'ng-blur="$mdTabsCtrl.hasFocus = false" ' + 36239 'ng-class="{ ' + 36240 '\'md-active\': tab.isActive(), ' + 36241 '\'md-focused\': tab.hasFocus(), ' + 36242 '\'md-disabled\': tab.scope.disabled ' + 36243 '}" ' + 36244 'ng-disabled="tab.scope.disabled" ' + 36245 'md-swipe-left="$mdTabsCtrl.nextPage()" ' + 36246 'md-swipe-right="$mdTabsCtrl.previousPage()" ' + 36247 'md-tabs-template="::tab.label" ' + 36248 'md-scope="::tab.parent"></md-tab-item> ' + 36249 '<md-ink-bar></md-ink-bar> ' + 36250 '</md-pagination-wrapper> ' + 36251 '<md-tabs-dummy-wrapper aria-hidden="true" class="md-visually-hidden md-dummy-wrapper"> ' + 36252 '<md-dummy-tab ' + 36253 'class="md-tab" ' + 36254 'tabindex="-1" ' + 36255 'ng-repeat="tab in $mdTabsCtrl.tabs" ' + 36256 'md-tabs-template="::tab.label" ' + 36257 'md-scope="::tab.parent"></md-dummy-tab> ' + 36258 '</md-tabs-dummy-wrapper> ' + 36259 '</md-tabs-canvas> ' + 36260 '</md-tabs-wrapper> ' + 36261 '<md-tabs-content-wrapper ng-show="$mdTabsCtrl.hasContent && $mdTabsCtrl.selectedIndex >= 0" class="_md"> ' + 36262 '<md-tab-content ' + 36263 'id="{{:: $mdTabsCtrl.tabContentPrefix + tab.id}}" ' + 36264 'class="_md" ' + 36265 'role="tabpanel" ' + 36266 'aria-labelledby="tab-item-{{::tab.id}}" ' + 36267 'md-swipe-left="$mdTabsCtrl.swipeContent && $mdTabsCtrl.incrementIndex(1)" ' + 36268 'md-swipe-right="$mdTabsCtrl.swipeContent && $mdTabsCtrl.incrementIndex(-1)" ' + 36269 'ng-if="tab.hasContent" ' + 36270 'ng-repeat="(index, tab) in $mdTabsCtrl.tabs" ' + 36271 'ng-class="{ ' + 36272 '\'md-no-transition\': $mdTabsCtrl.lastSelectedIndex == null, ' + 36273 '\'md-active\': tab.isActive(), ' + 36274 '\'md-left\': tab.isLeft(), ' + 36275 '\'md-right\': tab.isRight(), ' + 36276 '\'md-no-scroll\': $mdTabsCtrl.dynamicHeight ' + 36277 '}"> ' + 36278 '<div ' + 36279 'md-tabs-template="::tab.template" ' + 36280 'md-connected-if="tab.isActive()" ' + 36281 'md-scope="::tab.parent" ' + 36282 'ng-if="$mdTabsCtrl.enableDisconnect || tab.shouldRender()"></div> ' + 36283 '</md-tab-content> ' + 36284 '</md-tabs-content-wrapper>'; 36285 }, 36286 controller: 'MdTabsController', 36287 controllerAs: '$mdTabsCtrl', 36288 bindToController: true 36289 }; 36290} 36291 36292})(); 36293(function(){ 36294"use strict"; 36295 36296 36297MdTabsDummyWrapper.$inject = ["$mdUtil", "$window"];angular 36298 .module('material.components.tabs') 36299 .directive('mdTabsDummyWrapper', MdTabsDummyWrapper); 36300 36301/** 36302 * @private 36303 * 36304 * @param $mdUtil 36305 * @param $window 36306 * @returns {{require: string, link: link}} 36307 * @constructor 36308 * 36309 * @ngInject 36310 */ 36311function MdTabsDummyWrapper ($mdUtil, $window) { 36312 return { 36313 require: '^?mdTabs', 36314 link: function link (scope, element, attr, ctrl) { 36315 if (!ctrl) return; 36316 36317 var observer; 36318 var disconnect; 36319 36320 var mutationCallback = function() { 36321 ctrl.updatePagination(); 36322 ctrl.updateInkBarStyles(); 36323 }; 36324 36325 if('MutationObserver' in $window) { 36326 var config = { 36327 childList: true, 36328 subtree: true, 36329 // Per https://bugzilla.mozilla.org/show_bug.cgi?id=1138368, browsers will not fire 36330 // the childList mutation, once a <span> element's innerText changes. 36331 // The characterData of the <span> element will change. 36332 characterData: true 36333 }; 36334 36335 observer = new MutationObserver(mutationCallback); 36336 observer.observe(element[0], config); 36337 disconnect = observer.disconnect.bind(observer); 36338 } else { 36339 var debounced = $mdUtil.debounce(mutationCallback, 15, null, false); 36340 36341 element.on('DOMSubtreeModified', debounced); 36342 disconnect = element.off.bind(element, 'DOMSubtreeModified', debounced); 36343 } 36344 36345 // Disconnect the observer 36346 scope.$on('$destroy', function() { 36347 disconnect(); 36348 }); 36349 } 36350 }; 36351} 36352 36353})(); 36354(function(){ 36355"use strict"; 36356 36357 36358MdTabsTemplate.$inject = ["$compile", "$mdUtil"];angular 36359 .module('material.components.tabs') 36360 .directive('mdTabsTemplate', MdTabsTemplate); 36361 36362function MdTabsTemplate ($compile, $mdUtil) { 36363 return { 36364 restrict: 'A', 36365 link: link, 36366 scope: { 36367 template: '=mdTabsTemplate', 36368 connected: '=?mdConnectedIf', 36369 compileScope: '=mdScope' 36370 }, 36371 require: '^?mdTabs' 36372 }; 36373 function link (scope, element, attr, ctrl) { 36374 if (!ctrl) return; 36375 36376 var compileScope = ctrl.enableDisconnect ? scope.compileScope.$new() : scope.compileScope; 36377 36378 element.html(scope.template); 36379 $compile(element.contents())(compileScope); 36380 36381 return $mdUtil.nextTick(handleScope); 36382 36383 function handleScope () { 36384 scope.$watch('connected', function (value) { value === false ? disconnect() : reconnect(); }); 36385 scope.$on('$destroy', reconnect); 36386 } 36387 36388 function disconnect () { 36389 if (ctrl.enableDisconnect) $mdUtil.disconnectScope(compileScope); 36390 } 36391 36392 function reconnect () { 36393 if (ctrl.enableDisconnect) $mdUtil.reconnectScope(compileScope); 36394 } 36395 } 36396} 36397 36398})(); 36399(function(){ 36400angular.module("material.core").constant("$MD_THEME_CSS", "md-autocomplete.md-THEME_NAME-theme{background:\"{{background-A100}}\"}md-autocomplete.md-THEME_NAME-theme[disabled]:not([md-floating-label]){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-A100}}\"}.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.selected,.md-autocomplete-suggestions-container.md-THEME_NAME-theme li:hover{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}}\"}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}}\";color:\"{{foreground-1}}\"}.md-button.md-THEME_NAME-theme:not([disabled]).md-focused,.md-button.md-THEME_NAME-theme:not([disabled]):hover{background-color:\"{{background-500-0.2}}\"}.md-button.md-THEME_NAME-theme:not([disabled]).md-icon-button:hover{background-color:transparent}.md-button.md-THEME_NAME-theme.md-fab md-icon{color:\"{{accent-contrast}}\"}.md-button.md-THEME_NAME-theme.md-primary{color:\"{{primary-color}}\"}.md-button.md-THEME_NAME-theme.md-primary.md-fab,.md-button.md-THEME_NAME-theme.md-primary.md-raised{color:\"{{primary-contrast}}\";background-color:\"{{primary-color}}\"}.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{color:\"{{primary-contrast}}\"}.md-button.md-THEME_NAME-theme.md-primary.md-fab:not([disabled]).md-focused,.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]).md-focused,.md-button.md-THEME_NAME-theme.md-primary.md-raised:not([disabled]):hover{background-color:\"{{primary-600}}\"}.md-button.md-THEME_NAME-theme.md-primary:not([disabled]) md-icon{color:\"{{primary-color}}\"}.md-button.md-THEME_NAME-theme.md-fab{background-color:\"{{accent-color}}\";color:\"{{accent-contrast}}\"}.md-button.md-THEME_NAME-theme.md-fab:not([disabled]) .md-icon{color:\"{{accent-contrast}}\"}.md-button.md-THEME_NAME-theme.md-fab:not([disabled]).md-focused,.md-button.md-THEME_NAME-theme.md-fab:not([disabled]):hover{background-color:\"{{accent-A700}}\"}.md-button.md-THEME_NAME-theme.md-raised{color:\"{{background-900}}\";background-color:\"{{background-50}}\"}.md-button.md-THEME_NAME-theme.md-raised:not([disabled]) md-icon{color:\"{{background-900}}\"}.md-button.md-THEME_NAME-theme.md-raised:not([disabled]):hover{background-color:\"{{background-50}}\"}.md-button.md-THEME_NAME-theme.md-raised:not([disabled]).md-focused{background-color:\"{{background-200}}\"}.md-button.md-THEME_NAME-theme.md-warn{color:\"{{warn-color}}\"}.md-button.md-THEME_NAME-theme.md-warn.md-fab,.md-button.md-THEME_NAME-theme.md-warn.md-raised{color:\"{{warn-contrast}}\";background-color:\"{{warn-color}}\"}.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{color:\"{{warn-contrast}}\"}.md-button.md-THEME_NAME-theme.md-warn.md-fab:not([disabled]).md-focused,.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]).md-focused,.md-button.md-THEME_NAME-theme.md-warn.md-raised:not([disabled]):hover{background-color:\"{{warn-600}}\"}.md-button.md-THEME_NAME-theme.md-warn:not([disabled]) md-icon{color:\"{{warn-color}}\"}.md-button.md-THEME_NAME-theme.md-accent{color:\"{{accent-color}}\"}.md-button.md-THEME_NAME-theme.md-accent.md-fab,.md-button.md-THEME_NAME-theme.md-accent.md-raised{color:\"{{accent-contrast}}\";background-color:\"{{accent-color}}\"}.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{color:\"{{accent-contrast}}\"}.md-button.md-THEME_NAME-theme.md-accent.md-fab:not([disabled]).md-focused,.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]).md-focused,.md-button.md-THEME_NAME-theme.md-accent.md-raised:not([disabled]):hover{background-color:\"{{accent-A700}}\"}.md-button.md-THEME_NAME-theme.md-accent:not([disabled]) md-icon{color:\"{{accent-color}}\"}.md-button.md-THEME_NAME-theme.md-accent[disabled],.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-warn[disabled],.md-button.md-THEME_NAME-theme[disabled]{color:\"{{foreground-3}}\";cursor:default}.md-button.md-THEME_NAME-theme.md-accent[disabled] md-icon,.md-button.md-THEME_NAME-theme.md-fab[disabled] md-icon,.md-button.md-THEME_NAME-theme.md-raised[disabled] md-icon,.md-button.md-THEME_NAME-theme.md-warn[disabled] md-icon,.md-button.md-THEME_NAME-theme[disabled] md-icon{color:\"{{foreground-3}}\"}.md-button.md-THEME_NAME-theme.md-fab[disabled],.md-button.md-THEME_NAME-theme.md-raised[disabled]{background-color:\"{{foreground-4}}\"}.md-button.md-THEME_NAME-theme[disabled]{background-color:transparent}._md a.md-THEME_NAME-theme:not(.md-button).md-primary{color:\"{{primary-color}}\"}._md a.md-THEME_NAME-theme:not(.md-button).md-primary:hover{color:\"{{primary-700}}\"}._md a.md-THEME_NAME-theme:not(.md-button).md-accent{color:\"{{accent-color}}\"}._md a.md-THEME_NAME-theme:not(.md-button).md-accent:hover{color:\"{{accent-A700}}\"}._md a.md-THEME_NAME-theme:not(.md-button).md-warn{color:\"{{warn-color}}\"}._md a.md-THEME_NAME-theme:not(.md-button).md-warn:hover{color:\"{{warn-700}}\"}md-card.md-THEME_NAME-theme{color:\"{{foreground-1}}\";background-color:\"{{background-hue-1}}\";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,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-A700}}\"}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:not(.md-checked) .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:not(.md-checked) .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-primary .md-indeterminate[disabled] .md-container{c
36400olor:\"{{foreground-3}}\"}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:not(.md-checked) .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]:not(.md-checked) .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-icon:after{border-color:\"{{foreground-3}}\"}md-checkbox.md-THEME_NAME-theme[disabled] .md-label{color:\"{{foreground-3}}\"}md-chips.md-THEME_NAME-theme .md-chips{box-shadow:0 1px \"{{foreground-4}}\"}md-chips.md-THEME_NAME-theme .md-chips.md-focused{box-shadow:0 2px \"{{primary-color}}\"}md-chips.md-THEME_NAME-theme .md-chips .md-chip-input-container input{color:\"{{foreground-1}}\"}md-chips.md-THEME_NAME-theme .md-chips .md-chip-input-container input:-moz-placeholder,md-chips.md-THEME_NAME-theme .md-chips .md-chip-input-container input::-moz-placeholder{color:\"{{foreground-3}}\"}md-chips.md-THEME_NAME-theme .md-chips .md-chip-input-container input:-ms-input-placeholder{color:\"{{foreground-3}}\"}md-chips.md-THEME_NAME-theme .md-chips .md-chip-input-container input::-webkit-input-placeholder{color:\"{{foreground-3}}\"}md-chips.md-THEME_NAME-theme md-chip{background:\"{{background-300}}\";color:\"{{background-800}}\"}md-chips.md-THEME_NAME-theme md-chip md-icon{color:\"{{background-700}}\"}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._md-chip-editing{background:transparent;color:\"{{background-800}}\"}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}}\"}md-content.md-THEME_NAME-theme{color:\"{{foreground-1}}\";background-color:\"{{background-default}}\"}.md-calendar.md-THEME_NAME-theme{background:\"{{background-A100}}\";color:\"{{background-A200-0.87}}\"}.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-300}}\";color:\"{{background-A200-0.87}}\"}.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-calendar-date.md-focus .md-THEME_NAME-theme .md-calendar-date-selection-indicator,.md-THEME_NAME-theme .md-calendar-date-selection-indicator:hover{background:\"{{background-300}}\"}.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:\"{{background-A200-0.435}}\"}.md-THEME_NAME-theme .md-datepicker-input{color:\"{{foreground-1}}\"}.md-THEME_NAME-theme .md-datepicker-input:-moz-placeholder,.md-THEME_NAME-theme .md-datepicker-input::-moz-placeholder{color:\"{{foreground-3}}\"}.md-THEME_NAME-theme .md-datepicker-input:-ms-input-placeholder{color:\"{{foreground-3}}\"}.md-THEME_NAME-theme .md-datepicker-input::-webkit-input-placeholder{color:\"{{foreground-3}}\"}.md-THEME_NAME-theme .md-datepicker-input-container{border-bottom-color:\"{{foreground-4}}\"}.md-THEME_NAME-theme .md-datepicker-input-container.md-datepicker-focused{border-bottom-color:\"{{primary-color}}\"}.md-accent .md-THEME_NAME-theme .md-datepicker-input-container.md-datepicker-focused{border-bottom-color:\"{{accent-color}}\"}.md-THEME_NAME-theme .md-datepicker-input-container.md-datepicker-invalid,.md-warn .md-THEME_NAME-theme .md-datepicker-input-container.md-datepicker-focused{border-bottom-color:\"{{warn-A700}}\"}.md-THEME_NAME-theme .md-datepicker-calendar-pane{border-color:\"{{background-hue-1}}\"}.md-THEME_NAME-theme .md-datepicker-triangle-button .md-datepicker-expand-triangle{border-top-color:\"{{foreground-2}}\"}.md-THEME_NAME-theme .md-datepicker-open .md-datepicker-calendar-icon{color:\"{{primary-color}}\"}.md-accent .md-THEME_NAME-theme .md-datepicker-open .md-datepicker-calendar-icon,.md-THEME_NAME-theme .md-datepicker-open.md-accent .md-datepicker-calendar-icon{color:\"{{accent-color}}\"}.md-THEME_NAME-theme .md-datepicker-open.md-warn .md-datepicker-calendar-icon,.md-warn .md-THEME_NAME-theme .md-datepicker-open .md-datepicker-calendar-icon{color:\"{{warn-A700}}\"}.md-THEME_NAME-theme .md-datepicker-calendar{background:\"{{background-A100}}\"}.md-THEME_NAME-theme .md-datepicker-input-mask-opaque{box-shadow:0 0 0 9999px \"{{background-hue-1}}\"}.md-THEME_NAME-theme .md-datepicker-open .md-datepicker-input-container{background:\"{{background-hue-1}}\"}md-dialog.md-THEME_NAME-theme{border-radius:4px;background-color:\"{{background-hue-1}}\";color:\"{{foreground-1}}\"}md-dialog.md-THEME_NAME-theme.md-content-overflow .md-actions,md-dialog.md-THEME_NAME-theme.md-content-overflow md-dialog-actions,md-div
36400ider.md-THEME_NAME-theme{border-top-color:\"{{foreground-4}}\"}.layout-gt-lg-row>md-divider.md-THEME_NAME-theme,.layout-gt-md-row>md-divider.md-THEME_NAME-theme,.layout-gt-sm-row>md-divider.md-THEME_NAME-theme,.layout-gt-xs-row>md-divider.md-THEME_NAME-theme,.layout-lg-row>md-divider.md-THEME_NAME-theme,.layout-md-row>md-divider.md-THEME_NAME-theme,.layout-row>md-divider.md-THEME_NAME-theme,.layout-sm-row>md-divider.md-THEME_NAME-theme,.layout-xl-row>md-divider.md-THEME_NAME-theme,.layout-xs-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}}\"}md-input-container.md-THEME_NAME-theme .md-input:-moz-placeholder,md-input-container.md-THEME_NAME-theme .md-input::-moz-placeholder{color:\"{{foreground-3}}\"}md-input-container.md-THEME_NAME-theme .md-input:-ms-input-placeholder{color:\"{{foreground-3}}\"}md-input-container.md-THEME_NAME-theme .md-input::-webkit-input-placeholder{color:\"{{foreground-3}}\"}md-input-container.md-THEME_NAME-theme>md-icon{color:\"{{foreground-1}}\"}md-input-container.md-THEME_NAME-theme .md-placeholder,md-input-container.md-THEME_NAME-theme label{color:\"{{foreground-3}}\"}md-input-container.md-THEME_NAME-theme label.md-required:after{color:\"{{warn-A700}}\"}md-input-container.md-THEME_NAME-theme:not(.md-input-focused):not(.md-input-invalid) label.md-required:after{color:\"{{foreground-2}}\"}md-input-container.md-THEME_NAME-theme .md-input-message-animation,md-input-container.md-THEME_NAME-theme .md-input-messages-animation{color:\"{{warn-A700}}\"}md-input-container.md-THEME_NAME-theme .md-input-message-animation .md-char-counter,md-input-container.md-THEME_NAME-theme .md-input-messages-animation .md-char-counter{color:\"{{foreground-1}}\"}md-input-container.md-THEME_NAME-theme.md-input-focused .md-input:-moz-placeholder,md-input-container.md-THEME_NAME-theme.md-input-focused .md-input::-moz-placeholder{color:\"{{foreground-2}}\"}md-input-container.md-THEME_NAME-theme.md-input-focused .md-input:-ms-input-placeholder{color:\"{{foreground-2}}\"}md-input-container.md-THEME_NAME-theme.md-input-focused .md-input::-webkit-input-placeholder{color:\"{{foreground-2}}\"}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,md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-resized .md-input{border-color:\"{{primary-color}}\"}md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused label,md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused md-icon{color:\"{{primary-color}}\"}md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused.md-accent .md-input{border-color:\"{{accent-color}}\"}md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused.md-accent label,md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused.md-accent md-icon{color:\"{{accent-color}}\"}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,md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused.md-warn md-icon{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-char-counter,md-input-container.md-THEME_NAME-theme.md-input-invalid .md-input-message-animation,md-input-container.md-THEME_NAME-theme.md-input-invalid label{color:\"{{warn-A700}}\"}[disabled] md-input-container.md-THEME_NAME-theme .md-input,md-input-container.md-THEME_NAME-theme .md-input[disabled]{border-bottom-color:transparent;color:\"{{foreground-3}}\";background-image:linear-gradient(90deg,\"{{foreground-3}}\" 0,\"{{foreground-3}}\" 33%,transparent 0);background-image:-ms-linear-gradient(left,transparent 0,\"{{foreground-3}}\" 100%)}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}}\"}
36400md-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-content.md-THEME_NAME-theme{background-color:\"{{background-A100}}\"}md-menu-content.md-THEME_NAME-theme md-menu-item{color:\"{{background-A200-0.87}}\"}md-menu-content.md-THEME_NAME-theme md-menu-item md-icon{color:\"{{background-A200-0.54}}\"}md-menu-content.md-THEME_NAME-theme md-menu-item .md-button[disabled],md-menu-content.md-THEME_NAME-theme md-menu-item .md-button[disabled] md-icon{color:\"{{background-A200-0.25}}\"}md-menu-content.md-THEME_NAME-theme md-menu-divider{background-color:\"{{background-A200-0.11}}\"}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:focus,md-menu-bar.md-THEME_NAME-theme:not(.md-keyboard-mode):not(.md-open) md-menu button:hover{background:transparent}md-menu-content.md-THEME_NAME-theme .md-menu>.md-button:after{color:\"{{background-A200-0.54}}\"}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-A100}}\";color:\"{{background-A200}}\"}md-toolbar.md-THEME_NAME-theme.md-menu-toolbar md-toolbar-filler{background-color:\"{{primary-color}}\";color:\"{{background-A100-0.87}}\"}md-toolbar.md-THEME_NAME-theme.md-menu-toolbar md-toolbar-filler md-icon{color:\"{{background-A100-0.87}}\"}md-nav-bar.md-THEME_NAME-theme .md-nav-bar{background-color:transparent;border-color:\"{{foreground-4}}\"}md-nav-bar.md-THEME_NAME-theme .md-button._md-nav-button.md-unselected{color:\"{{foreground-2}}\"}md-nav-bar.md-THEME_NAME-theme md-nav-ink-bar{color:\"{{accent-color}}\";background:\"{{accent-color}}\"}md-nav-bar.md-THEME_NAME-theme.md-accent>.md-nav-bar{background-color:\"{{accent-color}}\"}md-nav-bar.md-THEME_NAME-theme.md-accent>.md-nav-bar .md-button._md-nav-button{color:\"{{accent-A100}}\"}md-nav-bar.md-THEME_NAME-theme.md-accent>.md-nav-bar .md-button._md-nav-button.md-active,md-nav-bar.md-THEME_NAME-theme.md-accent>.md-nav-bar .md-button._md-nav-button.md-focused{color:\"{{accent-contrast}}\"}md-nav-bar.md-THEME_NAME-theme.md-accent>.md-nav-bar .md-button._md-nav-button.md-focused{background:\"{{accent-contrast-0.1}}\"}md-nav-bar.md-THEME_NAME-theme.md-accent>.md-nav-bar md-nav-ink-bar{color:\"{{primary-600-1}}\";background:\"{{primary-600-1}}\"}md-nav-bar.md-THEME_NAME-theme.md-warn>.md-nav-bar{background-color:\"{{warn-color}}\"}md-nav-bar.md-THEME_NAME-theme.md-warn>.md-nav-bar .md-button._md-nav-button{color:\"{{warn-100}}\"}md-nav-bar.md-THEME_NAME-theme.md-warn>.md-nav-bar .md-button._md-nav-button.md-active,md-nav-bar.md-THEME_NAME-theme.md-warn>.md-nav-bar .md-button._md-nav-button.md-focused{color:\"{{warn-contrast}}\"}md-nav-bar.md-THEME_NAME-theme.md-warn>.md-nav-bar .md-button._md-nav-button.md-focused{background:\"{{warn-contrast-0.1}}\"}md-nav-bar.md-THEME_NAME-theme.md-primary>.md-nav-bar{background-color:\"{{primary-color}}\"}md-nav-bar.md-THEME_NAME-theme.md-primary>.md-nav-bar .md-button._md-nav-button{color:\"{{primary-100}}\"}md-nav-bar.md-THEME_NAME-theme.md-primary>.md-nav-bar .md-button._md-nav-button.md-active,md-nav-bar.md-THEME_NAME-theme.md-primary>.md-nav-bar .md-button._md-nav-button.md-focused{color:\"{{primary-contrast}}\"}md-nav-bar.md-THEME_NAME-theme.md-primary>.md-nav-bar .md-button._md-nav-button.md-focused{background:\"{{primary-contrast-0.1}}\"}md-toolbar>md-nav-bar.md-THEME_NAME-theme>.md-nav-bar{background-color:\"{{primary-color}}\"}md-toolbar>md-nav-bar.md-THEME_NAME-theme>.md-nav-bar .md-button._md-nav-button{color:\"{{primary-100}}\"}md-toolbar>md-nav-bar.md-THEME_NAME-theme>.md-nav-bar .md-button._md-nav-button.md-active,md-toolbar>md-nav-bar.md-THEME_NAME-theme>.md-nav-bar .md-button._md-nav-button.md-focused{color:\"{{primary-contrast}}\"}md-toolbar>md-nav-bar.md-THEME_NAME-theme>.md-nav-bar .md-button._md-nav-button.md-focused{background:\"{{primary-contrast-0.1}}\"}md-toolbar.md-accent>md-nav-bar.md-THEME_NAME-theme>.md-nav-bar{background-color:\"{{accent-color}}\"}md-toolbar.md-accent>md-nav-bar.md-THEME_NAME-theme>.md-nav-bar .md-button._md-nav-button{color:\"{{accent-A100}}\"}md-toolbar.md-accent>md-nav-bar.md-THEME_NAME-theme>.md-nav-bar .md-button._md-nav-button.md-active,md-toolbar.md-accent>md-nav-bar.md-THEME_NAME-theme>.md-nav-bar .md-button._md-nav-button.md-focused{color:\"{{accent-contrast}}\"}md-toolbar.md-accent>md-nav-bar.md-THEME_NAME-theme>.md-nav-bar .md-button._md-nav-button.md-focused{background:\"{{accent-contrast-0.1}}\"}md-toolbar.md-accent>md-nav-bar.md-THEME_NAME-theme>.md-nav-bar md-nav-ink-bar{color:\"{{primary-600-1}}\";background:\"{{primary-600-1}}\"}md-toolbar.md-warn>md-nav-bar.md-THEME_NAME-theme>.md-nav-bar{background-color:\"{{warn-color}}\"}md-toolbar.md-warn>md-nav-bar.md-THEME_NAME-theme>.md-nav-bar .md-button._md-nav-button{color:\"{{warn-100}}\"}md-toolbar.md-warn>md-nav-bar.md-THEME_NAME-theme>.md-nav-bar .md-button._md-nav-button.md-active,md-toolbar.md-warn>md-nav-bar.md-THEME_NAME-theme>.md-nav-bar .md-button._md-nav-button.md-focused{color:\"{{warn-contrast}}\"}md-toolbar.md-warn>md-nav-bar.md-THEME_NAME-theme>.md-nav-bar .md-button._md-nav-button.md-focused{background:\"{{warn-contrast-0.1}}\"}._md-panel-backdrop.md-THEME_NAME-theme{background-color:\"{{background-900-1.0}}\"}md-progress-circular.md-THEME_NAME-theme path{stroke:\"{{primary-color}}\"}md-progress-circular.md-THEME_NAME-theme.md-warn path{stroke:\"{{warn-color}}\"}md-progress-circular.md-THEME_NAME-theme.md-accent path{stroke:\"{{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-primary .md-bar
364001{background-color:\"{{primary-100}}\"}md-progress-linear.md-THEME_NAME-theme[md-mode=buffer].md-primary .md-dashed:before{background:radial-gradient(\"{{primary-100}}\" 0,\"{{primary-100}}\" 16%,transparent 42%)}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-A700}}\"}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,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{background-color:\"{{primary-color-0.87}}\"}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,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{border-color:\"{{primary-color-0.87}}\"}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,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{color:\"{{primary-color-0.87}}\"}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,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{color:\"{{primary-600}}\"}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,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{background-color:\"{{warn-color-0.87}}\"}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,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{border-color:\"{{warn-color-0.87}}\"}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,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{color:\"{{warn-color-0.87}}\"}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,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{color:\"{{warn-600}}\"}md-radio-button.md-THEME_NAME-theme[disabled],md-radio-group.md-THEME_NAME-theme[disabled]{color:\"{{foreground-3}}\"}md-radio-button.md-THEME_NAME-theme[disabled] .md-container .md-off,md-radio-button.md-THEME_NAME-theme[disabled] .md-container .md-on,md-radio-group.md-THEME_NAME-theme[disabled] .md-container .md-off,md-radio-group.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-checked:not([disabled]).md-primary .md-ink-ripple,md-radio-group.md-THEME_NAME-theme.md-primary .md-checked:not([disabled]) .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-checked.md-primary .md-container:before,md-radio-group.md-THEME_NAME-theme.md-focused:not(:empty).md-primary .md-checked .md-container:before{background-color:\"{{primary-color-0.26}}\"}md-radio-group.md-THEME_NAME-theme.md-focused:not(:empty) .md-checked.md-warn .md-container:before,md-radio-group.md-THEME_NAME-theme.md-focused:not(:empty).md-warn .md-checked .md-container:before{background-color:\"{{warn-color-0.26}}\"}md-input-container md-select.md-THEME_NAME-theme .md-select-value span:first-child:after{color:\"{{warn-A700}}\"}md-input-container:not(.md-input-focused):not(.md-input-invalid) md-select.md-THEME_NAME-theme .md-select-value span:first-child:after{color:\"{{foreground-3}}\"}md-input-container.md-input-focused:not(.md-input-has-value) md-select.md-THEME_NAME-theme .md-select-value,md-input-container.md-input-focused:not(.md-input-has-value) md-select.md-THEME_NAME-theme .md-select-value.md-select-placeholder{color:\"{{primary-color}}\"}md-input-container.md-input-invalid md-select.md-THEME_NAME-theme .md-select-value{color:\"{{warn-A700}}\"!important;border-bottom-color:\"{{warn-A700}}\"!important}md-input-container.md-input-invalid md-select.md-THEME_NAME-theme.md-no-underline .md-select-value{border-bottom-color:transparent!important}md-select.md-THEME_NAME-theme[disabled] .md-select-value{border-bottom-color:transparent;background-image:linear-gradient(90deg,\"{{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 .md-select-value span:first-child:after{color:\"{{warn-A700}}\"}md-select.md-THEME_NAME-theme.md-no-underline .md-select-value{border-bottom-color:transparent!important}md-select.md-THEME_NAME-theme.ng-invalid.ng-touched .md-select-value{color:\"{{warn-A700}}\"!important;border-bottom-color:\"{{warn-A700}}\"!important}md-select.md-THEME_NAME-theme.ng-invalid.ng-touched.md-no-underline .md-select-value{border-bottom-color:transparent!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-no-underline .md-select-value{border-bottom-color:transparent!important}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-icon,md-select.md-THEME_NAME-theme[disabled] .md-select-value,md-select.md-THEME_NAME-theme[disabled] .md-select-value.md-select-placeholder{color:\"{{foreground-3}}\"}md-select.md-THEME_NAME-theme .md-select-icon{color:\"{{foreground-2}}\"}md-select-menu.md-THEME_NAME-theme md-content{background:\"{{background-A100}}\"}md-select-menu.md-THEME_NAME-theme md-content md-optgroup{color:\"{{background-600-0.87}}\"}md-select-menu.md-THEME_NAME-theme md-content md-option{color:\"{{background-900-0.87}}\"}md-select-menu.md-THEME_NAME-theme md-content md-option[disabled] .md-text{color:\"{{background-400-0.87}}\"}md-select-menu.md-THEME_NAME-theme md-content md-option:not([disabled]):focus,md-select-menu.md-THEME_NAME-theme md-content md-option:not([disabled]):hover{background:\"{{background-200}}\"}md-select-menu.md-THEME_NAME-theme md-content md-option[selected]{color:\"{{primary-500}}\"}md-select-menu.md-THEME_NAME-theme md-content md-option[selected]:focus{color:\"{{primary-600}}\"}md-select-menu.md-THEME_NAME-theme md-content md-option[selected].md-accent{color:\"{{accent-color}}\"}md-select-menu.md-THEME_NAME-theme md-content md-option[selected].md-accent:focus{color:\"{{accent-A700}}\"}.md-checkbox-enabled.md-THEME_NAME-theme .md-ripple{color:\"{{primary-600}}\"}.md-checkbox-enabled.md-THEME_NAME-theme[selected] .md-ripple{color:\"{{background-600}}\"}.md-checkbox-enabled.md-THEME_NAME-theme .md-ink-ripple{color:\"{{foreground-2}}\"}.md-checkbox-enabled.md-THEME_NAME-theme[selected] .md-ink-ripple{color:\"{{primary-color-0.87}}\"}.md-checkbox-enabled.md-THEME_NAME-theme:not(.md-checked) .md-icon{border-color:\"{{foreground-2}}\"}.md-checkbox-enabled.md-THEME_NAME-theme[selected] .md-icon{background-color:\"{{primary-color-0.87}}\"}.md-checkbox-enabled.md-THEME_NAME-theme[selected].md-focused .md-container:before{background-color:\"{{primary-color-0.26}}\"}.md-checkbox-enabled.md-THEME_NAME-theme[selected] .md-icon:after{border-color:\"{{primary-contrast-0.87}}\"}.md-checkbox-enabled.md-THEME_NAME-theme .md-indeterminate[disabled] .md-container{c
36400olor:\"{{foreground-3}}\"}.md-checkbox-enabled.md-THEME_NAME-theme md-option .md-text{color:\"{{background-900-0.87}}\"}md-sidenav.md-THEME_NAME-theme,md-sidenav.md-THEME_NAME-theme md-content{background-color:\"{{background-hue-1}}\"}md-slider.md-THEME_NAME-theme .md-track{background-color:\"{{foreground-3}}\"}md-slider.md-THEME_NAME-theme .md-track-ticks{color:\"{{background-contrast}}\"}md-slider.md-THEME_NAME-theme .md-focus-ring{background-color:\"{{accent-A200-0.2}}\"}md-slider.md-THEME_NAME-theme .md-disabled-thumb{border-color:\"{{background-color}}\";background-color:\"{{background-color}}\"}md-slider.md-THEME_NAME-theme.md-min .md-thumb:after{background-color:\"{{background-color}}\";border-color:\"{{foreground-3}}\"}md-slider.md-THEME_NAME-theme.md-min .md-focus-ring{background-color:\"{{foreground-3-0.38}}\"}md-slider.md-THEME_NAME-theme.md-min[md-discrete] .md-thumb:after{background-color:\"{{background-contrast}}\";border-color:transparent}md-slider.md-THEME_NAME-theme.md-min[md-discrete] .md-sign{background-color:\"{{background-400}}\"}md-slider.md-THEME_NAME-theme.md-min[md-discrete] .md-sign:after{border-top-color:\"{{background-400}}\"}md-slider.md-THEME_NAME-theme.md-min[md-discrete][md-vertical] .md-sign:after{border-top-color:transparent;border-left-color:\"{{background-400}}\"}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-vertical] .md-sign:after{border-top-color:transparent;border-left-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-200-0.38}}\"}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-vertical] .md-sign:after{border-top-color:transparent;border-left-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-200-0.38}}\"}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-vertical] .md-sign:after{border-top-color:transparent;border-left-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:transparent}md-slider.md-THEME_NAME-theme[disabled]:not(.md-min) .md-thumb:after,md-slider.md-THEME_NAME-theme[disabled][md-discrete] .md-thumb:after{background-color:\"{{foreground-3}}\";border-color:transparent}md-slider.md-THEME_NAME-theme[disabled][readonly] .md-sign{background-color:\"{{background-400}}\"}md-slider.md-THEME_NAME-theme[disabled][readonly] .md-sign:after{border-top-color:\"{{background-400}}\"}md-slider.md-THEME_NAME-theme[disabled][readonly][md-vertical] .md-sign:after{border-top-color:transparent;border-left-color:\"{{background-400}}\"}md-slider.md-THEME_NAME-theme[disabled][readonly] .md-disabled-thumb{border-color:transparent;background-color:transparent}md-slider-container[disabled]>:first-child:not(md-slider),md-slider-container[disabled]>:last-child:not(md-slider){color:\"{{foreground-3}}\"}.md-subheader.md-THEME_NAME-theme{color:\"{{ foreground-2-0.23 }}\";background-color:\"{{background-default}}\"}.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}}\"}
36400md-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-A100}}\"}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]),md-tabs.md-THEME_NAME-theme.md-accent>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]) md-icon{color:\"{{accent-A100}}\"}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]),md-tabs.md-THEME_NAME-theme.md-primary>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]) md-icon{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]),md-tabs.md-THEME_NAME-theme.md-warn>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]) md-icon{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]),md-toolbar>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]) md-icon{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]),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-icon{color:\"{{accent-A100}}\"}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]),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-icon{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:\"{{accent-color}}\"}md-toast.md-THEME_NAME-theme .md-toast-content .md-button.md-highlight.md-primary{color:\"{{primary-color}}\"}md-toast.md-THEME_NAME-theme .md-toast-content .md-button.md-highlight.md-warn{color:\"{{warn-color}}\"}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}}\";fill:\"{{primary-contrast}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar) .md-button[disabled] md-icon{color:\"{{primary-contrast-0.26}}\";fill:\"{{primary-contrast-0.26}}\"}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-accent .md-ink-ripple{color:\"{{accent-contrast}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-accent md-icon{color:\"{{accent-contrast}}\";fill:\"{{accent-contrast}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-accent .md-button[disabled] md-icon{color:\"{{accent-contrast-0.26}}\";fill:\"{{accent-contrast-0.26}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-warn{background-color:\"{{warn-color}}\";color:\"{{warn-contrast}}\"}.md-panel.md-tooltip.md-THEME_NAME-theme{color:\"{{background-700-contrast}}\";background-color:\"{{background-700}}\"}body.md-THEME_NAME-theme,html.md-THEME_NAME-theme{color:\"{{foreground-1}}\";background-color:\"{{background-color}}\"}"); 36401})(); 36402 36403 36404})(window, window.angular);;window.ngMaterial={version:{full: "1.1.4-master-baa869a"}};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.