commonroutesApp/www/lib/angular-material/angular-material.js
www/lib/angular-material/angular-material.jsRaw
1/*!
2 * Angular Material Design
3 * https://github.com/angular/material
4 * @license MIT
5 * v1.1.1
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.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.chips","material.components.checkbox","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.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"];
25angular
26 .module('material.core', [
27 'ngAnimate',
28 'material.core.animate',
29 'material.core.layout',
30 'material.core.gestures',
31 'material.core.theming'
32 ])
33 .config(MdCoreConfigure)
34 .run(DetectNgTouch);
35
36
37/**
38 * Detect if the ng-Touch module is also being used.
39 * Warn if detected.
40 * @ngInject
41 */
42function DetectNgTouch($log, $injector) {
43 if ( $injector.has('$swipe') ) {
44 var msg = "" +
45 "You are using the ngTouch module. \n" +
46 "Angular Material already has mobile click, tap, and swipe support... \n" +
47 "ngTouch is not supported with Angular Material!";
48 $log.warn(msg);
49 }
50}
51
52/**
53 * @ngInject
54 */
55function MdCoreConfigure($provide, $mdThemingProvider) {
56
57 $provide.decorator('$$rAF', ["$delegate", rAFDecorator]);
58
59 $mdThemingProvider.theme('default')
60 .primaryPalette('indigo')
61 .accentPalette('pink')
62 .warnPalette('deep-orange')
63 .backgroundPalette('grey');
64}
65
66/**
67 * @ngInject
68 */
69function rAFDecorator($delegate) {
70 /**
71 * Use this to throttle events that come in often.
72 * The throttled function will always use the *last* invocation before the
73 * coming frame.
74 *
75 * For example, window resize events that fire many times a second:
76 * If we set to use an raf-throttled callback on window resize, then
77 * our callback will only be fired once per frame, with the last resize
78 * event that happened before that frame.
79 *
80 * @param {function} callback function to debounce
81 */
82 $delegate.throttle = function(cb) {
83 var queuedArgs, alreadyQueued, queueCb, context;
84 return function debounced() {
85 queuedArgs = arguments;
86 context = this;
87 queueCb = cb;
88 if (!alreadyQueued) {
89 alreadyQueued = true;
90 $delegate(function() {
91 queueCb.apply(context, Array.prototype.slice.call(queuedArgs));
92 alreadyQueued = false;
93 });
94 }
95 };
96 };
97 return $delegate;
98}
99
100})();
101(function(){
102"use strict";
103
104angular.module('material.core')
105 .directive('mdAutofocus', MdAutofocusDirective)
106
107 // Support the deprecated md-auto-focus and md-sidenav-focus as well
108 .directive('mdAutoFocus', MdAutofocusDirective)
109 .directive('mdSidenavFocus', MdAutofocusDirective);
110
111/**
112 * @ngdoc directive
113 * @name mdAutofocus
114 * @module material.core.util
115 *
116 * @description
117 *
118 * `[md-autofocus]` provides an optional way to identify the focused element when a `$mdDialog`,
119 * `$mdBottomSheet`, `$mdMenu` or `$mdSidenav` opens or upon page load for input-like elements.
120 *
121 * When one of these opens, it will find the first nested element with the `[md-autofocus]`
122 * attribute directive and optional expression. An expression may be specified as the directive
123 * value to enable conditional activation of the autofocus.
124 *
125 * @usage
126 *
127 * ### Dialog
128 * <hljs lang="html">
129 * <md-dialog>
130 * <form>
131 * <md-input-container>
132 * <label for="testInput">Label</label>
133 * <input id="testInput" type="text" md-autofocus>
134 * </md-input-container>
135 * </form>
136 * </md-dialog>
137 * </hljs>
138 *
139 * ### Bottomsheet
140 * <hljs lang="html">
141 * <md-bottom-sheet class="md-list md-has-header">
142 * <md-subheader>Comment Actions</md-subheader>
143 * <md-list>
144 * <md-list-item ng-repeat="item in items">
145 *
146 * <md-button md-autofocus="$index == 2">
147 * <md-icon md-svg-src="{{item.icon}}"></md-icon>
148 * <span class="md-inline-list-icon-label">{{ item.name }}</span>
149 * </md-button>
150 *
151 * </md-list-item>
152 * </md-list>
153 * </md-bottom-sheet>
154 * </hljs>
155 *
156 * ### Autocomplete
157 * <hljs lang="html">
158 * <md-autocomplete
159 * md-autofocus
160 * md-selected-item="selectedItem"
161 * md-search-text="searchText"
162 * md-items="item in getMatches(searchText)"
163 * md-item-text="item.display">
164 * <span md-highlight-text="searchText">{{item.display}}</span>
165 * </md-autocomplete>
166 * </hljs>
167 *
168 * ### Sidenav
169 * <hljs lang="html">
170 * <div layout="row" ng-controller="MyController">
171 * <md-sidenav md-component-id="left" class="md-sidenav-left">
172 * Left Nav!
173 * </md-sidenav>
174 *
175 * <md-content>
176 * Center Content
177 * <md-button ng-click="openLeftMenu()">
178 * Open Left Menu
179 * </md-button>
180 * </md-content>
181 *
182 * <md-sidenav md-component-id="right"
183 * md-is-locked-open="$mdMedia('min-width: 333px')"
184 * class="md-sidenav-right">
185 * <form>
186 * <md-input-container>
187 * <label for="testInput">Test input</label>
188 * <input id="testInput" type="text"
189 * ng-model="data" md-autofocus>
190 * </md-input-container>
191 * </form>
192 * </md-sidenav>
193 * </div>
194 * </hljs>
195 **/
196function MdAutofocusDirective() {
197 return {
198 restrict: 'A',
199
200 link: postLink
201 }
202}
203
204function postLink(scope, element, attrs) {
205 var attr = attrs.mdAutoFocus || attrs.mdAutofocus || attrs.mdSidenavFocus;
206
207 // Setup a watcher on the proper attribute to update a class we can check for in $mdUtil
208 scope.$watch(attr, function(canAutofocus) {
209 element.toggleClass('md-autofocus', canAutofocus);
210 });
211}
212
213})();
214(function(){
215"use strict";
216
217/**
218 * @ngdoc module
219 * @name material.core.colorUtil
220 * @description
221 * Color Util
222 */
223angular
224 .module('material.core')
225 .factory('$mdColorUtil', ColorUtilFactory);
226
227function ColorUtilFactory() {
228 /**
229 * Converts hex value to RGBA string
230 * @param color {string}
231 * @returns {string}
232 */
233 function hexToRgba (color) {
234 var hex = color[ 0 ] === '#' ? color.substr(1) : color,
235 dig = hex.length / 3,
236 red = hex.substr(0, dig),
237 green = hex.substr(dig, dig),
238 blue = hex.substr(dig * 2);
239 if (dig === 1) {
240 red += red;
241 green += green;
242 blue += blue;
243 }
244 return 'rgba(' + parseInt(red, 16) + ',' + parseInt(green, 16) + ',' + parseInt(blue, 16) + ',0.1)';
245 }
246
247 /**
248 * Converts rgba value to hex string
249 * @param color {string}
250 * @returns {string}
251 */
252 function rgbaToHex(color) {
253 color = color.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);
254
255 var hex = (color && color.length === 4) ? "#" +
256 ("0" + parseInt(color[1],10).toString(16)).slice(-2) +
257 ("0" + parseInt(color[2],10).toString(16)).slice(-2) +
258 ("0" + parseInt(color[3],10).toString(16)).slice(-2) : '';
259
260 return hex.toUpperCase();
261 }
262
263 /**
264 * Converts an RGB color to RGBA
265 * @param color {string}
266 * @returns {string}
267 */
268 function rgbToRgba (color) {
269 return color.replace(')', ', 0.1)').replace('(', 'a(');
270 }
271
272 /**
273 * Converts an RGBA color to RGB
274 * @param color {string}
275 * @returns {string}
276 */
277 function rgbaToRgb (color) {
278 return color
279 ? color.replace('rgba', 'rgb').replace(/,[^\),]+\)/, ')')
280 : 'rgb(0,0,0)';
281 }
282
283 return {
284 rgbaToHex: rgbaToHex,
285 hexToRgba: hexToRgba,
286 rgbToRgba: rgbToRgba,
287 rgbaToRgb: rgbaToRgb
288 }
289}
290})();
291(function(){
292"use strict";
293
294
295MdConstantFactory.$inject = ["$sniffer", "$window", "$document"];angular.module('material.core')
296.factory('$mdConstant', MdConstantFactory);
297
298/**
299 * Factory function that creates the grab-bag $mdConstant service.
300 * @ngInject
301 */
302function MdConstantFactory($sniffer, $window, $document) {
303
304 var vendorPrefix = $sniffer.vendorPrefix;
305 var isWebkit = /webkit/i.test(vendorPrefix);
306 var SPECIAL_CHARS_REGEXP = /([:\-_]+(.))/g;
307 var prefixTestEl = document.createElement('div');
308
309 function vendorProperty(name) {
310 // Add a dash between the prefix and name, to be able to transform the string into camelcase.
311 var prefixedName = vendorPrefix + '-' + name;
312 var ucPrefix = camelCase(prefixedName);
313 var lcPrefix = ucPrefix.charAt(0).toLowerCase() + ucPrefix.substring(1);
314
315 return hasStyleProperty(name) ? name : // The current browser supports the un-prefixed property
316 hasStyleProperty(ucPrefix) ? ucPrefix : // The current browser only supports the prefixed property.
317 hasStyleProperty(lcPrefix) ? lcPrefix : name; // Some browsers are only supporting the prefix in lowercase.
318 }
319
320 function hasStyleProperty(property) {
321 return angular.isDefined(prefixTestEl.style[property]);
322 }
323
324 function camelCase(input) {
325 return input.replace(SPECIAL_CHARS_REGEXP, function(matches, separator, letter, offset) {
326 return offset ? letter.toUpperCase() : letter;
327 });
328 }
329
330 var self = {
331 isInputKey : function(e) { return (e.keyCode >= 31 && e.keyCode <= 90); },
332 isNumPadKey : function (e){ return (3 === e.location && e.keyCode >= 97 && e.keyCode <= 105); },
333 isNavigationKey : function(e) {
334 var kc = self.KEY_CODE, NAVIGATION_KEYS = [kc.SPACE, kc.ENTER, kc.UP_ARROW, kc.DOWN_ARROW];
335 return (NAVIGATION_KEYS.indexOf(e.keyCode) != -1);
336 },
337
338 KEY_CODE: {
339 COMMA: 188,
340 SEMICOLON : 186,
341 ENTER: 13,
342 ESCAPE: 27,
343 SPACE: 32,
344 PAGE_UP: 33,
345 PAGE_DOWN: 34,
346 END: 35,
347 HOME: 36,
348 LEFT_ARROW : 37,
349 UP_ARROW : 38,
350 RIGHT_ARROW : 39,
351 DOWN_ARROW : 40,
352 TAB : 9,
353 BACKSPACE: 8,
354 DELETE: 46
355 },
356 CSS: {
357 /* Constants */
358 TRANSITIONEND: 'transitionend' + (isWebkit ? ' webkitTransitionEnd' : ''),
359 ANIMATIONEND: 'animationend' + (isWebkit ? ' webkitAnimationEnd' : ''),
360
361 TRANSFORM: vendorProperty('transform'),
362 TRANSFORM_ORIGIN: vendorProperty('transformOrigin'),
363 TRANSITION: vendorProperty('transition'),
364 TRANSITION_DURATION: vendorProperty('transitionDuration'),
365 ANIMATION_PLAY_STATE: vendorProperty('animationPlayState'),
366 ANIMATION_DURATION: vendorProperty('animationDuration'),
367 ANIMATION_NAME: vendorProperty('animationName'),
368 ANIMATION_TIMING: vendorProperty('animationTimingFunction'),
369 ANIMATION_DIRECTION: vendorProperty('animationDirection')
370 },
371 /**
372 * As defined in core/style/variables.scss
373 *
374 * $layout-breakpoint-xs: 600px !default;
375 * $layout-breakpoint-sm: 960px !default;
376 * $layout-breakpoint-md: 1280px !default;
377 * $layout-breakpoint-lg: 1920px !default;
378 *
379 */
380 MEDIA: {
381 'xs' : '(max-width: 599px)' ,
382 'gt-xs' : '(min-width: 600px)' ,
383 'sm' : '(min-width: 600px) and (max-width: 959px)' ,
384 'gt-sm' : '(min-width: 960px)' ,
385 'md' : '(min-width: 960px) and (max-width: 1279px)' ,
386 'gt-md' : '(min-width: 1280px)' ,
387 'lg' : '(min-width: 1280px) and (max-width: 1919px)',
388 'gt-lg' : '(min-width: 1920px)' ,
389 'xl' : '(min-width: 1920px)' ,
390 'landscape' : '(orientation: landscape)' ,
391 'portrait' : '(orientation: portrait)' ,
392 'print' : 'print'
393 },
394 MEDIA_PRIORITY: [
395 'xl',
396 'gt-lg',
397 'lg',
398 'gt-md',
399 'md',
400 'gt-sm',
401 'sm',
402 'gt-xs',
403 'xs',
404 'landscape',
405 'portrait',
406 'print'
407 ]
408 };
409
410 return self;
411}
412
413})();
414(function(){
415"use strict";
416
417 angular
418 .module('material.core')
419 .config( ["$provide", function($provide){
420 $provide.decorator('$mdUtil', ['$delegate', function ($delegate){
421 /**
422 * Inject the iterator facade to easily support iteration and accessors
423 * @see iterator below
424 */
425 $delegate.iterator = MdIterator;
426
427 return $delegate;
428 }
429 ]);
430 }]);
431
432 /**
433 * iterator is a list facade to easily support iteration and accessors
434 *
435 * @param items Array list which this iterator will enumerate
436 * @param reloop Boolean enables iterator to consider the list as an endless reloop
437 */
438 function MdIterator(items, reloop) {
439 var trueFn = function() { return true; };
440
441 if (items && !angular.isArray(items)) {
442 items = Array.prototype.slice.call(items);
443 }
444
445 reloop = !!reloop;
446 var _items = items || [ ];
447
448 // Published API
449 return {
450 items: getItems,
451 count: count,
452
453 inRange: inRange,
454 contains: contains,
455 indexOf: indexOf,
456 itemAt: itemAt,
457
458 findBy: findBy,
459
460 add: add,
461 remove: remove,
462
463 first: first,
464 last: last,
465 next: angular.bind(null, findSubsequentItem, false),
466 previous: angular.bind(null, findSubsequentItem, true),
467
468 hasPrevious: hasPrevious,
469 hasNext: hasNext
470
471 };
472
473 /**
474 * Publish copy of the enumerable set
475 * @returns {Array|*}
476 */
477 function getItems() {
478 return [].concat(_items);
479 }
480
481 /**
482 * Determine length of the list
483 * @returns {Array.length|*|number}
484 */
485 function count() {
486 return _items.length;
487 }
488
489 /**
490 * Is the index specified valid
491 * @param index
492 * @returns {Array.length|*|number|boolean}
493 */
494 function inRange(index) {
495 return _items.length && ( index > -1 ) && (index < _items.length );
496 }
497
498 /**
499 * Can the iterator proceed to the next item in the list; relative to
500 * the specified item.
501 *
502 * @param item
503 * @returns {Array.length|*|number|boolean}
504 */
505 function hasNext(item) {
506 return item ? inRange(indexOf(item) + 1) : false;
507 }
508
509 /**
510 * Can the iterator proceed to the previous item in the list; relative to
511 * the specified item.
512 *
513 * @param item
514 * @returns {Array.length|*|number|boolean}
515 */
516 function hasPrevious(item) {
517 return item ? inRange(indexOf(item) - 1) : false;
518 }
519
520 /**
521 * Get item at specified index/position
522 * @param index
523 * @returns {*}
524 */
525 function itemAt(index) {
526 return inRange(index) ? _items[index] : null;
527 }
528
529 /**
530 * Find all elements matching the key/value pair
531 * otherwise return null
532 *
533 * @param val
534 * @param key
535 *
536 * @return array
537 */
538 function findBy(key, val) {
539 return _items.filter(function(item) {
540 return item[key] === val;
541 });
542 }
543
544 /**
545 * Add item to list
546 * @param item
547 * @param index
548 * @returns {*}
549 */
550 function add(item, index) {
551 if ( !item ) return -1;
552
553 if (!angular.isNumber(index)) {
554 index = _items.length;
555 }
556
557 _items.splice(index, 0, item);
558
559 return indexOf(item);
560 }
561
562 /**
563 * Remove item from list...
564 * @param item
565 */
566 function remove(item) {
567 if ( contains(item) ){
568 _items.splice(indexOf(item), 1);
569 }
570 }
571
572 /**
573 * Get the zero-based index of the target item
574 * @param item
575 * @returns {*}
576 */
577 function indexOf(item) {
578 return _items.indexOf(item);
579 }
580
581 /**
582 * Boolean existence check
583 * @param item
584 * @returns {boolean}
585 */
586 function contains(item) {
587 return item && (indexOf(item) > -1);
588 }
589
590 /**
591 * Return first item in the list
592 * @returns {*}
593 */
594 function first() {
595 return _items.length ? _items[0] : null;
596 }
597
598 /**
599 * Return last item in the list...
600 * @returns {*}
601 */
602 function last() {
603 return _items.length ? _items[_items.length - 1] : null;
604 }
605
606 /**
607 * Find the next item. If reloop is true and at the end of the list, it will go back to the
608 * first item. If given, the `validate` callback will be used to determine whether the next item
609 * is valid. If not valid, it will try to find the next item again.
610 *
611 * @param {boolean} backwards Specifies the direction of searching (forwards/backwards)
612 * @param {*} item The item whose subsequent item we are looking for
613 * @param {Function=} validate The `validate` function
614 * @param {integer=} limit The recursion limit
615 *
616 * @returns {*} The subsequent item or null
617 */
618 function findSubsequentItem(backwards, item, validate, limit) {
619 validate = validate || trueFn;
620
621 var curIndex = indexOf(item);
622 while (true) {
623 if (!inRange(curIndex)) return null;
624
625 var nextIndex = curIndex + (backwards ? -1 : 1);
626 var foundItem = null;
627 if (inRange(nextIndex)) {
628 foundItem = _items[nextIndex];
629 } else if (reloop) {
630 foundItem = backwards ? last() : first();
631 nextIndex = indexOf(foundItem);
632 }
633
634 if ((foundItem === null) || (nextIndex === limit)) return null;
635 if (validate(foundItem)) return foundItem;
636
637 if (angular.isUndefined(limit)) limit = nextIndex;
638
639 curIndex = nextIndex;
640 }
641 }
642 }
643
644
645})();
646(function(){
647"use strict";
648
649
650mdMediaFactory.$inject = ["$mdConstant", "$rootScope", "$window"];angular.module('material.core')
651.factory('$mdMedia', mdMediaFactory);
652
653/**
654 * @ngdoc service
655 * @name $mdMedia
656 * @module material.core
657 *
658 * @description
659 * `$mdMedia` is used to evaluate whether a given media query is true or false given the
660 * current device's screen / window size. The media query will be re-evaluated on resize, allowing
661 * you to register a watch.
662 *
663 * `$mdMedia` also has pre-programmed support for media queries that match the layout breakpoints:
664 *
665 * <table class="md-api-table">
666 * <thead>
667 * <tr>
668 * <th>Breakpoint</th>
669 * <th>mediaQuery</th>
670 * </tr>
671 * </thead>
672 * <tbody>
673 * <tr>
674 * <td>xs</td>
675 * <td>(max-width: 599px)</td>
676 * </tr>
677 * <tr>
678 * <td>gt-xs</td>
679 * <td>(min-width: 600px)</td>
680 * </tr>
681 * <tr>
682 * <td>sm</td>
683 * <td>(min-width: 600px) and (max-width: 959px)</td>
684 * </tr>
685 * <tr>
686 * <td>gt-sm</td>
687 * <td>(min-width: 960px)</td>
688 * </tr>
689 * <tr>
690 * <td>md</td>
691 * <td>(min-width: 960px) and (max-width: 1279px)</td>
692 * </tr>
693 * <tr>
694 * <td>gt-md</td>
695 * <td>(min-width: 1280px)</td>
696 * </tr>
697 * <tr>
698 * <td>lg</td>
699 * <td>(min-width: 1280px) and (max-width: 1919px)</td>
700 * </tr>
701 * <tr>
702 * <td>gt-lg</td>
703 * <td>(min-width: 1920px)</td>
704 * </tr>
705 * <tr>
706 * <td>xl</td>
707 * <td>(min-width: 1920px)</td>
708 * </tr>
709 * <tr>
710 * <td>landscape</td>
711 * <td>landscape</td>
712 * </tr>
713 * <tr>
714 * <td>portrait</td>
715 * <td>portrait</td>
716 * </tr>
717 * <tr>
718 * <td>print</td>
719 * <td>print</td>
720 * </tr>
721 * </tbody>
722 * </table>
723 *
724 * See Material Design's <a href="https://material.google.com/layout/responsive-ui.html">Layout - Adaptive UI</a> for more details.
725 *
726 * <a href="https://www.google.com/design/spec/layout/adaptive-ui.html">
727 * <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>
728 * </a>
729 *
730 * @returns {boolean} a boolean representing whether or not the given media query is true or false.
731 *
732 * @usage
733 * <hljs lang="js">
734 * app.controller('MyController', function($mdMedia, $scope) {
735 * $scope.$watch(function() { return $mdMedia('lg'); }, function(big) {
736 * $scope.bigScreen = big;
737 * });
738 *
739 * $scope.screenIsSmall = $mdMedia('sm');
740 * $scope.customQuery = $mdMedia('(min-width: 1234px)');
741 * $scope.anotherCustom = $mdMedia('max-width: 300px');
742 * });
743 * </hljs>
744 */
745
746/* @ngInject */
747function mdMediaFactory($mdConstant, $rootScope, $window) {
748 var queries = {};
749 var mqls = {};
750 var results = {};
751 var normalizeCache = {};
752
753 $mdMedia.getResponsiveAttribute = getResponsiveAttribute;
754 $mdMedia.getQuery = getQuery;
755 $mdMedia.watchResponsiveAttributes = watchResponsiveAttributes;
756
757 return $mdMedia;
758
759 function $mdMedia(query) {
760 var validated = queries[query];
761 if (angular.isUndefined(validated)) {
762 validated = queries[query] = validate(query);
763 }
764
765 var result = results[validated];
766 if (angular.isUndefined(result)) {
767 result = add(validated);
768 }
769
770 return result;
771 }
772
773 function validate(query) {
774 return $mdConstant.MEDIA[query] ||
775 ((query.charAt(0) !== '(') ? ('(' + query + ')') : query);
776 }
777
778 function add(query) {
779 var result = mqls[query];
780 if ( !result ) {
781 result = mqls[query] = $window.matchMedia(query);
782 }
783
784 result.addListener(onQueryChange);
785 return (results[result.media] = !!result.matches);
786 }
787
788 function onQueryChange(query) {
789 $rootScope.$evalAsync(function() {
790 results[query.media] = !!query.matches;
791 });
792 }
793
794 function getQuery(name) {
795 return mqls[name];
796 }
797
798 function getResponsiveAttribute(attrs, attrName) {
799 for (var i = 0; i < $mdConstant.MEDIA_PRIORITY.length; i++) {
800 var mediaName = $mdConstant.MEDIA_PRIORITY[i];
801 if (!mqls[queries[mediaName]].matches) {
802 continue;
803 }
804
805 var normalizedName = getNormalizedName(attrs, attrName + '-' + mediaName);
806 if (attrs[normalizedName]) {
807 return attrs[normalizedName];
808 }
809 }
810
811 // fallback on unprefixed
812 return attrs[getNormalizedName(attrs, attrName)];
813 }
814
815 function watchResponsiveAttributes(attrNames, attrs, watchFn) {
816 var unwatchFns = [];
817 attrNames.forEach(function(attrName) {
818 var normalizedName = getNormalizedName(attrs, attrName);
819 if (angular.isDefined(attrs[normalizedName])) {
820 unwatchFns.push(
821 attrs.$observe(normalizedName, angular.bind(void 0, watchFn, null)));
822 }
823
824 for (var mediaName in $mdConstant.MEDIA) {
825 normalizedName = getNormalizedName(attrs, attrName + '-' + mediaName);
826 if (angular.isDefined(attrs[normalizedName])) {
827 unwatchFns.push(
828 attrs.$observe(normalizedName, angular.bind(void 0, watchFn, mediaName)));
829 }
830 }
831 });
832
833 return function unwatch() {
834 unwatchFns.forEach(function(fn) { fn(); })
835 };
836 }
837
838 // Improves performance dramatically
839 function getNormalizedName(attrs, attrName) {
840 return normalizeCache[attrName] ||
841 (normalizeCache[attrName] = attrs.$normalize(attrName));
842 }
843}
844
845})();
846(function(){
847"use strict";
848
849angular
850 .module('material.core')
851 .config( ["$provide", function($provide) {
852 $provide.decorator('$mdUtil', ['$delegate', function ($delegate) {
853
854 // Inject the prefixer into our original $mdUtil service.
855 $delegate.prefixer = MdPrefixer;
856
857 return $delegate;
858 }]);
859 }]);
860
861function MdPrefixer(initialAttributes, buildSelector) {
862 var PREFIXES = ['data', 'x'];
863
864 if (initialAttributes) {
865 // The prefixer also accepts attributes as a parameter, and immediately builds a list or selector for
866 // the specified attributes.
867 return buildSelector ? _buildSelector(initialAttributes) : _buildList(initialAttributes);
868 }
869
870 return {
871 buildList: _buildList,
872 buildSelector: _buildSelector,
873 hasAttribute: _hasAttribute,
874 removeAttribute: _removeAttribute
875 };
876
877 function _buildList(attributes) {
878 attributes = angular.isArray(attributes) ? attributes : [attributes];
879
880 attributes.forEach(function(item) {
881 PREFIXES.forEach(function(prefix) {
882 attributes.push(prefix + '-' + item);
883 });
884 });
885
886 return attributes;
887 }
888
889 function _buildSelector(attributes) {
890 attributes = angular.isArray(attributes) ? attributes : [attributes];
891
892 return _buildList(attributes)
893 .map(function(item) {
894 return '[' + item + ']'
895 })
896 .join(',');
897 }
898
899 function _hasAttribute(element, attribute) {
900 element = _getNativeElement(element);
901
902 if (!element) {
903 return false;
904 }
905
906 var prefixedAttrs = _buildList(attribute);
907
908 for (var i = 0; i < prefixedAttrs.length; i++) {
909 if (element.hasAttribute(prefixedAttrs[i])) {
910 return true;
911 }
912 }
913
914 return false;
915 }
916
917 function _removeAttribute(element, attribute) {
918 element = _getNativeElement(element);
919
920 if (!element) {
921 return;
922 }
923
924 _buildList(attribute).forEach(function(prefixedAttribute) {
925 element.removeAttribute(prefixedAttribute);
926 });
927 }
928
929 /**
930 * Transforms a jqLite or DOM element into a HTML element.
931 * This is useful when supporting jqLite elements and DOM elements at
932 * same time.
933 * @param element {JQLite|Element} Element to be parsed
934 * @returns {HTMLElement} Parsed HTMLElement
935 */
936 function _getNativeElement(element) {
937 element = element[0] || element;
938
939 if (element.nodeType) {
940 return element;
941 }
942 }
943
944}
945})();
946(function(){
947"use strict";
948
949/*
950 * This var has to be outside the angular factory, otherwise when
951 * there are multiple material apps on the same page, each app
952 * will create its own instance of this array and the app's IDs
953 * will not be unique.
954 */
955UtilFactory.$inject = ["$document", "$timeout", "$compile", "$rootScope", "$$mdAnimate", "$interpolate", "$log", "$rootElement", "$window", "$$rAF"];
956var nextUniqueId = 0;
957
958/**
959 * @ngdoc module
960 * @name material.core.util
961 * @description
962 * Util
963 */
964angular
965 .module('material.core')
966 .factory('$mdUtil', UtilFactory);
967
968/**
969 * @ngInject
970 */
971function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $interpolate, $log, $rootElement, $window, $$rAF) {
972 // Setup some core variables for the processTemplate method
973 var startSymbol = $interpolate.startSymbol(),
974 endSymbol = $interpolate.endSymbol(),
975 usesStandardSymbols = ((startSymbol === '{{') && (endSymbol === '}}'));
976
977 /**
978 * Checks if the target element has the requested style by key
979 * @param {DOMElement|JQLite} target Target element
980 * @param {string} key Style key
981 * @param {string=} expectedVal Optional expected value
982 * @returns {boolean} Whether the target element has the style or not
983 */
984 var hasComputedStyle = function (target, key, expectedVal) {
985 var hasValue = false;
986
987 if ( target && target.length ) {
988 var computedStyles = $window.getComputedStyle(target[0]);
989 hasValue = angular.isDefined(computedStyles[key]) && (expectedVal ? computedStyles[key] == expectedVal : true);
990 }
991
992 return hasValue;
993 };
994
995 function validateCssValue(value) {
996 return !value ? '0' :
997 hasPx(value) || hasPercent(value) ? value : value + 'px';
998 }
999
1000 function hasPx(value) {
1001 return String(value).indexOf('px') > -1;
1002 }
1003
1004 function hasPercent(value) {
1005 return String(value).indexOf('%') > -1;
1006
1007 }
1008
1009 var $mdUtil = {
1010 dom: {},
1011 now: window.performance ?
1012 angular.bind(window.performance, window.performance.now) : Date.now || function() {
1013 return new Date().getTime();
1014 },
1015
1016 /**
1017 * Bi-directional accessor/mutator used to easily update an element's
1018 * property based on the current 'dir'ectional value.
1019 */
1020 bidi : function(element, property, lValue, rValue) {
1021 var ltr = !($document[0].dir == 'rtl' || $document[0].body.dir == 'rtl');
1022
1023 // If accessor
1024 if ( arguments.length == 0 ) return ltr ? 'ltr' : 'rtl';
1025
1026 // If mutator
1027 var elem = angular.element(element);
1028
1029 if ( ltr && angular.isDefined(lValue)) {
1030 elem.css(property, validateCssValue(lValue));
1031 }
1032 else if ( !ltr && angular.isDefined(rValue)) {
1033 elem.css(property, validateCssValue(rValue) );
1034 }
1035 },
1036
1037 bidiProperty: function (element, lProperty, rProperty, value) {
1038 var ltr = !($document[0].dir == 'rtl' || $document[0].body.dir == 'rtl');
1039
1040 var elem = angular.element(element);
1041
1042 if ( ltr && angular.isDefined(lProperty)) {
1043 elem.css(lProperty, validateCssValue(value));
1044 elem.css(rProperty, '');
1045 }
1046 else if ( !ltr && angular.isDefined(rProperty)) {
1047 elem.css(rProperty, validateCssValue(value) );
1048 elem.css(lProperty, '');
1049 }
1050 },
1051
1052 clientRect: function(element, offsetParent, isOffsetRect) {
1053 var node = getNode(element);
1054 offsetParent = getNode(offsetParent || node.offsetParent || document.body);
1055 var nodeRect = node.getBoundingClientRect();
1056
1057 // The user can ask for an offsetRect: a rect relative to the offsetParent,
1058 // or a clientRect: a rect relative to the page
1059 var offsetRect = isOffsetRect ?
1060 offsetParent.getBoundingClientRect() :
1061 {left: 0, top: 0, width: 0, height: 0};
1062 return {
1063 left: nodeRect.left - offsetRect.left,
1064 top: nodeRect.top - offsetRect.top,
1065 width: nodeRect.width,
1066 height: nodeRect.height
1067 };
1068 },
1069 offsetRect: function(element, offsetParent) {
1070 return $mdUtil.clientRect(element, offsetParent, true);
1071 },
1072
1073 // Annoying method to copy nodes to an array, thanks to IE
1074 nodesToArray: function(nodes) {
1075 nodes = nodes || [];
1076
1077 var results = [];
1078 for (var i = 0; i < nodes.length; ++i) {
1079 results.push(nodes.item(i));
1080 }
1081 return results;
1082 },
1083
1084 /**
1085 * Calculate the positive scroll offset
1086 * TODO: Check with pinch-zoom in IE/Chrome;
1087 * https://code.google.com/p/chromium/issues/detail?id=496285
1088 */
1089 scrollTop: function(element) {
1090 element = angular.element(element || $document[0].body);
1091
1092 var body = (element[0] == $document[0].body) ? $document[0].body : undefined;
1093 var scrollTop = body ? body.scrollTop + body.parentElement.scrollTop : 0;
1094
1095 // Calculate the positive scroll offset
1096 return scrollTop || Math.abs(element[0].getBoundingClientRect().top);
1097 },
1098
1099 /**
1100 * Finds the proper focus target by searching the DOM.
1101 *
1102 * @param containerEl
1103 * @param attributeVal
1104 * @returns {*}
1105 */
1106 findFocusTarget: function(containerEl, attributeVal) {
1107 var AUTO_FOCUS = this.prefixer('md-autofocus', true);
1108 var elToFocus;
1109
1110 elToFocus = scanForFocusable(containerEl, attributeVal || AUTO_FOCUS);
1111
1112 if ( !elToFocus && attributeVal != AUTO_FOCUS) {
1113 // Scan for deprecated attribute
1114 elToFocus = scanForFocusable(containerEl, this.prefixer('md-auto-focus', true));
1115
1116 if ( !elToFocus ) {
1117 // Scan for fallback to 'universal' API
1118 elToFocus = scanForFocusable(containerEl, AUTO_FOCUS);
1119 }
1120 }
1121
1122 return elToFocus;
1123
1124 /**
1125 * Can target and nested children for specified Selector (attribute)
1126 * whose value may be an expression that evaluates to True/False.
1127 */
1128 function scanForFocusable(target, selector) {
1129 var elFound, items = target[0].querySelectorAll(selector);
1130
1131 // Find the last child element with the focus attribute
1132 if ( items && items.length ){
1133 items.length && angular.forEach(items, function(it) {
1134 it = angular.element(it);
1135
1136 // Check the element for the md-autofocus class to ensure any associated expression
1137 // evaluated to true.
1138 var isFocusable = it.hasClass('md-autofocus');
1139 if (isFocusable) elFound = it;
1140 });
1141 }
1142 return elFound;
1143 }
1144 },
1145
1146 /**
1147 * Disables scroll around the passed parent element.
1148 * @param element Unused
1149 * @param {!Element|!angular.JQLite} parent Element to disable scrolling within.
1150 * Defaults to body if none supplied.
1151 * @param options Object of options to modify functionality
1152 * - disableScrollMask Boolean of whether or not to create a scroll mask element or
1153 * use the passed parent element.
1154 */
1155 disableScrollAround: function(element, parent, options) {
1156 $mdUtil.disableScrollAround._count = $mdUtil.disableScrollAround._count || 0;
1157 ++$mdUtil.disableScrollAround._count;
1158 if ($mdUtil.disableScrollAround._enableScrolling) return $mdUtil.disableScrollAround._enableScrolling;
1159 var body = $document[0].body,
1160 restoreBody = disableBodyScroll(),
1161 restoreElement = disableElementScroll(parent);
1162
1163 return $mdUtil.disableScrollAround._enableScrolling = function() {
1164 if (!--$mdUtil.disableScrollAround._count) {
1165 restoreBody();
1166 restoreElement();
1167 delete $mdUtil.disableScrollAround._enableScrolling;
1168 }
1169 };
1170
1171 // Creates a virtual scrolling mask to absorb touchmove, keyboard, scrollbar clicking, and wheel events
1172 function disableElementScroll(element) {
1173 element = angular.element(element || body);
1174 var scrollMask;
1175 if (options && options.disableScrollMask) {
1176 scrollMask = element;
1177 } else {
1178 element = element[0];
1179 scrollMask = angular.element(
1180 '<div class="md-scroll-mask">' +
1181 ' <div class="md-scroll-mask-bar"></div>' +
1182 '</div>');
1183 element.appendChild(scrollMask[0]);
1184 }
1185
1186 scrollMask.on('wheel', preventDefault);
1187 scrollMask.on('touchmove', preventDefault);
1188
1189 return function restoreScroll() {
1190 scrollMask.off('wheel');
1191 scrollMask.off('touchmove');
1192 scrollMask[0].parentNode.removeChild(scrollMask[0]);
1193 delete $mdUtil.disableScrollAround._enableScrolling;
1194 };
1195
1196 function preventDefault(e) {
1197 e.preventDefault();
1198 }
1199 }
1200
1201 // Converts the body to a position fixed block and translate it to the proper scroll position
1202 function disableBodyScroll() {
1203 var htmlNode = body.parentNode;
1204 var restoreHtmlStyle = htmlNode.style.cssText || '';
1205 var restoreBodyStyle = body.style.cssText || '';
1206 var scrollOffset = $mdUtil.scrollTop(body);
1207 var clientWidth = body.clientWidth;
1208
1209 if (body.scrollHeight > body.clientHeight + 1) {
1210 applyStyles(body, {
1211 position: 'fixed',
1212 width: '100%',
1213 top: -scrollOffset + 'px'
1214 });
1215
1216 htmlNode.style.overflowY = 'scroll';
1217 }
1218
1219 if (body.clientWidth < clientWidth) applyStyles(body, {overflow: 'hidden'});
1220
1221 return function restoreScroll() {
1222 body.style.cssText = restoreBodyStyle;
1223 htmlNode.style.cssText = restoreHtmlStyle;
1224 body.scrollTop = scrollOffset;
1225 htmlNode.scrollTop = scrollOffset;
1226 };
1227 }
1228
1229 function applyStyles(el, styles) {
1230 for (var key in styles) {
1231 el.style[key] = styles[key];
1232 }
1233 }
1234 },
1235 enableScrolling: function() {
1236 var method = this.disableScrollAround._enableScrolling;
1237 method && method();
1238 },
1239 floatingScrollbars: function() {
1240 if (this.floatingScrollbars.cached === undefined) {
1241 var tempNode = angular.element('<div><div></div></div>').css({
1242 width: '100%',
1243 'z-index': -1,
1244 position: 'absolute',
1245 height: '35px',
1246 'overflow-y': 'scroll'
1247 });
1248 tempNode.children().css('height', '60px');
1249
1250 $document[0].body.appendChild(tempNode[0]);
1251 this.floatingScrollbars.cached = (tempNode[0].offsetWidth == tempNode[0].childNodes[0].offsetWidth);
1252 tempNode.remove();
1253 }
1254 return this.floatingScrollbars.cached;
1255 },
1256
1257 // Mobile safari only allows you to set focus in click event listeners...
1258 forceFocus: function(element) {
1259 var node = element[0] || element;
1260
1261 document.addEventListener('click', function focusOnClick(ev) {
1262 if (ev.target === node && ev.$focus) {
1263 node.focus();
1264 ev.stopImmediatePropagation();
1265 ev.preventDefault();
1266 node.removeEventListener('click', focusOnClick);
1267 }
1268 }, true);
1269
1270 var newEvent = document.createEvent('MouseEvents');
1271 newEvent.initMouseEvent('click', false, true, window, {}, 0, 0, 0, 0,
1272 false, false, false, false, 0, null);
1273 newEvent.$material = true;
1274 newEvent.$focus = true;
1275 node.dispatchEvent(newEvent);
1276 },
1277
1278 /**
1279 * facade to build md-backdrop element with desired styles
1280 * NOTE: Use $compile to trigger backdrop postLink function
1281 */
1282 createBackdrop: function(scope, addClass) {
1283 return $compile($mdUtil.supplant('<md-backdrop class="{0}">', [addClass]))(scope);
1284 },
1285
1286 /**
1287 * supplant() method from Crockford's `Remedial Javascript`
1288 * Equivalent to use of $interpolate; without dependency on
1289 * interpolation symbols and scope. Note: the '{<token>}' can
1290 * be property names, property chains, or array indices.
1291 */
1292 supplant: function(template, values, pattern) {
1293 pattern = pattern || /\{([^\{\}]*)\}/g;
1294 return template.replace(pattern, function(a, b) {
1295 var p = b.split('.'),
1296 r = values;
1297 try {
1298 for (var s in p) {
1299 if (p.hasOwnProperty(s) ) {
1300 r = r[p[s]];
1301 }
1302 }
1303 } catch (e) {
1304 r = a;
1305 }
1306 return (typeof r === 'string' || typeof r === 'number') ? r : a;
1307 });
1308 },
1309
1310 fakeNgModel: function() {
1311 return {
1312 $fake: true,
1313 $setTouched: angular.noop,
1314 $setViewValue: function(value) {
1315 this.$viewValue = value;
1316 this.$render(value);
1317 this.$viewChangeListeners.forEach(function(cb) {
1318 cb();
1319 });
1320 },
1321 $isEmpty: function(value) {
1322 return ('' + value).length === 0;
1323 },
1324 $parsers: [],
1325 $formatters: [],
1326 $viewChangeListeners: [],
1327 $render: angular.noop
1328 };
1329 },
1330
1331 // Returns a function, that, as long as it continues to be invoked, will not
1332 // be triggered. The function will be called after it stops being called for
1333 // N milliseconds.
1334 // @param wait Integer value of msecs to delay (since last debounce reset); default value 10 msecs
1335 // @param invokeApply should the $timeout trigger $digest() dirty checking
1336 debounce: function(func, wait, scope, invokeApply) {
1337 var timer;
1338
1339 return function debounced() {
1340 var context = scope,
1341 args = Array.prototype.slice.call(arguments);
1342
1343 $timeout.cancel(timer);
1344 timer = $timeout(function() {
1345
1346 timer = undefined;
1347 func.apply(context, args);
1348
1349 }, wait || 10, invokeApply);
1350 };
1351 },
1352
1353 // Returns a function that can only be triggered every `delay` milliseconds.
1354 // In other words, the function will not be called unless it has been more
1355 // than `delay` milliseconds since the last call.
1356 throttle: function throttle(func, delay) {
1357 var recent;
1358 return function throttled() {
1359 var context = this;
1360 var args = arguments;
1361 var now = $mdUtil.now();
1362
1363 if (!recent || (now - recent > delay)) {
1364 func.apply(context, args);
1365 recent = now;
1366 }
1367 };
1368 },
1369
1370 /**
1371 * Measures the number of milliseconds taken to run the provided callback
1372 * function. Uses a high-precision timer if available.
1373 */
1374 time: function time(cb) {
1375 var start = $mdUtil.now();
1376 cb();
1377 return $mdUtil.now() - start;
1378 },
1379
1380 /**
1381 * Create an implicit getter that caches its `getter()`
1382 * lookup value
1383 */
1384 valueOnUse : function (scope, key, getter) {
1385 var value = null, args = Array.prototype.slice.call(arguments);
1386 var params = (args.length > 3) ? args.slice(3) : [ ];
1387
1388 Object.defineProperty(scope, key, {
1389 get: function () {
1390 if (value === null) value = getter.apply(scope, params);
1391 return value;
1392 }
1393 });
1394 },
1395
1396 /**
1397 * Get a unique ID.
1398 *
1399 * @returns {string} an unique numeric string
1400 */
1401 nextUid: function() {
1402 return '' + nextUniqueId++;
1403 },
1404
1405 // Stop watchers and events from firing on a scope without destroying it,
1406 // by disconnecting it from its parent and its siblings' linked lists.
1407 disconnectScope: function disconnectScope(scope) {
1408 if (!scope) return;
1409
1410 // we can't destroy the root scope or a scope that has been already destroyed
1411 if (scope.$root === scope) return;
1412 if (scope.$$destroyed) return;
1413
1414 var parent = scope.$parent;
1415 scope.$$disconnected = true;
1416
1417 // See Scope.$destroy
1418 if (parent.$$childHead === scope) parent.$$childHead = scope.$$nextSibling;
1419 if (parent.$$childTail === scope) parent.$$childTail = scope.$$prevSibling;
1420 if (scope.$$prevSibling) scope.$$prevSibling.$$nextSibling = scope.$$nextSibling;
1421 if (scope.$$nextSibling) scope.$$nextSibling.$$prevSibling = scope.$$prevSibling;
1422
1423 scope.$$nextSibling = scope.$$prevSibling = null;
1424
1425 },
1426
1427 // Undo the effects of disconnectScope above.
1428 reconnectScope: function reconnectScope(scope) {
1429 if (!scope) return;
1430
1431 // we can't disconnect the root node or scope already disconnected
1432 if (scope.$root === scope) return;
1433 if (!scope.$$disconnected) return;
1434
1435 var child = scope;
1436
1437 var parent = child.$parent;
1438 child.$$disconnected = false;
1439 // See Scope.$new for this logic...
1440 child.$$prevSibling = parent.$$childTail;
1441 if (parent.$$childHead) {
1442 parent.$$childTail.$$nextSibling = child;
1443 parent.$$childTail = child;
1444 } else {
1445 parent.$$childHead = parent.$$childTail = child;
1446 }
1447 },
1448
1449 /*
1450 * getClosest replicates jQuery.closest() to walk up the DOM tree until it finds a matching nodeName
1451 *
1452 * @param el Element to start walking the DOM from
1453 * @param check Either a string or a function. If a string is passed, it will be evaluated against
1454 * each of the parent nodes' tag name. If a function is passed, the loop will call it with each of
1455 * the parents and will use the return value to determine whether the node is a match.
1456 * @param onlyParent Only start checking from the parent element, not `el`.
1457 */
1458 getClosest: function getClosest(el, validateWith, onlyParent) {
1459 if ( angular.isString(validateWith) ) {
1460 var tagName = validateWith.toUpperCase();
1461 validateWith = function(el) {
1462 return el.nodeName === tagName;
1463 };
1464 }
1465
1466 if (el instanceof angular.element) el = el[0];
1467 if (onlyParent) el = el.parentNode;
1468 if (!el) return null;
1469
1470 do {
1471 if (validateWith(el)) {
1472 return el;
1473 }
1474 } while (el = el.parentNode);
1475
1476 return null;
1477 },
1478
1479 /**
1480 * Build polyfill for the Node.contains feature (if needed)
1481 */
1482 elementContains: function(node, child) {
1483 var hasContains = (window.Node && window.Node.prototype && Node.prototype.contains);
1484 var findFn = hasContains ? angular.bind(node, node.contains) : angular.bind(node, function(arg) {
1485 // compares the positions of two nodes and returns a bitmask
1486 return (node === child) || !!(this.compareDocumentPosition(arg) & 16)
1487 });
1488
1489 return findFn(child);
1490 },
1491
1492 /**
1493 * Functional equivalent for $element.filter(‘md-bottom-sheet’)
1494 * useful with interimElements where the element and its container are important...
1495 *
1496 * @param {[]} elements to scan
1497 * @param {string} name of node to find (e.g. 'md-dialog')
1498 * @param {boolean=} optional flag to allow deep scans; defaults to 'false'.
1499 * @param {boolean=} optional flag to enable log warnings; defaults to false
1500 */
1501 extractElementByName: function(element, nodeName, scanDeep, warnNotFound) {
1502 var found = scanTree(element);
1503 if (!found && !!warnNotFound) {
1504 $log.warn( $mdUtil.supplant("Unable to find node '{0}' in element '{1}'.",[nodeName, element[0].outerHTML]) );
1505 }
1506
1507 return angular.element(found || element);
1508
1509 /**
1510 * Breadth-First tree scan for element with matching `nodeName`
1511 */
1512 function scanTree(element) {
1513 return scanLevel(element) || (!!scanDeep ? scanChildren(element) : null);
1514 }
1515
1516 /**
1517 * Case-insensitive scan of current elements only (do not descend).
1518 */
1519 function scanLevel(element) {
1520 if ( element ) {
1521 for (var i = 0, len = element.length; i < len; i++) {
1522 if (element[i].nodeName.toLowerCase() === nodeName) {
1523 return element[i];
1524 }
1525 }
1526 }
1527 return null;
1528 }
1529
1530 /**
1531 * Scan children of specified node
1532 */
1533 function scanChildren(element) {
1534 var found;
1535 if ( element ) {
1536 for (var i = 0, len = element.length; i < len; i++) {
1537 var target = element[i];
1538 if ( !found ) {
1539 for (var j = 0, numChild = target.childNodes.length; j < numChild; j++) {
1540 found = found || scanTree([target.childNodes[j]]);
1541 }
1542 }
1543 }
1544 }
1545 return found;
1546 }
1547
1548 },
1549
1550 /**
1551 * Give optional properties with no value a boolean true if attr provided or false otherwise
1552 */
1553 initOptionalProperties: function(scope, attr, defaults) {
1554 defaults = defaults || {};
1555 angular.forEach(scope.$$isolateBindings, function(binding, key) {
1556 if (binding.optional && angular.isUndefined(scope[key])) {
1557 var attrIsDefined = angular.isDefined(attr[binding.attrName]);
1558 scope[key] = angular.isDefined(defaults[key]) ? defaults[key] : attrIsDefined;
1559 }
1560 });
1561 },
1562
1563 /**
1564 * Alternative to $timeout calls with 0 delay.
1565 * nextTick() coalesces all calls within a single frame
1566 * to minimize $digest thrashing
1567 *
1568 * @param callback
1569 * @param digest
1570 * @returns {*}
1571 */
1572 nextTick: function(callback, digest, scope) {
1573 //-- grab function reference for storing state details
1574 var nextTick = $mdUtil.nextTick;
1575 var timeout = nextTick.timeout;
1576 var queue = nextTick.queue || [];
1577
1578 //-- add callback to the queue
1579 queue.push({scope: scope, callback: callback});
1580
1581 //-- set default value for digest
1582 if (digest == null) digest = true;
1583
1584 //-- store updated digest/queue values
1585 nextTick.digest = nextTick.digest || digest;
1586 nextTick.queue = queue;
1587
1588 //-- either return existing timeout or create a new one
1589 return timeout || (nextTick.timeout = $timeout(processQueue, 0, false));
1590
1591 /**
1592 * Grab a copy of the current queue
1593 * Clear the queue for future use
1594 * Process the existing queue
1595 * Trigger digest if necessary
1596 */
1597 function processQueue() {
1598 var queue = nextTick.queue;
1599 var digest = nextTick.digest;
1600
1601 nextTick.queue = [];
1602 nextTick.timeout = null;
1603 nextTick.digest = false;
1604
1605 queue.forEach(function(queueItem) {
1606 var skip = queueItem.scope && queueItem.scope.$$destroyed;
1607 if (!skip) {
1608 queueItem.callback();
1609 }
1610 });
1611
1612 if (digest) $rootScope.$digest();
1613 }
1614 },
1615
1616 /**
1617 * Processes a template and replaces the start/end symbols if the application has
1618 * overriden them.
1619 *
1620 * @param template The template to process whose start/end tags may be replaced.
1621 * @returns {*}
1622 */
1623 processTemplate: function(template) {
1624 if (usesStandardSymbols) {
1625 return template;
1626 } else {
1627 if (!template || !angular.isString(template)) return template;
1628 return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol);
1629 }
1630 },
1631
1632 /**
1633 * Scan up dom hierarchy for enabled parent;
1634 */
1635 getParentWithPointerEvents: function (element) {
1636 var parent = element.parent();
1637
1638 // jqLite might return a non-null, but still empty, parent; so check for parent and length
1639 while (hasComputedStyle(parent, 'pointer-events', 'none')) {
1640 parent = parent.parent();
1641 }
1642
1643 return parent;
1644 },
1645
1646 getNearestContentElement: function (element) {
1647 var current = element.parent()[0];
1648 // Look for the nearest parent md-content, stopping at the rootElement.
1649 while (current && current !== $rootElement[0] && current !== document.body && current.nodeName.toUpperCase() !== 'MD-CONTENT') {
1650 current = current.parentNode;
1651 }
1652 return current;
1653 },
1654
1655 /**
1656 * Checks if the current browser is natively supporting the `sticky` position.
1657 * @returns {string} supported sticky property name
1658 */
1659 checkStickySupport: function() {
1660 var stickyProp;
1661 var testEl = angular.element('<div>');
1662 $document[0].body.appendChild(testEl[0]);
1663
1664 var stickyProps = ['sticky', '-webkit-sticky'];
1665 for (var i = 0; i < stickyProps.length; ++i) {
1666 testEl.css({
1667 position: stickyProps[i],
1668 top: 0,
1669 'z-index': 2
1670 });
1671
1672 if (testEl.css('position') == stickyProps[i]) {
1673 stickyProp = stickyProps[i];
1674 break;
1675 }
1676 }
1677
1678 testEl.remove();
1679
1680 return stickyProp;
1681 },
1682
1683 /**
1684 * Parses an attribute value, mostly a string.
1685 * By default checks for negated values and returns `false´ if present.
1686 * Negated values are: (native falsy) and negative strings like:
1687 * `false` or `0`.
1688 * @param value Attribute value which should be parsed.
1689 * @param negatedCheck When set to false, won't check for negated values.
1690 * @returns {boolean}
1691 */
1692 parseAttributeBoolean: function(value, negatedCheck) {
1693 return value === '' || !!value && (negatedCheck === false || value !== 'false' && value !== '0');
1694 },
1695
1696 hasComputedStyle: hasComputedStyle,
1697
1698 /**
1699 * Returns true if the parent form of the element has been submitted.
1700 *
1701 * @param element An Angular or HTML5 element.
1702 *
1703 * @returns {boolean}
1704 */
1705 isParentFormSubmitted: function(element) {
1706 var parent = $mdUtil.getClosest(element, 'form');
1707 var form = parent ? angular.element(parent).controller('form') : null;
1708
1709 return form ? form.$submitted : false;
1710 },
1711
1712 /**
1713 * Animate the requested element's scrollTop to the requested scrollPosition with basic easing.
1714 *
1715 * @param element The element to scroll.
1716 * @param scrollEnd The new/final scroll position.
1717 */
1718 animateScrollTo: function(element, scrollEnd) {
1719 var scrollStart = element.scrollTop;
1720 var scrollChange = scrollEnd - scrollStart;
1721 var scrollingDown = scrollStart < scrollEnd;
1722 var startTime = $mdUtil.now();
1723
1724 $$rAF(scrollChunk);
1725
1726 function scrollChunk() {
1727 var newPosition = calculateNewPosition();
1728
1729 element.scrollTop = newPosition;
1730
1731 if (scrollingDown ? newPosition < scrollEnd : newPosition > scrollEnd) {
1732 $$rAF(scrollChunk);
1733 }
1734 }
1735
1736 function calculateNewPosition() {
1737 var duration = 1000;
1738 var currentTime = $mdUtil.now() - startTime;
1739
1740 return ease(currentTime, scrollStart, scrollChange, duration);
1741 }
1742
1743 function ease(currentTime, start, change, duration) {
1744 // If the duration has passed (which can occur if our app loses focus due to $$rAF), jump
1745 // straight to the proper position
1746 if (currentTime > duration) {
1747 return start + change;
1748 }
1749
1750 var ts = (currentTime /= duration) * currentTime;
1751 var tc = ts * currentTime;
1752
1753 return start + change * (-2 * tc + 3 * ts);
1754 }
1755 }
1756 };
1757
1758
1759// Instantiate other namespace utility methods
1760
1761 $mdUtil.dom.animator = $$mdAnimate($mdUtil);
1762
1763 return $mdUtil;
1764
1765 function getNode(el) {
1766 return el[0] || el;
1767 }
1768
1769}
1770
1771/*
1772 * Since removing jQuery from the demos, some code that uses `element.focus()` is broken.
1773 * We need to add `element.focus()`, because it's testable unlike `element[0].focus`.
1774 */
1775
1776angular.element.prototype.focus = angular.element.prototype.focus || function() {
1777 if (this.length) {
1778 this[0].focus();
1779 }
1780 return this;
1781 };
1782angular.element.prototype.blur = angular.element.prototype.blur || function() {
1783 if (this.length) {
1784 this[0].blur();
1785 }
1786 return this;
1787 };
1788
1789})();
1790(function(){
1791"use strict";
1792
1793/**
1794 * @ngdoc module
1795 * @name material.core.aria
1796 * @description
1797 * Aria Expectations for ngMaterial components.
1798 */
1799MdAriaService.$inject = ["$$rAF", "$log", "$window", "$interpolate"];
1800angular
1801 .module('material.core')
1802 .provider('$mdAria', MdAriaProvider);
1803
1804/**
1805 * @ngdoc service
1806 * @name $mdAriaProvider
1807 * @module material.core.aria
1808 *
1809 * @description
1810 *
1811 * Modify options of the `$mdAria` service, which will be used by most of the Angular Material
1812 * components.
1813 *
1814 * You are able to disable `$mdAria` warnings, by using the following markup.
1815 *
1816 * <hljs lang="js">
1817 * app.config(function($mdAriaProvider) {
1818 * // Globally disables all ARIA warnings.
1819 * $mdAriaProvider.disableWarnings();
1820 * });
1821 * </hljs>
1822 *
1823 */
1824function MdAriaProvider() {
1825
1826 var self = this;
1827
1828 /**
1829 * Whether we should show ARIA warnings in the console, if labels are missing on the element
1830 * By default the warnings are enabled
1831 */
1832 self.showWarnings = true;
1833
1834 return {
1835 disableWarnings: disableWarnings,
1836 $get: ["$$rAF", "$log", "$window", "$interpolate", function($$rAF, $log, $window, $interpolate) {
1837 return MdAriaService.apply(self, arguments);
1838 }]
1839 };
1840
1841 /**
1842 * @ngdoc method
1843 * @name $mdAriaProvider#disableWarnings
1844 * @description Disables all ARIA warnings generated by Angular Material.
1845 */
1846 function disableWarnings() {
1847 self.showWarnings = false;
1848 }
1849}
1850
1851/*
1852 * @ngInject
1853 */
1854function MdAriaService($$rAF, $log, $window, $interpolate) {
1855
1856 // Load the showWarnings option from the current context and store it inside of a scope variable,
1857 // because the context will be probably lost in some function calls.
1858 var showWarnings = this.showWarnings;
1859
1860 return {
1861 expect: expect,
1862 expectAsync: expectAsync,
1863 expectWithText: expectWithText,
1864 expectWithoutText: expectWithoutText
1865 };
1866
1867 /**
1868 * Check if expected attribute has been specified on the target element or child
1869 * @param element
1870 * @param attrName
1871 * @param {optional} defaultValue What to set the attr to if no value is found
1872 */
1873 function expect(element, attrName, defaultValue) {
1874
1875 var node = angular.element(element)[0] || element;
1876
1877 // if node exists and neither it nor its children have the attribute
1878 if (node &&
1879 ((!node.hasAttribute(attrName) ||
1880 node.getAttribute(attrName).length === 0) &&
1881 !childHasAttribute(node, attrName))) {
1882
1883 defaultValue = angular.isString(defaultValue) ? defaultValue.trim() : '';
1884 if (defaultValue.length) {
1885 element.attr(attrName, defaultValue);
1886 } else if (showWarnings) {
1887 $log.warn('ARIA: Attribute "', attrName, '", required for accessibility, is missing on node:', node);
1888 }
1889
1890 }
1891 }
1892
1893 function expectAsync(element, attrName, defaultValueGetter) {
1894 // Problem: when retrieving the element's contents synchronously to find the label,
1895 // the text may not be defined yet in the case of a binding.
1896 // There is a higher chance that a binding will be defined if we wait one frame.
1897 $$rAF(function() {
1898 expect(element, attrName, defaultValueGetter());
1899 });
1900 }
1901
1902 function expectWithText(element, attrName) {
1903 var content = getText(element) || "";
1904 var hasBinding = content.indexOf($interpolate.startSymbol()) > -1;
1905
1906 if ( hasBinding ) {
1907 expectAsync(element, attrName, function() {
1908 return getText(element);
1909 });
1910 } else {
1911 expect(element, attrName, content);
1912 }
1913 }
1914
1915 function expectWithoutText(element, attrName) {
1916 var content = getText(element);
1917 var hasBinding = content.indexOf($interpolate.startSymbol()) > -1;
1918
1919 if ( !hasBinding && !content) {
1920 expect(element, attrName, content);
1921 }
1922 }
1923
1924 function getText(element) {
1925 element = element[0] || element;
1926 var walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false);
1927 var text = '';
1928
1929 var node;
1930 while (node = walker.nextNode()) {
1931 if (!isAriaHiddenNode(node)) {
1932 text += node.textContent;
1933 }
1934 }
1935
1936 return text.trim() || '';
1937
1938 function isAriaHiddenNode(node) {
1939 while (node.parentNode && (node = node.parentNode) !== element) {
1940 if (node.getAttribute && node.getAttribute('aria-hidden') === 'true') {
1941 return true;
1942 }
1943 }
1944 }
1945 }
1946
1947 function childHasAttribute(node, attrName) {
1948 var hasChildren = node.hasChildNodes(),
1949 hasAttr = false;
1950
1951 function isHidden(el) {
1952 var style = el.currentStyle ? el.currentStyle : $window.getComputedStyle(el);
1953 return (style.display === 'none');
1954 }
1955
1956 if (hasChildren) {
1957 var children = node.childNodes;
1958 for (var i=0; i < children.length; i++) {
1959 var child = children[i];
1960 if (child.nodeType === 1 && child.hasAttribute(attrName)) {
1961 if (!isHidden(child)) {
1962 hasAttr = true;
1963 }
1964 }
1965 }
1966 }
1967
1968 return hasAttr;
1969 }
1970}
1971
1972})();
1973(function(){
1974"use strict";
1975
1976
1977mdCompilerService.$inject = ["$q", "$templateRequest", "$injector", "$compile", "$controller"];angular
1978 .module('material.core')
1979 .service('$mdCompiler', mdCompilerService);
1980
1981function mdCompilerService($q, $templateRequest, $injector, $compile, $controller) {
1982 /* jshint validthis: true */
1983
1984 /*
1985 * @ngdoc service
1986 * @name $mdCompiler
1987 * @module material.core
1988 * @description
1989 * The $mdCompiler service is an abstraction of angular's compiler, that allows the developer
1990 * to easily compile an element with a templateUrl, controller, and locals.
1991 *
1992 * @usage
1993 * <hljs lang="js">
1994 * $mdCompiler.compile({
1995 * templateUrl: 'modal.html',
1996 * controller: 'ModalCtrl',
1997 * locals: {
1998 * modal: myModalInstance;
1999 * }
2000 * }).then(function(compileData) {
2001 * compileData.element; // modal.html's template in an element
2002 * compileData.link(myScope); //attach controller & scope to element
2003 * });
2004 * </hljs>
2005 */
2006
2007 /*
2008 * @ngdoc method
2009 * @name $mdCompiler#compile
2010 * @description A helper to compile an HTML template/templateUrl with a given controller,
2011 * locals, and scope.
2012 * @param {object} options An options object, with the following properties:
2013 *
2014 * - `controller` - `{(string=|function()=}` Controller fn that should be associated with
2015 * newly created scope or the name of a registered controller if passed as a string.
2016 * - `controllerAs` - `{string=}` A controller alias name. If present the controller will be
2017 * published to scope under the `controllerAs` name.
2018 * - `template` - `{string=}` An html template as a string.
2019 * - `templateUrl` - `{string=}` A path to an html template.
2020 * - `transformTemplate` - `{function(template)=}` A function which transforms the template after
2021 * it is loaded. It will be given the template string as a parameter, and should
2022 * return a a new string representing the transformed template.
2023 * - `resolve` - `{Object.<string, function>=}` - An optional map of dependencies which should
2024 * be injected into the controller. If any of these dependencies are promises, the compiler
2025 * will wait for them all to be resolved, or if one is rejected before the controller is
2026 * instantiated `compile()` will fail..
2027 * * `key` - `{string}`: a name of a dependency to be injected into the controller.
2028 * * `factory` - `{string|function}`: If `string` then it is an alias for a service.
2029 * Otherwise if function, then it is injected and the return value is treated as the
2030 * dependency. If the result is a promise, it is resolved before its value is
2031 * injected into the controller.
2032 *
2033 * @returns {object=} promise A promise, which will be resolved with a `compileData` object.
2034 * `compileData` has the following properties:
2035 *
2036 * - `element` - `{element}`: an uncompiled element matching the provided template.
2037 * - `link` - `{function(scope)}`: A link function, which, when called, will compile
2038 * the element and instantiate the provided controller (if given).
2039 * - `locals` - `{object}`: The locals which will be passed into the controller once `link` is
2040 * called. If `bindToController` is true, they will be coppied to the ctrl instead
2041 * - `bindToController` - `bool`: bind the locals to the controller, instead of passing them in.
2042 */
2043 this.compile = function(options) {
2044 var templateUrl = options.templateUrl;
2045 var template = options.template || '';
2046 var controller = options.controller;
2047 var controllerAs = options.controllerAs;
2048 var resolve = angular.extend({}, options.resolve || {});
2049 var locals = angular.extend({}, options.locals || {});
2050 var transformTemplate = options.transformTemplate || angular.identity;
2051 var bindToController = options.bindToController;
2052
2053 // Take resolve values and invoke them.
2054 // Resolves can either be a string (value: 'MyRegisteredAngularConst'),
2055 // or an invokable 'factory' of sorts: (value: function ValueGetter($dependency) {})
2056 angular.forEach(resolve, function(value, key) {
2057 if (angular.isString(value)) {
2058 resolve[key] = $injector.get(value);
2059 } else {
2060 resolve[key] = $injector.invoke(value);
2061 }
2062 });
2063 //Add the locals, which are just straight values to inject
2064 //eg locals: { three: 3 }, will inject three into the controller
2065 angular.extend(resolve, locals);
2066
2067 if (templateUrl) {
2068 resolve.$template = $templateRequest(templateUrl)
2069 .then(function(response) {
2070 return response;
2071 });
2072 } else {
2073 resolve.$template = $q.when(template);
2074 }
2075
2076 // Wait for all the resolves to finish if they are promises
2077 return $q.all(resolve).then(function(locals) {
2078
2079 var compiledData;
2080 var template = transformTemplate(locals.$template, options);
2081 var element = options.element || angular.element('<div>').html(template.trim()).contents();
2082 var linkFn = $compile(element);
2083
2084 // Return a linking function that can be used later when the element is ready
2085 return compiledData = {
2086 locals: locals,
2087 element: element,
2088 link: function link(scope) {
2089 locals.$scope = scope;
2090
2091 //Instantiate controller if it exists, because we have scope
2092 if (controller) {
2093 var invokeCtrl = $controller(controller, locals, true, controllerAs);
2094 if (bindToController) {
2095 angular.extend(invokeCtrl.instance, locals);
2096 }
2097 var ctrl = invokeCtrl();
2098 //See angular-route source for this logic
2099 element.data('$ngControllerController', ctrl);
2100 element.children().data('$ngControllerController', ctrl);
2101
2102 // Publish reference to this controller
2103 compiledData.controller = ctrl;
2104 }
2105 return linkFn(scope);
2106 }
2107 };
2108 });
2109
2110 };
2111}
2112
2113})();
2114(function(){
2115"use strict";
2116
2117
2118MdGesture.$inject = ["$$MdGestureHandler", "$$rAF", "$timeout"];
2119attachToDocument.$inject = ["$mdGesture", "$$MdGestureHandler"];var HANDLERS = {};
2120
2121/* The state of the current 'pointer'
2122 * The pointer represents the state of the current touch.
2123 * It contains normalized x and y coordinates from DOM events,
2124 * as well as other information abstracted from the DOM.
2125 */
2126
2127var pointer, lastPointer, forceSkipClickHijack = false;
2128
2129/**
2130 * The position of the most recent click if that click was on a label element.
2131 * @type {{x: number, y: number}?}
2132 */
2133var lastLabelClickPos = null;
2134
2135// Used to attach event listeners once when multiple ng-apps are running.
2136var isInitialized = false;
2137
2138angular
2139 .module('material.core.gestures', [ ])
2140 .provider('$mdGesture', MdGestureProvider)
2141 .factory('$$MdGestureHandler', MdGestureHandler)
2142 .run( attachToDocument );
2143
2144/**
2145 * @ngdoc service
2146 * @name $mdGestureProvider
2147 * @module material.core.gestures
2148 *
2149 * @description
2150 * In some scenarios on Mobile devices (without jQuery), the click events should NOT be hijacked.
2151 * `$mdGestureProvider` is used to configure the Gesture module to ignore or skip click hijacking on mobile
2152 * devices.
2153 *
2154 * <hljs lang="js">
2155 * app.config(function($mdGestureProvider) {
2156 *
2157 * // For mobile devices without jQuery loaded, do not
2158 * // intercept click events during the capture phase.
2159 * $mdGestureProvider.skipClickHijack();
2160 *
2161 * });
2162 * </hljs>
2163 *
2164 */
2165function MdGestureProvider() { }
2166
2167MdGestureProvider.prototype = {
2168
2169 // Publish access to setter to configure a variable BEFORE the
2170 // $mdGesture service is instantiated...
2171 skipClickHijack: function() {
2172 return forceSkipClickHijack = true;
2173 },
2174
2175 /**
2176 * $get is used to build an instance of $mdGesture
2177 * @ngInject
2178 */
2179 $get : ["$$MdGestureHandler", "$$rAF", "$timeout", function($$MdGestureHandler, $$rAF, $timeout) {
2180 return new MdGesture($$MdGestureHandler, $$rAF, $timeout);
2181 }]
2182};
2183
2184
2185
2186/**
2187 * MdGesture factory construction function
2188 * @ngInject
2189 */
2190function MdGesture($$MdGestureHandler, $$rAF, $timeout) {
2191 var userAgent = navigator.userAgent || navigator.vendor || window.opera;
2192 var isIos = userAgent.match(/ipad|iphone|ipod/i);
2193 var isAndroid = userAgent.match(/android/i);
2194 var touchActionProperty = getTouchAction();
2195 var hasJQuery = (typeof window.jQuery !== 'undefined') && (angular.element === window.jQuery);
2196
2197 var self = {
2198 handler: addHandler,
2199 register: register,
2200 isIos: isIos,
2201 isAndroid: isAndroid,
2202 // On mobile w/out jQuery, we normally intercept clicks. Should we skip that?
2203 isHijackingClicks: (isIos || isAndroid) && !hasJQuery && !forceSkipClickHijack
2204 };
2205
2206 if (self.isHijackingClicks) {
2207 var maxClickDistance = 6;
2208 self.handler('click', {
2209 options: {
2210 maxDistance: maxClickDistance
2211 },
2212 onEnd: checkDistanceAndEmit('click')
2213 });
2214
2215 self.handler('focus', {
2216 options: {
2217 maxDistance: maxClickDistance
2218 },
2219 onEnd: function(ev, pointer) {
2220 if (pointer.distance < this.state.options.maxDistance) {
2221 if (canFocus(ev.target)) {
2222 this.dispatchEvent(ev, 'focus', pointer);
2223 ev.target.focus();
2224 }
2225 }
2226
2227 function canFocus(element) {
2228 var focusableElements = ['INPUT', 'SELECT', 'BUTTON', 'TEXTAREA', 'VIDEO', 'AUDIO'];
2229
2230 return (element.getAttribute('tabindex') != '-1') &&
2231 !element.hasAttribute('DISABLED') &&
2232 (element.hasAttribute('tabindex') || element.hasAttribute('href') || element.isContentEditable ||
2233 (focusableElements.indexOf(element.nodeName) != -1));
2234 }
2235 }
2236 });
2237
2238 self.handler('mouseup', {
2239 options: {
2240 maxDistance: maxClickDistance
2241 },
2242 onEnd: checkDistanceAndEmit('mouseup')
2243 });
2244
2245 self.handler('mousedown', {
2246 onStart: function(ev) {
2247 this.dispatchEvent(ev, 'mousedown');
2248 }
2249 });
2250 }
2251
2252 function checkDistanceAndEmit(eventName) {
2253 return function(ev, pointer) {
2254 if (pointer.distance < this.state.options.maxDistance) {
2255 this.dispatchEvent(ev, eventName, pointer);
2256 }
2257 };
2258 }
2259
2260 /*
2261 * Register an element to listen for a handler.
2262 * This allows an element to override the default options for a handler.
2263 * Additionally, some handlers like drag and hold only dispatch events if
2264 * the domEvent happens inside an element that's registered to listen for these events.
2265 *
2266 * @see GestureHandler for how overriding of default options works.
2267 * @example $mdGesture.register(myElement, 'drag', { minDistance: 20, horziontal: false })
2268 */
2269 function register(element, handlerName, options) {
2270 var handler = HANDLERS[handlerName.replace(/^\$md./, '')];
2271 if (!handler) {
2272 throw new Error('Failed to register element with handler ' + handlerName + '. ' +
2273 'Available handlers: ' + Object.keys(HANDLERS).join(', '));
2274 }
2275 return handler.registerElement(element, options);
2276 }
2277
2278 /*
2279 * add a handler to $mdGesture. see below.
2280 */
2281 function addHandler(name, definition) {
2282 var handler = new $$MdGestureHandler(name);
2283 angular.extend(handler, definition);
2284 HANDLERS[name] = handler;
2285
2286 return self;
2287 }
2288
2289 /*
2290 * Register handlers. These listen to touch/start/move events, interpret them,
2291 * and dispatch gesture events depending on options & conditions. These are all
2292 * instances of GestureHandler.
2293 * @see GestureHandler
2294 */
2295 return self
2296 /*
2297 * The press handler dispatches an event on touchdown/touchend.
2298 * It's a simple abstraction of touch/mouse/pointer start and end.
2299 */
2300 .handler('press', {
2301 onStart: function (ev, pointer) {
2302 this.dispatchEvent(ev, '$md.pressdown');
2303 },
2304 onEnd: function (ev, pointer) {
2305 this.dispatchEvent(ev, '$md.pressup');
2306 }
2307 })
2308
2309 /*
2310 * The hold handler dispatches an event if the user keeps their finger within
2311 * the same <maxDistance> area for <delay> ms.
2312 * The hold handler will only run if a parent of the touch target is registered
2313 * to listen for hold events through $mdGesture.register()
2314 */
2315 .handler('hold', {
2316 options: {
2317 maxDistance: 6,
2318 delay: 500
2319 },
2320 onCancel: function () {
2321 $timeout.cancel(this.state.timeout);
2322 },
2323 onStart: function (ev, pointer) {
2324 // For hold, require a parent to be registered with $mdGesture.register()
2325 // Because we prevent scroll events, this is necessary.
2326 if (!this.state.registeredParent) return this.cancel();
2327
2328 this.state.pos = {x: pointer.x, y: pointer.y};
2329 this.state.timeout = $timeout(angular.bind(this, function holdDelayFn() {
2330 this.dispatchEvent(ev, '$md.hold');
2331 this.cancel(); //we're done!
2332 }), this.state.options.delay, false);
2333 },
2334 onMove: function (ev, pointer) {
2335 // Don't scroll while waiting for hold.
2336 // If we don't preventDefault touchmove events here, Android will assume we don't
2337 // want to listen to anymore touch events. It will start scrolling and stop sending
2338 // touchmove events.
2339 if (!touchActionProperty && ev.type === 'touchmove') ev.preventDefault();
2340
2341 // If the user moves greater than <maxDistance> pixels, stop the hold timer
2342 // set in onStart
2343 var dx = this.state.pos.x - pointer.x;
2344 var dy = this.state.pos.y - pointer.y;
2345 if (Math.sqrt(dx * dx + dy * dy) > this.options.maxDistance) {
2346 this.cancel();
2347 }
2348 },
2349 onEnd: function () {
2350 this.onCancel();
2351 }
2352 })
2353
2354 /*
2355 * The drag handler dispatches a drag event if the user holds and moves his finger greater than
2356 * <minDistance> px in the x or y direction, depending on options.horizontal.
2357 * The drag will be cancelled if the user moves his finger greater than <minDistance>*<cancelMultiplier> in
2358 * the perpendicular direction. Eg if the drag is horizontal and the user moves his finger <minDistance>*<cancelMultiplier>
2359 * pixels vertically, this handler won't consider the move part of a drag.
2360 */
2361 .handler('drag', {
2362 options: {
2363 minDistance: 6,
2364 horizontal: true,
2365 cancelMultiplier: 1.5
2366 },
2367 onSetup: function(element, options) {
2368 if (touchActionProperty) {
2369 // We check for horizontal to be false, because otherwise we would overwrite the default opts.
2370 this.oldTouchAction = element[0].style[touchActionProperty];
2371 element[0].style[touchActionProperty] = options.horizontal === false ? 'pan-y' : 'pan-x';
2372 }
2373 },
2374 onCleanup: function(element) {
2375 if (this.oldTouchAction) {
2376 element[0].style[touchActionProperty] = this.oldTouchAction;
2377 }
2378 },
2379 onStart: function (ev) {
2380 // For drag, require a parent to be registered with $mdGesture.register()
2381 if (!this.state.registeredParent) this.cancel();
2382 },
2383 onMove: function (ev, pointer) {
2384 var shouldStartDrag, shouldCancel;
2385 // Don't scroll while deciding if this touchmove qualifies as a drag event.
2386 // If we don't preventDefault touchmove events here, Android will assume we don't
2387 // want to listen to anymore touch events. It will start scrolling and stop sending
2388 // touchmove events.
2389 if (!touchActionProperty && ev.type === 'touchmove') ev.preventDefault();
2390
2391 if (!this.state.dragPointer) {
2392 if (this.state.options.horizontal) {
2393 shouldStartDrag = Math.abs(pointer.distanceX) > this.state.options.minDistance;
2394 shouldCancel = Math.abs(pointer.distanceY) > this.state.options.minDistance * this.state.options.cancelMultiplier;
2395 } else {
2396 shouldStartDrag = Math.abs(pointer.distanceY) > this.state.options.minDistance;
2397 shouldCancel = Math.abs(pointer.distanceX) > this.state.options.minDistance * this.state.options.cancelMultiplier;
2398 }
2399
2400 if (shouldStartDrag) {
2401 // Create a new pointer representing this drag, starting at this point where the drag started.
2402 this.state.dragPointer = makeStartPointer(ev);
2403 updatePointerState(ev, this.state.dragPointer);
2404 this.dispatchEvent(ev, '$md.dragstart', this.state.dragPointer);
2405
2406 } else if (shouldCancel) {
2407 this.cancel();
2408 }
2409 } else {
2410 this.dispatchDragMove(ev);
2411 }
2412 },
2413 // Only dispatch dragmove events every frame; any more is unnecessary
2414 dispatchDragMove: $$rAF.throttle(function (ev) {
2415 // Make sure the drag didn't stop while waiting for the next frame
2416 if (this.state.isRunning) {
2417 updatePointerState(ev, this.state.dragPointer);
2418 this.dispatchEvent(ev, '$md.drag', this.state.dragPointer);
2419 }
2420 }),
2421 onEnd: function (ev, pointer) {
2422 if (this.state.dragPointer) {
2423 updatePointerState(ev, this.state.dragPointer);
2424 this.dispatchEvent(ev, '$md.dragend', this.state.dragPointer);
2425 }
2426 }
2427 })
2428
2429 /*
2430 * The swipe handler will dispatch a swipe event if, on the end of a touch,
2431 * the velocity and distance were high enough.
2432 */
2433 .handler('swipe', {
2434 options: {
2435 minVelocity: 0.65,
2436 minDistance: 10
2437 },
2438 onEnd: function (ev, pointer) {
2439 var eventType;
2440
2441 if (Math.abs(pointer.velocityX) > this.state.options.minVelocity &&
2442 Math.abs(pointer.distanceX) > this.state.options.minDistance) {
2443 eventType = pointer.directionX == 'left' ? '$md.swipeleft' : '$md.swiperight';
2444 this.dispatchEvent(ev, eventType);
2445 }
2446 else if (Math.abs(pointer.velocityY) > this.state.options.minVelocity &&
2447 Math.abs(pointer.distanceY) > this.state.options.minDistance) {
2448 eventType = pointer.directionY == 'up' ? '$md.swipeup' : '$md.swipedown';
2449 this.dispatchEvent(ev, eventType);
2450 }
2451 }
2452 });
2453
2454 function getTouchAction() {
2455 var testEl = document.createElement('div');
2456 var vendorPrefixes = ['', 'webkit', 'Moz', 'MS', 'ms', 'o'];
2457
2458 for (var i = 0; i < vendorPrefixes.length; i++) {
2459 var prefix = vendorPrefixes[i];
2460 var property = prefix ? prefix + 'TouchAction' : 'touchAction';
2461 if (angular.isDefined(testEl.style[property])) {
2462 return property;
2463 }
2464 }
2465 }
2466
2467}
2468
2469/**
2470 * MdGestureHandler
2471 * A GestureHandler is an object which is able to dispatch custom dom events
2472 * based on native dom {touch,pointer,mouse}{start,move,end} events.
2473 *
2474 * A gesture will manage its lifecycle through the start,move,end, and cancel
2475 * functions, which are called by native dom events.
2476 *
2477 * A gesture has the concept of 'options' (eg a swipe's required velocity), which can be
2478 * overridden by elements registering through $mdGesture.register()
2479 */
2480function GestureHandler (name) {
2481 this.name = name;
2482 this.state = {};
2483}
2484
2485function MdGestureHandler() {
2486 var hasJQuery = (typeof window.jQuery !== 'undefined') && (angular.element === window.jQuery);
2487
2488 GestureHandler.prototype = {
2489 options: {},
2490 // jQuery listeners don't work with custom DOMEvents, so we have to dispatch events
2491 // differently when jQuery is loaded
2492 dispatchEvent: hasJQuery ? jQueryDispatchEvent : nativeDispatchEvent,
2493
2494 // These are overridden by the registered handler
2495 onSetup: angular.noop,
2496 onCleanup: angular.noop,
2497 onStart: angular.noop,
2498 onMove: angular.noop,
2499 onEnd: angular.noop,
2500 onCancel: angular.noop,
2501
2502 // onStart sets up a new state for the handler, which includes options from the
2503 // nearest registered parent element of ev.target.
2504 start: function (ev, pointer) {
2505 if (this.state.isRunning) return;
2506 var parentTarget = this.getNearestParent(ev.target);
2507 // Get the options from the nearest registered parent
2508 var parentTargetOptions = parentTarget && parentTarget.$mdGesture[this.name] || {};
2509
2510 this.state = {
2511 isRunning: true,
2512 // Override the default options with the nearest registered parent's options
2513 options: angular.extend({}, this.options, parentTargetOptions),
2514 // Pass in the registered parent node to the state so the onStart listener can use
2515 registeredParent: parentTarget
2516 };
2517 this.onStart(ev, pointer);
2518 },
2519 move: function (ev, pointer) {
2520 if (!this.state.isRunning) return;
2521 this.onMove(ev, pointer);
2522 },
2523 end: function (ev, pointer) {
2524 if (!this.state.isRunning) return;
2525 this.onEnd(ev, pointer);
2526 this.state.isRunning = false;
2527 },
2528 cancel: function (ev, pointer) {
2529 this.onCancel(ev, pointer);
2530 this.state = {};
2531 },
2532
2533 // Find and return the nearest parent element that has been registered to
2534 // listen for this handler via $mdGesture.register(element, 'handlerName').
2535 getNearestParent: function (node) {
2536 var current = node;
2537 while (current) {
2538 if ((current.$mdGesture || {})[this.name]) {
2539 return current;
2540 }
2541 current = current.parentNode;
2542 }
2543 return null;
2544 },
2545
2546 // Called from $mdGesture.register when an element registers itself with a handler.
2547 // Store the options the user gave on the DOMElement itself. These options will
2548 // be retrieved with getNearestParent when the handler starts.
2549 registerElement: function (element, options) {
2550 var self = this;
2551 element[0].$mdGesture = element[0].$mdGesture || {};
2552 element[0].$mdGesture[this.name] = options || {};
2553 element.on('$destroy', onDestroy);
2554
2555 self.onSetup(element, options || {});
2556
2557 return onDestroy;
2558
2559 function onDestroy() {
2560 delete element[0].$mdGesture[self.name];
2561 element.off('$destroy', onDestroy);
2562
2563 self.onCleanup(element, options || {});
2564 }
2565 }
2566 };
2567
2568 return GestureHandler;
2569
2570 /*
2571 * Dispatch an event with jQuery
2572 * TODO: Make sure this sends bubbling events
2573 *
2574 * @param srcEvent the original DOM touch event that started this.
2575 * @param eventType the name of the custom event to send (eg 'click' or '$md.drag')
2576 * @param eventPointer the pointer object that matches this event.
2577 */
2578 function jQueryDispatchEvent(srcEvent, eventType, eventPointer) {
2579 eventPointer = eventPointer || pointer;
2580 var eventObj = new angular.element.Event(eventType);
2581
2582 eventObj.$material = true;
2583 eventObj.pointer = eventPointer;
2584 eventObj.srcEvent = srcEvent;
2585
2586 angular.extend(eventObj, {
2587 clientX: eventPointer.x,
2588 clientY: eventPointer.y,
2589 screenX: eventPointer.x,
2590 screenY: eventPointer.y,
2591 pageX: eventPointer.x,
2592 pageY: eventPointer.y,
2593 ctrlKey: srcEvent.ctrlKey,
2594 altKey: srcEvent.altKey,
2595 shiftKey: srcEvent.shiftKey,
2596 metaKey: srcEvent.metaKey
2597 });
2598 angular.element(eventPointer.target).trigger(eventObj);
2599 }
2600
2601 /*
2602 * NOTE: nativeDispatchEvent is very performance sensitive.
2603 * @param srcEvent the original DOM touch event that started this.
2604 * @param eventType the name of the custom event to send (eg 'click' or '$md.drag')
2605 * @param eventPointer the pointer object that matches this event.
2606 */
2607 function nativeDispatchEvent(srcEvent, eventType, eventPointer) {
2608 eventPointer = eventPointer || pointer;
2609 var eventObj;
2610
2611 if (eventType === 'click' || eventType == 'mouseup' || eventType == 'mousedown' ) {
2612 eventObj = document.createEvent('MouseEvents');
2613 eventObj.initMouseEvent(
2614 eventType, true, true, window, srcEvent.detail,
2615 eventPointer.x, eventPointer.y, eventPointer.x, eventPointer.y,
2616 srcEvent.ctrlKey, srcEvent.altKey, srcEvent.shiftKey, srcEvent.metaKey,
2617 srcEvent.button, srcEvent.relatedTarget || null
2618 );
2619
2620 } else {
2621 eventObj = document.createEvent('CustomEvent');
2622 eventObj.initCustomEvent(eventType, true, true, {});
2623 }
2624 eventObj.$material = true;
2625 eventObj.pointer = eventPointer;
2626 eventObj.srcEvent = srcEvent;
2627 eventPointer.target.dispatchEvent(eventObj);
2628 }
2629
2630}
2631
2632/**
2633 * Attach Gestures: hook document and check shouldHijack clicks
2634 * @ngInject
2635 */
2636function attachToDocument( $mdGesture, $$MdGestureHandler ) {
2637
2638 // Polyfill document.contains for IE11.
2639 // TODO: move to util
2640 document.contains || (document.contains = function (node) {
2641 return document.body.contains(node);
2642 });
2643
2644 if (!isInitialized && $mdGesture.isHijackingClicks ) {
2645 /*
2646 * If hijack clicks is true, we preventDefault any click that wasn't
2647 * sent by ngMaterial. This is because on older Android & iOS, a false, or 'ghost',
2648 * click event will be sent ~400ms after a touchend event happens.
2649 * The only way to know if this click is real is to prevent any normal
2650 * click events, and add a flag to events sent by material so we know not to prevent those.
2651 *
2652 * Two exceptions to click events that should be prevented are:
2653 * - click events sent by the keyboard (eg form submit)
2654 * - events that originate from an Ionic app
2655 */
2656 document.addEventListener('click' , clickHijacker , true);
2657 document.addEventListener('mouseup' , mouseInputHijacker, true);
2658 document.addEventListener('mousedown', mouseInputHijacker, true);
2659 document.addEventListener('focus' , mouseInputHijacker, true);
2660
2661 isInitialized = true;
2662 }
2663
2664 function mouseInputHijacker(ev) {
2665 var isKeyClick = !ev.clientX && !ev.clientY;
2666 if (!isKeyClick && !ev.$material && !ev.isIonicTap
2667 && !isInputEventFromLabelClick(ev)) {
2668 ev.preventDefault();
2669 ev.stopPropagation();
2670 }
2671 }
2672
2673 function clickHijacker(ev) {
2674 var isKeyClick = ev.clientX === 0 && ev.clientY === 0;
2675 if (!isKeyClick && !ev.$material && !ev.isIonicTap
2676 && !isInputEventFromLabelClick(ev)) {
2677 ev.preventDefault();
2678 ev.stopPropagation();
2679 lastLabelClickPos = null;
2680 } else {
2681 lastLabelClickPos = null;
2682 if (ev.target.tagName.toLowerCase() == 'label') {
2683 lastLabelClickPos = {x: ev.x, y: ev.y};
2684 }
2685 }
2686 }
2687
2688
2689 // Listen to all events to cover all platforms.
2690 var START_EVENTS = 'mousedown touchstart pointerdown';
2691 var MOVE_EVENTS = 'mousemove touchmove pointermove';
2692 var END_EVENTS = 'mouseup mouseleave touchend touchcancel pointerup pointercancel';
2693
2694 angular.element(document)
2695 .on(START_EVENTS, gestureStart)
2696 .on(MOVE_EVENTS, gestureMove)
2697 .on(END_EVENTS, gestureEnd)
2698 // For testing
2699 .on('$$mdGestureReset', function gestureClearCache () {
2700 lastPointer = pointer = null;
2701 });
2702
2703 /*
2704 * When a DOM event happens, run all registered gesture handlers' lifecycle
2705 * methods which match the DOM event.
2706 * Eg when a 'touchstart' event happens, runHandlers('start') will call and
2707 * run `handler.cancel()` and `handler.start()` on all registered handlers.
2708 */
2709 function runHandlers(handlerEvent, event) {
2710 var handler;
2711 for (var name in HANDLERS) {
2712 handler = HANDLERS[name];
2713 if( handler instanceof $$MdGestureHandler ) {
2714
2715 if (handlerEvent === 'start') {
2716 // Run cancel to reset any handlers' state
2717 handler.cancel();
2718 }
2719 handler[handlerEvent](event, pointer);
2720
2721 }
2722 }
2723 }
2724
2725 /*
2726 * gestureStart vets if a start event is legitimate (and not part of a 'ghost click' from iOS/Android)
2727 * If it is legitimate, we initiate the pointer state and mark the current pointer's type
2728 * For example, for a touchstart event, mark the current pointer as a 'touch' pointer, so mouse events
2729 * won't effect it.
2730 */
2731 function gestureStart(ev) {
2732 // If we're already touched down, abort
2733 if (pointer) return;
2734
2735 var now = +Date.now();
2736
2737 // iOS & old android bug: after a touch event, a click event is sent 350 ms later.
2738 // If <400ms have passed, don't allow an event of a different type than the previous event
2739 if (lastPointer && !typesMatch(ev, lastPointer) && (now - lastPointer.endTime < 1500)) {
2740 return;
2741 }
2742
2743 pointer = makeStartPointer(ev);
2744
2745 runHandlers('start', ev);
2746 }
2747 /*
2748 * If a move event happens of the right type, update the pointer and run all the move handlers.
2749 * "of the right type": if a mousemove happens but our pointer started with a touch event, do nothing.
2750 */
2751 function gestureMove(ev) {
2752 if (!pointer || !typesMatch(ev, pointer)) return;
2753
2754 updatePointerState(ev, pointer);
2755 runHandlers('move', ev);
2756 }
2757 /*
2758 * If an end event happens of the right type, update the pointer, run endHandlers, and save the pointer as 'lastPointer'
2759 */
2760 function gestureEnd(ev) {
2761 if (!pointer || !typesMatch(ev, pointer)) return;
2762
2763 updatePointerState(ev, pointer);
2764 pointer.endTime = +Date.now();
2765
2766 runHandlers('end', ev);
2767
2768 lastPointer = pointer;
2769 pointer = null;
2770 }
2771
2772}
2773
2774// ********************
2775// Module Functions
2776// ********************
2777
2778/*
2779 * Initiate the pointer. x, y, and the pointer's type.
2780 */
2781function makeStartPointer(ev) {
2782 var point = getEventPoint(ev);
2783 var startPointer = {
2784 startTime: +Date.now(),
2785 target: ev.target,
2786 // 'p' for pointer events, 'm' for mouse, 't' for touch
2787 type: ev.type.charAt(0)
2788 };
2789 startPointer.startX = startPointer.x = point.pageX;
2790 startPointer.startY = startPointer.y = point.pageY;
2791 return startPointer;
2792}
2793
2794/*
2795 * return whether the pointer's type matches the event's type.
2796 * Eg if a touch event happens but the pointer has a mouse type, return false.
2797 */
2798function typesMatch(ev, pointer) {
2799 return ev && pointer && ev.type.charAt(0) === pointer.type;
2800}
2801
2802/**
2803 * Gets whether the given event is an input event that was caused by clicking on an
2804 * associated label element.
2805 *
2806 * This is necessary because the browser will, upon clicking on a label element, fire an
2807 * *extra* click event on its associated input (if any). mdGesture is able to flag the label
2808 * click as with `$material` correctly, but not the second input click.
2809 *
2810 * In order to determine whether an input event is from a label click, we compare the (x, y) for
2811 * the event to the (x, y) for the most recent label click (which is cleared whenever a non-label
2812 * click occurs). Unfortunately, there are no event properties that tie the input and the label
2813 * together (such as relatedTarget).
2814 *
2815 * @param {MouseEvent} event
2816 * @returns {boolean}
2817 */
2818function isInputEventFromLabelClick(event) {
2819 return lastLabelClickPos
2820 && lastLabelClickPos.x == event.x
2821 && lastLabelClickPos.y == event.y;
2822}
2823
2824/*
2825 * Update the given pointer based upon the given DOMEvent.
2826 * Distance, velocity, direction, duration, etc
2827 */
2828function updatePointerState(ev, pointer) {
2829 var point = getEventPoint(ev);
2830 var x = pointer.x = point.pageX;
2831 var y = pointer.y = point.pageY;
2832
2833 pointer.distanceX = x - pointer.startX;
2834 pointer.distanceY = y - pointer.startY;
2835 pointer.distance = Math.sqrt(
2836 pointer.distanceX * pointer.distanceX + pointer.distanceY * pointer.distanceY
2837 );
2838
2839 pointer.directionX = pointer.distanceX > 0 ? 'right' : pointer.distanceX < 0 ? 'left' : '';
2840 pointer.directionY = pointer.distanceY > 0 ? 'down' : pointer.distanceY < 0 ? 'up' : '';
2841
2842 pointer.duration = +Date.now() - pointer.startTime;
2843 pointer.velocityX = pointer.distanceX / pointer.duration;
2844 pointer.velocityY = pointer.distanceY / pointer.duration;
2845}
2846
2847/*
2848 * Normalize the point where the DOM event happened whether it's touch or mouse.
2849 * @returns point event obj with pageX and pageY on it.
2850 */
2851function getEventPoint(ev) {
2852 ev = ev.originalEvent || ev; // support jQuery events
2853 return (ev.touches && ev.touches[0]) ||
2854 (ev.changedTouches && ev.changedTouches[0]) ||
2855 ev;
2856}
2857
2858})();
2859(function(){
2860"use strict";
2861
2862angular.module('material.core')
2863 .provider('$$interimElement', InterimElementProvider);
2864
2865/*
2866 * @ngdoc service
2867 * @name $$interimElement
2868 * @module material.core
2869 *
2870 * @description
2871 *
2872 * Factory that contructs `$$interimElement.$service` services.
2873 * Used internally in material design for elements that appear on screen temporarily.
2874 * The service provides a promise-like API for interacting with the temporary
2875 * elements.
2876 *
2877 * ```js
2878 * app.service('$mdToast', function($$interimElement) {
2879 * var $mdToast = $$interimElement(toastDefaultOptions);
2880 * return $mdToast;
2881 * });
2882 * ```
2883 * @param {object=} defaultOptions Options used by default for the `show` method on the service.
2884 *
2885 * @returns {$$interimElement.$service}
2886 *
2887 */
2888
2889function InterimElementProvider() {
2890 InterimElementFactory.$inject = ["$document", "$q", "$$q", "$rootScope", "$timeout", "$rootElement", "$animate", "$mdUtil", "$mdCompiler", "$mdTheming", "$injector"];
2891 createInterimElementProvider.$get = InterimElementFactory;
2892 return createInterimElementProvider;
2893
2894 /**
2895 * Returns a new provider which allows configuration of a new interimElement
2896 * service. Allows configuration of default options & methods for options,
2897 * as well as configuration of 'preset' methods (eg dialog.basic(): basic is a preset method)
2898 */
2899 function createInterimElementProvider(interimFactoryName) {
2900 factory.$inject = ["$$interimElement", "$injector"];
2901 var EXPOSED_METHODS = ['onHide', 'onShow', 'onRemove'];
2902
2903 var customMethods = {};
2904 var providerConfig = {
2905 presets: {}
2906 };
2907
2908 var provider = {
2909 setDefaults: setDefaults,
2910 addPreset: addPreset,
2911 addMethod: addMethod,
2912 $get: factory
2913 };
2914
2915 /**
2916 * all interim elements will come with the 'build' preset
2917 */
2918 provider.addPreset('build', {
2919 methods: ['controller', 'controllerAs', 'resolve',
2920 'template', 'templateUrl', 'themable', 'transformTemplate', 'parent']
2921 });
2922
2923 return provider;
2924
2925 /**
2926 * Save the configured defaults to be used when the factory is instantiated
2927 */
2928 function setDefaults(definition) {
2929 providerConfig.optionsFactory = definition.options;
2930 providerConfig.methods = (definition.methods || []).concat(EXPOSED_METHODS);
2931 return provider;
2932 }
2933
2934 /**
2935 * Add a method to the factory that isn't specific to any interim element operations
2936 */
2937
2938 function addMethod(name, fn) {
2939 customMethods[name] = fn;
2940 return provider;
2941 }
2942
2943 /**
2944 * Save the configured preset to be used when the factory is instantiated
2945 */
2946 function addPreset(name, definition) {
2947 definition = definition || {};
2948 definition.methods = definition.methods || [];
2949 definition.options = definition.options || function() { return {}; };
2950
2951 if (/^cancel|hide|show$/.test(name)) {
2952 throw new Error("Preset '" + name + "' in " + interimFactoryName + " is reserved!");
2953 }
2954 if (definition.methods.indexOf('_options') > -1) {
2955 throw new Error("Method '_options' in " + interimFactoryName + " is reserved!");
2956 }
2957 providerConfig.presets[name] = {
2958 methods: definition.methods.concat(EXPOSED_METHODS),
2959 optionsFactory: definition.options,
2960 argOption: definition.argOption
2961 };
2962 return provider;
2963 }
2964
2965 function addPresetMethod(presetName, methodName, method) {
2966 providerConfig.presets[presetName][methodName] = method;
2967 }
2968
2969 /**
2970 * Create a factory that has the given methods & defaults implementing interimElement
2971 */
2972 /* @ngInject */
2973 function factory($$interimElement, $injector) {
2974 var defaultMethods;
2975 var defaultOptions;
2976 var interimElementService = $$interimElement();
2977
2978 /*
2979 * publicService is what the developer will be using.
2980 * It has methods hide(), cancel(), show(), build(), and any other
2981 * presets which were set during the config phase.
2982 */
2983 var publicService = {
2984 hide: interimElementService.hide,
2985 cancel: interimElementService.cancel,
2986 show: showInterimElement,
2987
2988 // Special internal method to destroy an interim element without animations
2989 // used when navigation changes causes a $scope.$destroy() action
2990 destroy : destroyInterimElement
2991 };
2992
2993
2994 defaultMethods = providerConfig.methods || [];
2995 // This must be invoked after the publicService is initialized
2996 defaultOptions = invokeFactory(providerConfig.optionsFactory, {});
2997
2998 // Copy over the simple custom methods
2999 angular.forEach(customMethods, function(fn, name) {
3000 publicService[name] = fn;
3001 });
3002
3003 angular.forEach(providerConfig.presets, function(definition, name) {
3004 var presetDefaults = invokeFactory(definition.optionsFactory, {});
3005 var presetMethods = (definition.methods || []).concat(defaultMethods);
3006
3007 // Every interimElement built with a preset has a field called `$type`,
3008 // which matches the name of the preset.
3009 // Eg in preset 'confirm', options.$type === 'confirm'
3010 angular.extend(presetDefaults, { $type: name });
3011
3012 // This creates a preset class which has setter methods for every
3013 // method given in the `.addPreset()` function, as well as every
3014 // method given in the `.setDefaults()` function.
3015 //
3016 // @example
3017 // .setDefaults({
3018 // methods: ['hasBackdrop', 'clickOutsideToClose', 'escapeToClose', 'targetEvent'],
3019 // options: dialogDefaultOptions
3020 // })
3021 // .addPreset('alert', {
3022 // methods: ['title', 'ok'],
3023 // options: alertDialogOptions
3024 // })
3025 //
3026 // Set values will be passed to the options when interimElement.show() is called.
3027 function Preset(opts) {
3028 this._options = angular.extend({}, presetDefaults, opts);
3029 }
3030 angular.forEach(presetMethods, function(name) {
3031 Preset.prototype[name] = function(value) {
3032 this._options[name] = value;
3033 return this;
3034 };
3035 });
3036
3037 // Create shortcut method for one-linear methods
3038 if (definition.argOption) {
3039 var methodName = 'show' + name.charAt(0).toUpperCase() + name.slice(1);
3040 publicService[methodName] = function(arg) {
3041 var config = publicService[name](arg);
3042 return publicService.show(config);
3043 };
3044 }
3045
3046 // eg $mdDialog.alert() will return a new alert preset
3047 publicService[name] = function(arg) {
3048 // If argOption is supplied, eg `argOption: 'content'`, then we assume
3049 // if the argument is not an options object then it is the `argOption` option.
3050 //
3051 // @example `$mdToast.simple('hello')` // sets options.content to hello
3052 // // because argOption === 'content'
3053 if (arguments.length && definition.argOption &&
3054 !angular.isObject(arg) && !angular.isArray(arg)) {
3055
3056 return (new Preset())[definition.argOption](arg);
3057
3058 } else {
3059 return new Preset(arg);
3060 }
3061
3062 };
3063 });
3064
3065 return publicService;
3066
3067 /**
3068 *
3069 */
3070 function showInterimElement(opts) {
3071 // opts is either a preset which stores its options on an _options field,
3072 // or just an object made up of options
3073 opts = opts || { };
3074 if (opts._options) opts = opts._options;
3075
3076 return interimElementService.show(
3077 angular.extend({}, defaultOptions, opts)
3078 );
3079 }
3080
3081 /**
3082 * Special method to hide and destroy an interimElement WITHOUT
3083 * any 'leave` or hide animations ( an immediate force hide/remove )
3084 *
3085 * NOTE: This calls the onRemove() subclass method for each component...
3086 * which must have code to respond to `options.$destroy == true`
3087 */
3088 function destroyInterimElement(opts) {
3089 return interimElementService.destroy(opts);
3090 }
3091
3092 /**
3093 * Helper to call $injector.invoke with a local of the factory name for
3094 * this provider.
3095 * If an $mdDialog is providing options for a dialog and tries to inject
3096 * $mdDialog, a circular dependency error will happen.
3097 * We get around that by manually injecting $mdDialog as a local.
3098 */
3099 function invokeFactory(factory, defaultVal) {
3100 var locals = {};
3101 locals[interimFactoryName] = publicService;
3102 return $injector.invoke(factory || function() { return defaultVal; }, {}, locals);
3103 }
3104
3105 }
3106
3107 }
3108
3109 /* @ngInject */
3110 function InterimElementFactory($document, $q, $$q, $rootScope, $timeout, $rootElement, $animate,
3111 $mdUtil, $mdCompiler, $mdTheming, $injector ) {
3112 return function createInterimElementService() {
3113 var SHOW_CANCELLED = false;
3114
3115 /*
3116 * @ngdoc service
3117 * @name $$interimElement.$service
3118 *
3119 * @description
3120 * A service used to control inserting and removing an element into the DOM.
3121 *
3122 */
3123 var service, stack = [];
3124
3125 // Publish instance $$interimElement service;
3126 // ... used as $mdDialog, $mdToast, $mdMenu, and $mdSelect
3127
3128 return service = {
3129 show: show,
3130 hide: hide,
3131 cancel: cancel,
3132 destroy : destroy,
3133 $injector_: $injector
3134 };
3135
3136 /*
3137 * @ngdoc method
3138 * @name $$interimElement.$service#show
3139 * @kind function
3140 *
3141 * @description
3142 * Adds the `$interimElement` to the DOM and returns a special promise that will be resolved or rejected
3143 * with hide or cancel, respectively. To external cancel/hide, developers should use the
3144 *
3145 * @param {*} options is hashMap of settings
3146 * @returns a Promise
3147 *
3148 */
3149 function show(options) {
3150 options = options || {};
3151 var interimElement = new InterimElement(options || {});
3152 // When an interim element is currently showing, we have to cancel it.
3153 // Just hiding it, will resolve the InterimElement's promise, the promise should be
3154 // rejected instead.
3155 var hideExisting = !options.skipHide && stack.length ? service.cancel() : $q.when(true);
3156
3157 // This hide()s only the current interim element before showing the next, new one
3158 // NOTE: this is not reversible (e.g. interim elements are not stackable)
3159
3160 hideExisting.finally(function() {
3161
3162 stack.push(interimElement);
3163 interimElement
3164 .show()
3165 .catch(function( reason ) {
3166 //$log.error("InterimElement.show() error: " + reason );
3167 return reason;
3168 });
3169
3170 });
3171
3172 // Return a promise that will be resolved when the interim
3173 // element is hidden or cancelled...
3174
3175 return interimElement.deferred.promise;
3176 }
3177
3178 /*
3179 * @ngdoc method
3180 * @name $$interimElement.$service#hide
3181 * @kind function
3182 *
3183 * @description
3184 * Removes the `$interimElement` from the DOM and resolves the promise returned from `show`
3185 *
3186 * @param {*} resolveParam Data to resolve the promise with
3187 * @returns a Promise that will be resolved after the element has been removed.
3188 *
3189 */
3190 function hide(reason, options) {
3191 if ( !stack.length ) return $q.when(reason);
3192 options = options || {};
3193
3194 if (options.closeAll) {
3195 var promise = $q.all(stack.reverse().map(closeElement));
3196 stack = [];
3197 return promise;
3198 } else if (options.closeTo !== undefined) {
3199 return $q.all(stack.splice(options.closeTo).map(closeElement));
3200 } else {
3201 var interim = stack.pop();
3202 return closeElement(interim);
3203 }
3204
3205 function closeElement(interim) {
3206 interim
3207 .remove(reason, false, options || { })
3208 .catch(function( reason ) {
3209 //$log.error("InterimElement.hide() error: " + reason );
3210 return reason;
3211 });
3212 return interim.deferred.promise;
3213 }
3214 }
3215
3216 /*
3217 * @ngdoc method
3218 * @name $$interimElement.$service#cancel
3219 * @kind function
3220 *
3221 * @description
3222 * Removes the `$interimElement` from the DOM and rejects the promise returned from `show`
3223 *
3224 * @param {*} reason Data to reject the promise with
3225 * @returns Promise that will be resolved after the element has been removed.
3226 *
3227 */
3228 function cancel(reason, options) {
3229 var interim = stack.pop();
3230 if ( !interim ) return $q.when(reason);
3231
3232 interim
3233 .remove(reason, true, options || { })
3234 .catch(function( reason ) {
3235 //$log.error("InterimElement.cancel() error: " + reason );
3236 return reason;
3237 });
3238
3239 // Since Angular 1.6.7, promises will be logged to $exceptionHandler when the promise
3240 // is not handling the rejection. We create a pseudo catch handler, which will prevent the
3241 // promise from being logged to the $exceptionHandler.
3242 return interim.deferred.promise.catch(angular.noop);
3243 }
3244
3245 /*
3246 * Special method to quick-remove the interim element without animations
3247 * Note: interim elements are in "interim containers"
3248 */
3249 function destroy(target) {
3250 var interim = !target ? stack.shift() : null;
3251 var cntr = angular.element(target).length ? angular.element(target)[0].parentNode : null;
3252
3253 if (cntr) {
3254 // Try to find the interim element in the stack which corresponds to the supplied DOM element.
3255 var filtered = stack.filter(function(entry) {
3256 var currNode = entry.options.element[0];
3257 return (currNode === cntr);
3258 });
3259
3260 // Note: this function might be called when the element already has been removed, in which
3261 // case we won't find any matches. That's ok.
3262 if (filtered.length > 0) {
3263 interim = filtered[0];
3264 stack.splice(stack.indexOf(interim), 1);
3265 }
3266 }
3267
3268 return interim ? interim.remove(SHOW_CANCELLED, false, {'$destroy':true}) : $q.when(SHOW_CANCELLED);
3269 }
3270
3271 /*
3272 * Internal Interim Element Object
3273 * Used internally to manage the DOM element and related data
3274 */
3275 function InterimElement(options) {
3276 var self, element, showAction = $q.when(true);
3277
3278 options = configureScopeAndTransitions(options);
3279
3280 return self = {
3281 options : options,
3282 deferred: $q.defer(),
3283 show : createAndTransitionIn,
3284 remove : transitionOutAndRemove
3285 };
3286
3287 /**
3288 * Compile, link, and show this interim element
3289 * Use optional autoHided and transition-in effects
3290 */
3291 function createAndTransitionIn() {
3292 return $q(function(resolve, reject) {
3293
3294 // Trigger onCompiling callback before the compilation starts.
3295 // This is useful, when modifying options, which can be influenced by developers.
3296 options.onCompiling && options.onCompiling(options);
3297
3298 compileElement(options)
3299 .then(function( compiledData ) {
3300 element = linkElement( compiledData, options );
3301
3302 showAction = showElement(element, options, compiledData.controller)
3303 .then(resolve, rejectAll);
3304
3305 }, rejectAll);
3306
3307 function rejectAll(fault) {
3308 // Force the '$md<xxx>.show()' promise to reject
3309 self.deferred.reject(fault);
3310
3311 // Continue rejection propagation
3312 reject(fault);
3313 }
3314 });
3315 }
3316
3317 /**
3318 * After the show process has finished/rejected:
3319 * - announce 'removing',
3320 * - perform the transition-out, and
3321 * - perform optional clean up scope.
3322 */
3323 function transitionOutAndRemove(response, isCancelled, opts) {
3324
3325 // abort if the show() and compile failed
3326 if ( !element ) return $q.when(false);
3327
3328 options = angular.extend(options || {}, opts || {});
3329 options.cancelAutoHide && options.cancelAutoHide();
3330 options.element.triggerHandler('$mdInterimElementRemove');
3331
3332 if ( options.$destroy === true ) {
3333
3334 return hideElement(options.element, options).then(function(){
3335 (isCancelled && rejectAll(response)) || resolveAll(response);
3336 });
3337
3338 } else {
3339
3340 $q.when(showAction)
3341 .finally(function() {
3342 hideElement(options.element, options).then(function() {
3343
3344 (isCancelled && rejectAll(response)) || resolveAll(response);
3345
3346 }, rejectAll);
3347 });
3348
3349 return self.deferred.promise;
3350 }
3351
3352
3353 /**
3354 * The `show()` returns a promise that will be resolved when the interim
3355 * element is hidden or cancelled...
3356 */
3357 function resolveAll(response) {
3358 self.deferred.resolve(response);
3359 }
3360
3361 /**
3362 * Force the '$md<xxx>.show()' promise to reject
3363 */
3364 function rejectAll(fault) {
3365 self.deferred.reject(fault);
3366 }
3367 }
3368
3369 /**
3370 * Prepare optional isolated scope and prepare $animate with default enter and leave
3371 * transitions for the new element instance.
3372 */
3373 function configureScopeAndTransitions(options) {
3374 options = options || { };
3375 if ( options.template ) {
3376 options.template = $mdUtil.processTemplate(options.template);
3377 }
3378
3379 return angular.extend({
3380 preserveScope: false,
3381 cancelAutoHide : angular.noop,
3382 scope: options.scope || $rootScope.$new(options.isolateScope),
3383
3384 /**
3385 * Default usage to enable $animate to transition-in; can be easily overridden via 'options'
3386 */
3387 onShow: function transitionIn(scope, element, options) {
3388 return $animate.enter(element, options.parent);
3389 },
3390
3391 /**
3392 * Default usage to enable $animate to transition-out; can be easily overridden via 'options'
3393 */
3394 onRemove: function transitionOut(scope, element) {
3395 // Element could be undefined if a new element is shown before
3396 // the old one finishes compiling.
3397 return element && $animate.leave(element) || $q.when();
3398 }
3399 }, options );
3400
3401 }
3402
3403 /**
3404 * Compile an element with a templateUrl, controller, and locals
3405 */
3406 function compileElement(options) {
3407
3408 var compiled = !options.skipCompile ? $mdCompiler.compile(options) : null;
3409
3410 return compiled || $q(function (resolve) {
3411 resolve({
3412 locals: {},
3413 link: function () {
3414 return options.element;
3415 }
3416 });
3417 });
3418 }
3419
3420 /**
3421 * Link an element with compiled configuration
3422 */
3423 function linkElement(compileData, options){
3424 angular.extend(compileData.locals, options);
3425
3426 var element = compileData.link(options.scope);
3427
3428 // Search for parent at insertion time, if not specified
3429 options.element = element;
3430 options.parent = findParent(element, options);
3431 if (options.themable) $mdTheming(element);
3432
3433 return element;
3434 }
3435
3436 /**
3437 * Search for parent at insertion time, if not specified
3438 */
3439 function findParent(element, options) {
3440 var parent = options.parent;
3441
3442 // Search for parent at insertion time, if not specified
3443 if (angular.isFunction(parent)) {
3444 parent = parent(options.scope, element, options);
3445 } else if (angular.isString(parent)) {
3446 parent = angular.element($document[0].querySelector(parent));
3447 } else {
3448 parent = angular.element(parent);
3449 }
3450
3451 // If parent querySelector/getter function fails, or it's just null,
3452 // find a default.
3453 if (!(parent || {}).length) {
3454 var el;
3455 if ($rootElement[0] && $rootElement[0].querySelector) {
3456 el = $rootElement[0].querySelector(':not(svg) > body');
3457 }
3458 if (!el) el = $rootElement[0];
3459 if (el.nodeName == '#comment') {
3460 el = $document[0].body;
3461 }
3462 return angular.element(el);
3463 }
3464
3465 return parent;
3466 }
3467
3468 /**
3469 * If auto-hide is enabled, start timer and prepare cancel function
3470 */
3471 function startAutoHide() {
3472 var autoHideTimer, cancelAutoHide = angular.noop;
3473
3474 if (options.hideDelay) {
3475 autoHideTimer = $timeout(service.hide, options.hideDelay) ;
3476 cancelAutoHide = function() {
3477 $timeout.cancel(autoHideTimer);
3478 }
3479 }
3480
3481 // Cache for subsequent use
3482 options.cancelAutoHide = function() {
3483 cancelAutoHide();
3484 options.cancelAutoHide = undefined;
3485 }
3486 }
3487
3488 /**
3489 * Show the element ( with transitions), notify complete and start
3490 * optional auto-Hide
3491 */
3492 function showElement(element, options, controller) {
3493 // Trigger onShowing callback before the `show()` starts
3494 var notifyShowing = options.onShowing || angular.noop;
3495 // Trigger onComplete callback when the `show()` finishes
3496 var notifyComplete = options.onComplete || angular.noop;
3497
3498 notifyShowing(options.scope, element, options, controller);
3499
3500 return $q(function (resolve, reject) {
3501 try {
3502 // Start transitionIn
3503 $q.when(options.onShow(options.scope, element, options, controller))
3504 .then(function () {
3505 notifyComplete(options.scope, element, options);
3506 startAutoHide();
3507
3508 resolve(element);
3509
3510 }, reject );
3511
3512 } catch(e) {
3513 reject(e.message);
3514 }
3515 });
3516 }
3517
3518 function hideElement(element, options) {
3519 var announceRemoving = options.onRemoving || angular.noop;
3520
3521 return $$q(function (resolve, reject) {
3522 try {
3523 // Start transitionIn
3524 var action = $$q.when( options.onRemove(options.scope, element, options) || true );
3525
3526 // Trigger callback *before* the remove operation starts
3527 announceRemoving(element, action);
3528
3529 if ( options.$destroy == true ) {
3530
3531 // For $destroy, onRemove should be synchronous
3532 resolve(element);
3533
3534 } else {
3535
3536 // Wait until transition-out is done
3537 action.then(function () {
3538
3539 if (!options.preserveScope && options.scope ) {
3540 options.scope.$destroy();
3541 }
3542
3543 resolve(element);
3544
3545 }, reject );
3546 }
3547
3548 } catch(e) {
3549 reject(e);
3550 }
3551 });
3552 }
3553
3554 }
3555 };
3556
3557 }
3558
3559}
3560
3561})();
3562(function(){
3563"use strict";
3564
3565(function() {
3566 'use strict';
3567
3568 var $mdUtil, $interpolate, $log;
3569
3570 var SUFFIXES = /(-gt)?-(sm|md|lg|print)/g;
3571 var WHITESPACE = /\s+/g;
3572
3573 var FLEX_OPTIONS = ['grow', 'initial', 'auto', 'none', 'noshrink', 'nogrow' ];
3574 var LAYOUT_OPTIONS = ['row', 'column'];
3575 var ALIGNMENT_MAIN_AXIS= [ "", "start", "center", "end", "stretch", "space-around", "space-between" ];
3576 var ALIGNMENT_CROSS_AXIS= [ "", "start", "center", "end", "stretch" ];
3577
3578 var config = {
3579 /**
3580 * Enable directive attribute-to-class conversions
3581 * Developers can use `<body md-layout-css />` to quickly
3582 * disable the Layout directives and prohibit the injection of Layout classNames
3583 */
3584 enabled: true,
3585
3586 /**
3587 * List of mediaQuery breakpoints and associated suffixes
3588 *
3589 * [
3590 * { suffix: "sm", mediaQuery: "screen and (max-width: 599px)" },
3591 * { suffix: "md", mediaQuery: "screen and (min-width: 600px) and (max-width: 959px)" }
3592 * ]
3593 */
3594 breakpoints: []
3595 };
3596
3597 registerLayoutAPI( angular.module('material.core.layout', ['ng']) );
3598
3599 /**
3600 * registerLayoutAPI()
3601 *
3602 * The original ngMaterial Layout solution used attribute selectors and CSS.
3603 *
3604 * ```html
3605 * <div layout="column"> My Content </div>
3606 * ```
3607 *
3608 * ```css
3609 * [layout] {
3610 * box-sizing: border-box;
3611 * display:flex;
3612 * }
3613 * [layout=column] {
3614 * flex-direction : column
3615 * }
3616 * ```
3617 *
3618 * Use of attribute selectors creates significant performance impacts in some
3619 * browsers... mainly IE.
3620 *
3621 * This module registers directives that allow the same layout attributes to be
3622 * interpreted and converted to class selectors. The directive will add equivalent classes to each element that
3623 * contains a Layout directive.
3624 *
3625 * ```html
3626 * <div layout="column" class="layout layout-column"> My Content </div>
3627 *```
3628 *
3629 * ```css
3630 * .layout {
3631 * box-sizing: border-box;
3632 * display:flex;
3633 * }
3634 * .layout-column {
3635 * flex-direction : column
3636 * }
3637 * ```
3638 */
3639 function registerLayoutAPI(module){
3640 var PREFIX_REGEXP = /^((?:x|data)[\:\-_])/i;
3641 var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g;
3642
3643 // NOTE: these are also defined in constants::MEDIA_PRIORITY and constants::MEDIA
3644 var BREAKPOINTS = [ "", "xs", "gt-xs", "sm", "gt-sm", "md", "gt-md", "lg", "gt-lg", "xl", "print" ];
3645 var API_WITH_VALUES = [ "layout", "flex", "flex-order", "flex-offset", "layout-align" ];
3646 var API_NO_VALUES = [ "show", "hide", "layout-padding", "layout-margin" ];
3647
3648
3649 // Build directive registration functions for the standard Layout API... for all breakpoints.
3650 angular.forEach(BREAKPOINTS, function(mqb) {
3651
3652 // Attribute directives with expected, observable value(s)
3653 angular.forEach( API_WITH_VALUES, function(name){
3654 var fullName = mqb ? name + "-" + mqb : name;
3655 module.directive( directiveNormalize(fullName), attributeWithObserve(fullName));
3656 });
3657
3658 // Attribute directives with no expected value(s)
3659 angular.forEach( API_NO_VALUES, function(name){
3660 var fullName = mqb ? name + "-" + mqb : name;
3661 module.directive( directiveNormalize(fullName), attributeWithoutValue(fullName));
3662 });
3663
3664 });
3665
3666 // Register other, special directive functions for the Layout features:
3667 module
3668
3669 .provider('$$mdLayout' , function() {
3670 // Publish internal service for Layouts
3671 return {
3672 $get : angular.noop,
3673 validateAttributeValue : validateAttributeValue,
3674 validateAttributeUsage : validateAttributeUsage,
3675 /**
3676 * Easy way to disable/enable the Layout API.
3677 * When disabled, this stops all attribute-to-classname generations
3678 */
3679 disableLayouts : function(isDisabled) {
3680 config.enabled = (isDisabled !== true);
3681 }
3682 };
3683 })
3684
3685 .directive('mdLayoutCss' , disableLayoutDirective )
3686 .directive('ngCloak' , buildCloakInterceptor('ng-cloak'))
3687
3688 .directive('layoutWrap' , attributeWithoutValue('layout-wrap'))
3689 .directive('layoutNowrap' , attributeWithoutValue('layout-nowrap'))
3690 .directive('layoutNoWrap' , attributeWithoutValue('layout-no-wrap'))
3691 .directive('layoutFill' , attributeWithoutValue('layout-fill'))
3692
3693 // !! Deprecated attributes: use the `-lt` (aka less-than) notations
3694
3695 .directive('layoutLtMd' , warnAttrNotSupported('layout-lt-md', true))
3696 .directive('layoutLtLg' , warnAttrNotSupported('layout-lt-lg', true))
3697 .directive('flexLtMd' , warnAttrNotSupported('flex-lt-md', true))
3698 .directive('flexLtLg' , warnAttrNotSupported('flex-lt-lg', true))
3699
3700 .directive('layoutAlignLtMd', warnAttrNotSupported('layout-align-lt-md'))
3701 .directive('layoutAlignLtLg', warnAttrNotSupported('layout-align-lt-lg'))
3702 .directive('flexOrderLtMd' , warnAttrNotSupported('flex-order-lt-md'))
3703 .directive('flexOrderLtLg' , warnAttrNotSupported('flex-order-lt-lg'))
3704 .directive('offsetLtMd' , warnAttrNotSupported('flex-offset-lt-md'))
3705 .directive('offsetLtLg' , warnAttrNotSupported('flex-offset-lt-lg'))
3706
3707 .directive('hideLtMd' , warnAttrNotSupported('hide-lt-md'))
3708 .directive('hideLtLg' , warnAttrNotSupported('hide-lt-lg'))
3709 .directive('showLtMd' , warnAttrNotSupported('show-lt-md'))
3710 .directive('showLtLg' , warnAttrNotSupported('show-lt-lg'))
3711
3712 // Determine if
3713 .config( detectDisabledLayouts );
3714
3715 /**
3716 * Converts snake_case to camelCase.
3717 * Also there is special case for Moz prefix starting with upper case letter.
3718 * @param name Name to normalize
3719 */
3720 function directiveNormalize(name) {
3721 return name
3722 .replace(PREFIX_REGEXP, '')
3723 .replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) {
3724 return offset ? letter.toUpperCase() : letter;
3725 });
3726 }
3727
3728 }
3729
3730
3731 /**
3732 * Detect if any of the HTML tags has a [md-layouts-disabled] attribute;
3733 * If yes, then immediately disable all layout API features
3734 *
3735 * Note: this attribute should be specified on either the HTML or BODY tags
3736 */
3737 /**
3738 * @ngInject
3739 */
3740 function detectDisabledLayouts() {
3741 var isDisabled = !!document.querySelector('[md-layouts-disabled]');
3742 config.enabled = !isDisabled;
3743 }
3744
3745 /**
3746 * Special directive that will disable ALL Layout conversions of layout
3747 * attribute(s) to classname(s).
3748 *
3749 * <link rel="stylesheet" href="angular-material.min.css">
3750 * <link rel="stylesheet" href="angular-material.layout.css">
3751 *
3752 * <body md-layout-css>
3753 * ...
3754 * </body>
3755 *
3756 * Note: Using md-layout-css directive requires the developer to load the Material
3757 * Layout Attribute stylesheet (which only uses attribute selectors):
3758 *
3759 * `angular-material.layout.css`
3760 *
3761 * Another option is to use the LayoutProvider to configure and disable the attribute
3762 * conversions; this would obviate the use of the `md-layout-css` directive
3763 *
3764 */
3765 function disableLayoutDirective() {
3766 // Return a 1x-only, first-match attribute directive
3767 config.enabled = false;
3768
3769 return {
3770 restrict : 'A',
3771 priority : '900'
3772 };
3773 }
3774
3775 /**
3776 * Tail-hook ngCloak to delay the uncloaking while Layout transformers
3777 * finish processing. Eliminates flicker with Material.Layouts
3778 */
3779 function buildCloakInterceptor(className) {
3780 return [ '$timeout', function($timeout){
3781 return {
3782 restrict : 'A',
3783 priority : -10, // run after normal ng-cloak
3784 compile : function( element ) {
3785 if (!config.enabled) return angular.noop;
3786
3787 // Re-add the cloak
3788 element.addClass(className);
3789
3790 return function( scope, element ) {
3791 // Wait while layout injectors configure, then uncloak
3792 // NOTE: $rAF does not delay enough... and this is a 1x-only event,
3793 // $timeout is acceptable.
3794 $timeout( function(){
3795 element.removeClass(className);
3796 }, 10, false);
3797 };
3798 }
3799 };
3800 }];
3801 }
3802
3803
3804 // *********************************************************************************
3805 //
3806 // These functions create registration functions for ngMaterial Layout attribute directives
3807 // This provides easy translation to switch ngMaterial attribute selectors to
3808 // CLASS selectors and directives; which has huge performance implications
3809 // for IE Browsers
3810 //
3811 // *********************************************************************************
3812
3813 /**
3814 * Creates a directive registration function where a possible dynamic attribute
3815 * value will be observed/watched.
3816 * @param {string} className attribute name; eg `layout-gt-md` with value ="row"
3817 */
3818 function attributeWithObserve(className) {
3819
3820 return ['$mdUtil', '$interpolate', "$log", function(_$mdUtil_, _$interpolate_, _$log_) {
3821 $mdUtil = _$mdUtil_;
3822 $interpolate = _$interpolate_;
3823 $log = _$log_;
3824
3825 return {
3826 restrict: 'A',
3827 compile: function(element, attr) {
3828 var linkFn;
3829 if (config.enabled) {
3830 // immediately replace static (non-interpolated) invalid values...
3831
3832 validateAttributeUsage(className, attr, element, $log);
3833
3834 validateAttributeValue( className,
3835 getNormalizedAttrValue(className, attr, ""),
3836 buildUpdateFn(element, className, attr)
3837 );
3838
3839 linkFn = translateWithValueToCssClass;
3840 }
3841
3842 // Use for postLink to account for transforms after ng-transclude.
3843 return linkFn || angular.noop;
3844 }
3845 };
3846 }];
3847
3848 /**
3849 * Add as transformed class selector(s), then
3850 * remove the deprecated attribute selector
3851 */
3852 function translateWithValueToCssClass(scope, element, attrs) {
3853 var updateFn = updateClassWithValue(element, className, attrs);
3854 var unwatch = attrs.$observe(attrs.$normalize(className), updateFn);
3855
3856 updateFn(getNormalizedAttrValue(className, attrs, ""));
3857 scope.$on("$destroy", function() { unwatch(); });
3858 }
3859 }
3860
3861 /**
3862 * Creates a registration function for ngMaterial Layout attribute directive.
3863 * This is a `simple` transpose of attribute usage to class usage; where we ignore
3864 * any attribute value
3865 */
3866 function attributeWithoutValue(className) {
3867 return ['$mdUtil', '$interpolate', "$log", function(_$mdUtil_, _$interpolate_, _$log_) {
3868 $mdUtil = _$mdUtil_;
3869 $interpolate = _$interpolate_;
3870 $log = _$log_;
3871
3872 return {
3873 restrict: 'A',
3874 compile: function(element, attr) {
3875 var linkFn;
3876 if (config.enabled) {
3877 // immediately replace static (non-interpolated) invalid values...
3878
3879 validateAttributeValue( className,
3880 getNormalizedAttrValue(className, attr, ""),
3881 buildUpdateFn(element, className, attr)
3882 );
3883
3884 translateToCssClass(null, element);
3885
3886 // Use for postLink to account for transforms after ng-transclude.
3887 linkFn = translateToCssClass;
3888 }
3889
3890 return linkFn || angular.noop;
3891 }
3892 };
3893 }];
3894
3895 /**
3896 * Add as transformed class selector, then
3897 * remove the deprecated attribute selector
3898 */
3899 function translateToCssClass(scope, element) {
3900 element.addClass(className);
3901 }
3902 }
3903
3904
3905
3906 /**
3907 * After link-phase, do NOT remove deprecated layout attribute selector.
3908 * Instead watch the attribute so interpolated data-bindings to layout
3909 * selectors will continue to be supported.
3910 *
3911 * $observe() the className and update with new class (after removing the last one)
3912 *
3913 * e.g. `layout="{{layoutDemo.direction}}"` will update...
3914 *
3915 * NOTE: The value must match one of the specified styles in the CSS.
3916 * For example `flex-gt-md="{{size}}` where `scope.size == 47` will NOT work since
3917 * only breakpoints for 0, 5, 10, 15... 100, 33, 34, 66, 67 are defined.
3918 *
3919 */
3920 function updateClassWithValue(element, className) {
3921 var lastClass;
3922
3923 return function updateClassFn(newValue) {
3924 var value = validateAttributeValue(className, newValue || "");
3925 if ( angular.isDefined(value) ) {
3926 if (lastClass) element.removeClass(lastClass);
3927 lastClass = !value ? className : className + "-" + value.replace(WHITESPACE, "-");
3928 element.addClass(lastClass);
3929 }
3930 };
3931 }
3932
3933 /**
3934 * Provide console warning that this layout attribute has been deprecated
3935 *
3936 */
3937 function warnAttrNotSupported(className) {
3938 var parts = className.split("-");
3939 return ["$log", function($log) {
3940 $log.warn(className + "has been deprecated. Please use a `" + parts[0] + "-gt-<xxx>` variant.");
3941 return angular.noop;
3942 }];
3943 }
3944
3945 /**
3946 * Centralize warnings for known flexbox issues (especially IE-related issues)
3947 */
3948 function validateAttributeUsage(className, attr, element, $log){
3949 var message, usage, url;
3950 var nodeName = element[0].nodeName.toLowerCase();
3951
3952 switch(className.replace(SUFFIXES,"")) {
3953 case "flex":
3954 if ((nodeName == "md-button") || (nodeName == "fieldset")){
3955 // @see https://github.com/philipwalton/flexbugs#9-some-html-elements-cant-be-flex-containers
3956 // Use <div flex> wrapper inside (preferred) or outside
3957
3958 usage = "<" + nodeName + " " + className + "></" + nodeName + ">";
3959 url = "https://github.com/philipwalton/flexbugs#9-some-html-elements-cant-be-flex-containers";
3960 message = "Markup '{0}' may not work as expected in IE Browsers. Consult '{1}' for details.";
3961
3962 $log.warn( $mdUtil.supplant(message, [usage, url]) );
3963 }
3964 }
3965
3966 }
3967
3968
3969 /**
3970 * For the Layout attribute value, validate or replace with default
3971 * fallback value
3972 */
3973 function validateAttributeValue(className, value, updateFn) {
3974 var origValue = value;
3975
3976 if (!needsInterpolation(value)) {
3977 switch (className.replace(SUFFIXES,"")) {
3978 case 'layout' :
3979 if ( !findIn(value, LAYOUT_OPTIONS) ) {
3980 value = LAYOUT_OPTIONS[0]; // 'row';
3981 }
3982 break;
3983
3984 case 'flex' :
3985 if (!findIn(value, FLEX_OPTIONS)) {
3986 if (isNaN(value)) {
3987 value = '';
3988 }
3989 }
3990 break;
3991
3992 case 'flex-offset' :
3993 case 'flex-order' :
3994 if (!value || isNaN(+value)) {
3995 value = '0';
3996 }
3997 break;
3998
3999 case 'layout-align' :
4000 var axis = extractAlignAxis(value);
4001 value = $mdUtil.supplant("{main}-{cross}",axis);
4002 break;
4003
4004 case 'layout-padding' :
4005 case 'layout-margin' :
4006 case 'layout-fill' :
4007 case 'layout-wrap' :
4008 case 'layout-nowrap' :
4009 case 'layout-nowrap' :
4010 value = '';
4011 break;
4012 }
4013
4014 if (value != origValue) {
4015 (updateFn || angular.noop)(value);
4016 }
4017 }
4018
4019 return value;
4020 }
4021
4022 /**
4023 * Replace current attribute value with fallback value
4024 */
4025 function buildUpdateFn(element, className, attrs) {
4026 return function updateAttrValue(fallback) {
4027 if (!needsInterpolation(fallback)) {
4028 // Do not modify the element's attribute value; so
4029 // uses '<ui-layout layout="/api/sidebar.html" />' will not
4030 // be affected. Just update the attrs value.
4031 attrs[attrs.$normalize(className)] = fallback;
4032 }
4033 };
4034 }
4035
4036 /**
4037 * See if the original value has interpolation symbols:
4038 * e.g. flex-gt-md="{{triggerPoint}}"
4039 */
4040 function needsInterpolation(value) {
4041 return (value || "").indexOf($interpolate.startSymbol()) > -1;
4042 }
4043
4044 function getNormalizedAttrValue(className, attrs, defaultVal) {
4045 var normalizedAttr = attrs.$normalize(className);
4046 return attrs[normalizedAttr] ? attrs[normalizedAttr].replace(WHITESPACE, "-") : defaultVal || null;
4047 }
4048
4049 function findIn(item, list, replaceWith) {
4050 item = replaceWith && item ? item.replace(WHITESPACE, replaceWith) : item;
4051
4052 var found = false;
4053 if (item) {
4054 list.forEach(function(it) {
4055 it = replaceWith ? it.replace(WHITESPACE, replaceWith) : it;
4056 found = found || (it === item);
4057 });
4058 }
4059 return found;
4060 }
4061
4062 function extractAlignAxis(attrValue) {
4063 var axis = {
4064 main : "start",
4065 cross: "stretch"
4066 }, values;
4067
4068 attrValue = (attrValue || "");
4069
4070 if ( attrValue.indexOf("-") === 0 || attrValue.indexOf(" ") === 0) {
4071 // For missing main-axis values
4072 attrValue = "none" + attrValue;
4073 }
4074
4075 values = attrValue.toLowerCase().trim().replace(WHITESPACE, "-").split("-");
4076 if ( values.length && (values[0] === "space") ) {
4077 // for main-axis values of "space-around" or "space-between"
4078 values = [ values[0]+"-"+values[1],values[2] ];
4079 }
4080
4081 if ( values.length > 0 ) axis.main = values[0] || axis.main;
4082 if ( values.length > 1 ) axis.cross = values[1] || axis.cross;
4083
4084 if ( ALIGNMENT_MAIN_AXIS.indexOf(axis.main) < 0 ) axis.main = "start";
4085 if ( ALIGNMENT_CROSS_AXIS.indexOf(axis.cross) < 0 ) axis.cross = "stretch";
4086
4087 return axis;
4088 }
4089
4090
4091})();
4092
4093})();
4094(function(){
4095"use strict";
4096
4097/**
4098 * @ngdoc service
4099 * @name $$mdMeta
4100 * @module material.core.meta
4101 *
4102 * @description
4103 *
4104 * A provider and a service that simplifies meta tags access
4105 *
4106 * Note: This is intended only for use with dynamic meta tags such as browser color and title.
4107 * Tags that are only processed when the page is rendered (such as `charset`, and `http-equiv`)
4108 * will not work since `$$mdMeta` adds the tags after the page has already been loaded.
4109 *
4110 * ```js
4111 * app.config(function($$mdMetaProvider) {
4112 * var removeMeta = $$mdMetaProvider.setMeta('meta-name', 'content');
4113 * var metaValue = $$mdMetaProvider.getMeta('meta-name'); // -> 'content'
4114 *
4115 * removeMeta();
4116 * });
4117 *
4118 * app.controller('myController', function($$mdMeta) {
4119 * var removeMeta = $$mdMeta.setMeta('meta-name', 'content');
4120 * var metaValue = $$mdMeta.getMeta('meta-name'); // -> 'content'
4121 *
4122 * removeMeta();
4123 * });
4124 * ```
4125 *
4126 * @returns {$$mdMeta.$service}
4127 *
4128 */
4129angular.module('material.core.meta', [])
4130 .provider('$$mdMeta', function () {
4131 var head = angular.element(document.head);
4132 var metaElements = {};
4133
4134 /**
4135 * Checks if the requested element was written manually and maps it
4136 *
4137 * @param {string} name meta tag 'name' attribute value
4138 * @returns {boolean} returns true if there is an element with the requested name
4139 */
4140 function mapExistingElement(name) {
4141 if (metaElements[name]) {
4142 return true;
4143 }
4144
4145 var element = document.getElementsByName(name)[0];
4146
4147 if (!element) {
4148 return false;
4149 }
4150
4151 metaElements[name] = angular.element(element);
4152
4153 return true;
4154 }
4155
4156 /**
4157 * @ngdoc method
4158 * @name $$mdMeta#setMeta
4159 *
4160 * @description
4161 * Creates meta element with the 'name' and 'content' attributes,
4162 * if the meta tag is already created than we replace the 'content' value
4163 *
4164 * @param {string} name meta tag 'name' attribute value
4165 * @param {string} content meta tag 'content' attribute value
4166 * @returns {function} remove function
4167 *
4168 */
4169 function setMeta(name, content) {
4170 mapExistingElement(name);
4171
4172 if (!metaElements[name]) {
4173 var newMeta = angular.element('<meta name="' + name + '" content="' + content + '"/>');
4174 head.append(newMeta);
4175 metaElements[name] = newMeta;
4176 }
4177 else {
4178 metaElements[name].attr('content', content);
4179 }
4180
4181 return function () {
4182 metaElements[name].attr('content', '');
4183 metaElements[name].remove();
4184 delete metaElements[name];
4185 };
4186 }
4187
4188 /**
4189 * @ngdoc method
4190 * @name $$mdMeta#getMeta
4191 *
4192 * @description
4193 * Gets the 'content' attribute value of the wanted meta element
4194 *
4195 * @param {string} name meta tag 'name' attribute value
4196 * @returns {string} content attribute value
4197 */
4198 function getMeta(name) {
4199 if (!mapExistingElement(name)) {
4200 throw Error('$$mdMeta: could not find a meta tag with the name \'' + name + '\'');
4201 }
4202
4203 return metaElements[name].attr('content');
4204 }
4205
4206 var module = {
4207 setMeta: setMeta,
4208 getMeta: getMeta
4209 };
4210
4211 return angular.extend({}, module, {
4212 $get: function () {
4213 return module;
4214 }
4215 });
4216 });
4217})();
4218(function(){
4219"use strict";
4220
4221 /**
4222 * @ngdoc module
4223 * @name material.core.componentRegistry
4224 *
4225 * @description
4226 * A component instance registration service.
4227 * Note: currently this as a private service in the SideNav component.
4228 */
4229 ComponentRegistry.$inject = ["$log", "$q"];
4230 angular.module('material.core')
4231 .factory('$mdComponentRegistry', ComponentRegistry);
4232
4233 /*
4234 * @private
4235 * @ngdoc factory
4236 * @name ComponentRegistry
4237 * @module material.core.componentRegistry
4238 *
4239 */
4240 function ComponentRegistry($log, $q) {
4241
4242 var self;
4243 var instances = [ ];
4244 var pendings = { };
4245
4246 return self = {
4247 /**
4248 * Used to print an error when an instance for a handle isn't found.
4249 */
4250 notFoundError: function(handle, msgContext) {
4251 $log.error( (msgContext || "") + 'No instance found for handle', handle);
4252 },
4253 /**
4254 * Return all registered instances as an array.
4255 */
4256 getInstances: function() {
4257 return instances;
4258 },
4259
4260 /**
4261 * Get a registered instance.
4262 * @param handle the String handle to look up for a registered instance.
4263 */
4264 get: function(handle) {
4265 if ( !isValidID(handle) ) return null;
4266
4267 var i, j, instance;
4268 for(i = 0, j = instances.length; i < j; i++) {
4269 instance = instances[i];
4270 if(instance.$$mdHandle === handle) {
4271 return instance;
4272 }
4273 }
4274 return null;
4275 },
4276
4277 /**
4278 * Register an instance.
4279 * @param instance the instance to register
4280 * @param handle the handle to identify the instance under.
4281 */
4282 register: function(instance, handle) {
4283 if ( !handle ) return angular.noop;
4284
4285 instance.$$mdHandle = handle;
4286 instances.push(instance);
4287 resolveWhen();
4288
4289 return deregister;
4290
4291 /**
4292 * Remove registration for an instance
4293 */
4294 function deregister() {
4295 var index = instances.indexOf(instance);
4296 if (index !== -1) {
4297 instances.splice(index, 1);
4298 }
4299 }
4300
4301 /**
4302 * Resolve any pending promises for this instance
4303 */
4304 function resolveWhen() {
4305 var dfd = pendings[handle];
4306 if ( dfd ) {
4307 dfd.forEach(function (promise) {
4308 promise.resolve(instance);
4309 });
4310 delete pendings[handle];
4311 }
4312 }
4313 },
4314
4315 /**
4316 * Async accessor to registered component instance
4317 * If not available then a promise is created to notify
4318 * all listeners when the instance is registered.
4319 */
4320 when : function(handle) {
4321 if ( isValidID(handle) ) {
4322 var deferred = $q.defer();
4323 var instance = self.get(handle);
4324
4325 if ( instance ) {
4326 deferred.resolve( instance );
4327 } else {
4328 if (pendings[handle] === undefined) {
4329 pendings[handle] = [];
4330 }
4331 pendings[handle].push(deferred);
4332 }
4333
4334 return deferred.promise;
4335 }
4336 return $q.reject("Invalid `md-component-id` value.");
4337 }
4338
4339 };
4340
4341 function isValidID(handle){
4342 return handle && (handle !== "");
4343 }
4344
4345 }
4346
4347})();
4348(function(){
4349"use strict";
4350
4351(function() {
4352 'use strict';
4353
4354 /**
4355 * @ngdoc service
4356 * @name $mdButtonInkRipple
4357 * @module material.core
4358 *
4359 * @description
4360 * Provides ripple effects for md-button. See $mdInkRipple service for all possible configuration options.
4361 *
4362 * @param {object=} scope Scope within the current context
4363 * @param {object=} element The element the ripple effect should be applied to
4364 * @param {object=} options (Optional) Configuration options to override the default ripple configuration
4365 */
4366
4367 MdButtonInkRipple.$inject = ["$mdInkRipple"];
4368 angular.module('material.core')
4369 .factory('$mdButtonInkRipple', MdButtonInkRipple);
4370
4371 function MdButtonInkRipple($mdInkRipple) {
4372 return {
4373 attach: function attachRipple(scope, element, options) {
4374 options = angular.extend(optionsForElement(element), options);
4375
4376 return $mdInkRipple.attach(scope, element, options);
4377 }
4378 };
4379
4380 function optionsForElement(element) {
4381 if (element.hasClass('md-icon-button')) {
4382 return {
4383 isMenuItem: element.hasClass('md-menu-item'),
4384 fitRipple: true,
4385 center: true
4386 };
4387 } else {
4388 return {
4389 isMenuItem: element.hasClass('md-menu-item'),
4390 dimBackground: true
4391 }
4392 }
4393 };
4394 };
4395})();
4396
4397})();
4398(function(){
4399"use strict";
4400
4401(function() {
4402 'use strict';
4403
4404 /**
4405 * @ngdoc service
4406 * @name $mdCheckboxInkRipple
4407 * @module material.core
4408 *
4409 * @description
4410 * Provides ripple effects for md-checkbox. See $mdInkRipple service for all possible configuration options.
4411 *
4412 * @param {object=} scope Scope within the current context
4413 * @param {object=} element The element the ripple effect should be applied to
4414 * @param {object=} options (Optional) Configuration options to override the defaultripple configuration
4415 */
4416
4417 MdCheckboxInkRipple.$inject = ["$mdInkRipple"];
4418 angular.module('material.core')
4419 .factory('$mdCheckboxInkRipple', MdCheckboxInkRipple);
4420
4421 function MdCheckboxInkRipple($mdInkRipple) {
4422 return {
4423 attach: attach
4424 };
4425
4426 function attach(scope, element, options) {
4427 return $mdInkRipple.attach(scope, element, angular.extend({
4428 center: true,
4429 dimBackground: false,
4430 fitRipple: true
4431 }, options));
4432 };
4433 };
4434})();
4435
4436})();
4437(function(){
4438"use strict";
4439
4440(function() {
4441 'use strict';
4442
4443 /**
4444 * @ngdoc service
4445 * @name $mdListInkRipple
4446 * @module material.core
4447 *
4448 * @description
4449 * Provides ripple effects for md-list. See $mdInkRipple service for all possible configuration options.
4450 *
4451 * @param {object=} scope Scope within the current context
4452 * @param {object=} element The element the ripple effect should be applied to
4453 * @param {object=} options (Optional) Configuration options to override the defaultripple configuration
4454 */
4455
4456 MdListInkRipple.$inject = ["$mdInkRipple"];
4457 angular.module('material.core')
4458 .factory('$mdListInkRipple', MdListInkRipple);
4459
4460 function MdListInkRipple($mdInkRipple) {
4461 return {
4462 attach: attach
4463 };
4464
4465 function attach(scope, element, options) {
4466 return $mdInkRipple.attach(scope, element, angular.extend({
4467 center: false,
4468 dimBackground: true,
4469 outline: false,
4470 rippleSize: 'full'
4471 }, options));
4472 };
4473 };
4474})();
4475
4476})();
4477(function(){
4478"use strict";
4479
4480/**
4481 * @ngdoc module
4482 * @name material.core.ripple
4483 * @description
4484 * Ripple
4485 */
4486InkRippleCtrl.$inject = ["$scope", "$element", "rippleOptions", "$window", "$timeout", "$mdUtil", "$mdColorUtil"];
4487InkRippleDirective.$inject = ["$mdButtonInkRipple", "$mdCheckboxInkRipple"];
4488angular.module('material.core')
4489 .provider('$mdInkRipple', InkRippleProvider)
4490 .directive('mdInkRipple', InkRippleDirective)
4491 .directive('mdNoInk', attrNoDirective)
4492 .directive('mdNoBar', attrNoDirective)
4493 .directive('mdNoStretch', attrNoDirective);
4494
4495var DURATION = 450;
4496
4497/**
4498 * @ngdoc directive
4499 * @name mdInkRipple
4500 * @module material.core.ripple
4501 *
4502 * @description
4503 * The `md-ink-ripple` directive allows you to specify the ripple color or id a ripple is allowed.
4504 *
4505 * @param {string|boolean} md-ink-ripple A color string `#FF0000` or boolean (`false` or `0`) for preventing ripple
4506 *
4507 * @usage
4508 * ### String values
4509 * <hljs lang="html">
4510 * <ANY md-ink-ripple="#FF0000">
4511 * Ripples in red
4512 * </ANY>
4513 *
4514 * <ANY md-ink-ripple="false">
4515 * Not rippling
4516 * </ANY>
4517 * </hljs>
4518 *
4519 * ### Interpolated values
4520 * <hljs lang="html">
4521 * <ANY md-ink-ripple="{{ randomColor() }}">
4522 * Ripples with the return value of 'randomColor' function
4523 * </ANY>
4524 *
4525 * <ANY md-ink-ripple="{{ canRipple() }}">
4526 * Ripples if 'canRipple' function return value is not 'false' or '0'
4527 * </ANY>
4528 * </hljs>
4529 */
4530function InkRippleDirective ($mdButtonInkRipple, $mdCheckboxInkRipple) {
4531 return {
4532 controller: angular.noop,
4533 link: function (scope, element, attr) {
4534 attr.hasOwnProperty('mdInkRippleCheckbox')
4535 ? $mdCheckboxInkRipple.attach(scope, element)
4536 : $mdButtonInkRipple.attach(scope, element);
4537 }
4538 };
4539}
4540
4541/**
4542 * @ngdoc service
4543 * @name $mdInkRipple
4544 * @module material.core.ripple
4545 *
4546 * @description
4547 * `$mdInkRipple` is a service for adding ripples to any element
4548 *
4549 * @usage
4550 * <hljs lang="js">
4551 * app.factory('$myElementInkRipple', function($mdInkRipple) {
4552 * return {
4553 * attach: function (scope, element, options) {
4554 * return $mdInkRipple.attach(scope, element, angular.extend({
4555 * center: false,
4556 * dimBackground: true
4557 * }, options));
4558 * }
4559 * };
4560 * });
4561 *
4562 * app.controller('myController', function ($scope, $element, $myElementInkRipple) {
4563 * $scope.onClick = function (ev) {
4564 * $myElementInkRipple.attach($scope, angular.element(ev.target), { center: true });
4565 * }
4566 * });
4567 * </hljs>
4568 *
4569 * ### Disabling ripples globally
4570 * If you want to disable ink ripples globally, for all components, you can call the
4571 * `disableInkRipple` method in your app's config.
4572 *
4573 * <hljs lang="js">
4574 * app.config(function ($mdInkRippleProvider) {
4575 * $mdInkRippleProvider.disableInkRipple();
4576 * });
4577 */
4578
4579function InkRippleProvider () {
4580 var isDisabledGlobally = false;
4581
4582 return {
4583 disableInkRipple: disableInkRipple,
4584 $get: ["$injector", function($injector) {
4585 return { attach: attach };
4586
4587 /**
4588 * @ngdoc method
4589 * @name $mdInkRipple#attach
4590 *
4591 * @description
4592 * Attaching given scope, element and options to inkRipple controller
4593 *
4594 * @param {object=} scope Scope within the current context
4595 * @param {object=} element The element the ripple effect should be applied to
4596 * @param {object=} options (Optional) Configuration options to override the defaultRipple configuration
4597 * * `center` - Whether the ripple should start from the center of the container element
4598 * * `dimBackground` - Whether the background should be dimmed with the ripple color
4599 * * `colorElement` - The element the ripple should take its color from, defined by css property `color`
4600 * * `fitRipple` - Whether the ripple should fill the element
4601 */
4602 function attach (scope, element, options) {
4603 if (isDisabledGlobally || element.controller('mdNoInk')) return angular.noop;
4604 return $injector.instantiate(InkRippleCtrl, {
4605 $scope: scope,
4606 $element: element,
4607 rippleOptions: options
4608 });
4609 }
4610 }]
4611 };
4612
4613 /**
4614 * @ngdoc method
4615 * @name $mdInkRipple#disableInkRipple
4616 *
4617 * @description
4618 * A config-time method that, when called, disables ripples globally.
4619 */
4620 function disableInkRipple () {
4621 isDisabledGlobally = true;
4622 }
4623}
4624
4625/**
4626 * Controller used by the ripple service in order to apply ripples
4627 * @ngInject
4628 */
4629function InkRippleCtrl ($scope, $element, rippleOptions, $window, $timeout, $mdUtil, $mdColorUtil) {
4630 this.$window = $window;
4631 this.$timeout = $timeout;
4632 this.$mdUtil = $mdUtil;
4633 this.$mdColorUtil = $mdColorUtil;
4634 this.$scope = $scope;
4635 this.$element = $element;
4636 this.options = rippleOptions;
4637 this.mousedown = false;
4638 this.ripples = [];
4639 this.timeout = null; // Stores a reference to the most-recent ripple timeout
4640 this.lastRipple = null;
4641
4642 $mdUtil.valueOnUse(this, 'container', this.createContainer);
4643
4644 this.$element.addClass('md-ink-ripple');
4645
4646 // attach method for unit tests
4647 ($element.controller('mdInkRipple') || {}).createRipple = angular.bind(this, this.createRipple);
4648 ($element.controller('mdInkRipple') || {}).setColor = angular.bind(this, this.color);
4649
4650 this.bindEvents();
4651}
4652
4653
4654/**
4655 * Either remove or unlock any remaining ripples when the user mouses off of the element (either by
4656 * mouseup or mouseleave event)
4657 */
4658function autoCleanup (self, cleanupFn) {
4659
4660 if ( self.mousedown || self.lastRipple ) {
4661 self.mousedown = false;
4662 self.$mdUtil.nextTick( angular.bind(self, cleanupFn), false);
4663 }
4664
4665}
4666
4667
4668/**
4669 * Returns the color that the ripple should be (either based on CSS or hard-coded)
4670 * @returns {string}
4671 */
4672InkRippleCtrl.prototype.color = function (value) {
4673 var self = this;
4674
4675 // If assigning a color value, apply it to background and the ripple color
4676 if (angular.isDefined(value)) {
4677 self._color = self._parseColor(value);
4678 }
4679
4680 // If color lookup, use assigned, defined, or inherited
4681 return self._color || self._parseColor( self.inkRipple() ) || self._parseColor( getElementColor() );
4682
4683 /**
4684 * Finds the color element and returns its text color for use as default ripple color
4685 * @returns {string}
4686 */
4687 function getElementColor () {
4688 var items = self.options && self.options.colorElement ? self.options.colorElement : [];
4689 var elem = items.length ? items[ 0 ] : self.$element[ 0 ];
4690
4691 return elem ? self.$window.getComputedStyle(elem).color : 'rgb(0,0,0)';
4692 }
4693};
4694
4695/**
4696 * Updating the ripple colors based on the current inkRipple value
4697 * or the element's computed style color
4698 */
4699InkRippleCtrl.prototype.calculateColor = function () {
4700 return this.color();
4701};
4702
4703
4704/**
4705 * Takes a string color and converts it to RGBA format
4706 * @param color {string}
4707 * @param [multiplier] {int}
4708 * @returns {string}
4709 */
4710
4711InkRippleCtrl.prototype._parseColor = function parseColor (color, multiplier) {
4712 multiplier = multiplier || 1;
4713 var colorUtil = this.$mdColorUtil;
4714
4715 if (!color) return;
4716 if (color.indexOf('rgba') === 0) return color.replace(/\d?\.?\d*\s*\)\s*$/, (0.1 * multiplier).toString() + ')');
4717 if (color.indexOf('rgb') === 0) return colorUtil.rgbToRgba(color);
4718 if (color.indexOf('#') === 0) return colorUtil.hexToRgba(color);
4719
4720};
4721
4722/**
4723 * Binds events to the root element for
4724 */
4725InkRippleCtrl.prototype.bindEvents = function () {
4726 this.$element.on('mousedown', angular.bind(this, this.handleMousedown));
4727 this.$element.on('mouseup touchend', angular.bind(this, this.handleMouseup));
4728 this.$element.on('mouseleave', angular.bind(this, this.handleMouseup));
4729 this.$element.on('touchmove', angular.bind(this, this.handleTouchmove));
4730};
4731
4732/**
4733 * Create a new ripple on every mousedown event from the root element
4734 * @param event {MouseEvent}
4735 */
4736InkRippleCtrl.prototype.handleMousedown = function (event) {
4737 if ( this.mousedown ) return;
4738
4739 // When jQuery is loaded, we have to get the original event
4740 if (event.hasOwnProperty('originalEvent')) event = event.originalEvent;
4741 this.mousedown = true;
4742 if (this.options.center) {
4743 this.createRipple(this.container.prop('clientWidth') / 2, this.container.prop('clientWidth') / 2);
4744 } else {
4745
4746 // We need to calculate the relative coordinates if the target is a sublayer of the ripple element
4747 if (event.srcElement !== this.$element[0]) {
4748 var layerRect = this.$element[0].getBoundingClientRect();
4749 var layerX = event.clientX - layerRect.left;
4750 var layerY = event.clientY - layerRect.top;
4751
4752 this.createRipple(layerX, layerY);
4753 } else {
4754 this.createRipple(event.offsetX, event.offsetY);
4755 }
4756 }
4757};
4758
4759/**
4760 * Either remove or unlock any remaining ripples when the user mouses off of the element (either by
4761 * mouseup, touchend or mouseleave event)
4762 */
4763InkRippleCtrl.prototype.handleMouseup = function () {
4764 autoCleanup(this, this.clearRipples);
4765};
4766
4767/**
4768 * Either remove or unlock any remaining ripples when the user mouses off of the element (by
4769 * touchmove)
4770 */
4771InkRippleCtrl.prototype.handleTouchmove = function () {
4772 autoCleanup(this, this.deleteRipples);
4773};
4774
4775/**
4776 * Cycles through all ripples and attempts to remove them.
4777 */
4778InkRippleCtrl.prototype.deleteRipples = function () {
4779 for (var i = 0; i < this.ripples.length; i++) {
4780 this.ripples[ i ].remove();
4781 }
4782};
4783
4784/**
4785 * Cycles through all ripples and attempts to remove them with fade.
4786 * Depending on logic within `fadeInComplete`, some removals will be postponed.
4787 */
4788InkRippleCtrl.prototype.clearRipples = function () {
4789 for (var i = 0; i < this.ripples.length; i++) {
4790 this.fadeInComplete(this.ripples[ i ]);
4791 }
4792};
4793
4794/**
4795 * Creates the ripple container element
4796 * @returns {*}
4797 */
4798InkRippleCtrl.prototype.createContainer = function () {
4799 var container = angular.element('<div class="md-ripple-container"></div>');
4800 this.$element.append(container);
4801 return container;
4802};
4803
4804InkRippleCtrl.prototype.clearTimeout = function () {
4805 if (this.timeout) {
4806 this.$timeout.cancel(this.timeout);
4807 this.timeout = null;
4808 }
4809};
4810
4811InkRippleCtrl.prototype.isRippleAllowed = function () {
4812 var element = this.$element[0];
4813 do {
4814 if (!element.tagName || element.tagName === 'BODY') break;
4815
4816 if (element && angular.isFunction(element.hasAttribute)) {
4817 if (element.hasAttribute('disabled')) return false;
4818 if (this.inkRipple() === 'false' || this.inkRipple() === '0') return false;
4819 }
4820
4821 } while (element = element.parentNode);
4822 return true;
4823};
4824
4825/**
4826 * The attribute `md-ink-ripple` may be a static or interpolated
4827 * color value OR a boolean indicator (used to disable ripples)
4828 */
4829InkRippleCtrl.prototype.inkRipple = function () {
4830 return this.$element.attr('md-ink-ripple');
4831};
4832
4833/**
4834 * Creates a new ripple and adds it to the container. Also tracks ripple in `this.ripples`.
4835 * @param left
4836 * @param top
4837 */
4838InkRippleCtrl.prototype.createRipple = function (left, top) {
4839 if (!this.isRippleAllowed()) return;
4840
4841 var ctrl = this;
4842 var colorUtil = ctrl.$mdColorUtil;
4843 var ripple = angular.element('<div class="md-ripple"></div>');
4844 var width = this.$element.prop('clientWidth');
4845 var height = this.$element.prop('clientHeight');
4846 var x = Math.max(Math.abs(width - left), left) * 2;
4847 var y = Math.max(Math.abs(height - top), top) * 2;
4848 var size = getSize(this.options.fitRipple, x, y);
4849 var color = this.calculateColor();
4850
4851 ripple.css({
4852 left: left + 'px',
4853 top: top + 'px',
4854 background: 'black',
4855 width: size + 'px',
4856 height: size + 'px',
4857 backgroundColor: colorUtil.rgbaToRgb(color),
4858 borderColor: colorUtil.rgbaToRgb(color)
4859 });
4860 this.lastRipple = ripple;
4861
4862 // we only want one timeout to be running at a time
4863 this.clearTimeout();
4864 this.timeout = this.$timeout(function () {
4865 ctrl.clearTimeout();
4866 if (!ctrl.mousedown) ctrl.fadeInComplete(ripple);
4867 }, DURATION * 0.35, false);
4868
4869 if (this.options.dimBackground) this.container.css({ backgroundColor: color });
4870 this.container.append(ripple);
4871 this.ripples.push(ripple);
4872 ripple.addClass('md-ripple-placed');
4873
4874 this.$mdUtil.nextTick(function () {
4875
4876 ripple.addClass('md-ripple-scaled md-ripple-active');
4877 ctrl.$timeout(function () {
4878 ctrl.clearRipples();
4879 }, DURATION, false);
4880
4881 }, false);
4882
4883 function getSize (fit, x, y) {
4884 return fit
4885 ? Math.max(x, y)
4886 : Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
4887 }
4888};
4889
4890
4891
4892/**
4893 * After fadeIn finishes, either kicks off the fade-out animation or queues the element for removal on mouseup
4894 * @param ripple
4895 */
4896InkRippleCtrl.prototype.fadeInComplete = function (ripple) {
4897 if (this.lastRipple === ripple) {
4898 if (!this.timeout && !this.mousedown) {
4899 this.removeRipple(ripple);
4900 }
4901 } else {
4902 this.removeRipple(ripple);
4903 }
4904};
4905
4906/**
4907 * Kicks off the animation for removing a ripple
4908 * @param ripple {Element}
4909 */
4910InkRippleCtrl.prototype.removeRipple = function (ripple) {
4911 var ctrl = this;
4912 var index = this.ripples.indexOf(ripple);
4913 if (index < 0) return;
4914 this.ripples.splice(this.ripples.indexOf(ripple), 1);
4915 ripple.removeClass('md-ripple-active');
4916 ripple.addClass('md-ripple-remove');
4917 if (this.ripples.length === 0) this.container.css({ backgroundColor: '' });
4918 // use a 2-second timeout in order to allow for the animation to finish
4919 // we don't actually care how long the animation takes
4920 this.$timeout(function () {
4921 ctrl.fadeOutComplete(ripple);
4922 }, DURATION, false);
4923};
4924
4925/**
4926 * Removes the provided ripple from the DOM
4927 * @param ripple
4928 */
4929InkRippleCtrl.prototype.fadeOutComplete = function (ripple) {
4930 ripple.remove();
4931 this.lastRipple = null;
4932};
4933
4934/**
4935 * Used to create an empty directive. This is used to track flag-directives whose children may have
4936 * functionality based on them.
4937 *
4938 * Example: `md-no-ink` will potentially be used by all child directives.
4939 */
4940function attrNoDirective () {
4941 return { controller: angular.noop };
4942}
4943
4944})();
4945(function(){
4946"use strict";
4947
4948(function() {
4949 'use strict';
4950
4951 /**
4952 * @ngdoc service
4953 * @name $mdTabInkRipple
4954 * @module material.core
4955 *
4956 * @description
4957 * Provides ripple effects for md-tabs. See $mdInkRipple service for all possible configuration options.
4958 *
4959 * @param {object=} scope Scope within the current context
4960 * @param {object=} element The element the ripple effect should be applied to
4961 * @param {object=} options (Optional) Configuration options to override the defaultripple configuration
4962 */
4963
4964 MdTabInkRipple.$inject = ["$mdInkRipple"];
4965 angular.module('material.core')
4966 .factory('$mdTabInkRipple', MdTabInkRipple);
4967
4968 function MdTabInkRipple($mdInkRipple) {
4969 return {
4970 attach: attach
4971 };
4972
4973 function attach(scope, element, options) {
4974 return $mdInkRipple.attach(scope, element, angular.extend({
4975 center: false,
4976 dimBackground: true,
4977 outline: false,
4978 rippleSize: 'full'
4979 }, options));
4980 };
4981 };
4982})();
4983
4984})();
4985(function(){
4986"use strict";
4987
4988angular.module('material.core.theming.palette', [])
4989.constant('$mdColorPalette', {
4990 'red': {
4991 '50': '#ffebee',
4992 '100': '#ffcdd2',
4993 '200': '#ef9a9a',
4994 '300': '#e57373',
4995 '400': '#ef5350',
4996 '500': '#f44336',
4997 '600': '#e53935',
4998 '700': '#d32f2f',
4999 '800': '#c62828',
5000 '900': '#b71c1c',
5001 'A100': '#ff8a80',
5002 'A200': '#ff5252',
5003 'A400': '#ff1744',
5004 'A700': '#d50000',
5005 'contrastDefaultColor': 'light',
5006 'contrastDarkColors': '50 100 200 300 A100',
5007 'contrastStrongLightColors': '400 500 600 700 A200 A400 A700'
5008 },
5009 'pink': {
5010 '50': '#fce4ec',
5011 '100': '#f8bbd0',
5012 '200': '#f48fb1',
5013 '300': '#f06292',
5014 '400': '#ec407a',
5015 '500': '#e91e63',
5016 '600': '#d81b60',
5017 '700': '#c2185b',
5018 '800': '#ad1457',
5019 '900': '#880e4f',
5020 'A100': '#ff80ab',
5021 'A200': '#ff4081',
5022 'A400': '#f50057',
5023 'A700': '#c51162',
5024 'contrastDefaultColor': 'light',
5025 'contrastDarkColors': '50 100 200 A100',
5026 'contrastStrongLightColors': '500 600 A200 A400 A700'
5027 },
5028 'purple': {
5029 '50': '#f3e5f5',
5030 '100': '#e1bee7',
5031 '200': '#ce93d8',
5032 '300': '#ba68c8',
5033 '400': '#ab47bc',
5034 '500': '#9c27b0',
5035 '600': '#8e24aa',
5036 '700': '#7b1fa2',
5037 '800': '#6a1b9a',
5038 '900': '#4a148c',
5039 'A100': '#ea80fc',
5040 'A200': '#e040fb',
5041 'A400': '#d500f9',
5042 'A700': '#aa00ff',
5043 'contrastDefaultColor': 'light',
5044 'contrastDarkColors': '50 100 200 A100',
5045 'contrastStrongLightColors': '300 400 A200 A400 A700'
5046 },
5047 'deep-purple': {
5048 '50': '#ede7f6',
5049 '100': '#d1c4e9',
5050 '200': '#b39ddb',
5051 '300': '#9575cd',
5052 '400': '#7e57c2',
5053 '500': '#673ab7',
5054 '600': '#5e35b1',
5055 '700': '#512da8',
5056 '800': '#4527a0',
5057 '900': '#311b92',
5058 'A100': '#b388ff',
5059 'A200': '#7c4dff',
5060 'A400': '#651fff',
5061 'A700': '#6200ea',
5062 'contrastDefaultColor': 'light',
5063 'contrastDarkColors': '50 100 200 A100',
5064 'contrastStrongLightColors': '300 400 A200'
5065 },
5066 'indigo': {
5067 '50': '#e8eaf6',
5068 '100': '#c5cae9',
5069 '200': '#9fa8da',
5070 '300': '#7986cb',
5071 '400': '#5c6bc0',
5072 '500': '#3f51b5',
5073 '600': '#3949ab',
5074 '700': '#303f9f',
5075 '800': '#283593',
5076 '900': '#1a237e',
5077 'A100': '#8c9eff',
5078 'A200': '#536dfe',
5079 'A400': '#3d5afe',
5080 'A700': '#304ffe',
5081 'contrastDefaultColor': 'light',
5082 'contrastDarkColors': '50 100 200 A100',
5083 'contrastStrongLightColors': '300 400 A200 A400'
5084 },
5085 'blue': {
5086 '50': '#e3f2fd',
5087 '100': '#bbdefb',
5088 '200': '#90caf9',
5089 '300': '#64b5f6',
5090 '400': '#42a5f5',
5091 '500': '#2196f3',
5092 '600': '#1e88e5',
5093 '700': '#1976d2',
5094 '800': '#1565c0',
5095 '900': '#0d47a1',
5096 'A100': '#82b1ff',
5097 'A200': '#448aff',
5098 'A400': '#2979ff',
5099 'A700': '#2962ff',
5100 'contrastDefaultColor': 'light',
5101 'contrastDarkColors': '50 100 200 300 400 A100',
5102 'contrastStrongLightColors': '500 600 700 A200 A400 A700'
5103 },
5104 'light-blue': {
5105 '50': '#e1f5fe',
5106 '100': '#b3e5fc',
5107 '200': '#81d4fa',
5108 '300': '#4fc3f7',
5109 '400': '#29b6f6',
5110 '500': '#03a9f4',
5111 '600': '#039be5',
5112 '700': '#0288d1',
5113 '800': '#0277bd',
5114 '900': '#01579b',
5115 'A100': '#80d8ff',
5116 'A200': '#40c4ff',
5117 'A400': '#00b0ff',
5118 'A700': '#0091ea',
5119 'contrastDefaultColor': 'dark',
5120 'contrastLightColors': '600 700 800 900 A700',
5121 'contrastStrongLightColors': '600 700 800 A700'
5122 },
5123 'cyan': {
5124 '50': '#e0f7fa',
5125 '100': '#b2ebf2',
5126 '200': '#80deea',
5127 '300': '#4dd0e1',
5128 '400': '#26c6da',
5129 '500': '#00bcd4',
5130 '600': '#00acc1',
5131 '700': '#0097a7',
5132 '800': '#00838f',
5133 '900': '#006064',
5134 'A100': '#84ffff',
5135 'A200': '#18ffff',
5136 'A400': '#00e5ff',
5137 'A700': '#00b8d4',
5138 'contrastDefaultColor': 'dark',
5139 'contrastLightColors': '700 800 900',
5140 'contrastStrongLightColors': '700 800 900'
5141 },
5142 'teal': {
5143 '50': '#e0f2f1',
5144 '100': '#b2dfdb',
5145 '200': '#80cbc4',
5146 '300': '#4db6ac',
5147 '400': '#26a69a',
5148 '500': '#009688',
5149 '600': '#00897b',
5150 '700': '#00796b',
5151 '800': '#00695c',
5152 '900': '#004d40',
5153 'A100': '#a7ffeb',
5154 'A200': '#64ffda',
5155 'A400': '#1de9b6',
5156 'A700': '#00bfa5',
5157 'contrastDefaultColor': 'dark',
5158 'contrastLightColors': '500 600 700 800 900',
5159 'contrastStrongLightColors': '500 600 700'
5160 },
5161 'green': {
5162 '50': '#e8f5e9',
5163 '100': '#c8e6c9',
5164 '200': '#a5d6a7',
5165 '300': '#81c784',
5166 '400': '#66bb6a',
5167 '500': '#4caf50',
5168 '600': '#43a047',
5169 '700': '#388e3c',
5170 '800': '#2e7d32',
5171 '900': '#1b5e20',
5172 'A100': '#b9f6ca',
5173 'A200': '#69f0ae',
5174 'A400': '#00e676',
5175 'A700': '#00c853',
5176 'contrastDefaultColor': 'dark',
5177 'contrastLightColors': '500 600 700 800 900',
5178 'contrastStrongLightColors': '500 600 700'
5179 },
5180 'light-green': {
5181 '50': '#f1f8e9',
5182 '100': '#dcedc8',
5183 '200': '#c5e1a5',
5184 '300': '#aed581',
5185 '400': '#9ccc65',
5186 '500': '#8bc34a',
5187 '600': '#7cb342',
5188 '700': '#689f38',
5189 '800': '#558b2f',
5190 '900': '#33691e',
5191 'A100': '#ccff90',
5192 'A200': '#b2ff59',
5193 'A400': '#76ff03',
5194 'A700': '#64dd17',
5195 'contrastDefaultColor': 'dark',
5196 'contrastLightColors': '700 800 900',
5197 'contrastStrongLightColors': '700 800 900'
5198 },
5199 'lime': {
5200 '50': '#f9fbe7',
5201 '100': '#f0f4c3',
5202 '200': '#e6ee9c',
5203 '300': '#dce775',
5204 '400': '#d4e157',
5205 '500': '#cddc39',
5206 '600': '#c0ca33',
5207 '700': '#afb42b',
5208 '800': '#9e9d24',
5209 '900': '#827717',
5210 'A100': '#f4ff81',
5211 'A200': '#eeff41',
5212 'A400': '#c6ff00',
5213 'A700': '#aeea00',
5214 'contrastDefaultColor': 'dark',
5215 'contrastLightColors': '900',
5216 'contrastStrongLightColors': '900'
5217 },
5218 'yellow': {
5219 '50': '#fffde7',
5220 '100': '#fff9c4',
5221 '200': '#fff59d',
5222 '300': '#fff176',
5223 '400': '#ffee58',
5224 '500': '#ffeb3b',
5225 '600': '#fdd835',
5226 '700': '#fbc02d',
5227 '800': '#f9a825',
5228 '900': '#f57f17',
5229 'A100': '#ffff8d',
5230 'A200': '#ffff00',
5231 'A400': '#ffea00',
5232 'A700': '#ffd600',
5233 'contrastDefaultColor': 'dark'
5234 },
5235 'amber': {
5236 '50': '#fff8e1',
5237 '100': '#ffecb3',
5238 '200': '#ffe082',
5239 '300': '#ffd54f',
5240 '400': '#ffca28',
5241 '500': '#ffc107',
5242 '600': '#ffb300',
5243 '700': '#ffa000',
5244 '800': '#ff8f00',
5245 '900': '#ff6f00',
5246 'A100': '#ffe57f',
5247 'A200': '#ffd740',
5248 'A400': '#ffc400',
5249 'A700': '#ffab00',
5250 'contrastDefaultColor': 'dark'
5251 },
5252 'orange': {
5253 '50': '#fff3e0',
5254 '100': '#ffe0b2',
5255 '200': '#ffcc80',
5256 '300': '#ffb74d',
5257 '400': '#ffa726',
5258 '500': '#ff9800',
5259 '600': '#fb8c00',
5260 '700': '#f57c00',
5261 '800': '#ef6c00',
5262 '900': '#e65100',
5263 'A100': '#ffd180',
5264 'A200': '#ffab40',
5265 'A400': '#ff9100',
5266 'A700': '#ff6d00',
5267 'contrastDefaultColor': 'dark',
5268 'contrastLightColors': '800 900',
5269 'contrastStrongLightColors': '800 900'
5270 },
5271 'deep-orange': {
5272 '50': '#fbe9e7',
5273 '100': '#ffccbc',
5274 '200': '#ffab91',
5275 '300': '#ff8a65',
5276 '400': '#ff7043',
5277 '500': '#ff5722',
5278 '600': '#f4511e',
5279 '700': '#e64a19',
5280 '800': '#d84315',
5281 '900': '#bf360c',
5282 'A100': '#ff9e80',
5283 'A200': '#ff6e40',
5284 'A400': '#ff3d00',
5285 'A700': '#dd2c00',
5286 'contrastDefaultColor': 'light',
5287 'contrastDarkColors': '50 100 200 300 400 A100 A200',
5288 'contrastStrongLightColors': '500 600 700 800 900 A400 A700'
5289 },
5290 'brown': {
5291 '50': '#efebe9',
5292 '100': '#d7ccc8',
5293 '200': '#bcaaa4',
5294 '300': '#a1887f',
5295 '400': '#8d6e63',
5296 '500': '#795548',
5297 '600': '#6d4c41',
5298 '700': '#5d4037',
5299 '800': '#4e342e',
5300 '900': '#3e2723',
5301 'A100': '#d7ccc8',
5302 'A200': '#bcaaa4',
5303 'A400': '#8d6e63',
5304 'A700': '#5d4037',
5305 'contrastDefaultColor': 'light',
5306 'contrastDarkColors': '50 100 200 A100 A200',
5307 'contrastStrongLightColors': '300 400'
5308 },
5309 'grey': {
5310 '50': '#fafafa',
5311 '100': '#f5f5f5',
5312 '200': '#eeeeee',
5313 '300': '#e0e0e0',
5314 '400': '#bdbdbd',
5315 '500': '#9e9e9e',
5316 '600': '#757575',
5317 '700': '#616161',
5318 '800': '#424242',
5319 '900': '#212121',
5320 'A100': '#ffffff',
5321 'A200': '#000000',
5322 'A400': '#303030',
5323 'A700': '#616161',
5324 'contrastDefaultColor': 'dark',
5325 'contrastLightColors': '600 700 800 900 A200 A400 A700'
5326 },
5327 'blue-grey': {
5328 '50': '#eceff1',
5329 '100': '#cfd8dc',
5330 '200': '#b0bec5',
5331 '300': '#90a4ae',
5332 '400': '#78909c',
5333 '500': '#607d8b',
5334 '600': '#546e7a',
5335 '700': '#455a64',
5336 '800': '#37474f',
5337 '900': '#263238',
5338 'A100': '#cfd8dc',
5339 'A200': '#b0bec5',
5340 'A400': '#78909c',
5341 'A700': '#455a64',
5342 'contrastDefaultColor': 'light',
5343 'contrastDarkColors': '50 100 200 300 A100 A200',
5344 'contrastStrongLightColors': '400 500 700'
5345 }
5346});
5347
5348})();
5349(function(){
5350"use strict";
5351
5352(function(angular) {
5353 'use strict';
5354/**
5355 * @ngdoc module
5356 * @name material.core.theming
5357 * @description
5358 * Theming
5359 */
5360detectDisabledThemes.$inject = ["$mdThemingProvider"];
5361ThemingDirective.$inject = ["$mdTheming", "$interpolate", "$log"];
5362ThemableDirective.$inject = ["$mdTheming"];
5363ThemingProvider.$inject = ["$mdColorPalette", "$$mdMetaProvider"];
5364generateAllThemes.$inject = ["$injector", "$mdTheming"];
5365angular.module('material.core.theming', ['material.core.theming.palette', 'material.core.meta'])
5366 .directive('mdTheme', ThemingDirective)
5367 .directive('mdThemable', ThemableDirective)
5368 .directive('mdThemesDisabled', disableThemesDirective )
5369 .provider('$mdTheming', ThemingProvider)
5370 .config( detectDisabledThemes )
5371 .run(generateAllThemes);
5372
5373/**
5374 * Detect if the HTML or the BODY tags has a [md-themes-disabled] attribute
5375 * If yes, then immediately disable all theme stylesheet generation and DOM injection
5376 */
5377/**
5378 * @ngInject
5379 */
5380function detectDisabledThemes($mdThemingProvider) {
5381 var isDisabled = !!document.querySelector('[md-themes-disabled]');
5382 $mdThemingProvider.disableTheming(isDisabled);
5383}
5384
5385/**
5386 * @ngdoc service
5387 * @name $mdThemingProvider
5388 * @module material.core.theming
5389 *
5390 * @description Provider to configure the `$mdTheming` service.
5391 *
5392 * ### Default Theme
5393 * The `$mdThemingProvider` uses by default the following theme configuration:
5394 *
5395 * - Primary Palette: `Primary`
5396 * - Accent Palette: `Pink`
5397 * - Warn Palette: `Deep-Orange`
5398 * - Background Palette: `Grey`
5399 *
5400 * If you don't want to use the `md-theme` directive on the elements itself, you may want to overwrite
5401 * the default theme.<br/>
5402 * This can be done by using the following markup.
5403 *
5404 * <hljs lang="js">
5405 * myAppModule.config(function($mdThemingProvider) {
5406 * $mdThemingProvider
5407 * .theme('default')
5408 * .primaryPalette('blue')
5409 * .accentPalette('teal')
5410 * .warnPalette('red')
5411 * .backgroundPalette('grey');
5412 * });
5413 * </hljs>
5414 *
5415
5416 * ### Dynamic Themes
5417 *
5418 * By default, if you change a theme at runtime, the `$mdTheming` service will not detect those changes.<br/>
5419 * If you have an application, which changes its theme on runtime, you have to enable theme watching.
5420 *
5421 * <hljs lang="js">
5422 * myAppModule.config(function($mdThemingProvider) {
5423 * // Enable theme watching.
5424 * $mdThemingProvider.alwaysWatchTheme(true);
5425 * });
5426 * </hljs>
5427 *
5428 * ### Custom Theme Styles
5429 *
5430 * Sometimes you may want to use your own theme styles for some custom components.<br/>
5431 * You are able to register your own styles by using the following markup.
5432 *
5433 * <hljs lang="js">
5434 * myAppModule.config(function($mdThemingProvider) {
5435 * // Register our custom stylesheet into the theming provider.
5436 * $mdThemingProvider.registerStyles(STYLESHEET);
5437 * });
5438 * </hljs>
5439 *
5440 * The `registerStyles` method only accepts strings as value, so you're actually not able to load an external
5441 * stylesheet file into the `$mdThemingProvider`.
5442 *
5443 * If it's necessary to load an external stylesheet, we suggest using a bundler, which supports including raw content,
5444 * like [raw-loader](https://github.com/webpack/raw-loader) for `webpack`.
5445 *
5446 * <hljs lang="js">
5447 * myAppModule.config(function($mdThemingProvider) {
5448 * // Register your custom stylesheet into the theming provider.
5449 * $mdThemingProvider.registerStyles(require('../styles/my-component.theme.css'));
5450 * });
5451 * </hljs>
5452 *
5453 * ### Browser color
5454 *
5455 * Enables browser header coloring
5456 * for more info please visit:
5457 * https://developers.google.com/web/fundamentals/design-and-ui/browser-customization/theme-color
5458 *
5459 * Options parameter: <br/>
5460 * `theme` - A defined theme via `$mdThemeProvider` to use the palettes from. Default is `default` theme. <br/>
5461 * `palette` - Can be any one of the basic material design palettes, extended defined palettes and 'primary',
5462 * 'accent', 'background' and 'warn'. Default is `primary`. <br/>
5463 * `hue` - The hue from the selected palette. Default is `800`<br/>
5464 *
5465 * <hljs lang="js">
5466 * myAppModule.config(function($mdThemingProvider) {
5467 * // Enable browser color
5468 * $mdThemingProvider.enableBrowserColor({
5469 * theme: 'myTheme', // Default is 'default'
5470 * palette: 'accent', // Default is 'primary', any basic material palette and extended palettes are available
5471 * hue: '200' // Default is '800'
5472 * });
5473 * });
5474 * </hljs>
5475 */
5476
5477/**
5478 * @ngdoc method
5479 * @name $mdThemingProvider#registerStyles
5480 * @param {string} styles The styles to be appended to Angular Material's built in theme css.
5481 */
5482/**
5483 * @ngdoc method
5484 * @name $mdThemingProvider#setNonce
5485 * @param {string} nonceValue The nonce to be added as an attribute to the theme style tags.
5486 * Setting a value allows the use of CSP policy without using the unsafe-inline directive.
5487 */
5488
5489/**
5490 * @ngdoc method
5491 * @name $mdThemingProvider#setDefaultTheme
5492 * @param {string} themeName Default theme name to be applied to elements. Default value is `default`.
5493 */
5494
5495/**
5496 * @ngdoc method
5497 * @name $mdThemingProvider#alwaysWatchTheme
5498 * @param {boolean} watch Whether or not to always watch themes for changes and re-apply
5499 * classes when they change. Default is `false`. Enabling can reduce performance.
5500 */
5501
5502/**
5503 * @ngdoc method
5504 * @name $mdThemingProvider#enableBrowserColor
5505 * @param {Object=} options Options object for the browser color<br/>
5506 * `theme` - A defined theme via `$mdThemeProvider` to use the palettes from. Default is `default` theme. <br/>
5507 * `palette` - Can be any one of the basic material design palettes, extended defined palettes and 'primary',
5508 * 'accent', 'background' and 'warn'. Default is `primary`. <br/>
5509 * `hue` - The hue from the selected palette. Default is `800`<br/>
5510 * @returns {Function} remove function of the browser color
5511 */
5512
5513/* Some Example Valid Theming Expressions
5514 * =======================================
5515 *
5516 * Intention group expansion: (valid for primary, accent, warn, background)
5517 *
5518 * {{primary-100}} - grab shade 100 from the primary palette
5519 * {{primary-100-0.7}} - grab shade 100, apply opacity of 0.7
5520 * {{primary-100-contrast}} - grab shade 100's contrast color
5521 * {{primary-hue-1}} - grab the shade assigned to hue-1 from the primary palette
5522 * {{primary-hue-1-0.7}} - apply 0.7 opacity to primary-hue-1
5523 * {{primary-color}} - Generates .md-hue-1, .md-hue-2, .md-hue-3 with configured shades set for each hue
5524 * {{primary-color-0.7}} - Apply 0.7 opacity to each of the above rules
5525 * {{primary-contrast}} - Generates .md-hue-1, .md-hue-2, .md-hue-3 with configured contrast (ie. text) color shades set for each hue
5526 * {{primary-contrast-0.7}} - Apply 0.7 opacity to each of the above rules
5527 *
5528 * Foreground expansion: Applies rgba to black/white foreground text
5529 *
5530 * {{foreground-1}} - used for primary text
5531 * {{foreground-2}} - used for secondary text/divider
5532 * {{foreground-3}} - used for disabled text
5533 * {{foreground-4}} - used for dividers
5534 *
5535 */
5536
5537// In memory generated CSS rules; registered by theme.name
5538var GENERATED = { };
5539
5540// In memory storage of defined themes and color palettes (both loaded by CSS, and user specified)
5541var PALETTES;
5542
5543// Text Colors on light and dark backgrounds
5544// @see https://www.google.com/design/spec/style/color.html#color-text-background-colors
5545var DARK_FOREGROUND = {
5546 name: 'dark',
5547 '1': 'rgba(0,0,0,0.87)',
5548 '2': 'rgba(0,0,0,0.54)',
5549 '3': 'rgba(0,0,0,0.38)',
5550 '4': 'rgba(0,0,0,0.12)'
5551};
5552var LIGHT_FOREGROUND = {
5553 name: 'light',
5554 '1': 'rgba(255,255,255,1.0)',
5555 '2': 'rgba(255,255,255,0.7)',
5556 '3': 'rgba(255,255,255,0.5)',
5557 '4': 'rgba(255,255,255,0.12)'
5558};
5559
5560var DARK_SHADOW = '1px 1px 0px rgba(0,0,0,0.4), -1px -1px 0px rgba(0,0,0,0.4)';
5561var LIGHT_SHADOW = '';
5562
5563var DARK_CONTRAST_COLOR = colorToRgbaArray('rgba(0,0,0,0.87)');
5564var LIGHT_CONTRAST_COLOR = colorToRgbaArray('rgba(255,255,255,0.87)');
5565var STRONG_LIGHT_CONTRAST_COLOR = colorToRgbaArray('rgb(255,255,255)');
5566
5567var THEME_COLOR_TYPES = ['primary', 'accent', 'warn', 'background'];
5568var DEFAULT_COLOR_TYPE = 'primary';
5569
5570// A color in a theme will use these hues by default, if not specified by user.
5571var LIGHT_DEFAULT_HUES = {
5572 'accent': {
5573 'default': 'A200',
5574 'hue-1': 'A100',
5575 'hue-2': 'A400',
5576 'hue-3': 'A700'
5577 },
5578 'background': {
5579 'default': '50',
5580 'hue-1': 'A100',
5581 'hue-2': '100',
5582 'hue-3': '300'
5583 }
5584};
5585
5586var DARK_DEFAULT_HUES = {
5587 'background': {
5588 'default': 'A400',
5589 'hue-1': '800',
5590 'hue-2': '900',
5591 'hue-3': 'A200'
5592 }
5593};
5594THEME_COLOR_TYPES.forEach(function(colorType) {
5595 // Color types with unspecified default hues will use these default hue values
5596 var defaultDefaultHues = {
5597 'default': '500',
5598 'hue-1': '300',
5599 'hue-2': '800',
5600 'hue-3': 'A100'
5601 };
5602 if (!LIGHT_DEFAULT_HUES[colorType]) LIGHT_DEFAULT_HUES[colorType] = defaultDefaultHues;
5603 if (!DARK_DEFAULT_HUES[colorType]) DARK_DEFAULT_HUES[colorType] = defaultDefaultHues;
5604});
5605
5606var VALID_HUE_VALUES = [
5607 '50', '100', '200', '300', '400', '500', '600',
5608 '700', '800', '900', 'A100', 'A200', 'A400', 'A700'
5609];
5610
5611var themeConfig = {
5612 disableTheming : false, // Generate our themes at run time; also disable stylesheet DOM injection
5613 generateOnDemand : false, // Whether or not themes are to be generated on-demand (vs. eagerly).
5614 registeredStyles : [], // Custom styles registered to be used in the theming of custom components.
5615 nonce : null // Nonce to be added as an attribute to the generated themes style tags.
5616};
5617
5618/**
5619 *
5620 */
5621function ThemingProvider($mdColorPalette, $$mdMetaProvider) {
5622 ThemingService.$inject = ["$rootScope", "$log"];
5623 PALETTES = { };
5624 var THEMES = { };
5625
5626 var themingProvider;
5627
5628 var alwaysWatchTheme = false;
5629 var defaultTheme = 'default';
5630
5631 // Load JS Defined Palettes
5632 angular.extend(PALETTES, $mdColorPalette);
5633
5634 // Default theme defined in core.js
5635
5636 /**
5637 * Adds `theme-color` and `msapplication-navbutton-color` meta tags with the color parameter
5638 * @param {string} color Hex value of the wanted browser color
5639 * @returns {Function} Remove function of the meta tags
5640 */
5641 var setBrowserColor = function (color) {
5642 // Chrome, Firefox OS and Opera
5643 var removeChrome = $$mdMetaProvider.setMeta('theme-color', color);
5644 // Windows Phone
5645 var removeWindows = $$mdMetaProvider.setMeta('msapplication-navbutton-color', color);
5646
5647 return function () {
5648 removeChrome();
5649 removeWindows();
5650 }
5651 };
5652
5653 /**
5654 * Enables browser header coloring
5655 * for more info please visit:
5656 * https://developers.google.com/web/fundamentals/design-and-ui/browser-customization/theme-color
5657 *
5658 * The default color is `800` from `primary` palette of the `default` theme
5659 *
5660 * options are:
5661 * `theme` - A defined theme via `$mdThemeProvider` to use the palettes from. Default is `default` theme
5662 * `palette` - Can be any one of the basic material design palettes, extended defined palettes and 'primary',
5663 * 'accent', 'background' and 'warn'. Default is `primary`
5664 * `hue` - The hue from the selected palette. Default is `800`
5665 *
5666 * @param {Object=} options Options object for the browser color
5667 * @returns {Function} remove function of the browser color
5668 */
5669 var enableBrowserColor = function (options) {
5670 options = angular.isObject(options) ? options : {};
5671
5672 var theme = options.theme || 'default';
5673 var hue = options.hue || '800';
5674
5675 var palette = PALETTES[options.palette] ||
5676 PALETTES[THEMES[theme].colors[options.palette || 'primary'].name];
5677
5678 var color = angular.isObject(palette[hue]) ? palette[hue].hex : palette[hue];
5679
5680 return setBrowserColor(color);
5681 };
5682
5683 return themingProvider = {
5684 definePalette: definePalette,
5685 extendPalette: extendPalette,
5686 theme: registerTheme,
5687
5688 /**
5689 * return a read-only clone of the current theme configuration
5690 */
5691 configuration : function() {
5692 return angular.extend( { }, themeConfig, {
5693 defaultTheme : defaultTheme,
5694 alwaysWatchTheme : alwaysWatchTheme,
5695 registeredStyles : [].concat(themeConfig.registeredStyles)
5696 });
5697 },
5698
5699 /**
5700 * Easy way to disable theming without having to use
5701 * `.constant("$MD_THEME_CSS","");` This disables
5702 * all dynamic theme style sheet generations and injections...
5703 */
5704 disableTheming: function(isDisabled) {
5705 themeConfig.disableTheming = angular.isUndefined(isDisabled) || !!isDisabled;
5706 },
5707
5708 registerStyles: function(styles) {
5709 themeConfig.registeredStyles.push(styles);
5710 },
5711
5712 setNonce: function(nonceValue) {
5713 themeConfig.nonce = nonceValue;
5714 },
5715
5716 generateThemesOnDemand: function(onDemand) {
5717 themeConfig.generateOnDemand = onDemand;
5718 },
5719
5720 setDefaultTheme: function(theme) {
5721 defaultTheme = theme;
5722 },
5723
5724 alwaysWatchTheme: function(alwaysWatch) {
5725 alwaysWatchTheme = alwaysWatch;
5726 },
5727
5728 enableBrowserColor: enableBrowserColor,
5729
5730 $get: ThemingService,
5731 _LIGHT_DEFAULT_HUES: LIGHT_DEFAULT_HUES,
5732 _DARK_DEFAULT_HUES: DARK_DEFAULT_HUES,
5733 _PALETTES: PALETTES,
5734 _THEMES: THEMES,
5735 _parseRules: parseRules,
5736 _rgba: rgba
5737 };
5738
5739 // Example: $mdThemingProvider.definePalette('neonRed', { 50: '#f5fafa', ... });
5740 function definePalette(name, map) {
5741 map = map || {};
5742 PALETTES[name] = checkPaletteValid(name, map);
5743 return themingProvider;
5744 }
5745
5746 // Returns an new object which is a copy of a given palette `name` with variables from
5747 // `map` overwritten
5748 // Example: var neonRedMap = $mdThemingProvider.extendPalette('red', { 50: '#f5fafafa' });
5749 function extendPalette(name, map) {
5750 return checkPaletteValid(name, angular.extend({}, PALETTES[name] || {}, map) );
5751 }
5752
5753 // Make sure that palette has all required hues
5754 function checkPaletteValid(name, map) {
5755 var missingColors = VALID_HUE_VALUES.filter(function(field) {
5756 return !map[field];
5757 });
5758 if (missingColors.length) {
5759 throw new Error("Missing colors %1 in palette %2!"
5760 .replace('%1', missingColors.join(', '))
5761 .replace('%2', name));
5762 }
5763
5764 return map;
5765 }
5766
5767 // Register a theme (which is a collection of color palettes to use with various states
5768 // ie. warn, accent, primary )
5769 // Optionally inherit from an existing theme
5770 // $mdThemingProvider.theme('custom-theme').primaryPalette('red');
5771 function registerTheme(name, inheritFrom) {
5772 if (THEMES[name]) return THEMES[name];
5773
5774 inheritFrom = inheritFrom || 'default';
5775
5776 var parentTheme = typeof inheritFrom === 'string' ? THEMES[inheritFrom] : inheritFrom;
5777 var theme = new Theme(name);
5778
5779 if (parentTheme) {
5780 angular.forEach(parentTheme.colors, function(color, colorType) {
5781 theme.colors[colorType] = {
5782 name: color.name,
5783 // Make sure a COPY of the hues is given to the child color,
5784 // not the same reference.
5785 hues: angular.extend({}, color.hues)
5786 };
5787 });
5788 }
5789 THEMES[name] = theme;
5790
5791 return theme;
5792 }
5793
5794 function Theme(name) {
5795 var self = this;
5796 self.name = name;
5797 self.colors = {};
5798
5799 self.dark = setDark;
5800 setDark(false);
5801
5802 function setDark(isDark) {
5803 isDark = arguments.length === 0 ? true : !!isDark;
5804
5805 // If no change, abort
5806 if (isDark === self.isDark) return;
5807
5808 self.isDark = isDark;
5809
5810 self.foregroundPalette = self.isDark ? LIGHT_FOREGROUND : DARK_FOREGROUND;
5811 self.foregroundShadow = self.isDark ? DARK_SHADOW : LIGHT_SHADOW;
5812
5813 // Light and dark themes have different default hues.
5814 // Go through each existing color type for this theme, and for every
5815 // hue value that is still the default hue value from the previous light/dark setting,
5816 // set it to the default hue value from the new light/dark setting.
5817 var newDefaultHues = self.isDark ? DARK_DEFAULT_HUES : LIGHT_DEFAULT_HUES;
5818 var oldDefaultHues = self.isDark ? LIGHT_DEFAULT_HUES : DARK_DEFAULT_HUES;
5819 angular.forEach(newDefaultHues, function(newDefaults, colorType) {
5820 var color = self.colors[colorType];
5821 var oldDefaults = oldDefaultHues[colorType];
5822 if (color) {
5823 for (var hueName in color.hues) {
5824 if (color.hues[hueName] === oldDefaults[hueName]) {
5825 color.hues[hueName] = newDefaults[hueName];
5826 }
5827 }
5828 }
5829 });
5830
5831 return self;
5832 }
5833
5834 THEME_COLOR_TYPES.forEach(function(colorType) {
5835 var defaultHues = (self.isDark ? DARK_DEFAULT_HUES : LIGHT_DEFAULT_HUES)[colorType];
5836 self[colorType + 'Palette'] = function setPaletteType(paletteName, hues) {
5837 var color = self.colors[colorType] = {
5838 name: paletteName,
5839 hues: angular.extend({}, defaultHues, hues)
5840 };
5841
5842 Object.keys(color.hues).forEach(function(name) {
5843 if (!defaultHues[name]) {
5844 throw new Error("Invalid hue name '%1' in theme %2's %3 color %4. Available hue names: %4"
5845 .replace('%1', name)
5846 .replace('%2', self.name)
5847 .replace('%3', paletteName)
5848 .replace('%4', Object.keys(defaultHues).join(', '))
5849 );
5850 }
5851 });
5852 Object.keys(color.hues).map(function(key) {
5853 return color.hues[key];
5854 }).forEach(function(hueValue) {
5855 if (VALID_HUE_VALUES.indexOf(hueValue) == -1) {
5856 throw new Error("Invalid hue value '%1' in theme %2's %3 color %4. Available hue values: %5"
5857 .replace('%1', hueValue)
5858 .replace('%2', self.name)
5859 .replace('%3', colorType)
5860 .replace('%4', paletteName)
5861 .replace('%5', VALID_HUE_VALUES.join(', '))
5862 );
5863 }
5864 });
5865 return self;
5866 };
5867
5868 self[colorType + 'Color'] = function() {
5869 var args = Array.prototype.slice.call(arguments);
5870 console.warn('$mdThemingProviderTheme.' + colorType + 'Color() has been deprecated. ' +
5871 'Use $mdThemingProviderTheme.' + colorType + 'Palette() instead.');
5872 return self[colorType + 'Palette'].apply(self, args);
5873 };
5874 });
5875 }
5876
5877 /**
5878 * @ngdoc service
5879 * @name $mdTheming
5880 *
5881 * @description
5882 *
5883 * Service that makes an element apply theming related classes to itself.
5884 *
5885 * ```js
5886 * app.directive('myFancyDirective', function($mdTheming) {
5887 * return {
5888 * restrict: 'e',
5889 * link: function(scope, el, attrs) {
5890 * $mdTheming(el);
5891 * }
5892 * };
5893 * });
5894 * ```
5895 * @param {el=} element to apply theming to
5896 */
5897 /* @ngInject */
5898 function ThemingService($rootScope, $log) {
5899 // Allow us to be invoked via a linking function signature.
5900 var applyTheme = function (scope, el) {
5901 if (el === undefined) { el = scope; scope = undefined; }
5902 if (scope === undefined) { scope = $rootScope; }
5903 applyTheme.inherit(el, el);
5904 };
5905
5906 applyTheme.THEMES = angular.extend({}, THEMES);
5907 applyTheme.PALETTES = angular.extend({}, PALETTES);
5908 applyTheme.inherit = inheritTheme;
5909 applyTheme.registered = registered;
5910 applyTheme.defaultTheme = function() { return defaultTheme; };
5911 applyTheme.generateTheme = function(name) { generateTheme(THEMES[name], name, themeConfig.nonce); };
5912 applyTheme.setBrowserColor = enableBrowserColor;
5913
5914 return applyTheme;
5915
5916 /**
5917 * Determine is specified theme name is a valid, registered theme
5918 */
5919 function registered(themeName) {
5920 if (themeName === undefined || themeName === '') return true;
5921 return applyTheme.THEMES[themeName] !== undefined;
5922 }
5923
5924 /**
5925 * Get theme name for the element, then update with Theme CSS class
5926 */
5927 function inheritTheme (el, parent) {
5928 var ctrl = parent.controller('mdTheme');
5929 var attrThemeValue = el.attr('md-theme-watch');
5930 var watchTheme = (alwaysWatchTheme || angular.isDefined(attrThemeValue)) && attrThemeValue != 'false';
5931
5932 updateThemeClass(lookupThemeName());
5933
5934 if ((alwaysWatchTheme && !registerChangeCallback()) || (!alwaysWatchTheme && watchTheme)) {
5935 el.on('$destroy', $rootScope.$watch(lookupThemeName, updateThemeClass) );
5936 }
5937
5938 /**
5939 * Find the theme name from the parent controller or element data
5940 */
5941 function lookupThemeName() {
5942 // As a few components (dialog) add their controllers later, we should also watch for a controller init.
5943 ctrl = parent.controller('mdTheme') || el.data('$mdThemeController');
5944 return ctrl && ctrl.$mdTheme || (defaultTheme == 'default' ? '' : defaultTheme);
5945 }
5946
5947 /**
5948 * Remove old theme class and apply a new one
5949 * NOTE: if not a valid theme name, then the current name is not changed
5950 */
5951 function updateThemeClass(theme) {
5952 if (!theme) return;
5953 if (!registered(theme)) {
5954 $log.warn('Attempted to use unregistered theme \'' + theme + '\'. ' +
5955 'Register it with $mdThemingProvider.theme().');
5956 }
5957
5958 var oldTheme = el.data('$mdThemeName');
5959 if (oldTheme) el.removeClass('md-' + oldTheme +'-theme');
5960 el.addClass('md-' + theme + '-theme');
5961 el.data('$mdThemeName', theme);
5962 if (ctrl) {
5963 el.data('$mdThemeController', ctrl);
5964 }
5965 }
5966
5967 /**
5968 * Register change callback with parent mdTheme controller
5969 */
5970 function registerChangeCallback() {
5971 var parentController = parent.controller('mdTheme');
5972 if (!parentController) return false;
5973 el.on('$destroy', parentController.registerChanges( function() {
5974 updateThemeClass(lookupThemeName());
5975 }));
5976 return true;
5977 }
5978 }
5979
5980 }
5981}
5982
5983function ThemingDirective($mdTheming, $interpolate, $log) {
5984 return {
5985 priority: 100,
5986 link: {
5987 pre: function(scope, el, attrs) {
5988 var registeredCallbacks = [];
5989 var ctrl = {
5990 registerChanges: function (cb, context) {
5991 if (context) {
5992 cb = angular.bind(context, cb);
5993 }
5994
5995 registeredCallbacks.push(cb);
5996
5997 return function () {
5998 var index = registeredCallbacks.indexOf(cb);
5999
6000 if (index > -1) {
6001 registeredCallbacks.splice(index, 1);
6002 }
6003 };
6004 },
6005 $setTheme: function (theme) {
6006 if (!$mdTheming.registered(theme)) {
6007 $log.warn('attempted to use unregistered theme \'' + theme + '\'');
6008 }
6009 ctrl.$mdTheme = theme;
6010
6011 registeredCallbacks.forEach(function (cb) {
6012 cb();
6013 });
6014 }
6015 };
6016 el.data('$mdThemeController', ctrl);
6017 ctrl.$setTheme($interpolate(attrs.mdTheme)(scope));
6018 attrs.$observe('mdTheme', ctrl.$setTheme);
6019 }
6020 }
6021 };
6022}
6023
6024/**
6025 * Special directive that will disable ALL runtime Theme style generation and DOM injection
6026 *
6027 * <link rel="stylesheet" href="angular-material.min.css">
6028 * <link rel="stylesheet" href="angular-material.themes.css">
6029 *
6030 * <body md-themes-disabled>
6031 * ...
6032 * </body>
6033 *
6034 * Note: Using md-themes-css directive requires the developer to load external
6035 * theme stylesheets; e.g. custom themes from Material-Tools:
6036 *
6037 * `angular-material.themes.css`
6038 *
6039 * Another option is to use the ThemingProvider to configure and disable the attribute
6040 * conversions; this would obviate the use of the `md-themes-css` directive
6041 *
6042 */
6043function disableThemesDirective() {
6044 themeConfig.disableTheming = true;
6045
6046 // Return a 1x-only, first-match attribute directive
6047 return {
6048 restrict : 'A',
6049 priority : '900'
6050 };
6051}
6052
6053function ThemableDirective($mdTheming) {
6054 return $mdTheming;
6055}
6056
6057function parseRules(theme, colorType, rules) {
6058 checkValidPalette(theme, colorType);
6059
6060 rules = rules.replace(/THEME_NAME/g, theme.name);
6061 var generatedRules = [];
6062 var color = theme.colors[colorType];
6063
6064 var themeNameRegex = new RegExp('\\.md-' + theme.name + '-theme', 'g');
6065 // Matches '{{ primary-color }}', etc
6066 var hueRegex = new RegExp('(\'|")?{{\\s*(' + colorType + ')-(color|contrast)-?(\\d\\.?\\d*)?\\s*}}(\"|\')?','g');
6067 var simpleVariableRegex = /'?"?\{\{\s*([a-zA-Z]+)-(A?\d+|hue\-[0-3]|shadow|default)-?(\d\.?\d*)?(contrast)?\s*\}\}'?"?/g;
6068 var palette = PALETTES[color.name];
6069
6070 // find and replace simple variables where we use a specific hue, not an entire palette
6071 // eg. "{{primary-100}}"
6072 //\(' + THEME_COLOR_TYPES.join('\|') + '\)'
6073 rules = rules.replace(simpleVariableRegex, function(match, colorType, hue, opacity, contrast) {
6074 if (colorType === 'foreground') {
6075 if (hue == 'shadow') {
6076 return theme.foregroundShadow;
6077 } else {
6078 return theme.foregroundPalette[hue] || theme.foregroundPalette['1'];
6079 }
6080 }
6081
6082 // `default` is also accepted as a hue-value, because the background palettes are
6083 // using it as a name for the default hue.
6084 if (hue.indexOf('hue') === 0 || hue === 'default') {
6085 hue = theme.colors[colorType].hues[hue];
6086 }
6087
6088 return rgba( (PALETTES[ theme.colors[colorType].name ][hue] || '')[contrast ? 'contrast' : 'value'], opacity );
6089 });
6090
6091 // For each type, generate rules for each hue (ie. default, md-hue-1, md-hue-2, md-hue-3)
6092 angular.forEach(color.hues, function(hueValue, hueName) {
6093 var newRule = rules
6094 .replace(hueRegex, function(match, _, colorType, hueType, opacity) {
6095 return rgba(palette[hueValue][hueType === 'color' ? 'value' : 'contrast'], opacity);
6096 });
6097 if (hueName !== 'default') {
6098 newRule = newRule.replace(themeNameRegex, '.md-' + theme.name + '-theme.md-' + hueName);
6099 }
6100
6101 // Don't apply a selector rule to the default theme, making it easier to override
6102 // styles of the base-component
6103 if (theme.name == 'default') {
6104 var themeRuleRegex = /((?:(?:(?: |>|\.|\w|-|:|\(|\)|\[|\]|"|'|=)+) )?)((?:(?:\w|\.|-)+)?)\.md-default-theme((?: |>|\.|\w|-|:|\(|\)|\[|\]|"|'|=)*)/g;
6105 newRule = newRule.replace(themeRuleRegex, function(match, prefix, target, suffix) {
6106 return match + ', ' + prefix + target + suffix;
6107 });
6108 }
6109 generatedRules.push(newRule);
6110 });
6111
6112 return generatedRules;
6113}
6114
6115var rulesByType = {};
6116
6117// Generate our themes at run time given the state of THEMES and PALETTES
6118function generateAllThemes($injector, $mdTheming) {
6119 var head = document.head;
6120 var firstChild = head ? head.firstElementChild : null;
6121 var themeCss = !themeConfig.disableTheming && $injector.has('$MD_THEME_CSS') ? $injector.get('$MD_THEME_CSS') : '';
6122
6123 // Append our custom registered styles to the theme stylesheet.
6124 themeCss += themeConfig.registeredStyles.join('');
6125
6126 if ( !firstChild ) return;
6127 if (themeCss.length === 0) return; // no rules, so no point in running this expensive task
6128
6129 // Expose contrast colors for palettes to ensure that text is always readable
6130 angular.forEach(PALETTES, sanitizePalette);
6131
6132 // MD_THEME_CSS is a string generated by the build process that includes all the themable
6133 // components as templates
6134
6135 // Break the CSS into individual rules
6136 var rules = themeCss
6137 .split(/\}(?!(\}|'|"|;))/)
6138 .filter(function(rule) { return rule && rule.trim().length; })
6139 .map(function(rule) { return rule.trim() + '}'; });
6140
6141
6142 var ruleMatchRegex = new RegExp('md-(' + THEME_COLOR_TYPES.join('|') + ')', 'g');
6143
6144 THEME_COLOR_TYPES.forEach(function(type) {
6145 rulesByType[type] = '';
6146 });
6147
6148
6149 // Sort the rules based on type, allowing us to do color substitution on a per-type basis
6150 rules.forEach(function(rule) {
6151 var match = rule.match(ruleMatchRegex);
6152 // First: test that if the rule has '.md-accent', it goes into the accent set of rules
6153 for (var i = 0, type; type = THEME_COLOR_TYPES[i]; i++) {
6154 if (rule.indexOf('.md-' + type) > -1) {
6155 return rulesByType[type] += rule;
6156 }
6157 }
6158
6159 // If no eg 'md-accent' class is found, try to just find 'accent' in the rule and guess from
6160 // there
6161 for (i = 0; type = THEME_COLOR_TYPES[i]; i++) {
6162 if (rule.indexOf(type) > -1) {
6163 return rulesByType[type] += rule;
6164 }
6165 }
6166
6167 // Default to the primary array
6168 return rulesByType[DEFAULT_COLOR_TYPE] += rule;
6169 });
6170
6171 // If themes are being generated on-demand, quit here. The user will later manually
6172 // call generateTheme to do this on a theme-by-theme basis.
6173 if (themeConfig.generateOnDemand) return;
6174
6175 angular.forEach($mdTheming.THEMES, function(theme) {
6176 if (!GENERATED[theme.name] && !($mdTheming.defaultTheme() !== 'default' && theme.name === 'default')) {
6177 generateTheme(theme, theme.name, themeConfig.nonce);
6178 }
6179 });
6180
6181
6182 // *************************
6183 // Internal functions
6184 // *************************
6185
6186 // The user specifies a 'default' contrast color as either light or dark,
6187 // then explicitly lists which hues are the opposite contrast (eg. A100 has dark, A200 has light)
6188 function sanitizePalette(palette, name) {
6189 var defaultContrast = palette.contrastDefaultColor;
6190 var lightColors = palette.contrastLightColors || [];
6191 var strongLightColors = palette.contrastStrongLightColors || [];
6192 var darkColors = palette.contrastDarkColors || [];
6193
6194 // These colors are provided as space-separated lists
6195 if (typeof lightColors === 'string') lightColors = lightColors.split(' ');
6196 if (typeof strongLightColors === 'string') strongLightColors = strongLightColors.split(' ');
6197 if (typeof darkColors === 'string') darkColors = darkColors.split(' ');
6198
6199 // Cleanup after ourselves
6200 delete palette.contrastDefaultColor;
6201 delete palette.contrastLightColors;
6202 delete palette.contrastStrongLightColors;
6203 delete palette.contrastDarkColors;
6204
6205 // Change { 'A100': '#fffeee' } to { 'A100': { value: '#fffeee', contrast:DARK_CONTRAST_COLOR }
6206 angular.forEach(palette, function(hueValue, hueName) {
6207 if (angular.isObject(hueValue)) return; // Already converted
6208 // Map everything to rgb colors
6209 var rgbValue = colorToRgbaArray(hueValue);
6210 if (!rgbValue) {
6211 throw new Error("Color %1, in palette %2's hue %3, is invalid. Hex or rgb(a) color expected."
6212 .replace('%1', hueValue)
6213 .replace('%2', palette.name)
6214 .replace('%3', hueName));
6215 }
6216
6217 palette[hueName] = {
6218 hex: palette[hueName],
6219 value: rgbValue,
6220 contrast: getContrastColor()
6221 };
6222 function getContrastColor() {
6223 if (defaultContrast === 'light') {
6224 if (darkColors.indexOf(hueName) > -1) {
6225 return DARK_CONTRAST_COLOR;
6226 } else {
6227 return strongLightColors.indexOf(hueName) > -1 ? STRONG_LIGHT_CONTRAST_COLOR
6228 : LIGHT_CONTRAST_COLOR;
6229 }
6230 } else {
6231 if (lightColors.indexOf(hueName) > -1) {
6232 return strongLightColors.indexOf(hueName) > -1 ? STRONG_LIGHT_CONTRAST_COLOR
6233 : LIGHT_CONTRAST_COLOR;
6234 } else {
6235 return DARK_CONTRAST_COLOR;
6236 }
6237 }
6238 }
6239 });
6240 }
6241}
6242
6243function generateTheme(theme, name, nonce) {
6244 var head = document.head;
6245 var firstChild = head ? head.firstElementChild : null;
6246
6247 if (!GENERATED[name]) {
6248 // For each theme, use the color palettes specified for
6249 // `primary`, `warn` and `accent` to generate CSS rules.
6250 THEME_COLOR_TYPES.forEach(function(colorType) {
6251 var styleStrings = parseRules(theme, colorType, rulesByType[colorType]);
6252 while (styleStrings.length) {
6253 var styleContent = styleStrings.shift();
6254 if (styleContent) {
6255 var style = document.createElement('style');
6256 style.setAttribute('md-theme-style', '');
6257 if (nonce) {
6258 style.setAttribute('nonce', nonce);
6259 }
6260 style.appendChild(document.createTextNode(styleContent));
6261 head.insertBefore(style, firstChild);
6262 }
6263 }
6264 });
6265
6266 GENERATED[theme.name] = true;
6267 }
6268
6269}
6270
6271
6272function checkValidPalette(theme, colorType) {
6273 // If theme attempts to use a palette that doesnt exist, throw error
6274 if (!PALETTES[ (theme.colors[colorType] || {}).name ]) {
6275 throw new Error(
6276 "You supplied an invalid color palette for theme %1's %2 palette. Available palettes: %3"
6277 .replace('%1', theme.name)
6278 .replace('%2', colorType)
6279 .replace('%3', Object.keys(PALETTES).join(', '))
6280 );
6281 }
6282}
6283
6284function colorToRgbaArray(clr) {
6285 if (angular.isArray(clr) && clr.length == 3) return clr;
6286 if (/^rgb/.test(clr)) {
6287 return clr.replace(/(^\s*rgba?\(|\)\s*$)/g, '').split(',').map(function(value, i) {
6288 return i == 3 ? parseFloat(value, 10) : parseInt(value, 10);
6289 });
6290 }
6291 if (clr.charAt(0) == '#') clr = clr.substring(1);
6292 if (!/^([a-fA-F0-9]{3}){1,2}$/g.test(clr)) return;
6293
6294 var dig = clr.length / 3;
6295 var red = clr.substr(0, dig);
6296 var grn = clr.substr(dig, dig);
6297 var blu = clr.substr(dig * 2);
6298 if (dig === 1) {
6299 red += red;
6300 grn += grn;
6301 blu += blu;
6302 }
6303 return [parseInt(red, 16), parseInt(grn, 16), parseInt(blu, 16)];
6304}
6305
6306function rgba(rgbArray, opacity) {
6307 if ( !rgbArray ) return "rgb('0,0,0')";
6308
6309 if (rgbArray.length == 4) {
6310 rgbArray = angular.copy(rgbArray);
6311 opacity ? rgbArray.pop() : opacity = rgbArray.pop();
6312 }
6313 return opacity && (typeof opacity == 'number' || (typeof opacity == 'string' && opacity.length)) ?
6314 'rgba(' + rgbArray.join(',') + ',' + opacity + ')' :
6315 'rgb(' + rgbArray.join(',') + ')';
6316}
6317
6318
6319})(window.angular);
6320
6321})();
6322(function(){
6323"use strict";
6324
6325// Polyfill angular < 1.4 (provide $animateCss)
6326angular
6327 .module('material.core')
6328 .factory('$$mdAnimate', ["$q", "$timeout", "$mdConstant", "$animateCss", function($q, $timeout, $mdConstant, $animateCss){
6329
6330 // Since $$mdAnimate is injected into $mdUtil... use a wrapper function
6331 // to subsequently inject $mdUtil as an argument to the AnimateDomUtils
6332
6333 return function($mdUtil) {
6334 return AnimateDomUtils( $mdUtil, $q, $timeout, $mdConstant, $animateCss);
6335 };
6336 }]);
6337
6338/**
6339 * Factory function that requires special injections
6340 */
6341function AnimateDomUtils($mdUtil, $q, $timeout, $mdConstant, $animateCss) {
6342 var self;
6343 return self = {
6344 /**
6345 *
6346 */
6347 translate3d : function( target, from, to, options ) {
6348 return $animateCss(target,{
6349 from:from,
6350 to:to,
6351 addClass:options.transitionInClass,
6352 removeClass:options.transitionOutClass
6353 })
6354 .start()
6355 .then(function(){
6356 // Resolve with reverser function...
6357 return reverseTranslate;
6358 });
6359
6360 /**
6361 * Specific reversal of the request translate animation above...
6362 */
6363 function reverseTranslate (newFrom) {
6364 return $animateCss(target, {
6365 to: newFrom || from,
6366 addClass: options.transitionOutClass,
6367 removeClass: options.transitionInClass
6368 }).start();
6369
6370 }
6371 },
6372
6373 /**
6374 * Listen for transitionEnd event (with optional timeout)
6375 * Announce completion or failure via promise handlers
6376 */
6377 waitTransitionEnd: function (element, opts) {
6378 var TIMEOUT = 3000; // fallback is 3 secs
6379
6380 return $q(function(resolve, reject){
6381 opts = opts || { };
6382
6383 // If there is no transition is found, resolve immediately
6384 //
6385 // NOTE: using $mdUtil.nextTick() causes delays/issues
6386 if (noTransitionFound(opts.cachedTransitionStyles)) {
6387 TIMEOUT = 0;
6388 }
6389
6390 var timer = $timeout(finished, opts.timeout || TIMEOUT);
6391 element.on($mdConstant.CSS.TRANSITIONEND, finished);
6392
6393 /**
6394 * Upon timeout or transitionEnd, reject or resolve (respectively) this promise.
6395 * NOTE: Make sure this transitionEnd didn't bubble up from a child
6396 */
6397 function finished(ev) {
6398 if ( ev && ev.target !== element[0]) return;
6399
6400 if ( ev ) $timeout.cancel(timer);
6401 element.off($mdConstant.CSS.TRANSITIONEND, finished);
6402
6403 // Never reject since ngAnimate may cause timeouts due missed transitionEnd events
6404 resolve();
6405
6406 }
6407
6408 /**
6409 * Checks whether or not there is a transition.
6410 *
6411 * @param styles The cached styles to use for the calculation. If null, getComputedStyle()
6412 * will be used.
6413 *
6414 * @returns {boolean} True if there is no transition/duration; false otherwise.
6415 */
6416 function noTransitionFound(styles) {
6417 styles = styles || window.getComputedStyle(element[0]);
6418
6419 return styles.transitionDuration == '0s' || (!styles.transition && !styles.transitionProperty);
6420 }
6421
6422 });
6423 },
6424
6425 calculateTransformValues: function (element, originator) {
6426 var origin = originator.element;
6427 var bounds = originator.bounds;
6428
6429 if (origin || bounds) {
6430 var originBnds = origin ? self.clientRect(origin) || currentBounds() : self.copyRect(bounds);
6431 var dialogRect = self.copyRect(element[0].getBoundingClientRect());
6432 var dialogCenterPt = self.centerPointFor(dialogRect);
6433 var originCenterPt = self.centerPointFor(originBnds);
6434
6435 return {
6436 centerX: originCenterPt.x - dialogCenterPt.x,
6437 centerY: originCenterPt.y - dialogCenterPt.y,
6438 scaleX: Math.round(100 * Math.min(0.5, originBnds.width / dialogRect.width)) / 100,
6439 scaleY: Math.round(100 * Math.min(0.5, originBnds.height / dialogRect.height)) / 100
6440 };
6441 }
6442 return {centerX: 0, centerY: 0, scaleX: 0.5, scaleY: 0.5};
6443
6444 /**
6445 * This is a fallback if the origin information is no longer valid, then the
6446 * origin bounds simply becomes the current bounds for the dialogContainer's parent
6447 */
6448 function currentBounds() {
6449 var cntr = element ? element.parent() : null;
6450 var parent = cntr ? cntr.parent() : null;
6451
6452 return parent ? self.clientRect(parent) : null;
6453 }
6454 },
6455
6456 /**
6457 * Calculate the zoom transform from dialog to origin.
6458 *
6459 * We use this to set the dialog position immediately;
6460 * then the md-transition-in actually translates back to
6461 * `translate3d(0,0,0) scale(1.0)`...
6462 *
6463 * NOTE: all values are rounded to the nearest integer
6464 */
6465 calculateZoomToOrigin: function (element, originator) {
6466 var zoomTemplate = "translate3d( {centerX}px, {centerY}px, 0 ) scale( {scaleX}, {scaleY} )";
6467 var buildZoom = angular.bind(null, $mdUtil.supplant, zoomTemplate);
6468
6469 return buildZoom(self.calculateTransformValues(element, originator));
6470 },
6471
6472 /**
6473 * Calculate the slide transform from panel to origin.
6474 * NOTE: all values are rounded to the nearest integer
6475 */
6476 calculateSlideToOrigin: function (element, originator) {
6477 var slideTemplate = "translate3d( {centerX}px, {centerY}px, 0 )";
6478 var buildSlide = angular.bind(null, $mdUtil.supplant, slideTemplate);
6479
6480 return buildSlide(self.calculateTransformValues(element, originator));
6481 },
6482
6483 /**
6484 * Enhance raw values to represent valid css stylings...
6485 */
6486 toCss : function( raw ) {
6487 var css = { };
6488 var lookups = 'left top right bottom width height x y min-width min-height max-width max-height';
6489
6490 angular.forEach(raw, function(value,key) {
6491 if ( angular.isUndefined(value) ) return;
6492
6493 if ( lookups.indexOf(key) >= 0 ) {
6494 css[key] = value + 'px';
6495 } else {
6496 switch (key) {
6497 case 'transition':
6498 convertToVendor(key, $mdConstant.CSS.TRANSITION, value);
6499 break;
6500 case 'transform':
6501 convertToVendor(key, $mdConstant.CSS.TRANSFORM, value);
6502 break;
6503 case 'transformOrigin':
6504 convertToVendor(key, $mdConstant.CSS.TRANSFORM_ORIGIN, value);
6505 break;
6506 case 'font-size':
6507 css['font-size'] = value; // font sizes aren't always in px
6508 break;
6509 }
6510 }
6511 });
6512
6513 return css;
6514
6515 function convertToVendor(key, vendor, value) {
6516 angular.forEach(vendor.split(' '), function (key) {
6517 css[key] = value;
6518 });
6519 }
6520 },
6521
6522 /**
6523 * Convert the translate CSS value to key/value pair(s).
6524 */
6525 toTransformCss: function (transform, addTransition, transition) {
6526 var css = {};
6527 angular.forEach($mdConstant.CSS.TRANSFORM.split(' '), function (key) {
6528 css[key] = transform;
6529 });
6530
6531 if (addTransition) {
6532 transition = transition || "all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) !important";
6533 css.transition = transition;
6534 }
6535
6536 return css;
6537 },
6538
6539 /**
6540 * Clone the Rect and calculate the height/width if needed
6541 */
6542 copyRect: function (source, destination) {
6543 if (!source) return null;
6544
6545 destination = destination || {};
6546
6547 angular.forEach('left top right bottom width height'.split(' '), function (key) {
6548 destination[key] = Math.round(source[key]);
6549 });
6550
6551 destination.width = destination.width || (destination.right - destination.left);
6552 destination.height = destination.height || (destination.bottom - destination.top);
6553
6554 return destination;
6555 },
6556
6557 /**
6558 * Calculate ClientRect of element; return null if hidden or zero size
6559 */
6560 clientRect: function (element) {
6561 var bounds = angular.element(element)[0].getBoundingClientRect();
6562 var isPositiveSizeClientRect = function (rect) {
6563 return rect && (rect.width > 0) && (rect.height > 0);
6564 };
6565
6566 // If the event origin element has zero size, it has probably been hidden.
6567 return isPositiveSizeClientRect(bounds) ? self.copyRect(bounds) : null;
6568 },
6569
6570 /**
6571 * Calculate 'rounded' center point of Rect
6572 */
6573 centerPointFor: function (targetRect) {
6574 return targetRect ? {
6575 x: Math.round(targetRect.left + (targetRect.width / 2)),
6576 y: Math.round(targetRect.top + (targetRect.height / 2))
6577 } : { x : 0, y : 0 };
6578 }
6579
6580 };
6581}
6582
6583
6584})();
6585(function(){
6586"use strict";
6587
6588"use strict";
6589
6590if (angular.version.minor >= 4) {
6591 angular.module('material.core.animate', []);
6592} else {
6593(function() {
6594
6595 var forEach = angular.forEach;
6596
6597 var WEBKIT = angular.isDefined(document.documentElement.style.WebkitAppearance);
6598 var TRANSITION_PROP = WEBKIT ? 'WebkitTransition' : 'transition';
6599 var ANIMATION_PROP = WEBKIT ? 'WebkitAnimation' : 'animation';
6600 var PREFIX = WEBKIT ? '-webkit-' : '';
6601
6602 var TRANSITION_EVENTS = (WEBKIT ? 'webkitTransitionEnd ' : '') + 'transitionend';
6603 var ANIMATION_EVENTS = (WEBKIT ? 'webkitAnimationEnd ' : '') + 'animationend';
6604
6605 var $$ForceReflowFactory = ['$document', function($document) {
6606 return function() {
6607 return $document[0].body.clientWidth + 1;
6608 }
6609 }];
6610
6611 var $$rAFMutexFactory = ['$$rAF', function($$rAF) {
6612 return function() {
6613 var passed = false;
6614 $$rAF(function() {
6615 passed = true;
6616 });
6617 return function(fn) {
6618 passed ? fn() : $$rAF(fn);
6619 };
6620 };
6621 }];
6622
6623 var $$AnimateRunnerFactory = ['$q', '$$rAFMutex', function($q, $$rAFMutex) {
6624 var INITIAL_STATE = 0;
6625 var DONE_PENDING_STATE = 1;
6626 var DONE_COMPLETE_STATE = 2;
6627
6628 function AnimateRunner(host) {
6629 this.setHost(host);
6630
6631 this._doneCallbacks = [];
6632 this._runInAnimationFrame = $$rAFMutex();
6633 this._state = 0;
6634 }
6635
6636 AnimateRunner.prototype = {
6637 setHost: function(host) {
6638 this.host = host || {};
6639 },
6640
6641 done: function(fn) {
6642 if (this._state === DONE_COMPLETE_STATE) {
6643 fn();
6644 } else {
6645 this._doneCallbacks.push(fn);
6646 }
6647 },
6648
6649 progress: angular.noop,
6650
6651 getPromise: function() {
6652 if (!this.promise) {
6653 var self = this;
6654 this.promise = $q(function(resolve, reject) {
6655 self.done(function(status) {
6656 status === false ? reject() : resolve();
6657 });
6658 });
6659 }
6660 return this.promise;
6661 },
6662
6663 then: function(resolveHandler, rejectHandler) {
6664 return this.getPromise().then(resolveHandler, rejectHandler);
6665 },
6666
6667 'catch': function(handler) {
6668 return this.getPromise()['catch'](handler);
6669 },
6670
6671 'finally': function(handler) {
6672 return this.getPromise()['finally'](handler);
6673 },
6674
6675 pause: function() {
6676 if (this.host.pause) {
6677 this.host.pause();
6678 }
6679 },
6680
6681 resume: function() {
6682 if (this.host.resume) {
6683 this.host.resume();
6684 }
6685 },
6686
6687 end: function() {
6688 if (this.host.end) {
6689 this.host.end();
6690 }
6691 this._resolve(true);
6692 },
6693
6694 cancel: function() {
6695 if (this.host.cancel) {
6696 this.host.cancel();
6697 }
6698 this._resolve(false);
6699 },
6700
6701 complete: function(response) {
6702 var self = this;
6703 if (self._state === INITIAL_STATE) {
6704 self._state = DONE_PENDING_STATE;
6705 self._runInAnimationFrame(function() {
6706 self._resolve(response);
6707 });
6708 }
6709 },
6710
6711 _resolve: function(response) {
6712 if (this._state !== DONE_COMPLETE_STATE) {
6713 forEach(this._doneCallbacks, function(fn) {
6714 fn(response);
6715 });
6716 this._doneCallbacks.length = 0;
6717 this._state = DONE_COMPLETE_STATE;
6718 }
6719 }
6720 };
6721
6722 // Polyfill AnimateRunner.all which is used by input animations
6723 AnimateRunner.all = function(runners, callback) {
6724 var count = 0;
6725 var status = true;
6726 forEach(runners, function(runner) {
6727 runner.done(onProgress);
6728 });
6729
6730 function onProgress(response) {
6731 status = status && response;
6732 if (++count === runners.length) {
6733 callback(status);
6734 }
6735 }
6736 };
6737
6738 return AnimateRunner;
6739 }];
6740
6741 angular
6742 .module('material.core.animate', [])
6743 .factory('$$forceReflow', $$ForceReflowFactory)
6744 .factory('$$AnimateRunner', $$AnimateRunnerFactory)
6745 .factory('$$rAFMutex', $$rAFMutexFactory)
6746 .factory('$animateCss', ['$window', '$$rAF', '$$AnimateRunner', '$$forceReflow', '$$jqLite', '$timeout', '$animate',
6747 function($window, $$rAF, $$AnimateRunner, $$forceReflow, $$jqLite, $timeout, $animate) {
6748
6749 function init(element, options) {
6750
6751 var temporaryStyles = [];
6752 var node = getDomNode(element);
6753 var areAnimationsAllowed = node && $animate.enabled();
6754
6755 var hasCompleteStyles = false;
6756 var hasCompleteClasses = false;
6757
6758 if (areAnimationsAllowed) {
6759 if (options.transitionStyle) {
6760 temporaryStyles.push([PREFIX + 'transition', options.transitionStyle]);
6761 }
6762
6763 if (options.keyframeStyle) {
6764 temporaryStyles.push([PREFIX + 'animation', options.keyframeStyle]);
6765 }
6766
6767 if (options.delay) {
6768 temporaryStyles.push([PREFIX + 'transition-delay', options.delay + 's']);
6769 }
6770
6771 if (options.duration) {
6772 temporaryStyles.push([PREFIX + 'transition-duration', options.duration + 's']);
6773 }
6774
6775 hasCompleteStyles = options.keyframeStyle ||
6776 (options.to && (options.duration > 0 || options.transitionStyle));
6777 hasCompleteClasses = !!options.addClass || !!options.removeClass;
6778
6779 blockTransition(element, true);
6780 }
6781
6782 var hasCompleteAnimation = areAnimationsAllowed && (hasCompleteStyles || hasCompleteClasses);
6783
6784 applyAnimationFromStyles(element, options);
6785
6786 var animationClosed = false;
6787 var events, eventFn;
6788
6789 return {
6790 close: $window.close,
6791 start: function() {
6792 var runner = new $$AnimateRunner();
6793 waitUntilQuiet(function() {
6794 blockTransition(element, false);
6795 if (!hasCompleteAnimation) {
6796 return close();
6797 }
6798
6799 forEach(temporaryStyles, function(entry) {
6800 var key = entry[0];
6801 var value = entry[1];
6802 node.style[camelCase(key)] = value;
6803 });
6804
6805 applyClasses(element, options);
6806
6807 var timings = computeTimings(element);
6808 if (timings.duration === 0) {
6809 return close();
6810 }
6811
6812 var moreStyles = [];
6813
6814 if (options.easing) {
6815 if (timings.transitionDuration) {
6816 moreStyles.push([PREFIX + 'transition-timing-function', options.easing]);
6817 }
6818 if (timings.animationDuration) {
6819 moreStyles.push([PREFIX + 'animation-timing-function', options.easing]);
6820 }
6821 }
6822
6823 if (options.delay && timings.animationDelay) {
6824 moreStyles.push([PREFIX + 'animation-delay', options.delay + 's']);
6825 }
6826
6827 if (options.duration && timings.animationDuration) {
6828 moreStyles.push([PREFIX + 'animation-duration', options.duration + 's']);
6829 }
6830
6831 forEach(moreStyles, function(entry) {
6832 var key = entry[0];
6833 var value = entry[1];
6834 node.style[camelCase(key)] = value;
6835 temporaryStyles.push(entry);
6836 });
6837
6838 var maxDelay = timings.delay;
6839 var maxDelayTime = maxDelay * 1000;
6840 var maxDuration = timings.duration;
6841 var maxDurationTime = maxDuration * 1000;
6842 var startTime = Date.now();
6843
6844 events = [];
6845 if (timings.transitionDuration) {
6846 events.push(TRANSITION_EVENTS);
6847 }
6848 if (timings.animationDuration) {
6849 events.push(ANIMATION_EVENTS);
6850 }
6851 events = events.join(' ');
6852 eventFn = function(event) {
6853 event.stopPropagation();
6854 var ev = event.originalEvent || event;
6855 var timeStamp = ev.timeStamp || Date.now();
6856 var elapsedTime = parseFloat(ev.elapsedTime.toFixed(3));
6857 if (Math.max(timeStamp - startTime, 0) >= maxDelayTime && elapsedTime >= maxDuration) {
6858 close();
6859 }
6860 };
6861 element.on(events, eventFn);
6862
6863 applyAnimationToStyles(element, options);
6864
6865 $timeout(close, maxDelayTime + maxDurationTime * 1.5, false);
6866 });
6867
6868 return runner;
6869
6870 function close() {
6871 if (animationClosed) return;
6872 animationClosed = true;
6873
6874 if (events && eventFn) {
6875 element.off(events, eventFn);
6876 }
6877 applyClasses(element, options);
6878 applyAnimationStyles(element, options);
6879 forEach(temporaryStyles, function(entry) {
6880 node.style[camelCase(entry[0])] = '';
6881 });
6882 runner.complete(true);
6883 return runner;
6884 }
6885 }
6886 }
6887 }
6888
6889 function applyClasses(element, options) {
6890 if (options.addClass) {
6891 $$jqLite.addClass(element, options.addClass);
6892 options.addClass = null;
6893 }
6894 if (options.removeClass) {
6895 $$jqLite.removeClass(element, options.removeClass);
6896 options.removeClass = null;
6897 }
6898 }
6899
6900 function computeTimings(element) {
6901 var node = getDomNode(element);
6902 var cs = $window.getComputedStyle(node)
6903 var tdr = parseMaxTime(cs[prop('transitionDuration')]);
6904 var adr = parseMaxTime(cs[prop('animationDuration')]);
6905 var tdy = parseMaxTime(cs[prop('transitionDelay')]);
6906 var ady = parseMaxTime(cs[prop('animationDelay')]);
6907
6908 adr *= (parseInt(cs[prop('animationIterationCount')], 10) || 1);
6909 var duration = Math.max(adr, tdr);
6910 var delay = Math.max(ady, tdy);
6911
6912 return {
6913 duration: duration,
6914 delay: delay,
6915 animationDuration: adr,
6916 transitionDuration: tdr,
6917 animationDelay: ady,
6918 transitionDelay: tdy
6919 };
6920
6921 function prop(key) {
6922 return WEBKIT ? 'Webkit' + key.charAt(0).toUpperCase() + key.substr(1)
6923 : key;
6924 }
6925 }
6926
6927 function parseMaxTime(str) {
6928 var maxValue = 0;
6929 var values = (str || "").split(/\s*,\s*/);
6930 forEach(values, function(value) {
6931 // it's always safe to consider only second values and omit `ms` values since
6932 // getComputedStyle will always handle the conversion for us
6933 if (value.charAt(value.length - 1) == 's') {
6934 value = value.substring(0, value.length - 1);
6935 }
6936 value = parseFloat(value) || 0;
6937 maxValue = maxValue ? Math.max(value, maxValue) : value;
6938 });
6939 return maxValue;
6940 }
6941
6942 var cancelLastRAFRequest;
6943 var rafWaitQueue = [];
6944 function waitUntilQuiet(callback) {
6945 if (cancelLastRAFRequest) {
6946 cancelLastRAFRequest(); //cancels the request
6947 }
6948 rafWaitQueue.push(callback);
6949 cancelLastRAFRequest = $$rAF(function() {
6950 cancelLastRAFRequest = null;
6951
6952 // DO NOT REMOVE THIS LINE OR REFACTOR OUT THE `pageWidth` variable.
6953 // PLEASE EXAMINE THE `$$forceReflow` service to understand why.
6954 var pageWidth = $$forceReflow();
6955
6956 // we use a for loop to ensure that if the queue is changed
6957 // during this looping then it will consider new requests
6958 for (var i = 0; i < rafWaitQueue.length; i++) {
6959 rafWaitQueue[i](pageWidth);
6960 }
6961 rafWaitQueue.length = 0;
6962 });
6963 }
6964
6965 function applyAnimationStyles(element, options) {
6966 applyAnimationFromStyles(element, options);
6967 applyAnimationToStyles(element, options);
6968 }
6969
6970 function applyAnimationFromStyles(element, options) {
6971 if (options.from) {
6972 element.css(options.from);
6973 options.from = null;
6974 }
6975 }
6976
6977 function applyAnimationToStyles(element, options) {
6978 if (options.to) {
6979 element.css(options.to);
6980 options.to = null;
6981 }
6982 }
6983
6984 function getDomNode(element) {
6985 for (var i = 0; i < element.length; i++) {
6986 if (element[i].nodeType === 1) return element[i];
6987 }
6988 }
6989
6990 function blockTransition(element, bool) {
6991 var node = getDomNode(element);
6992 var key = camelCase(PREFIX + 'transition-delay');
6993 node.style[key] = bool ? '-9999s' : '';
6994 }
6995
6996 return init;
6997 }]);
6998
6999 /**
7000 * Older browsers [FF31] expect camelCase
7001 * property keys.
7002 * e.g.
7003 * animation-duration --> animationDuration
7004 */
7005 function camelCase(str) {
7006 return str.replace(/-[a-z]/g, function(str) {
7007 return str.charAt(1).toUpperCase();
7008 });
7009 }
7010
7011})();
7012
7013}
7014
7015})();
7016(function(){
7017"use strict";
7018
7019/**
7020 * @ngdoc module
7021 * @name material.components.autocomplete
7022 */
7023/*
7024 * @see js folder for autocomplete implementation
7025 */
7026angular.module('material.components.autocomplete', [
7027 'material.core',
7028 'material.components.icon',
7029 'material.components.virtualRepeat'
7030]);
7031
7032})();
7033(function(){
7034"use strict";
7035
7036/*
7037 * @ngdoc module
7038 * @name material.components.backdrop
7039 * @description Backdrop
7040 */
7041
7042/**
7043 * @ngdoc directive
7044 * @name mdBackdrop
7045 * @module material.components.backdrop
7046 *
7047 * @restrict E
7048 *
7049 * @description
7050 * `<md-backdrop>` is a backdrop element used by other components, such as dialog and bottom sheet.
7051 * Apply class `opaque` to make the backdrop use the theme backdrop color.
7052 *
7053 */
7054
7055angular
7056 .module('material.components.backdrop', ['material.core'])
7057 .directive('mdBackdrop', ["$mdTheming", "$mdUtil", "$animate", "$rootElement", "$window", "$log", "$$rAF", "$document", function BackdropDirective($mdTheming, $mdUtil, $animate, $rootElement, $window, $log, $$rAF, $document) {
7058 var ERROR_CSS_POSITION = '<md-backdrop> may not work properly in a scrolled, static-positioned parent container.';
7059
7060 return {
7061 restrict: 'E',
7062 link: postLink
7063 };
7064
7065 function postLink(scope, element, attrs) {
7066 // backdrop may be outside the $rootElement, tell ngAnimate to animate regardless
7067 if ($animate.pin) $animate.pin(element, $rootElement);
7068
7069 var bodyStyles;
7070
7071 $$rAF(function() {
7072 // If body scrolling has been disabled using mdUtil.disableBodyScroll(),
7073 // adjust the 'backdrop' height to account for the fixed 'body' top offset.
7074 // Note that this can be pretty expensive and is better done inside the $$rAF.
7075 bodyStyles = $window.getComputedStyle($document[0].body);
7076
7077 if (bodyStyles.position === 'fixed') {
7078 var resizeHandler = $mdUtil.debounce(function(){
7079 bodyStyles = $window.getComputedStyle($document[0].body);
7080 resize();
7081 }, 60, null, false);
7082
7083 resize();
7084 angular.element($window).on('resize', resizeHandler);
7085
7086 scope.$on('$destroy', function() {
7087 angular.element($window).off('resize', resizeHandler);
7088 });
7089 }
7090
7091 // Often $animate.enter() is used to append the backDrop element
7092 // so let's wait until $animate is done...
7093 var parent = element.parent();
7094
7095 if (parent.length) {
7096 if (parent[0].nodeName === 'BODY') {
7097 element.css('position', 'fixed');
7098 }
7099
7100 var styles = $window.getComputedStyle(parent[0]);
7101
7102 if (styles.position === 'static') {
7103 // backdrop uses position:absolute and will not work properly with parent position:static (default)
7104 $log.warn(ERROR_CSS_POSITION);
7105 }
7106
7107 // Only inherit the parent if the backdrop has a parent.
7108 $mdTheming.inherit(element, parent);
7109 }
7110 });
7111
7112 function resize() {
7113 var viewportHeight = parseInt(bodyStyles.height, 10) + Math.abs(parseInt(bodyStyles.top, 10));
7114 element.css('height', viewportHeight + 'px');
7115 }
7116 }
7117
7118 }]);
7119
7120})();
7121(function(){
7122"use strict";
7123
7124/**
7125 * @ngdoc module
7126 * @name material.components.bottomSheet
7127 * @description
7128 * BottomSheet
7129 */
7130MdBottomSheetDirective.$inject = ["$mdBottomSheet"];
7131MdBottomSheetProvider.$inject = ["$$interimElementProvider"];
7132angular
7133 .module('material.components.bottomSheet', [
7134 'material.core',
7135 'material.components.backdrop'
7136 ])
7137 .directive('mdBottomSheet', MdBottomSheetDirective)
7138 .provider('$mdBottomSheet', MdBottomSheetProvider);
7139
7140/* @ngInject */
7141function MdBottomSheetDirective($mdBottomSheet) {
7142 return {
7143 restrict: 'E',
7144 link : function postLink(scope, element) {
7145 element.addClass('_md'); // private md component indicator for styling
7146
7147 // When navigation force destroys an interimElement, then
7148 // listen and $destroy() that interim instance...
7149 scope.$on('$destroy', function() {
7150 $mdBottomSheet.destroy();
7151 });
7152 }
7153 };
7154}
7155
7156
7157/**
7158 * @ngdoc service
7159 * @name $mdBottomSheet
7160 * @module material.components.bottomSheet
7161 *
7162 * @description
7163 * `$mdBottomSheet` opens a bottom sheet over the app and provides a simple promise API.
7164 *
7165 * ## Restrictions
7166 *
7167 * - The bottom sheet's template must have an outer `<md-bottom-sheet>` element.
7168 * - Add the `md-grid` class to the bottom sheet for a grid layout.
7169 * - Add the `md-list` class to the bottom sheet for a list layout.
7170 *
7171 * @usage
7172 * <hljs lang="html">
7173 * <div ng-controller="MyController">
7174 * <md-button ng-click="openBottomSheet()">
7175 * Open a Bottom Sheet!
7176 * </md-button>
7177 * </div>
7178 * </hljs>
7179 * <hljs lang="js">
7180 * var app = angular.module('app', ['ngMaterial']);
7181 * app.controller('MyController', function($scope, $mdBottomSheet) {
7182 * $scope.openBottomSheet = function() {
7183 * $mdBottomSheet.show({
7184 * template: '<md-bottom-sheet>Hello!</md-bottom-sheet>'
7185 * });
7186 * };
7187 * });
7188 * </hljs>
7189 */
7190
7191 /**
7192 * @ngdoc method
7193 * @name $mdBottomSheet#show
7194 *
7195 * @description
7196 * Show a bottom sheet with the specified options.
7197 *
7198 * @param {object} options An options object, with the following properties:
7199 *
7200 * - `templateUrl` - `{string=}`: The url of an html template file that will
7201 * be used as the content of the bottom sheet. Restrictions: the template must
7202 * have an outer `md-bottom-sheet` element.
7203 * - `template` - `{string=}`: Same as templateUrl, except this is an actual
7204 * template string.
7205 * - `scope` - `{object=}`: the scope to link the template / controller to. If none is specified, it will create a new child scope.
7206 * This scope will be destroyed when the bottom sheet is removed unless `preserveScope` is set to true.
7207 * - `preserveScope` - `{boolean=}`: whether to preserve the scope when the element is removed. Default is false
7208 * - `controller` - `{string=}`: The controller to associate with this bottom sheet.
7209 * - `locals` - `{string=}`: An object containing key/value pairs. The keys will
7210 * be used as names of values to inject into the controller. For example,
7211 * `locals: {three: 3}` would inject `three` into the controller with the value
7212 * of 3.
7213 * - `clickOutsideToClose` - `{boolean=}`: Whether the user can click outside the bottom sheet to
7214 * close it. Default true.
7215 * - `disableBackdrop` - `{boolean=}`: When set to true, the bottomsheet will not show a backdrop.
7216 * - `escapeToClose` - `{boolean=}`: Whether the user can press escape to close the bottom sheet.
7217 * Default true.
7218 * - `resolve` - `{object=}`: Similar to locals, except it takes promises as values
7219 * and the bottom sheet will not open until the promises resolve.
7220 * - `controllerAs` - `{string=}`: An alias to assign the controller to on the scope.
7221 * - `parent` - `{element=}`: The element to append the bottom sheet to. The `parent` may be a `function`, `string`,
7222 * `object`, or null. Defaults to appending to the body of the root element (or the root element) of the application.
7223 * e.g. angular.element(document.getElementById('content')) or "#content"
7224 * - `disableParentScroll` - `{boolean=}`: Whether to disable scrolling while the bottom sheet is open.
7225 * Default true.
7226 *
7227 * @returns {promise} A promise that can be resolved with `$mdBottomSheet.hide()` or
7228 * rejected with `$mdBottomSheet.cancel()`.
7229 */
7230
7231/**
7232 * @ngdoc method
7233 * @name $mdBottomSheet#hide
7234 *
7235 * @description
7236 * Hide the existing bottom sheet and resolve the promise returned from
7237 * `$mdBottomSheet.show()`. This call will close the most recently opened/current bottomsheet (if any).
7238 *
7239 * @param {*=} response An argument for the resolved promise.
7240 *
7241 */
7242
7243/**
7244 * @ngdoc method
7245 * @name $mdBottomSheet#cancel
7246 *
7247 * @description
7248 * Hide the existing bottom sheet and reject the promise returned from
7249 * `$mdBottomSheet.show()`.
7250 *
7251 * @param {*=} response An argument for the rejected promise.
7252 *
7253 */
7254
7255function MdBottomSheetProvider($$interimElementProvider) {
7256 // how fast we need to flick down to close the sheet, pixels/ms
7257 bottomSheetDefaults.$inject = ["$animate", "$mdConstant", "$mdUtil", "$mdTheming", "$mdBottomSheet", "$rootElement", "$mdGesture", "$log"];
7258 var CLOSING_VELOCITY = 0.5;
7259 var PADDING = 80; // same as css
7260
7261 return $$interimElementProvider('$mdBottomSheet')
7262 .setDefaults({
7263 methods: ['disableParentScroll', 'escapeToClose', 'clickOutsideToClose'],
7264 options: bottomSheetDefaults
7265 });
7266
7267 /* @ngInject */
7268 function bottomSheetDefaults($animate, $mdConstant, $mdUtil, $mdTheming, $mdBottomSheet, $rootElement,
7269 $mdGesture, $log) {
7270 var backdrop;
7271
7272 return {
7273 themable: true,
7274 onShow: onShow,
7275 onRemove: onRemove,
7276 disableBackdrop: false,
7277 escapeToClose: true,
7278 clickOutsideToClose: true,
7279 disableParentScroll: true
7280 };
7281
7282
7283 function onShow(scope, element, options, controller) {
7284
7285 element = $mdUtil.extractElementByName(element, 'md-bottom-sheet');
7286
7287 // prevent tab focus or click focus on the bottom-sheet container
7288 element.attr('tabindex',"-1");
7289
7290 // Once the md-bottom-sheet has `ng-cloak` applied on his template the opening animation will not work properly.
7291 // This is a very common problem, so we have to notify the developer about this.
7292 if (element.hasClass('ng-cloak')) {
7293 var message = '$mdBottomSheet: using `<md-bottom-sheet ng-cloak >` will affect the bottom-sheet opening animations.';
7294 $log.warn( message, element[0] );
7295 }
7296
7297 if (!options.disableBackdrop) {
7298 // Add a backdrop that will close on click
7299 backdrop = $mdUtil.createBackdrop(scope, "md-bottom-sheet-backdrop md-opaque");
7300
7301 // Prevent mouse focus on backdrop; ONLY programatic focus allowed.
7302 // This allows clicks on backdrop to propogate to the $rootElement and
7303 // ESC key events to be detected properly.
7304
7305 backdrop[0].tabIndex = -1;
7306
7307 if (options.clickOutsideToClose) {
7308 backdrop.on('click', function() {
7309 $mdUtil.nextTick($mdBottomSheet.cancel,true);
7310 });
7311 }
7312
7313 $mdTheming.inherit(backdrop, options.parent);
7314
7315 $animate.enter(backdrop, options.parent, null);
7316 }
7317
7318 var bottomSheet = new BottomSheet(element, options.parent);
7319 options.bottomSheet = bottomSheet;
7320
7321 $mdTheming.inherit(bottomSheet.element, options.parent);
7322
7323 if (options.disableParentScroll) {
7324 options.restoreScroll = $mdUtil.disableScrollAround(bottomSheet.element, options.parent);
7325 }
7326
7327 return $animate.enter(bottomSheet.element, options.parent, backdrop)
7328 .then(function() {
7329 var focusable = $mdUtil.findFocusTarget(element) || angular.element(
7330 element[0].querySelector('button') ||
7331 element[0].querySelector('a') ||
7332 element[0].querySelector($mdUtil.prefixer('ng-click', true))
7333 ) || backdrop;
7334
7335 if (options.escapeToClose) {
7336 options.rootElementKeyupCallback = function(e) {
7337 if (e.keyCode === $mdConstant.KEY_CODE.ESCAPE) {
7338 $mdUtil.nextTick($mdBottomSheet.cancel,true);
7339 }
7340 };
7341
7342 $rootElement.on('keyup', options.rootElementKeyupCallback);
7343 focusable && focusable.focus();
7344 }
7345 });
7346
7347 }
7348
7349 function onRemove(scope, element, options) {
7350
7351 var bottomSheet = options.bottomSheet;
7352
7353 if (!options.disableBackdrop) $animate.leave(backdrop);
7354 return $animate.leave(bottomSheet.element).then(function() {
7355 if (options.disableParentScroll) {
7356 options.restoreScroll();
7357 delete options.restoreScroll;
7358 }
7359
7360 bottomSheet.cleanup();
7361 });
7362 }
7363
7364 /**
7365 * BottomSheet class to apply bottom-sheet behavior to an element
7366 */
7367 function BottomSheet(element, parent) {
7368 var deregister = $mdGesture.register(parent, 'drag', { horizontal: false });
7369 parent.on('$md.dragstart', onDragStart)
7370 .on('$md.drag', onDrag)
7371 .on('$md.dragend', onDragEnd);
7372
7373 return {
7374 element: element,
7375 cleanup: function cleanup() {
7376 deregister();
7377 parent.off('$md.dragstart', onDragStart);
7378 parent.off('$md.drag', onDrag);
7379 parent.off('$md.dragend', onDragEnd);
7380 }
7381 };
7382
7383 function onDragStart(ev) {
7384 // Disable transitions on transform so that it feels fast
7385 element.css($mdConstant.CSS.TRANSITION_DURATION, '0ms');
7386 }
7387
7388 function onDrag(ev) {
7389 var transform = ev.pointer.distanceY;
7390 if (transform < 5) {
7391 // Slow down drag when trying to drag up, and stop after PADDING
7392 transform = Math.max(-PADDING, transform / 2);
7393 }
7394 element.css($mdConstant.CSS.TRANSFORM, 'translate3d(0,' + (PADDING + transform) + 'px,0)');
7395 }
7396
7397 function onDragEnd(ev) {
7398 if (ev.pointer.distanceY > 0 &&
7399 (ev.pointer.distanceY > 20 || Math.abs(ev.pointer.velocityY) > CLOSING_VELOCITY)) {
7400 var distanceRemaining = element.prop('offsetHeight') - ev.pointer.distanceY;
7401 var transitionDuration = Math.min(distanceRemaining / ev.pointer.velocityY * 0.75, 500);
7402 element.css($mdConstant.CSS.TRANSITION_DURATION, transitionDuration + 'ms');
7403 $mdUtil.nextTick($mdBottomSheet.cancel,true);
7404 } else {
7405 element.css($mdConstant.CSS.TRANSITION_DURATION, '');
7406 element.css($mdConstant.CSS.TRANSFORM, '');
7407 }
7408 }
7409 }
7410
7411 }
7412
7413}
7414
7415})();
7416(function(){
7417"use strict";
7418
7419/**
7420 * @ngdoc module
7421 * @name material.components.button
7422 * @description
7423 *
7424 * Button
7425 */
7426MdButtonDirective.$inject = ["$mdButtonInkRipple", "$mdTheming", "$mdAria", "$timeout"];
7427MdAnchorDirective.$inject = ["$mdTheming"];
7428angular
7429 .module('material.components.button', [ 'material.core' ])
7430 .directive('mdButton', MdButtonDirective)
7431 .directive('a', MdAnchorDirective);
7432
7433
7434/**
7435 * @private
7436 * @restrict E
7437 *
7438 * @description
7439 * `a` is an anchor directive used to inherit theme colors for md-primary, md-accent, etc.
7440 *
7441 * @usage
7442 *
7443 * <hljs lang="html">
7444 * <md-content md-theme="myTheme">
7445 * <a href="#chapter1" class="md-accent"></a>
7446 * </md-content>
7447 * </hljs>
7448 */
7449function MdAnchorDirective($mdTheming) {
7450 return {
7451 restrict : 'E',
7452 link : function postLink(scope, element) {
7453 // Make sure to inherit theme so stand-alone anchors
7454 // support theme colors for md-primary, md-accent, etc.
7455 $mdTheming(element);
7456 }
7457 };
7458}
7459
7460
7461/**
7462 * @ngdoc directive
7463 * @name mdButton
7464 * @module material.components.button
7465 *
7466 * @restrict E
7467 *
7468 * @description
7469 * `<md-button>` is a button directive with optional ink ripples (default enabled).
7470 *
7471 * If you supply a `href` or `ng-href` attribute, it will become an `<a>` element. Otherwise, it
7472 * will become a `<button>` element. As per the
7473 * [Material Design specifications](https://material.google.com/style/color.html#color-color-palette)
7474 * the FAB button background is filled with the accent color [by default]. The primary color palette
7475 * may be used with the `md-primary` class.
7476 *
7477 * Developers can also change the color palette of the button, by using the following classes
7478 * - `md-primary`
7479 * - `md-accent`
7480 * - `md-warn`
7481 *
7482 * See for example
7483 *
7484 * <hljs lang="html">
7485 * <md-button class="md-primary">Primary Button</md-button>
7486 * </hljs>
7487 *
7488 * Button can be also raised, which means that they will use the current color palette to fill the button.
7489 *
7490 * <hljs lang="html">
7491 * <md-button class="md-accent md-raised">Raised and Accent Button</md-button>
7492 * </hljs>
7493 *
7494 * It is also possible to disable the focus effect on the button, by using the following markup.
7495 *
7496 * <hljs lang="html">
7497 * <md-button class="md-no-focus">No Focus Style</md-button>
7498 * </hljs>
7499 *
7500 * @param {boolean=} md-no-ink If present, disable ripple ink effects.
7501 * @param {expression=} ng-disabled En/Disable based on the expression
7502 * @param {string=} md-ripple-size Overrides the default ripple size logic. Options: `full`, `partial`, `auto`
7503 * @param {string=} aria-label Adds alternative text to button for accessibility, useful for icon buttons.
7504 * If no default text is found, a warning will be logged.
7505 *
7506 * @usage
7507 *
7508 * Regular buttons:
7509 *
7510 * <hljs lang="html">
7511 * <md-button> Flat Button </md-button>
7512 * <md-button href="http://google.com"> Flat link </md-button>
7513 * <md-button class="md-raised"> Raised Button </md-button>
7514 * <md-button ng-disabled="true"> Disabled Button </md-button>
7515 * <md-button>
7516 * <md-icon md-svg-src="your/icon.svg"></md-icon>
7517 * Register Now
7518 * </md-button>
7519 * </hljs>
7520 *
7521 * FAB buttons:
7522 *
7523 * <hljs lang="html">
7524 * <md-button class="md-fab" aria-label="FAB">
7525 * <md-icon md-svg-src="your/icon.svg"></md-icon>
7526 * </md-button>
7527 * <!-- mini-FAB -->
7528 * <md-button class="md-fab md-mini" aria-label="Mini FAB">
7529 * <md-icon md-svg-src="your/icon.svg"></md-icon>
7530 * </md-button>
7531 * <!-- Button with SVG Icon -->
7532 * <md-button class="md-icon-button" aria-label="Custom Icon Button">
7533 * <md-icon md-svg-icon="path/to/your.svg"></md-icon>
7534 * </md-button>
7535 * </hljs>
7536 */
7537function MdButtonDirective($mdButtonInkRipple, $mdTheming, $mdAria, $timeout) {
7538
7539 return {
7540 restrict: 'EA',
7541 replace: true,
7542 transclude: true,
7543 template: getTemplate,
7544 link: postLink
7545 };
7546
7547 function isAnchor(attr) {
7548 return angular.isDefined(attr.href) || angular.isDefined(attr.ngHref) || angular.isDefined(attr.ngLink) || angular.isDefined(attr.uiSref);
7549 }
7550
7551 function getTemplate(element, attr) {
7552 if (isAnchor(attr)) {
7553 return '<a class="md-button" ng-transclude></a>';
7554 } else {
7555 //If buttons don't have type="button", they will submit forms automatically.
7556 var btnType = (typeof attr.type === 'undefined') ? 'button' : attr.type;
7557 return '<button class="md-button" type="' + btnType + '" ng-transclude></button>';
7558 }
7559 }
7560
7561 function postLink(scope, element, attr) {
7562 $mdTheming(element);
7563 $mdButtonInkRipple.attach(scope, element);
7564
7565 // Use async expect to support possible bindings in the button label
7566 $mdAria.expectWithoutText(element, 'aria-label');
7567
7568 // For anchor elements, we have to set tabindex manually when the
7569 // element is disabled
7570 if (isAnchor(attr) && angular.isDefined(attr.ngDisabled) ) {
7571 scope.$watch(attr.ngDisabled, function(isDisabled) {
7572 element.attr('tabindex', isDisabled ? -1 : 0);
7573 });
7574 }
7575
7576 // disabling click event when disabled is true
7577 element.on('click', function(e){
7578 if (attr.disabled === true) {
7579 e.preventDefault();
7580 e.stopImmediatePropagation();
7581 }
7582 });
7583
7584 if (!element.hasClass('md-no-focus')) {
7585 // restrict focus styles to the keyboard
7586 scope.mouseActive = false;
7587 element.on('mousedown', function() {
7588 scope.mouseActive = true;
7589 $timeout(function(){
7590 scope.mouseActive = false;
7591 }, 100);
7592 })
7593 .on('focus', function() {
7594 if (scope.mouseActive === false) {
7595 element.addClass('md-focused');
7596 }
7597 })
7598 .on('blur', function(ev) {
7599 element.removeClass('md-focused');
7600 });
7601 }
7602
7603 }
7604
7605}
7606
7607})();
7608(function(){
7609"use strict";
7610
7611/**
7612 * @ngdoc module
7613 * @name material.components.card
7614 *
7615 * @description
7616 * Card components.
7617 */
7618mdCardDirective.$inject = ["$mdTheming"];
7619angular.module('material.components.card', [
7620 'material.core'
7621 ])
7622 .directive('mdCard', mdCardDirective);
7623
7624
7625/**
7626 * @ngdoc directive
7627 * @name mdCard
7628 * @module material.components.card
7629 *
7630 * @restrict E
7631 *
7632 * @description
7633 * The `<md-card>` directive is a container element used within `<md-content>` containers.
7634 *
7635 * An image included as a direct descendant will fill the card's width. If you want to avoid this,
7636 * you can add the `md-image-no-fill` class to the parent element. The `<md-card-content>`
7637 * container will wrap text content and provide padding. An `<md-card-footer>` element can be
7638 * optionally included to put content flush against the bottom edge of the card.
7639 *
7640 * Action buttons can be included in an `<md-card-actions>` element, similar to `<md-dialog-actions>`.
7641 * You can then position buttons using layout attributes.
7642 *
7643 * Card is built with:
7644 * * `<md-card-header>` - Header for the card, holds avatar, text and squared image
7645 * - `<md-card-avatar>` - Card avatar
7646 * - `md-user-avatar` - Class for user image
7647 * - `<md-icon>`
7648 * - `<md-card-header-text>` - Contains elements for the card description
7649 * - `md-title` - Class for the card title
7650 * - `md-subhead` - Class for the card sub header
7651 * * `<img>` - Image for the card
7652 * * `<md-card-title>` - Card content title
7653 * - `<md-card-title-text>`
7654 * - `md-headline` - Class for the card content title
7655 * - `md-subhead` - Class for the card content sub header
7656 * - `<md-card-title-media>` - Squared image within the title
7657 * - `md-media-sm` - Class for small image
7658 * - `md-media-md` - Class for medium image
7659 * - `md-media-lg` - Class for large image
7660 * * `<md-card-content>` - Card content
7661 * - `md-media-xl` - Class for extra large image
7662 * * `<md-card-actions>` - Card actions
7663 * - `<md-card-icon-actions>` - Icon actions
7664 *
7665 * Cards have constant width and variable heights; where the maximum height is limited to what can
7666 * fit within a single view on a platform, but it can temporarily expand as needed.
7667 *
7668 * @usage
7669 * ### Card with optional footer
7670 * <hljs lang="html">
7671 * <md-card>
7672 * <img src="card-image.png" class="md-card-image" alt="image caption">
7673 * <md-card-content>
7674 * <h2>Card headline</h2>
7675 * <p>Card content</p>
7676 * </md-card-content>
7677 * <md-card-footer>
7678 * Card footer
7679 * </md-card-footer>
7680 * </md-card>
7681 * </hljs>
7682 *
7683 * ### Card with actions
7684 * <hljs lang="html">
7685 * <md-card>
7686 * <img src="card-image.png" class="md-card-image" alt="image caption">
7687 * <md-card-content>
7688 * <h2>Card headline</h2>
7689 * <p>Card content</p>
7690 * </md-card-content>
7691 * <md-card-actions layout="row" layout-align="end center">
7692 * <md-button>Action 1</md-button>
7693 * <md-button>Action 2</md-button>
7694 * </md-card-actions>
7695 * </md-card>
7696 * </hljs>
7697 *
7698 * ### Card with header, image, title actions and content
7699 * <hljs lang="html">
7700 * <md-card>
7701 * <md-card-header>
7702 * <md-card-avatar>
7703 * <img class="md-user-avatar" src="avatar.png"/>
7704 * </md-card-avatar>
7705 * <md-card-header-text>
7706 * <span class="md-title">Title</span>
7707 * <span class="md-subhead">Sub header</span>
7708 * </md-card-header-text>
7709 * </md-card-header>
7710 * <img ng-src="card-image.png" class="md-card-image" alt="image caption">
7711 * <md-card-title>
7712 * <md-card-title-text>
7713 * <span class="md-headline">Card headline</span>
7714 * <span class="md-subhead">Card subheader</span>
7715 * </md-card-title-text>
7716 * </md-card-title>
7717 * <md-card-actions layout="row" layout-align="start center">
7718 * <md-button>Action 1</md-button>
7719 * <md-button>Action 2</md-button>
7720 * <md-card-icon-actions>
7721 * <md-button class="md-icon-button" aria-label="icon">
7722 * <md-icon md-svg-icon="icon"></md-icon>
7723 * </md-button>
7724 * </md-card-icon-actions>
7725 * </md-card-actions>
7726 * <md-card-content>
7727 * <p>
7728 * Card content
7729 * </p>
7730 * </md-card-content>
7731 * </md-card>
7732 * </hljs>
7733 */
7734function mdCardDirective($mdTheming) {
7735 return {
7736 restrict: 'E',
7737 link: function ($scope, $element, attr) {
7738 $element.addClass('_md'); // private md component indicator for styling
7739 $mdTheming($element);
7740 }
7741 };
7742}
7743
7744})();
7745(function(){
7746"use strict";
7747
7748/**
7749 * @ngdoc module
7750 * @name material.components.chips
7751 */
7752/*
7753 * @see js folder for chips implementation
7754 */
7755angular.module('material.components.chips', [
7756 'material.core',
7757 'material.components.autocomplete'
7758]);
7759
7760})();
7761(function(){
7762"use strict";
7763
7764/**
7765 * @ngdoc module
7766 * @name material.components.checkbox
7767 * @description Checkbox module!
7768 */
7769MdCheckboxDirective.$inject = ["inputDirective", "$mdAria", "$mdConstant", "$mdTheming", "$mdUtil", "$timeout"];
7770angular
7771 .module('material.components.checkbox', ['material.core'])
7772 .directive('mdCheckbox', MdCheckboxDirective);
7773
7774/**
7775 * @ngdoc directive
7776 * @name mdCheckbox
7777 * @module material.components.checkbox
7778 * @restrict E
7779 *
7780 * @description
7781 * The checkbox directive is used like the normal [angular checkbox](https://docs.angularjs.org/api/ng/input/input%5Bcheckbox%5D).
7782 *
7783 * As per the [material design spec](http://www.google.com/design/spec/style/color.html#color-color-schemes)
7784 * the checkbox is in the accent color by default. The primary color palette may be used with
7785 * the `md-primary` class.
7786 *
7787 * @param {string} ng-model Assignable angular expression to data-bind to.
7788 * @param {string=} name Property name of the form under which the control is published.
7789 * @param {expression=} ng-true-value The value to which the expression should be set when selected.
7790 * @param {expression=} ng-false-value The value to which the expression should be set when not selected.
7791 * @param {string=} ng-change Angular expression to be executed when input changes due to user interaction with the input element.
7792 * @param {boolean=} md-no-ink Use of attribute indicates use of ripple ink effects
7793 * @param {string=} aria-label Adds label to checkbox for accessibility.
7794 * Defaults to checkbox's text. If no default text is found, a warning will be logged.
7795 * @param {expression=} md-indeterminate This determines when the checkbox should be rendered as 'indeterminate'.
7796 * If a truthy expression or no value is passed in the checkbox renders in the md-indeterminate state.
7797 * If falsy expression is passed in it just looks like a normal unchecked checkbox.
7798 * The indeterminate, checked, and unchecked states are mutually exclusive. A box cannot be in any two states at the same time.
7799 * Adding the 'md-indeterminate' attribute overrides any checked/unchecked rendering logic.
7800 * When using the 'md-indeterminate' attribute use 'ng-checked' to define rendering logic instead of using 'ng-model'.
7801 * @param {expression=} ng-checked If this expression evaluates as truthy, the 'md-checked' css class is added to the checkbox and it
7802 * will appear checked.
7803 *
7804 * @usage
7805 * <hljs lang="html">
7806 * <md-checkbox ng-model="isChecked" aria-label="Finished?">
7807 * Finished ?
7808 * </md-checkbox>
7809 *
7810 * <md-checkbox md-no-ink ng-model="hasInk" aria-label="No Ink Effects">
7811 * No Ink Effects
7812 * </md-checkbox>
7813 *
7814 * <md-checkbox ng-disabled="true" ng-model="isDisabled" aria-label="Disabled">
7815 * Disabled
7816 * </md-checkbox>
7817 *
7818 * </hljs>
7819 *
7820 */
7821function MdCheckboxDirective(inputDirective, $mdAria, $mdConstant, $mdTheming, $mdUtil, $timeout) {
7822 inputDirective = inputDirective[0];
7823
7824 return {
7825 restrict: 'E',
7826 transclude: true,
7827 require: '?ngModel',
7828 priority: 210, // Run before ngAria
7829 template:
7830 '<div class="md-container" md-ink-ripple md-ink-ripple-checkbox>' +
7831 '<div class="md-icon"></div>' +
7832 '</div>' +
7833 '<div ng-transclude class="md-label"></div>',
7834 compile: compile
7835 };
7836
7837 // **********************************************************
7838 // Private Methods
7839 // **********************************************************
7840
7841 function compile (tElement, tAttrs) {
7842 tAttrs.$set('tabindex', tAttrs.tabindex || '0');
7843 tAttrs.$set('type', 'checkbox');
7844 tAttrs.$set('role', tAttrs.type);
7845
7846 return {
7847 pre: function(scope, element) {
7848 // Attach a click handler during preLink, in order to immediately stop propagation
7849 // (especially for ng-click) when the checkbox is disabled.
7850 element.on('click', function(e) {
7851 if (this.hasAttribute('disabled')) {
7852 e.stopImmediatePropagation();
7853 }
7854 });
7855 },
7856 post: postLink
7857 };
7858
7859 function postLink(scope, element, attr, ngModelCtrl) {
7860 var isIndeterminate;
7861 ngModelCtrl = ngModelCtrl || $mdUtil.fakeNgModel();
7862 $mdTheming(element);
7863
7864 // Redirect focus events to the root element, because IE11 is always focusing the container element instead
7865 // of the md-checkbox element. This causes issues when using ngModelOptions: `updateOnBlur`
7866 element.children().on('focus', function() {
7867 element.focus();
7868 });
7869
7870 if ($mdUtil.parseAttributeBoolean(attr.mdIndeterminate)) {
7871 setIndeterminateState();
7872 scope.$watch(attr.mdIndeterminate, setIndeterminateState);
7873 }
7874
7875 if (attr.ngChecked) {
7876 scope.$watch(scope.$eval.bind(scope, attr.ngChecked), function(value) {
7877 ngModelCtrl.$setViewValue(value);
7878 ngModelCtrl.$render();
7879 });
7880 }
7881
7882 $$watchExpr('ngDisabled', 'tabindex', {
7883 true: '-1',
7884 false: attr.tabindex
7885 });
7886
7887 $mdAria.expectWithText(element, 'aria-label');
7888
7889 // Reuse the original input[type=checkbox] directive from Angular core.
7890 // This is a bit hacky as we need our own event listener and own render
7891 // function.
7892 inputDirective.link.pre(scope, {
7893 on: angular.noop,
7894 0: {}
7895 }, attr, [ngModelCtrl]);
7896
7897 scope.mouseActive = false;
7898 element.on('click', listener)
7899 .on('keypress', keypressHandler)
7900 .on('mousedown', function() {
7901 scope.mouseActive = true;
7902 $timeout(function() {
7903 scope.mouseActive = false;
7904 }, 100);
7905 })
7906 .on('focus', function() {
7907 if (scope.mouseActive === false) {
7908 element.addClass('md-focused');
7909 }
7910 })
7911 .on('blur', function() {
7912 element.removeClass('md-focused');
7913 });
7914
7915 ngModelCtrl.$render = render;
7916
7917 function $$watchExpr(expr, htmlAttr, valueOpts) {
7918 if (attr[expr]) {
7919 scope.$watch(attr[expr], function(val) {
7920 if (valueOpts[val]) {
7921 element.attr(htmlAttr, valueOpts[val]);
7922 }
7923 });
7924 }
7925 }
7926
7927 function keypressHandler(ev) {
7928 var keyCode = ev.which || ev.keyCode;
7929 if (keyCode === $mdConstant.KEY_CODE.SPACE || keyCode === $mdConstant.KEY_CODE.ENTER) {
7930 ev.preventDefault();
7931 element.addClass('md-focused');
7932 listener(ev);
7933 }
7934 }
7935
7936 function listener(ev) {
7937 // skipToggle boolean is used by the switch directive to prevent the click event
7938 // when releasing the drag. There will be always a click if releasing the drag over the checkbox
7939 if (element[0].hasAttribute('disabled') || scope.skipToggle) {
7940 return;
7941 }
7942
7943 scope.$apply(function() {
7944 // Toggle the checkbox value...
7945 var viewValue = attr.ngChecked ? attr.checked : !ngModelCtrl.$viewValue;
7946
7947 ngModelCtrl.$setViewValue(viewValue, ev && ev.type);
7948 ngModelCtrl.$render();
7949 });
7950 }
7951
7952 function render() {
7953 // Cast the $viewValue to a boolean since it could be undefined
7954 element.toggleClass('md-checked', !!ngModelCtrl.$viewValue && !isIndeterminate);
7955 }
7956
7957 function setIndeterminateState(newValue) {
7958 isIndeterminate = newValue !== false;
7959 if (isIndeterminate) {
7960 element.attr('aria-checked', 'mixed');
7961 }
7962 element.toggleClass('md-indeterminate', isIndeterminate);
7963 }
7964 }
7965 }
7966}
7967
7968})();
7969(function(){
7970"use strict";
7971
7972(function () {
7973 "use strict";
7974
7975 /**
7976 * Use a RegExp to check if the `md-colors="<expression>"` is static string
7977 * or one that should be observed and dynamically interpolated.
7978 */
7979 MdColorsDirective.$inject = ["$mdColors", "$mdUtil", "$log", "$parse"];
7980 MdColorsService.$inject = ["$mdTheming", "$mdUtil", "$log"];
7981 var STATIC_COLOR_EXPRESSION = /^{((\s|,)*?["'a-zA-Z-]+?\s*?:\s*?('|")[a-zA-Z0-9-.]*('|"))+\s*}$/;
7982 var colorPalettes = null;
7983
7984 /**
7985 * @ngdoc module
7986 * @name material.components.colors
7987 *
7988 * @description
7989 * Define $mdColors service and a `md-colors=""` attribute directive
7990 */
7991 angular
7992 .module('material.components.colors', ['material.core'])
7993 .directive('mdColors', MdColorsDirective)
7994 .service('$mdColors', MdColorsService);
7995
7996 /**
7997 * @ngdoc service
7998 * @name $mdColors
7999 * @module material.components.colors
8000 *
8001 * @description
8002 * With only defining themes, one couldn't get non ngMaterial elements colored with Material colors,
8003 * `$mdColors` service is used by the md-color directive to convert the 1..n color expressions to RGBA values and will apply
8004 * those values to element as CSS property values.
8005 *
8006 * @usage
8007 * <hljs lang="js">
8008 * angular.controller('myCtrl', function ($mdColors) {
8009 * var color = $mdColors.getThemeColor('myTheme-red-200-0.5');
8010 * ...
8011 * });
8012 * </hljs>
8013 *
8014 */
8015 function MdColorsService($mdTheming, $mdUtil, $log) {
8016 colorPalettes = colorPalettes || Object.keys($mdTheming.PALETTES);
8017
8018 // Publish service instance
8019 return {
8020 applyThemeColors: applyThemeColors,
8021 getThemeColor: getThemeColor,
8022 hasTheme: hasTheme
8023 };
8024
8025 // ********************************************
8026 // Internal Methods
8027 // ********************************************
8028
8029 /**
8030 * @ngdoc method
8031 * @name $mdColors#applyThemeColors
8032 *
8033 * @description
8034 * Gets a color json object, keys are css properties and values are string of the wanted color
8035 * Then calculate the rgba() values based on the theme color parts
8036 *
8037 * @param {DOMElement} element the element to apply the styles on.
8038 * @param {object} colorExpression json object, keys are css properties and values are string of the wanted color,
8039 * for example: `{color: 'red-A200-0.3'}`.
8040 *
8041 * @usage
8042 * <hljs lang="js">
8043 * app.directive('myDirective', function($mdColors) {
8044 * return {
8045 * ...
8046 * link: function (scope, elem) {
8047 * $mdColors.applyThemeColors(elem, {color: 'red'});
8048 * }
8049 * }
8050 * });
8051 * </hljs>
8052 */
8053 function applyThemeColors(element, colorExpression) {
8054 try {
8055 if (colorExpression) {
8056 // Assign the calculate RGBA color values directly as inline CSS
8057 element.css(interpolateColors(colorExpression));
8058 }
8059 } catch (e) {
8060 $log.error(e.message);
8061 }
8062
8063 }
8064
8065 /**
8066 * @ngdoc method
8067 * @name $mdColors#getThemeColor
8068 *
8069 * @description
8070 * Get parsed color from expression
8071 *
8072 * @param {string} expression string of a color expression (for instance `'red-700-0.8'`)
8073 *
8074 * @returns {string} a css color expression (for instance `rgba(211, 47, 47, 0.8)`)
8075 *
8076 * @usage
8077 * <hljs lang="js">
8078 * angular.controller('myCtrl', function ($mdColors) {
8079 * var color = $mdColors.getThemeColor('myTheme-red-200-0.5');
8080 * ...
8081 * });
8082 * </hljs>
8083 */
8084 function getThemeColor(expression) {
8085 var color = extractColorOptions(expression);
8086
8087 return parseColor(color);
8088 }
8089
8090 /**
8091 * Return the parsed color
8092 * @param color hashmap of color definitions
8093 * @param contrast whether use contrast color for foreground
8094 * @returns rgba color string
8095 */
8096 function parseColor(color, contrast) {
8097 contrast = contrast || false;
8098 var rgbValues = $mdTheming.PALETTES[color.palette][color.hue];
8099
8100 rgbValues = contrast ? rgbValues.contrast : rgbValues.value;
8101
8102 return $mdUtil.supplant('rgba({0}, {1}, {2}, {3})',
8103 [rgbValues[0], rgbValues[1], rgbValues[2], rgbValues[3] || color.opacity]
8104 );
8105 }
8106
8107 /**
8108 * Convert the color expression into an object with scope-interpolated values
8109 * Then calculate the rgba() values based on the theme color parts
8110 *
8111 * @results Hashmap of CSS properties with associated `rgba( )` string vales
8112 *
8113 *
8114 */
8115 function interpolateColors(themeColors) {
8116 var rgbColors = {};
8117
8118 var hasColorProperty = themeColors.hasOwnProperty('color');
8119
8120 angular.forEach(themeColors, function (value, key) {
8121 var color = extractColorOptions(value);
8122 var hasBackground = key.indexOf('background') > -1;
8123
8124 rgbColors[key] = parseColor(color);
8125 if (hasBackground && !hasColorProperty) {
8126 rgbColors.color = parseColor(color, true);
8127 }
8128 });
8129
8130 return rgbColors;
8131 }
8132
8133 /**
8134 * Check if expression has defined theme
8135 * e.g.
8136 * 'myTheme-primary' => true
8137 * 'red-800' => false
8138 */
8139 function hasTheme(expression) {
8140 return angular.isDefined($mdTheming.THEMES[expression.split('-')[0]]);
8141 }
8142
8143 /**
8144 * For the evaluated expression, extract the color parts into a hash map
8145 */
8146 function extractColorOptions(expression) {
8147 var parts = expression.split('-');
8148 var hasTheme = angular.isDefined($mdTheming.THEMES[parts[0]]);
8149 var theme = hasTheme ? parts.splice(0, 1)[0] : $mdTheming.defaultTheme();
8150
8151 return {
8152 theme: theme,
8153 palette: extractPalette(parts, theme),
8154 hue: extractHue(parts, theme),
8155 opacity: parts[2] || 1
8156 };
8157 }
8158
8159 /**
8160 * Calculate the theme palette name
8161 */
8162 function extractPalette(parts, theme) {
8163 // If the next section is one of the palettes we assume it's a two word palette
8164 // Two word palette can be also written in camelCase, forming camelCase to dash-case
8165
8166 var isTwoWord = parts.length > 1 && colorPalettes.indexOf(parts[1]) !== -1;
8167 var palette = parts[0].replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
8168
8169 if (isTwoWord) palette = parts[0] + '-' + parts.splice(1, 1);
8170
8171 if (colorPalettes.indexOf(palette) === -1) {
8172 // If the palette is not in the palette list it's one of primary/accent/warn/background
8173 var scheme = $mdTheming.THEMES[theme].colors[palette];
8174 if (!scheme) {
8175 throw new Error($mdUtil.supplant('mdColors: couldn\'t find \'{palette}\' in the palettes.', {palette: palette}));
8176 }
8177 palette = scheme.name;
8178 }
8179
8180 return palette;
8181 }
8182
8183 function extractHue(parts, theme) {
8184 var themeColors = $mdTheming.THEMES[theme].colors;
8185
8186 if (parts[1] === 'hue') {
8187 var hueNumber = parseInt(parts.splice(2, 1)[0], 10);
8188
8189 if (hueNumber < 1 || hueNumber > 3) {
8190 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}));
8191 }
8192 parts[1] = 'hue-' + hueNumber;
8193
8194 if (!(parts[0] in themeColors)) {
8195 throw new Error($mdUtil.supplant('mdColors: \'hue-x\' can only be used with [{availableThemes}], but was used with \'{usedTheme}\'', {
8196 availableThemes: Object.keys(themeColors).join(', '),
8197 usedTheme: parts[0]
8198 }));
8199 }
8200
8201 return themeColors[parts[0]].hues[parts[1]];
8202 }
8203
8204 return parts[1] || themeColors[parts[0] in themeColors ? parts[0] : 'primary'].hues['default'];
8205 }
8206 }
8207
8208 /**
8209 * @ngdoc directive
8210 * @name mdColors
8211 * @module material.components.colors
8212 *
8213 * @restrict A
8214 *
8215 * @description
8216 * `mdColors` directive will apply the theme-based color expression as RGBA CSS style values.
8217 *
8218 * The format will be similar to our color defining in the scss files:
8219 *
8220 * ## `[?theme]-[palette]-[?hue]-[?opacity]`
8221 * - [theme] - default value is the default theme
8222 * - [palette] - can be either palette name or primary/accent/warn/background
8223 * - [hue] - default is 500 (hue-x can be used with primary/accent/warn/background)
8224 * - [opacity] - default is 1
8225 *
8226 * > `?` indicates optional parameter
8227 *
8228 * @usage
8229 * <hljs lang="html">
8230 * <div md-colors="{background: 'myTheme-accent-900-0.43'}">
8231 * <div md-colors="{color: 'red-A100', 'border-color': 'primary-600'}">
8232 * <span>Color demo</span>
8233 * </div>
8234 * </div>
8235 * </hljs>
8236 *
8237 * `mdColors` directive will automatically watch for changes in the expression if it recognizes an interpolation
8238 * expression or a function. For performance options, you can use `::` prefix to the `md-colors` expression
8239 * to indicate a one-time data binding.
8240 * <hljs lang="html">
8241 * <md-card md-colors="::{background: '{{theme}}-primary-700'}">
8242 * </md-card>
8243 * </hljs>
8244 *
8245 */
8246 function MdColorsDirective($mdColors, $mdUtil, $log, $parse) {
8247 return {
8248 restrict: 'A',
8249 require: ['^?mdTheme'],
8250 compile: function (tElem, tAttrs) {
8251 var shouldWatch = shouldColorsWatch();
8252
8253 return function (scope, element, attrs, ctrl) {
8254 var mdThemeController = ctrl[0];
8255
8256 var lastColors = {};
8257
8258 var parseColors = function (theme) {
8259 if (typeof theme !== 'string') {
8260 theme = '';
8261 }
8262
8263 if (!attrs.mdColors) {
8264 attrs.mdColors = '{}';
8265 }
8266
8267 /**
8268 * Json.parse() does not work because the keys are not quoted;
8269 * use $parse to convert to a hash map
8270 */
8271 var colors = $parse(attrs.mdColors)(scope);
8272
8273 /**
8274 * If mdTheme is defined up the DOM tree
8275 * we add mdTheme theme to colors who doesn't specified a theme
8276 *
8277 * # example
8278 * <hljs lang="html">
8279 * <div md-theme="myTheme">
8280 * <div md-colors="{background: 'primary-600'}">
8281 * <span md-colors="{background: 'mySecondTheme-accent-200'}">Color demo</span>
8282 * </div>
8283 * </div>
8284 * </hljs>
8285 *
8286 * 'primary-600' will be 'myTheme-primary-600',
8287 * but 'mySecondTheme-accent-200' will stay the same cause it has a theme prefix
8288 */
8289 if (mdThemeController) {
8290 Object.keys(colors).forEach(function (prop) {
8291 var color = colors[prop];
8292 if (!$mdColors.hasTheme(color)) {
8293 colors[prop] = (theme || mdThemeController.$mdTheme) + '-' + color;
8294 }
8295 });
8296 }
8297
8298 cleanElement(colors);
8299
8300 return colors;
8301 };
8302
8303 var cleanElement = function (colors) {
8304 if (!angular.equals(colors, lastColors)) {
8305 var keys = Object.keys(lastColors);
8306
8307 if (lastColors.background && !keys.color) {
8308 keys.push('color');
8309 }
8310
8311 keys.forEach(function (key) {
8312 element.css(key, '');
8313 });
8314 }
8315
8316 lastColors = colors;
8317 };
8318
8319 /**
8320 * Registering for mgTheme changes and asking mdTheme controller run our callback whenever a theme changes
8321 */
8322 var unregisterChanges = angular.noop;
8323
8324 if (mdThemeController) {
8325 unregisterChanges = mdThemeController.registerChanges(function (theme) {
8326 $mdColors.applyThemeColors(element, parseColors(theme));
8327 });
8328 }
8329
8330 scope.$on('$destroy', function () {
8331 unregisterChanges();
8332 });
8333
8334 try {
8335 if (shouldWatch) {
8336 scope.$watch(parseColors, angular.bind(this,
8337 $mdColors.applyThemeColors, element
8338 ), true);
8339 }
8340 else {
8341 $mdColors.applyThemeColors(element, parseColors());
8342 }
8343
8344 }
8345 catch (e) {
8346 $log.error(e.message);
8347 }
8348
8349 };
8350
8351 function shouldColorsWatch() {
8352 // Simulate 1x binding and mark mdColorsWatch == false
8353 var rawColorExpression = tAttrs.mdColors;
8354 var bindOnce = rawColorExpression.indexOf('::') > -1;
8355 var isStatic = bindOnce ? true : STATIC_COLOR_EXPRESSION.test(tAttrs.mdColors);
8356
8357 // Remove it for the postLink...
8358 tAttrs.mdColors = rawColorExpression.replace('::', '');
8359
8360 var hasWatchAttr = angular.isDefined(tAttrs.mdColorsWatch);
8361
8362 return (bindOnce || isStatic) ? false :
8363 hasWatchAttr ? $mdUtil.parseAttributeBoolean(tAttrs.mdColorsWatch) : true;
8364 }
8365 }
8366 };
8367
8368 }
8369
8370
8371})();
8372
8373})();
8374(function(){
8375"use strict";
8376
8377/**
8378 * @ngdoc module
8379 * @name material.components.content
8380 *
8381 * @description
8382 * Scrollable content
8383 */
8384mdContentDirective.$inject = ["$mdTheming"];
8385angular.module('material.components.content', [
8386 'material.core'
8387])
8388 .directive('mdContent', mdContentDirective);
8389
8390/**
8391 * @ngdoc directive
8392 * @name mdContent
8393 * @module material.components.content
8394 *
8395 * @restrict E
8396 *
8397 * @description
8398 *
8399 * The `<md-content>` directive is a container element useful for scrollable content. It achieves
8400 * this by setting the CSS `overflow` property to `auto` so that content can properly scroll.
8401 *
8402 * In general, `<md-content>` components are not designed to be nested inside one another. If
8403 * possible, it is better to make them siblings. This often results in a better user experience as
8404 * having nested scrollbars may confuse the user.
8405 *
8406 * ## Troubleshooting
8407 *
8408 * In some cases, you may wish to apply the `md-no-momentum` class to ensure that Safari's
8409 * momentum scrolling is disabled. Momentum scrolling can cause flickering issues while scrolling
8410 * SVG icons and some other components.
8411 *
8412 * Additionally, we now also offer the `md-no-flicker` class which can be applied to any element
8413 * and uses a Webkit-specific filter of `blur(0px)` that forces GPU rendering of all elements
8414 * inside (which eliminates the flicker on iOS devices).
8415 *
8416 * _<b>Note:</b> Forcing an element to render on the GPU can have unintended side-effects, especially
8417 * related to the z-index of elements. Please use with caution and only on the elements needed._
8418 *
8419 * @usage
8420 *
8421 * Add the `[layout-padding]` attribute to make the content padded.
8422 *
8423 * <hljs lang="html">
8424 * <md-content layout-padding>
8425 * Lorem ipsum dolor sit amet, ne quod novum mei.
8426 * </md-content>
8427 * </hljs>
8428 */
8429
8430function mdContentDirective($mdTheming) {
8431 return {
8432 restrict: 'E',
8433 controller: ['$scope', '$element', ContentController],
8434 link: function(scope, element) {
8435 element.addClass('_md'); // private md component indicator for styling
8436
8437 $mdTheming(element);
8438 scope.$broadcast('$mdContentLoaded', element);
8439
8440 iosScrollFix(element[0]);
8441 }
8442 };
8443
8444 function ContentController($scope, $element) {
8445 this.$scope = $scope;
8446 this.$element = $element;
8447 }
8448}
8449
8450function iosScrollFix(node) {
8451 // IOS FIX:
8452 // If we scroll where there is no more room for the webview to scroll,
8453 // by default the webview itself will scroll up and down, this looks really
8454 // bad. So if we are scrolling to the very top or bottom, add/subtract one
8455 angular.element(node).on('$md.pressdown', function(ev) {
8456 // Only touch events
8457 if (ev.pointer.type !== 't') return;
8458 // Don't let a child content's touchstart ruin it for us.
8459 if (ev.$materialScrollFixed) return;
8460 ev.$materialScrollFixed = true;
8461
8462 if (node.scrollTop === 0) {
8463 node.scrollTop = 1;
8464 } else if (node.scrollHeight === node.scrollTop + node.offsetHeight) {
8465 node.scrollTop -= 1;
8466 }
8467 });
8468}
8469
8470})();
8471(function(){
8472"use strict";
8473
8474/**
8475 * @ngdoc module
8476 * @name material.components.datepicker
8477 * @description Module for the datepicker component.
8478 */
8479
8480angular.module('material.components.datepicker', [
8481 'material.core',
8482 'material.components.icon',
8483 'material.components.virtualRepeat'
8484]);
8485
8486})();
8487(function(){
8488"use strict";
8489
8490/**
8491 * @ngdoc module
8492 * @name material.components.dialog
8493 */
8494MdDialogDirective.$inject = ["$$rAF", "$mdTheming", "$mdDialog"];
8495MdDialogProvider.$inject = ["$$interimElementProvider"];
8496angular
8497 .module('material.components.dialog', [
8498 'material.core',
8499 'material.components.backdrop'
8500 ])
8501 .directive('mdDialog', MdDialogDirective)
8502 .provider('$mdDialog', MdDialogProvider);
8503
8504/**
8505 * @ngdoc directive
8506 * @name mdDialog
8507 * @module material.components.dialog
8508 *
8509 * @restrict E
8510 *
8511 * @description
8512 * `<md-dialog>` - The dialog's template must be inside this element.
8513 *
8514 * Inside, use an `<md-dialog-content>` element for the dialog's content, and use
8515 * an `<md-dialog-actions>` element for the dialog's actions.
8516 *
8517 * ## CSS
8518 * - `.md-dialog-content` - class that sets the padding on the content as the spec file
8519 *
8520 * ## Notes
8521 * - If you specify an `id` for the `<md-dialog>`, the `<md-dialog-content>` will have the same `id`
8522 * prefixed with `dialogContent_`.
8523 *
8524 * @usage
8525 * ### Dialog template
8526 * <hljs lang="html">
8527 * <md-dialog aria-label="List dialog">
8528 * <md-dialog-content>
8529 * <md-list>
8530 * <md-list-item ng-repeat="item in items">
8531 * <p>Number {{item}}</p>
8532 * </md-list-item>
8533 * </md-list>
8534 * </md-dialog-content>
8535 * <md-dialog-actions>
8536 * <md-button ng-click="closeDialog()" class="md-primary">Close Dialog</md-button>
8537 * </md-dialog-actions>
8538 * </md-dialog>
8539 * </hljs>
8540 */
8541function MdDialogDirective($$rAF, $mdTheming, $mdDialog) {
8542 return {
8543 restrict: 'E',
8544 link: function(scope, element) {
8545 element.addClass('_md'); // private md component indicator for styling
8546
8547 $mdTheming(element);
8548 $$rAF(function() {
8549 var images;
8550 var content = element[0].querySelector('md-dialog-content');
8551
8552 if (content) {
8553 images = content.getElementsByTagName('img');
8554 addOverflowClass();
8555 //-- delayed image loading may impact scroll height, check after images are loaded
8556 angular.element(images).on('load', addOverflowClass);
8557 }
8558
8559 scope.$on('$destroy', function() {
8560 $mdDialog.destroy(element);
8561 });
8562
8563 /**
8564 *
8565 */
8566 function addOverflowClass() {
8567 element.toggleClass('md-content-overflow', content.scrollHeight > content.clientHeight);
8568 }
8569
8570
8571 });
8572 }
8573 };
8574}
8575
8576/**
8577 * @ngdoc service
8578 * @name $mdDialog
8579 * @module material.components.dialog
8580 *
8581 * @description
8582 * `$mdDialog` opens a dialog over the app to inform users about critical information or require
8583 * them to make decisions. There are two approaches for setup: a simple promise API
8584 * and regular object syntax.
8585 *
8586 * ## Restrictions
8587 *
8588 * - The dialog is always given an isolate scope.
8589 * - The dialog's template must have an outer `<md-dialog>` element.
8590 * Inside, use an `<md-dialog-content>` element for the dialog's content, and use
8591 * an `<md-dialog-actions>` element for the dialog's actions.
8592 * - Dialogs must cover the entire application to keep interactions inside of them.
8593 * Use the `parent` option to change where dialogs are appended.
8594 *
8595 * ## Sizing
8596 * - Complex dialogs can be sized with `flex="percentage"`, i.e. `flex="66"`.
8597 * - Default max-width is 80% of the `rootElement` or `parent`.
8598 *
8599 * ## CSS
8600 * - `.md-dialog-content` - class that sets the padding on the content as the spec file
8601 *
8602 * @usage
8603 * <hljs lang="html">
8604 * <div ng-app="demoApp" ng-controller="EmployeeController">
8605 * <div>
8606 * <md-button ng-click="showAlert()" class="md-raised md-warn">
8607 * Employee Alert!
8608 * </md-button>
8609 * </div>
8610 * <div>
8611 * <md-button ng-click="showDialog($event)" class="md-raised">
8612 * Custom Dialog
8613 * </md-button>
8614 * </div>
8615 * <div>
8616 * <md-button ng-click="closeAlert()" ng-disabled="!hasAlert()" class="md-raised">
8617 * Close Alert
8618 * </md-button>
8619 * </div>
8620 * <div>
8621 * <md-button ng-click="showGreeting($event)" class="md-raised md-primary" >
8622 * Greet Employee
8623 * </md-button>
8624 * </div>
8625 * </div>
8626 * </hljs>
8627 *
8628 * ### JavaScript: object syntax
8629 * <hljs lang="js">
8630 * (function(angular, undefined){
8631 * "use strict";
8632 *
8633 * angular
8634 * .module('demoApp', ['ngMaterial'])
8635 * .controller('AppCtrl', AppController);
8636 *
8637 * function AppController($scope, $mdDialog) {
8638 * var alert;
8639 * $scope.showAlert = showAlert;
8640 * $scope.showDialog = showDialog;
8641 * $scope.items = [1, 2, 3];
8642 *
8643 * // Internal method
8644 * function showAlert() {
8645 * alert = $mdDialog.alert({
8646 * title: 'Attention',
8647 * textContent: 'This is an example of how easy dialogs can be!',
8648 * ok: 'Close'
8649 * });
8650 *
8651 * $mdDialog
8652 * .show( alert )
8653 * .finally(function() {
8654 * alert = undefined;
8655 * });
8656 * }
8657 *
8658 * function showDialog($event) {
8659 * var parentEl = angular.element(document.body);
8660 * $mdDialog.show({
8661 * parent: parentEl,
8662 * targetEvent: $event,
8663 * template:
8664 * '<md-dialog aria-label="List dialog">' +
8665 * ' <md-dialog-content>'+
8666 * ' <md-list>'+
8667 * ' <md-list-item ng-repeat="item in items">'+
8668 * ' <p>Number {{item}}</p>' +
8669 * ' </md-item>'+
8670 * ' </md-list>'+
8671 * ' </md-dialog-content>' +
8672 * ' <md-dialog-actions>' +
8673 * ' <md-button ng-click="closeDialog()" class="md-primary">' +
8674 * ' Close Dialog' +
8675 * ' </md-button>' +
8676 * ' </md-dialog-actions>' +
8677 * '</md-dialog>',
8678 * locals: {
8679 * items: $scope.items
8680 * },
8681 * controller: DialogController
8682 * });
8683 * function DialogController($scope, $mdDialog, items) {
8684 * $scope.items = items;
8685 * $scope.closeDialog = function() {
8686 * $mdDialog.hide();
8687 * }
8688 * }
8689 * }
8690 * }
8691 * })(angular);
8692 * </hljs>
8693 *
8694 * ### Pre-Rendered Dialogs
8695 * By using the `contentElement` option, it is possible to use an already existing element in the DOM.
8696 *
8697 * <hljs lang="js">
8698 * $scope.showPrerenderedDialog = function() {
8699 * $mdDialog.show({
8700 * contentElement: '#myStaticDialog',
8701 * parent: angular.element(document.body)
8702 * });
8703 * };
8704 * </hljs>
8705 *
8706 * When using a string as value, `$mdDialog` will automatically query the DOM for the specified CSS selector.
8707 *
8708 * <hljs lang="html">
8709 * <div style="visibility: hidden">
8710 * <div class="md-dialog-container" id="myStaticDialog">
8711 * <md-dialog>
8712 * This is a pre-rendered dialog.
8713 * </md-dialog>
8714 * </div>
8715 * </div>
8716 * </hljs>
8717 *
8718 * **Notice**: It is important, to use the `.md-dialog-container` as the content element, otherwise the dialog
8719 * will not show up.
8720 *
8721 * It also possible to use a DOM element for the `contentElement` option.
8722 * - `contentElement: document.querySelector('#myStaticDialog')`
8723 * - `contentElement: angular.element(TEMPLATE)`
8724 *
8725 * When using a `template` as content element, it will be not compiled upon open.
8726 * This allows you to compile the element yourself and use it each time the dialog opens.
8727 *
8728 * ### Custom Presets
8729 * Developers are also able to create their own preset, which can be easily used without repeating
8730 * their options each time.
8731 *
8732 * <hljs lang="js">
8733 * $mdDialogProvider.addPreset('testPreset', {
8734 * options: function() {
8735 * return {
8736 * template:
8737 * '<md-dialog>' +
8738 * 'This is a custom preset' +
8739 * '</md-dialog>',
8740 * controllerAs: 'dialog',
8741 * bindToController: true,
8742 * clickOutsideToClose: true,
8743 * escapeToClose: true
8744 * };
8745 * }
8746 * });
8747 * </hljs>
8748 *
8749 * After you created your preset at config phase, you can easily access it.
8750 *
8751 * <hljs lang="js">
8752 * $mdDialog.show(
8753 * $mdDialog.testPreset()
8754 * );
8755 * </hljs>
8756 *
8757 * ### JavaScript: promise API syntax, custom dialog template
8758 * <hljs lang="js">
8759 * (function(angular, undefined){
8760 * "use strict";
8761 *
8762 * angular
8763 * .module('demoApp', ['ngMaterial'])
8764 * .controller('EmployeeController', EmployeeEditor)
8765 * .controller('GreetingController', GreetingController);
8766 *
8767 * // Fictitious Employee Editor to show how to use simple and complex dialogs.
8768 *
8769 * function EmployeeEditor($scope, $mdDialog) {
8770 * var alert;
8771 *
8772 * $scope.showAlert = showAlert;
8773 * $scope.closeAlert = closeAlert;
8774 * $scope.showGreeting = showCustomGreeting;
8775 *
8776 * $scope.hasAlert = function() { return !!alert };
8777 * $scope.userName = $scope.userName || 'Bobby';
8778 *
8779 * // Dialog #1 - Show simple alert dialog and cache
8780 * // reference to dialog instance
8781 *
8782 * function showAlert() {
8783 * alert = $mdDialog.alert()
8784 * .title('Attention, ' + $scope.userName)
8785 * .textContent('This is an example of how easy dialogs can be!')
8786 * .ok('Close');
8787 *
8788 * $mdDialog
8789 * .show( alert )
8790 * .finally(function() {
8791 * alert = undefined;
8792 * });
8793 * }
8794 *
8795 * // Close the specified dialog instance and resolve with 'finished' flag
8796 * // Normally this is not needed, just use '$mdDialog.hide()' to close
8797 * // the most recent dialog popup.
8798 *
8799 * function closeAlert() {
8800 * $mdDialog.hide( alert, "finished" );
8801 * alert = undefined;
8802 * }
8803 *
8804 * // Dialog #2 - Demonstrate more complex dialogs construction and popup.
8805 *
8806 * function showCustomGreeting($event) {
8807 * $mdDialog.show({
8808 * targetEvent: $event,
8809 * template:
8810 * '<md-dialog>' +
8811 *
8812 * ' <md-dialog-content>Hello {{ employee }}!</md-dialog-content>' +
8813 *
8814 * ' <md-dialog-actions>' +
8815 * ' <md-button ng-click="closeDialog()" class="md-primary">' +
8816 * ' Close Greeting' +
8817 * ' </md-button>' +
8818 * ' </md-dialog-actions>' +
8819 * '</md-dialog>',
8820 * controller: 'GreetingController',
8821 * onComplete: afterShowAnimation,
8822 * locals: { employee: $scope.userName }
8823 * });
8824 *
8825 * // When the 'enter' animation finishes...
8826 *
8827 * function afterShowAnimation(scope, element, options) {
8828 * // post-show code here: DOM element focus, etc.
8829 * }
8830 * }
8831 *
8832 * // Dialog #3 - Demonstrate use of ControllerAs and passing $scope to dialog
8833 * // Here we used ng-controller="GreetingController as vm" and
8834 * // $scope.vm === <controller instance>
8835 *
8836 * function showCustomGreeting() {
8837 *
8838 * $mdDialog.show({
8839 * clickOutsideToClose: true,
8840 *
8841 * scope: $scope, // use parent scope in template
8842 * preserveScope: true, // do not forget this if use parent scope
8843
8844 * // Since GreetingController is instantiated with ControllerAs syntax
8845 * // AND we are passing the parent '$scope' to the dialog, we MUST
8846 * // use 'vm.<xxx>' in the template markup
8847 *
8848 * template: '<md-dialog>' +
8849 * ' <md-dialog-content>' +
8850 * ' Hi There {{vm.employee}}' +
8851 * ' </md-dialog-content>' +
8852 * '</md-dialog>',
8853 *
8854 * controller: function DialogController($scope, $mdDialog) {
8855 * $scope.closeDialog = function() {
8856 * $mdDialog.hide();
8857 * }
8858 * }
8859 * });
8860 * }
8861 *
8862 * }
8863 *
8864 * // Greeting controller used with the more complex 'showCustomGreeting()' custom dialog
8865 *
8866 * function GreetingController($scope, $mdDialog, employee) {
8867 * // Assigned from construction <code>locals</code> options...
8868 * $scope.employee = employee;
8869 *
8870 * $scope.closeDialog = function() {
8871 * // Easily hides most recent dialog shown...
8872 * // no specific instance reference is needed.
8873 * $mdDialog.hide();
8874 * };
8875 * }
8876 *
8877 * })(angular);
8878 * </hljs>
8879 */
8880
8881/**
8882 * @ngdoc method
8883 * @name $mdDialog#alert
8884 *
8885 * @description
8886 * Builds a preconfigured dialog with the specified message.
8887 *
8888 * @returns {obj} an `$mdDialogPreset` with the chainable configuration methods:
8889 *
8890 * - $mdDialogPreset#title(string) - Sets the alert title.
8891 * - $mdDialogPreset#textContent(string) - Sets the alert message.
8892 * - $mdDialogPreset#htmlContent(string) - Sets the alert message as HTML. Requires ngSanitize
8893 * module to be loaded. HTML is not run through Angular's compiler.
8894 * - $mdDialogPreset#ok(string) - Sets the alert "Okay" button text.
8895 * - $mdDialogPreset#theme(string) - Sets the theme of the alert dialog.
8896 * - $mdDialogPreset#targetEvent(DOMClickEvent=) - A click's event object. When passed in as an option,
8897 * the location of the click will be used as the starting point for the opening animation
8898 * of the the dialog.
8899 *
8900 */
8901
8902/**
8903 * @ngdoc method
8904 * @name $mdDialog#confirm
8905 *
8906 * @description
8907 * Builds a preconfigured dialog with the specified message. You can call show and the promise returned
8908 * will be resolved only if the user clicks the confirm action on the dialog.
8909 *
8910 * @returns {obj} an `$mdDialogPreset` with the chainable configuration methods:
8911 *
8912 * Additionally, it supports the following methods:
8913 *
8914 * - $mdDialogPreset#title(string) - Sets the confirm title.
8915 * - $mdDialogPreset#textContent(string) - Sets the confirm message.
8916 * - $mdDialogPreset#htmlContent(string) - Sets the confirm message as HTML. Requires ngSanitize
8917 * module to be loaded. HTML is not run through Angular's compiler.
8918 * - $mdDialogPreset#ok(string) - Sets the confirm "Okay" button text.
8919 * - $mdDialogPreset#cancel(string) - Sets the confirm "Cancel" button text.
8920 * - $mdDialogPreset#theme(string) - Sets the theme of the confirm dialog.
8921 * - $mdDialogPreset#targetEvent(DOMClickEvent=) - A click's event object. When passed in as an option,
8922 * the location of the click will be used as the starting point for the opening animation
8923 * of the the dialog.
8924 *
8925 */
8926
8927/**
8928 * @ngdoc method
8929 * @name $mdDialog#prompt
8930 *
8931 * @description
8932 * Builds a preconfigured dialog with the specified message and input box. You can call show and the promise returned
8933 * will be resolved only if the user clicks the prompt action on the dialog, passing the input value as the first argument.
8934 *
8935 * @returns {obj} an `$mdDialogPreset` with the chainable configuration methods:
8936 *
8937 * Additionally, it supports the following methods:
8938 *
8939 * - $mdDialogPreset#title(string) - Sets the prompt title.
8940 * - $mdDialogPreset#textContent(string) - Sets the prompt message.
8941 * - $mdDialogPreset#htmlContent(string) - Sets the prompt message as HTML. Requires ngSanitize
8942 * module to be loaded. HTML is not run through Angular's compiler.
8943 * - $mdDialogPreset#placeholder(string) - Sets the placeholder text for the input.
8944 * - $mdDialogPreset#initialValue(string) - Sets the initial value for the prompt input.
8945 * - $mdDialogPreset#ok(string) - Sets the prompt "Okay" button text.
8946 * - $mdDialogPreset#cancel(string) - Sets the prompt "Cancel" button text.
8947 * - $mdDialogPreset#theme(string) - Sets the theme of the prompt dialog.
8948 * - $mdDialogPreset#targetEvent(DOMClickEvent=) - A click's event object. When passed in as an option,
8949 * the location of the click will be used as the starting point for the opening animation
8950 * of the the dialog.
8951 *
8952 */
8953
8954/**
8955 * @ngdoc method
8956 * @name $mdDialog#show
8957 *
8958 * @description
8959 * Show a dialog with the specified options.
8960 *
8961 * @param {object} optionsOrPreset Either provide an `$mdDialogPreset` returned from `alert()`, and
8962 * `confirm()`, or an options object with the following properties:
8963 * - `templateUrl` - `{string=}`: The url of a template that will be used as the content
8964 * of the dialog.
8965 * - `template` - `{string=}`: HTML template to show in the dialog. This **must** be trusted HTML
8966 * with respect to Angular's [$sce service](https://docs.angularjs.org/api/ng/service/$sce).
8967 * This template should **never** be constructed with any kind of user input or user data.
8968 * - `contentElement` - `{string|Element}`: Instead of using a template, which will be compiled each time a
8969 * dialog opens, you can also use a DOM element.<br/>
8970 * * When specifying an element, which is present on the DOM, `$mdDialog` will temporary fetch the element into
8971 * the dialog and restores it at the old DOM position upon close.
8972 * * When specifying a string, the string be used as a CSS selector, to lookup for the element in the DOM.
8973 * - `autoWrap` - `{boolean=}`: Whether or not to automatically wrap the template with a
8974 * `<md-dialog>` tag if one is not provided. Defaults to true. Can be disabled if you provide a
8975 * custom dialog directive.
8976 * - `targetEvent` - `{DOMClickEvent=}`: A click's event object. When passed in as an option,
8977 * the location of the click will be used as the starting point for the opening animation
8978 * of the the dialog.
8979 * - `openFrom` - `{string|Element|object}`: The query selector, DOM element or the Rect object
8980 * that is used to determine the bounds (top, left, height, width) from which the Dialog will
8981 * originate.
8982 * - `closeTo` - `{string|Element|object}`: The query selector, DOM element or the Rect object
8983 * that is used to determine the bounds (top, left, height, width) to which the Dialog will
8984 * target.
8985 * - `scope` - `{object=}`: the scope to link the template / controller to. If none is specified,
8986 * it will create a new isolate scope.
8987 * This scope will be destroyed when the dialog is removed unless `preserveScope` is set to true.
8988 * - `preserveScope` - `{boolean=}`: whether to preserve the scope when the element is removed. Default is false
8989 * - `disableParentScroll` - `{boolean=}`: Whether to disable scrolling while the dialog is open.
8990 * Default true.
8991 * - `hasBackdrop` - `{boolean=}`: Whether there should be an opaque backdrop behind the dialog.
8992 * Default true.
8993 * - `clickOutsideToClose` - `{boolean=}`: Whether the user can click outside the dialog to
8994 * close it. Default false.
8995 * - `escapeToClose` - `{boolean=}`: Whether the user can press escape to close the dialog.
8996 * Default true.
8997 * - `focusOnOpen` - `{boolean=}`: An option to override focus behavior on open. Only disable if
8998 * focusing some other way, as focus management is required for dialogs to be accessible.
8999 * Defaults to true.
9000 * - `controller` - `{function|string=}`: The controller to associate with the dialog. The controller
9001 * will be injected with the local `$mdDialog`, which passes along a scope for the dialog.
9002 * - `locals` - `{object=}`: An object containing key/value pairs. The keys will be used as names
9003 * of values to inject into the controller. For example, `locals: {three: 3}` would inject
9004 * `three` into the controller, with the value 3. If `bindToController` is true, they will be
9005 * copied to the controller instead.
9006 * - `bindToController` - `bool`: bind the locals to the controller, instead of passing them in.
9007 * - `resolve` - `{object=}`: Similar to locals, except it takes promises as values, and the
9008 * dialog will not open until all of the promises resolve.
9009 * - `controllerAs` - `{string=}`: An alias to assign the controller to on the scope.
9010 * - `parent` - `{element=}`: The element to append the dialog to. Defaults to appending
9011 * to the root element of the application.
9012 * - `onShowing` - `function(scope, element)`: Callback function used to announce the show() action is
9013 * starting.
9014 * - `onComplete` - `function(scope, element)`: Callback function used to announce when the show() action is
9015 * finished.
9016 * - `onRemoving` - `function(element, removePromise)`: Callback function used to announce the
9017 * close/hide() action is starting. This allows developers to run custom animations
9018 * in parallel the close animations.
9019 * - `fullscreen` `{boolean=}`: An option to toggle whether the dialog should show in fullscreen
9020 * or not. Defaults to `false`.
9021 * @returns {promise} A promise that can be resolved with `$mdDialog.hide()` or
9022 * rejected with `$mdDialog.cancel()`.
9023 */
9024
9025/**
9026 * @ngdoc method
9027 * @name $mdDialog#hide
9028 *
9029 * @description
9030 * Hide an existing dialog and resolve the promise returned from `$mdDialog.show()`.
9031 *
9032 * @param {*=} response An argument for the resolved promise.
9033 *
9034 * @returns {promise} A promise that is resolved when the dialog has been closed.
9035 */
9036
9037/**
9038 * @ngdoc method
9039 * @name $mdDialog#cancel
9040 *
9041 * @description
9042 * Hide an existing dialog and reject the promise returned from `$mdDialog.show()`.
9043 *
9044 * @param {*=} response An argument for the rejected promise.
9045 *
9046 * @returns {promise} A promise that is resolved when the dialog has been closed.
9047 */
9048
9049function MdDialogProvider($$interimElementProvider) {
9050 // Elements to capture and redirect focus when the user presses tab at the dialog boundary.
9051 advancedDialogOptions.$inject = ["$mdDialog", "$mdConstant"];
9052 dialogDefaultOptions.$inject = ["$mdDialog", "$mdAria", "$mdUtil", "$mdConstant", "$animate", "$document", "$window", "$rootElement", "$log", "$injector", "$mdTheming"];
9053 var topFocusTrap, bottomFocusTrap;
9054
9055 return $$interimElementProvider('$mdDialog')
9056 .setDefaults({
9057 methods: ['disableParentScroll', 'hasBackdrop', 'clickOutsideToClose', 'escapeToClose',
9058 'targetEvent', 'closeTo', 'openFrom', 'parent', 'fullscreen', 'contentElement'],
9059 options: dialogDefaultOptions
9060 })
9061 .addPreset('alert', {
9062 methods: ['title', 'htmlContent', 'textContent', 'content', 'ariaLabel', 'ok', 'theme',
9063 'css'],
9064 options: advancedDialogOptions
9065 })
9066 .addPreset('confirm', {
9067 methods: ['title', 'htmlContent', 'textContent', 'content', 'ariaLabel', 'ok', 'cancel',
9068 'theme', 'css'],
9069 options: advancedDialogOptions
9070 })
9071 .addPreset('prompt', {
9072 methods: ['title', 'htmlContent', 'textContent', 'initialValue', 'content', 'placeholder', 'ariaLabel',
9073 'ok', 'cancel', 'theme', 'css'],
9074 options: advancedDialogOptions
9075 });
9076
9077 /* @ngInject */
9078 function advancedDialogOptions($mdDialog, $mdConstant) {
9079 return {
9080 template: [
9081 '<md-dialog md-theme="{{ dialog.theme }}" aria-label="{{ dialog.ariaLabel }}" ng-class="dialog.css">',
9082 ' <md-dialog-content class="md-dialog-content" role="document" tabIndex="-1">',
9083 ' <h2 class="md-title">{{ dialog.title }}</h2>',
9084 ' <div ng-if="::dialog.mdHtmlContent" class="md-dialog-content-body" ',
9085 ' ng-bind-html="::dialog.mdHtmlContent"></div>',
9086 ' <div ng-if="::!dialog.mdHtmlContent" class="md-dialog-content-body">',
9087 ' <p>{{::dialog.mdTextContent}}</p>',
9088 ' </div>',
9089 ' <md-input-container md-no-float ng-if="::dialog.$type == \'prompt\'" class="md-prompt-input-container">',
9090 ' <input ng-keypress="dialog.keypress($event)" md-autofocus ng-model="dialog.result" ' +
9091 ' placeholder="{{::dialog.placeholder}}">',
9092 ' </md-input-container>',
9093 ' </md-dialog-content>',
9094 ' <md-dialog-actions>',
9095 ' <md-button ng-if="dialog.$type === \'confirm\' || dialog.$type === \'prompt\'"' +
9096 ' ng-click="dialog.abort()" class="md-primary md-cancel-button">',
9097 ' {{ dialog.cancel }}',
9098 ' </md-button>',
9099 ' <md-button ng-click="dialog.hide()" class="md-primary md-confirm-button" md-autofocus="dialog.$type===\'alert\'">',
9100 ' {{ dialog.ok }}',
9101 ' </md-button>',
9102 ' </md-dialog-actions>',
9103 '</md-dialog>'
9104 ].join('').replace(/\s\s+/g, ''),
9105 controller: function mdDialogCtrl() {
9106 var isPrompt = this.$type == 'prompt';
9107
9108 if (isPrompt && this.initialValue) {
9109 this.result = this.initialValue;
9110 }
9111
9112 this.hide = function() {
9113 $mdDialog.hide(isPrompt ? this.result : true);
9114 };
9115 this.abort = function() {
9116 $mdDialog.cancel();
9117 };
9118 this.keypress = function($event) {
9119 if ($event.keyCode === $mdConstant.KEY_CODE.ENTER) {
9120 $mdDialog.hide(this.result);
9121 }
9122 };
9123 },
9124 controllerAs: 'dialog',
9125 bindToController: true,
9126 };
9127 }
9128
9129 /* @ngInject */
9130 function dialogDefaultOptions($mdDialog, $mdAria, $mdUtil, $mdConstant, $animate, $document, $window, $rootElement,
9131 $log, $injector, $mdTheming) {
9132
9133 return {
9134 hasBackdrop: true,
9135 isolateScope: true,
9136 onCompiling: beforeCompile,
9137 onShow: onShow,
9138 onShowing: beforeShow,
9139 onRemove: onRemove,
9140 clickOutsideToClose: false,
9141 escapeToClose: true,
9142 targetEvent: null,
9143 contentElement: null,
9144 closeTo: null,
9145 openFrom: null,
9146 focusOnOpen: true,
9147 disableParentScroll: true,
9148 autoWrap: true,
9149 fullscreen: false,
9150 transformTemplate: function(template, options) {
9151 // Make the dialog container focusable, because otherwise the focus will be always redirected to
9152 // an element outside of the container, and the focus trap won't work probably..
9153 // Also the tabindex is needed for the `escapeToClose` functionality, because
9154 // the keyDown event can't be triggered when the focus is outside of the container.
9155 return '<div class="md-dialog-container" tabindex="-1">' + validatedTemplate(template) + '</div>';
9156
9157 /**
9158 * The specified template should contain a <md-dialog> wrapper element....
9159 */
9160 function validatedTemplate(template) {
9161 if (options.autoWrap && !/<\/md-dialog>/g.test(template)) {
9162 return '<md-dialog>' + (template || '') + '</md-dialog>';
9163 } else {
9164 return template || '';
9165 }
9166 }
9167 }
9168 };
9169
9170 function beforeCompile(options) {
9171 // Automatically apply the theme, if the user didn't specify a theme explicitly.
9172 // Those option changes need to be done, before the compilation has started, because otherwise
9173 // the option changes will be not available in the $mdCompilers locales.
9174 detectTheming(options);
9175
9176 if (options.contentElement) {
9177 options.restoreContentElement = installContentElement(options);
9178 }
9179 }
9180
9181 function beforeShow(scope, element, options, controller) {
9182
9183 if (controller) {
9184 controller.mdHtmlContent = controller.htmlContent || options.htmlContent || '';
9185 controller.mdTextContent = controller.textContent || options.textContent ||
9186 controller.content || options.content || '';
9187
9188 if (controller.mdHtmlContent && !$injector.has('$sanitize')) {
9189 throw Error('The ngSanitize module must be loaded in order to use htmlContent.');
9190 }
9191
9192 if (controller.mdHtmlContent && controller.mdTextContent) {
9193 throw Error('md-dialog cannot have both `htmlContent` and `textContent`');
9194 }
9195 }
9196 }
9197
9198 /** Show method for dialogs */
9199 function onShow(scope, element, options, controller) {
9200 angular.element($document[0].body).addClass('md-dialog-is-showing');
9201
9202 var dialogElement = element.find('md-dialog');
9203
9204 // Once a dialog has `ng-cloak` applied on his template the dialog animation will not work properly.
9205 // This is a very common problem, so we have to notify the developer about this.
9206 if (dialogElement.hasClass('ng-cloak')) {
9207 var message = '$mdDialog: using `<md-dialog ng-cloak >` will affect the dialog opening animations.';
9208 $log.warn( message, element[0] );
9209 }
9210
9211 captureParentAndFromToElements(options);
9212 configureAria(dialogElement, options);
9213 showBackdrop(scope, element, options);
9214 activateListeners(element, options);
9215
9216 return dialogPopIn(element, options)
9217 .then(function() {
9218 lockScreenReader(element, options);
9219 warnDeprecatedActions();
9220 focusOnOpen();
9221 });
9222
9223 /**
9224 * Check to see if they used the deprecated .md-actions class and log a warning
9225 */
9226 function warnDeprecatedActions() {
9227 if (element[0].querySelector('.md-actions')) {
9228 $log.warn('Using a class of md-actions is deprecated, please use <md-dialog-actions>.');
9229 }
9230 }
9231
9232 /**
9233 * For alerts, focus on content... otherwise focus on
9234 * the close button (or equivalent)
9235 */
9236 function focusOnOpen() {
9237 if (options.focusOnOpen) {
9238 var target = $mdUtil.findFocusTarget(element) || findCloseButton() || dialogElement;
9239 target.focus();
9240 }
9241
9242 /**
9243 * If no element with class dialog-close, try to find the last
9244 * button child in md-actions and assume it is a close button.
9245 *
9246 * If we find no actions at all, log a warning to the console.
9247 */
9248 function findCloseButton() {
9249 var closeButton = element[0].querySelector('.dialog-close');
9250
9251 if (!closeButton) {
9252 var actionButtons = element[0].querySelectorAll('.md-actions button, md-dialog-actions button');
9253 closeButton = actionButtons[actionButtons.length - 1];
9254 }
9255
9256 return closeButton;
9257 }
9258 }
9259 }
9260
9261 /**
9262 * Remove function for all dialogs
9263 */
9264 function onRemove(scope, element, options) {
9265 options.deactivateListeners();
9266 options.unlockScreenReader();
9267 options.hideBackdrop(options.$destroy);
9268
9269 // Remove the focus traps that we added earlier for keeping focus within the dialog.
9270 if (topFocusTrap && topFocusTrap.parentNode) {
9271 topFocusTrap.parentNode.removeChild(topFocusTrap);
9272 }
9273
9274 if (bottomFocusTrap && bottomFocusTrap.parentNode) {
9275 bottomFocusTrap.parentNode.removeChild(bottomFocusTrap);
9276 }
9277
9278 // For navigation $destroy events, do a quick, non-animated removal,
9279 // but for normal closes (from clicks, etc) animate the removal
9280 return !!options.$destroy ? detachAndClean() : animateRemoval().then( detachAndClean );
9281
9282 /**
9283 * For normal closes, animate the removal.
9284 * For forced closes (like $destroy events), skip the animations
9285 */
9286 function animateRemoval() {
9287 return dialogPopOut(element, options);
9288 }
9289
9290 /**
9291 * Detach the element
9292 */
9293 function detachAndClean() {
9294 angular.element($document[0].body).removeClass('md-dialog-is-showing');
9295 // Only remove the element, if it's not provided through the contentElement option.
9296 if (!options.contentElement) {
9297 element.remove();
9298 } else {
9299 options.reverseContainerStretch();
9300 options.restoreContentElement();
9301 }
9302
9303 if (!options.$destroy) options.origin.focus();
9304 }
9305 }
9306
9307 function detectTheming(options) {
9308 // Only detect the theming, if the developer didn't specify the theme specifically.
9309 if (options.theme) return;
9310
9311 options.theme = $mdTheming.defaultTheme();
9312
9313 if (options.targetEvent && options.targetEvent.target) {
9314 var targetEl = angular.element(options.targetEvent.target);
9315
9316 // Once the user specifies a targetEvent, we will automatically try to find the correct
9317 // nested theme.
9318 options.theme = (targetEl.controller('mdTheme') || {}).$mdTheme || options.theme;
9319 }
9320
9321 }
9322
9323 /**
9324 * Installs a content element to the current $$interimElement provider options.
9325 * @returns {Function} Function to restore the content element at its old DOM location.
9326 */
9327 function installContentElement(options) {
9328 var contentEl = options.contentElement;
9329 var restoreFn = null;
9330
9331 if (angular.isString(contentEl)) {
9332 contentEl = document.querySelector(contentEl);
9333 restoreFn = createRestoreFn(contentEl);
9334 } else {
9335 contentEl = contentEl[0] || contentEl;
9336
9337 // When the element is visible in the DOM, then we restore it at close of the dialog.
9338 // Otherwise it will be removed from the DOM after close.
9339 if (document.contains(contentEl)) {
9340 restoreFn = createRestoreFn(contentEl);
9341 } else {
9342 restoreFn = function() {
9343 contentEl.parentNode.removeChild(contentEl);
9344 }
9345 }
9346 }
9347
9348 // Overwrite the options to use the content element.
9349 options.element = angular.element(contentEl);
9350 options.skipCompile = true;
9351
9352 return restoreFn;
9353
9354 function createRestoreFn(element) {
9355 var parent = element.parentNode;
9356 var nextSibling = element.nextElementSibling;
9357
9358 return function() {
9359 if (!nextSibling) {
9360 // When the element didn't had any sibling, then it can be simply appended to the
9361 // parent, because it plays no role, which index it had before.
9362 parent.appendChild(element);
9363 } else {
9364 // When the element had a sibling, which marks the previous position of the element
9365 // in the DOM, we insert it correctly before the sibling, to have the same index as
9366 // before.
9367 parent.insertBefore(element, nextSibling);
9368 }
9369 }
9370 }
9371 }
9372
9373 /**
9374 * Capture originator/trigger/from/to element information (if available)
9375 * and the parent container for the dialog; defaults to the $rootElement
9376 * unless overridden in the options.parent
9377 */
9378 function captureParentAndFromToElements(options) {
9379 options.origin = angular.extend({
9380 element: null,
9381 bounds: null,
9382 focus: angular.noop
9383 }, options.origin || {});
9384
9385 options.parent = getDomElement(options.parent, $rootElement);
9386 options.closeTo = getBoundingClientRect(getDomElement(options.closeTo));
9387 options.openFrom = getBoundingClientRect(getDomElement(options.openFrom));
9388
9389 if ( options.targetEvent ) {
9390 options.origin = getBoundingClientRect(options.targetEvent.target, options.origin);
9391 }
9392
9393
9394 /**
9395 * Identify the bounding RECT for the target element
9396 *
9397 */
9398 function getBoundingClientRect (element, orig) {
9399 var source = angular.element((element || {}));
9400 if (source && source.length) {
9401 // Compute and save the target element's bounding rect, so that if the
9402 // element is hidden when the dialog closes, we can shrink the dialog
9403 // back to the same position it expanded from.
9404 //
9405 // Checking if the source is a rect object or a DOM element
9406 var bounds = {top:0,left:0,height:0,width:0};
9407 var hasFn = angular.isFunction(source[0].getBoundingClientRect);
9408
9409 return angular.extend(orig || {}, {
9410 element : hasFn ? source : undefined,
9411 bounds : hasFn ? source[0].getBoundingClientRect() : angular.extend({}, bounds, source[0]),
9412 focus : angular.bind(source, source.focus),
9413 });
9414 }
9415 }
9416
9417 /**
9418 * If the specifier is a simple string selector, then query for
9419 * the DOM element.
9420 */
9421 function getDomElement(element, defaultElement) {
9422 if (angular.isString(element)) {
9423 element = $document[0].querySelector(element);
9424 }
9425
9426 // If we have a reference to a raw dom element, always wrap it in jqLite
9427 return angular.element(element || defaultElement);
9428 }
9429
9430 }
9431
9432 /**
9433 * Listen for escape keys and outside clicks to auto close
9434 */
9435 function activateListeners(element, options) {
9436 var window = angular.element($window);
9437 var onWindowResize = $mdUtil.debounce(function() {
9438 stretchDialogContainerToViewport(element, options);
9439 }, 60);
9440
9441 var removeListeners = [];
9442 var smartClose = function() {
9443 // Only 'confirm' dialogs have a cancel button... escape/clickOutside will
9444 // cancel or fallback to hide.
9445 var closeFn = ( options.$type == 'alert' ) ? $mdDialog.hide : $mdDialog.cancel;
9446 $mdUtil.nextTick(closeFn, true);
9447 };
9448
9449 if (options.escapeToClose) {
9450 var parentTarget = options.parent;
9451 var keyHandlerFn = function(ev) {
9452 if (ev.keyCode === $mdConstant.KEY_CODE.ESCAPE) {
9453 ev.stopPropagation();
9454 ev.preventDefault();
9455
9456 smartClose();
9457 }
9458 };
9459
9460 // Add keydown listeners
9461 element.on('keydown', keyHandlerFn);
9462 parentTarget.on('keydown', keyHandlerFn);
9463
9464 // Queue remove listeners function
9465 removeListeners.push(function() {
9466
9467 element.off('keydown', keyHandlerFn);
9468 parentTarget.off('keydown', keyHandlerFn);
9469
9470 });
9471 }
9472
9473 // Register listener to update dialog on window resize
9474 window.on('resize', onWindowResize);
9475
9476 removeListeners.push(function() {
9477 window.off('resize', onWindowResize);
9478 });
9479
9480 if (options.clickOutsideToClose) {
9481 var target = element;
9482 var sourceElem;
9483
9484 // Keep track of the element on which the mouse originally went down
9485 // so that we can only close the backdrop when the 'click' started on it.
9486 // A simple 'click' handler does not work,
9487 // it sets the target object as the element the mouse went down on.
9488 var mousedownHandler = function(ev) {
9489 sourceElem = ev.target;
9490 };
9491
9492 // We check if our original element and the target is the backdrop
9493 // because if the original was the backdrop and the target was inside the dialog
9494 // we don't want to dialog to close.
9495 var mouseupHandler = function(ev) {
9496 if (sourceElem === target[0] && ev.target === target[0]) {
9497 ev.stopPropagation();
9498 ev.preventDefault();
9499
9500 smartClose();
9501 }
9502 };
9503
9504 // Add listeners
9505 target.on('mousedown', mousedownHandler);
9506 target.on('mouseup', mouseupHandler);
9507
9508 // Queue remove listeners function
9509 removeListeners.push(function() {
9510 target.off('mousedown', mousedownHandler);
9511 target.off('mouseup', mouseupHandler);
9512 });
9513 }
9514
9515 // Attach specific `remove` listener handler
9516 options.deactivateListeners = function() {
9517 removeListeners.forEach(function(removeFn) {
9518 removeFn();
9519 });
9520 options.deactivateListeners = null;
9521 };
9522 }
9523
9524 /**
9525 * Show modal backdrop element...
9526 */
9527 function showBackdrop(scope, element, options) {
9528
9529 if (options.disableParentScroll) {
9530 // !! DO this before creating the backdrop; since disableScrollAround()
9531 // configures the scroll offset; which is used by mdBackDrop postLink()
9532 options.restoreScroll = $mdUtil.disableScrollAround(element, options.parent);
9533 }
9534
9535 if (options.hasBackdrop) {
9536 options.backdrop = $mdUtil.createBackdrop(scope, "md-dialog-backdrop md-opaque");
9537 $animate.enter(options.backdrop, options.parent);
9538 }
9539
9540 /**
9541 * Hide modal backdrop element...
9542 */
9543 options.hideBackdrop = function hideBackdrop($destroy) {
9544 if (options.backdrop) {
9545 if ( !!$destroy ) options.backdrop.remove();
9546 else $animate.leave(options.backdrop);
9547 }
9548
9549
9550 if (options.disableParentScroll) {
9551 options.restoreScroll();
9552 delete options.restoreScroll;
9553 }
9554
9555 options.hideBackdrop = null;
9556 };
9557 }
9558
9559 /**
9560 * Inject ARIA-specific attributes appropriate for Dialogs
9561 */
9562 function configureAria(element, options) {
9563
9564 var role = (options.$type === 'alert') ? 'alertdialog' : 'dialog';
9565 var dialogContent = element.find('md-dialog-content');
9566 var existingDialogId = element.attr('id');
9567 var dialogContentId = 'dialogContent_' + (existingDialogId || $mdUtil.nextUid());
9568
9569 element.attr({
9570 'role': role,
9571 'tabIndex': '-1'
9572 });
9573
9574 if (dialogContent.length === 0) {
9575 dialogContent = element;
9576 // If the dialog element already had an ID, don't clobber it.
9577 if (existingDialogId) {
9578 dialogContentId = existingDialogId;
9579 }
9580 }
9581
9582 dialogContent.attr('id', dialogContentId);
9583 element.attr('aria-describedby', dialogContentId);
9584
9585 if (options.ariaLabel) {
9586 $mdAria.expect(element, 'aria-label', options.ariaLabel);
9587 }
9588 else {
9589 $mdAria.expectAsync(element, 'aria-label', function() {
9590 var words = dialogContent.text().split(/\s+/);
9591 if (words.length > 3) words = words.slice(0, 3).concat('...');
9592 return words.join(' ');
9593 });
9594 }
9595
9596 // Set up elements before and after the dialog content to capture focus and
9597 // redirect back into the dialog.
9598 topFocusTrap = document.createElement('div');
9599 topFocusTrap.classList.add('md-dialog-focus-trap');
9600 topFocusTrap.tabIndex = 0;
9601
9602 bottomFocusTrap = topFocusTrap.cloneNode(false);
9603
9604 // When focus is about to move out of the dialog, we want to intercept it and redirect it
9605 // back to the dialog element.
9606 var focusHandler = function() {
9607 element.focus();
9608 };
9609 topFocusTrap.addEventListener('focus', focusHandler);
9610 bottomFocusTrap.addEventListener('focus', focusHandler);
9611
9612 // The top focus trap inserted immeidately before the md-dialog element (as a sibling).
9613 // The bottom focus trap is inserted at the very end of the md-dialog element (as a child).
9614 element[0].parentNode.insertBefore(topFocusTrap, element[0]);
9615 element.after(bottomFocusTrap);
9616 }
9617
9618 /**
9619 * Prevents screen reader interaction behind modal window
9620 * on swipe interfaces
9621 */
9622 function lockScreenReader(element, options) {
9623 var isHidden = true;
9624
9625 // get raw DOM node
9626 walkDOM(element[0]);
9627
9628 options.unlockScreenReader = function() {
9629 isHidden = false;
9630 walkDOM(element[0]);
9631
9632 options.unlockScreenReader = null;
9633 };
9634
9635 /**
9636 * Walk DOM to apply or remove aria-hidden on sibling nodes
9637 * and parent sibling nodes
9638 *
9639 */
9640 function walkDOM(element) {
9641 while (element.parentNode) {
9642 if (element === document.body) {
9643 return;
9644 }
9645 var children = element.parentNode.children;
9646 for (var i = 0; i < children.length; i++) {
9647 // skip over child if it is an ascendant of the dialog
9648 // or a script or style tag
9649 if (element !== children[i] && !isNodeOneOf(children[i], ['SCRIPT', 'STYLE'])) {
9650 children[i].setAttribute('aria-hidden', isHidden);
9651 }
9652 }
9653
9654 walkDOM(element = element.parentNode);
9655 }
9656 }
9657 }
9658
9659 /**
9660 * Ensure the dialog container fill-stretches to the viewport
9661 */
9662 function stretchDialogContainerToViewport(container, options) {
9663 var isFixed = $window.getComputedStyle($document[0].body).position == 'fixed';
9664 var backdrop = options.backdrop ? $window.getComputedStyle(options.backdrop[0]) : null;
9665 var height = backdrop ? Math.min($document[0].body.clientHeight, Math.ceil(Math.abs(parseInt(backdrop.height, 10)))) : 0;
9666
9667 var previousStyles = {
9668 top: container.css('top'),
9669 height: container.css('height')
9670 };
9671
9672 container.css({
9673 top: (isFixed ? $mdUtil.scrollTop(options.parent) : 0) + 'px',
9674 height: height ? height + 'px' : '100%'
9675 });
9676
9677 return function() {
9678 // Reverts the modified styles back to the previous values.
9679 // This is needed for contentElements, which should have the same styles after close
9680 // as before.
9681 container.css(previousStyles);
9682 };
9683 }
9684
9685 /**
9686 * Dialog open and pop-in animation
9687 */
9688 function dialogPopIn(container, options) {
9689 // Add the `md-dialog-container` to the DOM
9690 options.parent.append(container);
9691 options.reverseContainerStretch = stretchDialogContainerToViewport(container, options);
9692
9693 var dialogEl = container.find('md-dialog');
9694 var animator = $mdUtil.dom.animator;
9695 var buildTranslateToOrigin = animator.calculateZoomToOrigin;
9696 var translateOptions = {transitionInClass: 'md-transition-in', transitionOutClass: 'md-transition-out'};
9697 var from = animator.toTransformCss(buildTranslateToOrigin(dialogEl, options.openFrom || options.origin));
9698 var to = animator.toTransformCss(""); // defaults to center display (or parent or $rootElement)
9699
9700 dialogEl.toggleClass('md-dialog-fullscreen', !!options.fullscreen);
9701
9702 return animator
9703 .translate3d(dialogEl, from, to, translateOptions)
9704 .then(function(animateReversal) {
9705
9706 // Build a reversal translate function synced to this translation...
9707 options.reverseAnimate = function() {
9708 delete options.reverseAnimate;
9709
9710 if (options.closeTo) {
9711 // Using the opposite classes to create a close animation to the closeTo element
9712 translateOptions = {transitionInClass: 'md-transition-out', transitionOutClass: 'md-transition-in'};
9713 from = to;
9714 to = animator.toTransformCss(buildTranslateToOrigin(dialogEl, options.closeTo));
9715
9716 return animator
9717 .translate3d(dialogEl, from, to,translateOptions);
9718 }
9719
9720 return animateReversal(
9721 to = animator.toTransformCss(
9722 // in case the origin element has moved or is hidden,
9723 // let's recalculate the translateCSS
9724 buildTranslateToOrigin(dialogEl, options.origin)
9725 )
9726 );
9727
9728 };
9729
9730 // Function to revert the generated animation styles on the dialog element.
9731 // Useful when using a contentElement instead of a template.
9732 options.clearAnimate = function() {
9733 delete options.clearAnimate;
9734
9735 // Remove the transition classes, added from $animateCSS, since those can't be removed
9736 // by reversely running the animator.
9737 dialogEl.removeClass([
9738 translateOptions.transitionOutClass,
9739 translateOptions.transitionInClass
9740 ].join(' '));
9741
9742 // Run the animation reversely to remove the previous added animation styles.
9743 return animator.translate3d(dialogEl, to, animator.toTransformCss(''), {});
9744 };
9745
9746 return true;
9747 });
9748 }
9749
9750 /**
9751 * Dialog close and pop-out animation
9752 */
9753 function dialogPopOut(container, options) {
9754 return options.reverseAnimate().then(function() {
9755 if (options.contentElement) {
9756 // When we use a contentElement, we want the element to be the same as before.
9757 // That means, that we have to clear all the animation properties, like transform.
9758 options.clearAnimate();
9759 }
9760 });
9761 }
9762
9763 /**
9764 * Utility function to filter out raw DOM nodes
9765 */
9766 function isNodeOneOf(elem, nodeTypeArray) {
9767 if (nodeTypeArray.indexOf(elem.nodeName) !== -1) {
9768 return true;
9769 }
9770 }
9771
9772 }
9773}
9774
9775})();
9776(function(){
9777"use strict";
9778
9779/**
9780 * @ngdoc module
9781 * @name material.components.divider
9782 * @description Divider module!
9783 */
9784MdDividerDirective.$inject = ["$mdTheming"];
9785angular.module('material.components.divider', [
9786 'material.core'
9787])
9788 .directive('mdDivider', MdDividerDirective);
9789
9790/**
9791 * @ngdoc directive
9792 * @name mdDivider
9793 * @module material.components.divider
9794 * @restrict E
9795 *
9796 * @description
9797 * 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.
9798 *
9799 * @param {boolean=} md-inset Add this attribute to activate the inset divider style.
9800 * @usage
9801 * <hljs lang="html">
9802 * <md-divider></md-divider>
9803 *
9804 * <md-divider md-inset></md-divider>
9805 * </hljs>
9806 *
9807 */
9808function MdDividerDirective($mdTheming) {
9809 return {
9810 restrict: 'E',
9811 link: $mdTheming
9812 };
9813}
9814
9815})();
9816(function(){
9817"use strict";
9818
9819(function() {
9820 'use strict';
9821
9822 /**
9823 * @ngdoc module
9824 * @name material.components.fabActions
9825 */
9826 MdFabActionsDirective.$inject = ["$mdUtil"];
9827 angular
9828 .module('material.components.fabActions', ['material.core'])
9829 .directive('mdFabActions', MdFabActionsDirective);
9830
9831 /**
9832 * @ngdoc directive
9833 * @name mdFabActions
9834 * @module material.components.fabActions
9835 *
9836 * @restrict E
9837 *
9838 * @description
9839 * The `<md-fab-actions>` directive is used inside of a `<md-fab-speed-dial>` or
9840 * `<md-fab-toolbar>` directive to mark an element (or elements) as the actions and setup the
9841 * proper event listeners.
9842 *
9843 * @usage
9844 * See the `<md-fab-speed-dial>` or `<md-fab-toolbar>` directives for example usage.
9845 */
9846 function MdFabActionsDirective($mdUtil) {
9847 return {
9848 restrict: 'E',
9849
9850 require: ['^?mdFabSpeedDial', '^?mdFabToolbar'],
9851
9852 compile: function(element, attributes) {
9853 var children = element.children();
9854
9855 var hasNgRepeat = $mdUtil.prefixer().hasAttribute(children, 'ng-repeat');
9856
9857 // Support both ng-repeat and static content
9858 if (hasNgRepeat) {
9859 children.addClass('md-fab-action-item');
9860 } else {
9861 // Wrap every child in a new div and add a class that we can scale/fling independently
9862 children.wrap('<div class="md-fab-action-item">');
9863 }
9864 }
9865 }
9866 }
9867
9868})();
9869
9870})();
9871(function(){
9872"use strict";
9873
9874(function() {
9875 'use strict';
9876
9877 MdFabController.$inject = ["$scope", "$element", "$animate", "$mdUtil", "$mdConstant", "$timeout"];
9878 angular.module('material.components.fabShared', ['material.core'])
9879 .controller('MdFabController', MdFabController);
9880
9881 function MdFabController($scope, $element, $animate, $mdUtil, $mdConstant, $timeout) {
9882 var vm = this;
9883
9884 // NOTE: We use async eval(s) below to avoid conflicts with any existing digest loops
9885
9886 vm.open = function() {
9887 $scope.$evalAsync("vm.isOpen = true");
9888 };
9889
9890 vm.close = function() {
9891 // Async eval to avoid conflicts with existing digest loops
9892 $scope.$evalAsync("vm.isOpen = false");
9893
9894 // Focus the trigger when the element closes so users can still tab to the next item
9895 $element.find('md-fab-trigger')[0].focus();
9896 };
9897
9898 // Toggle the open/close state when the trigger is clicked
9899 vm.toggle = function() {
9900 $scope.$evalAsync("vm.isOpen = !vm.isOpen");
9901 };
9902
9903 setupDefaults();
9904 setupListeners();
9905 setupWatchers();
9906
9907 var initialAnimationAttempts = 0;
9908 fireInitialAnimations();
9909
9910 function setupDefaults() {
9911 // Set the default direction to 'down' if none is specified
9912 vm.direction = vm.direction || 'down';
9913
9914 // Set the default to be closed
9915 vm.isOpen = vm.isOpen || false;
9916
9917 // Start the keyboard interaction at the first action
9918 resetActionIndex();
9919
9920 // Add an animations waiting class so we know not to run
9921 $element.addClass('md-animations-waiting');
9922 }
9923
9924 function setupListeners() {
9925 var eventTypes = [
9926 'click', 'focusin', 'focusout'
9927 ];
9928
9929 // Add our listeners
9930 angular.forEach(eventTypes, function(eventType) {
9931 $element.on(eventType, parseEvents);
9932 });
9933
9934 // Remove our listeners when destroyed
9935 $scope.$on('$destroy', function() {
9936 angular.forEach(eventTypes, function(eventType) {
9937 $element.off(eventType, parseEvents);
9938 });
9939
9940 // remove any attached keyboard handlers in case element is removed while
9941 // speed dial is open
9942 disableKeyboard();
9943 });
9944 }
9945
9946 var closeTimeout;
9947 function parseEvents(event) {
9948 // If the event is a click, just handle it
9949 if (event.type == 'click') {
9950 handleItemClick(event);
9951 }
9952
9953 // If we focusout, set a timeout to close the element
9954 if (event.type == 'focusout' && !closeTimeout) {
9955 closeTimeout = $timeout(function() {
9956 vm.close();
9957 }, 100, false);
9958 }
9959
9960 // If we see a focusin and there is a timeout about to run, cancel it so we stay open
9961 if (event.type == 'focusin' && closeTimeout) {
9962 $timeout.cancel(closeTimeout);
9963 closeTimeout = null;
9964 }
9965 }
9966
9967 function resetActionIndex() {
9968 vm.currentActionIndex = -1;
9969 }
9970
9971 function setupWatchers() {
9972 // Watch for changes to the direction and update classes/attributes
9973 $scope.$watch('vm.direction', function(newDir, oldDir) {
9974 // Add the appropriate classes so we can target the direction in the CSS
9975 $animate.removeClass($element, 'md-' + oldDir);
9976 $animate.addClass($element, 'md-' + newDir);
9977
9978 // Reset the action index since it may have changed
9979 resetActionIndex();
9980 });
9981
9982 var trigger, actions;
9983
9984 // Watch for changes to md-open
9985 $scope.$watch('vm.isOpen', function(isOpen) {
9986 // Reset the action index since it may have changed
9987 resetActionIndex();
9988
9989 // We can't get the trigger/actions outside of the watch because the component hasn't been
9990 // linked yet, so we wait until the first watch fires to cache them.
9991 if (!trigger || !actions) {
9992 trigger = getTriggerElement();
9993 actions = getActionsElement();
9994 }
9995
9996 if (isOpen) {
9997 enableKeyboard();
9998 } else {
9999 disableKeyboard();
10000 }
10001
10002 var toAdd = isOpen ? 'md-is-open' : '';
10003 var toRemove = isOpen ? '' : 'md-is-open';
10004
10005 // Set the proper ARIA attributes
10006 trigger.attr('aria-haspopup', true);
10007 trigger.attr('aria-expanded', isOpen);
10008 actions.attr('aria-hidden', !isOpen);
10009
10010 // Animate the CSS classes
10011 $animate.setClass($element, toAdd, toRemove);
10012 });
10013 }
10014
10015 function fireInitialAnimations() {
10016 // If the element is actually visible on the screen
10017 if ($element[0].scrollHeight > 0) {
10018 // Fire our animation
10019 $animate.addClass($element, '_md-animations-ready').then(function() {
10020 // Remove the waiting class
10021 $element.removeClass('md-animations-waiting');
10022 });
10023 }
10024
10025 // Otherwise, try for up to 1 second before giving up
10026 else if (initialAnimationAttempts < 10) {
10027 $timeout(fireInitialAnimations, 100);
10028
10029 // Increment our counter
10030 initialAnimationAttempts = initialAnimationAttempts + 1;
10031 }
10032 }
10033
10034 function enableKeyboard() {
10035 $element.on('keydown', keyPressed);
10036
10037 // On the next tick, setup a check for outside clicks; we do this on the next tick to avoid
10038 // clicks/touches that result in the isOpen attribute changing (e.g. a bound radio button)
10039 $mdUtil.nextTick(function() {
10040 angular.element(document).on('click touchend', checkForOutsideClick);
10041 });
10042
10043 // TODO: On desktop, we should be able to reset the indexes so you cannot tab through, but
10044 // this breaks accessibility, especially on mobile, since you have no arrow keys to press
10045 //resetActionTabIndexes();
10046 }
10047
10048 function disableKeyboard() {
10049 $element.off('keydown', keyPressed);
10050 angular.element(document).off('click touchend', checkForOutsideClick);
10051 }
10052
10053 function checkForOutsideClick(event) {
10054 if (event.target) {
10055 var closestTrigger = $mdUtil.getClosest(event.target, 'md-fab-trigger');
10056 var closestActions = $mdUtil.getClosest(event.target, 'md-fab-actions');
10057
10058 if (!closestTrigger && !closestActions) {
10059 vm.close();
10060 }
10061 }
10062 }
10063
10064 function keyPressed(event) {
10065 switch (event.which) {
10066 case $mdConstant.KEY_CODE.ESCAPE: vm.close(); event.preventDefault(); return false;
10067 case $mdConstant.KEY_CODE.LEFT_ARROW: doKeyLeft(event); return false;
10068 case $mdConstant.KEY_CODE.UP_ARROW: doKeyUp(event); return false;
10069 case $mdConstant.KEY_CODE.RIGHT_ARROW: doKeyRight(event); return false;
10070 case $mdConstant.KEY_CODE.DOWN_ARROW: doKeyDown(event); return false;
10071 }
10072 }
10073
10074 function doActionPrev(event) {
10075 focusAction(event, -1);
10076 }
10077
10078 function doActionNext(event) {
10079 focusAction(event, 1);
10080 }
10081
10082 function focusAction(event, direction) {
10083 var actions = resetActionTabIndexes();
10084
10085 // Increment/decrement the counter with restrictions
10086 vm.currentActionIndex = vm.currentActionIndex + direction;
10087 vm.currentActionIndex = Math.min(actions.length - 1, vm.currentActionIndex);
10088 vm.currentActionIndex = Math.max(0, vm.currentActionIndex);
10089
10090 // Focus the element
10091 var focusElement = angular.element(actions[vm.currentActionIndex]).children()[0];
10092 angular.element(focusElement).attr('tabindex', 0);
10093 focusElement.focus();
10094
10095 // Make sure the event doesn't bubble and cause something else
10096 event.preventDefault();
10097 event.stopImmediatePropagation();
10098 }
10099
10100 function resetActionTabIndexes() {
10101 // Grab all of the actions
10102 var actions = getActionsElement()[0].querySelectorAll('.md-fab-action-item');
10103
10104 // Disable all other actions for tabbing
10105 angular.forEach(actions, function(action) {
10106 angular.element(angular.element(action).children()[0]).attr('tabindex', -1);
10107 });
10108
10109 return actions;
10110 }
10111
10112 function doKeyLeft(event) {
10113 if (vm.direction === 'left') {
10114 doActionNext(event);
10115 } else {
10116 doActionPrev(event);
10117 }
10118 }
10119
10120 function doKeyUp(event) {
10121 if (vm.direction === 'down') {
10122 doActionPrev(event);
10123 } else {
10124 doActionNext(event);
10125 }
10126 }
10127
10128 function doKeyRight(event) {
10129 if (vm.direction === 'left') {
10130 doActionPrev(event);
10131 } else {
10132 doActionNext(event);
10133 }
10134 }
10135
10136 function doKeyDown(event) {
10137 if (vm.direction === 'up') {
10138 doActionPrev(event);
10139 } else {
10140 doActionNext(event);
10141 }
10142 }
10143
10144 function isTrigger(element) {
10145 return $mdUtil.getClosest(element, 'md-fab-trigger');
10146 }
10147
10148 function isAction(element) {
10149 return $mdUtil.getClosest(element, 'md-fab-actions');
10150 }
10151
10152 function handleItemClick(event) {
10153 if (isTrigger(event.target)) {
10154 vm.toggle();
10155 }
10156
10157 if (isAction(event.target)) {
10158 vm.close();
10159 }
10160 }
10161
10162 function getTriggerElement() {
10163 return $element.find('md-fab-trigger');
10164 }
10165
10166 function getActionsElement() {
10167 return $element.find('md-fab-actions');
10168 }
10169 }
10170})();
10171
10172})();
10173(function(){
10174"use strict";
10175
10176(function() {
10177 'use strict';
10178
10179 /**
10180 * The duration of the CSS animation in milliseconds.
10181 *
10182 * @type {number}
10183 */
10184 MdFabSpeedDialFlingAnimation.$inject = ["$timeout"];
10185 MdFabSpeedDialScaleAnimation.$inject = ["$timeout"];
10186 var cssAnimationDuration = 300;
10187
10188 /**
10189 * @ngdoc module
10190 * @name material.components.fabSpeedDial
10191 */
10192 angular
10193 // Declare our module
10194 .module('material.components.fabSpeedDial', [
10195 'material.core',
10196 'material.components.fabShared',
10197 'material.components.fabActions'
10198 ])
10199
10200 // Register our directive
10201 .directive('mdFabSpeedDial', MdFabSpeedDialDirective)
10202
10203 // Register our custom animations
10204 .animation('.md-fling', MdFabSpeedDialFlingAnimation)
10205 .animation('.md-scale', MdFabSpeedDialScaleAnimation)
10206
10207 // Register a service for each animation so that we can easily inject them into unit tests
10208 .service('mdFabSpeedDialFlingAnimation', MdFabSpeedDialFlingAnimation)
10209 .service('mdFabSpeedDialScaleAnimation', MdFabSpeedDialScaleAnimation);
10210
10211 /**
10212 * @ngdoc directive
10213 * @name mdFabSpeedDial
10214 * @module material.components.fabSpeedDial
10215 *
10216 * @restrict E
10217 *
10218 * @description
10219 * The `<md-fab-speed-dial>` directive is used to present a series of popup elements (usually
10220 * `<md-button>`s) for quick access to common actions.
10221 *
10222 * There are currently two animations available by applying one of the following classes to
10223 * the component:
10224 *
10225 * - `md-fling` - The speed dial items appear from underneath the trigger and move into their
10226 * appropriate positions.
10227 * - `md-scale` - The speed dial items appear in their proper places by scaling from 0% to 100%.
10228 *
10229 * You may also easily position the trigger by applying one one of the following classes to the
10230 * `<md-fab-speed-dial>` element:
10231 * - `md-fab-top-left`
10232 * - `md-fab-top-right`
10233 * - `md-fab-bottom-left`
10234 * - `md-fab-bottom-right`
10235 *
10236 * These CSS classes use `position: absolute`, so you need to ensure that the container element
10237 * also uses `position: absolute` or `position: relative` in order for them to work.
10238 *
10239 * Additionally, you may use the standard `ng-mouseenter` and `ng-mouseleave` directives to
10240 * open or close the speed dial. However, if you wish to allow users to hover over the empty
10241 * space where the actions will appear, you must also add the `md-hover-full` class to the speed
10242 * dial element. Without this, the hover effect will only occur on top of the trigger.
10243 *
10244 * See the demos for more information.
10245 *
10246 * ## Troubleshooting
10247 *
10248 * If your speed dial shows the closing animation upon launch, you may need to use `ng-cloak` on
10249 * the parent container to ensure that it is only visible once ready. We have plans to remove this
10250 * necessity in the future.
10251 *
10252 * @usage
10253 * <hljs lang="html">
10254 * <md-fab-speed-dial md-direction="up" class="md-fling">
10255 * <md-fab-trigger>
10256 * <md-button aria-label="Add..."><md-icon icon="/img/icons/plus.svg"></md-icon></md-button>
10257 * </md-fab-trigger>
10258 *
10259 * <md-fab-actions>
10260 * <md-button aria-label="Add User">
10261 * <md-icon icon="/img/icons/user.svg"></md-icon>
10262 * </md-button>
10263 *
10264 * <md-button aria-label="Add Group">
10265 * <md-icon icon="/img/icons/group.svg"></md-icon>
10266 * </md-button>
10267 * </md-fab-actions>
10268 * </md-fab-speed-dial>
10269 * </hljs>
10270 *
10271 * @param {string} md-direction From which direction you would like the speed dial to appear
10272 * relative to the trigger element.
10273 * @param {expression=} md-open Programmatically control whether or not the speed-dial is visible.
10274 */
10275 function MdFabSpeedDialDirective() {
10276 return {
10277 restrict: 'E',
10278
10279 scope: {
10280 direction: '@?mdDirection',
10281 isOpen: '=?mdOpen'
10282 },
10283
10284 bindToController: true,
10285 controller: 'MdFabController',
10286 controllerAs: 'vm',
10287
10288 link: FabSpeedDialLink
10289 };
10290
10291 function FabSpeedDialLink(scope, element) {
10292 // Prepend an element to hold our CSS variables so we can use them in the animations below
10293 element.prepend('<div class="_md-css-variables"></div>');
10294 }
10295 }
10296
10297 function MdFabSpeedDialFlingAnimation($timeout) {
10298 function delayDone(done) { $timeout(done, cssAnimationDuration, false); }
10299
10300 function runAnimation(element) {
10301 // Don't run if we are still waiting and we are not ready
10302 if (element.hasClass('md-animations-waiting') && !element.hasClass('_md-animations-ready')) {
10303 return;
10304 }
10305
10306 var el = element[0];
10307 var ctrl = element.controller('mdFabSpeedDial');
10308 var items = el.querySelectorAll('.md-fab-action-item');
10309
10310 // Grab our trigger element
10311 var triggerElement = el.querySelector('md-fab-trigger');
10312
10313 // Grab our element which stores CSS variables
10314 var variablesElement = el.querySelector('._md-css-variables');
10315
10316 // Setup JS variables based on our CSS variables
10317 var startZIndex = parseInt(window.getComputedStyle(variablesElement).zIndex);
10318
10319 // Always reset the items to their natural position/state
10320 angular.forEach(items, function(item, index) {
10321 var styles = item.style;
10322
10323 styles.transform = styles.webkitTransform = '';
10324 styles.transitionDelay = '';
10325 styles.opacity = 1;
10326
10327 // Make the items closest to the trigger have the highest z-index
10328 styles.zIndex = (items.length - index) + startZIndex;
10329 });
10330
10331 // Set the trigger to be above all of the actions so they disappear behind it.
10332 triggerElement.style.zIndex = startZIndex + items.length + 1;
10333
10334 // If the control is closed, hide the items behind the trigger
10335 if (!ctrl.isOpen) {
10336 angular.forEach(items, function(item, index) {
10337 var newPosition, axis;
10338 var styles = item.style;
10339
10340 // Make sure to account for differences in the dimensions of the trigger verses the items
10341 // so that we can properly center everything; this helps hide the item's shadows behind
10342 // the trigger.
10343 var triggerItemHeightOffset = (triggerElement.clientHeight - item.clientHeight) / 2;
10344 var triggerItemWidthOffset = (triggerElement.clientWidth - item.clientWidth) / 2;
10345
10346 switch (ctrl.direction) {
10347 case 'up':
10348 newPosition = (item.scrollHeight * (index + 1) + triggerItemHeightOffset);
10349 axis = 'Y';
10350 break;
10351 case 'down':
10352 newPosition = -(item.scrollHeight * (index + 1) + triggerItemHeightOffset);
10353 axis = 'Y';
10354 break;
10355 case 'left':
10356 newPosition = (item.scrollWidth * (index + 1) + triggerItemWidthOffset);
10357 axis = 'X';
10358 break;
10359 case 'right':
10360 newPosition = -(item.scrollWidth * (index + 1) + triggerItemWidthOffset);
10361 axis = 'X';
10362 break;
10363 }
10364
10365 var newTranslate = 'translate' + axis + '(' + newPosition + 'px)';
10366
10367 styles.transform = styles.webkitTransform = newTranslate;
10368 });
10369 }
10370 }
10371
10372 return {
10373 addClass: function(element, className, done) {
10374 if (element.hasClass('md-fling')) {
10375 runAnimation(element);
10376 delayDone(done);
10377 } else {
10378 done();
10379 }
10380 },
10381 removeClass: function(element, className, done) {
10382 runAnimation(element);
10383 delayDone(done);
10384 }
10385 }
10386 }
10387
10388 function MdFabSpeedDialScaleAnimation($timeout) {
10389 function delayDone(done) { $timeout(done, cssAnimationDuration, false); }
10390
10391 var delay = 65;
10392
10393 function runAnimation(element) {
10394 var el = element[0];
10395 var ctrl = element.controller('mdFabSpeedDial');
10396 var items = el.querySelectorAll('.md-fab-action-item');
10397
10398 // Grab our element which stores CSS variables
10399 var variablesElement = el.querySelector('._md-css-variables');
10400
10401 // Setup JS variables based on our CSS variables
10402 var startZIndex = parseInt(window.getComputedStyle(variablesElement).zIndex);
10403
10404 // Always reset the items to their natural position/state
10405 angular.forEach(items, function(item, index) {
10406 var styles = item.style,
10407 offsetDelay = index * delay;
10408
10409 styles.opacity = ctrl.isOpen ? 1 : 0;
10410 styles.transform = styles.webkitTransform = ctrl.isOpen ? 'scale(1)' : 'scale(0)';
10411 styles.transitionDelay = (ctrl.isOpen ? offsetDelay : (items.length - offsetDelay)) + 'ms';
10412
10413 // Make the items closest to the trigger have the highest z-index
10414 styles.zIndex = (items.length - index) + startZIndex;
10415 });
10416 }
10417
10418 return {
10419 addClass: function(element, className, done) {
10420 runAnimation(element);
10421 delayDone(done);
10422 },
10423
10424 removeClass: function(element, className, done) {
10425 runAnimation(element);
10426 delayDone(done);
10427 }
10428 }
10429 }
10430})();
10431
10432})();
10433(function(){
10434"use strict";
10435
10436(function() {
10437 'use strict';
10438
10439 /**
10440 * @ngdoc module
10441 * @name material.components.fabToolbar
10442 */
10443 angular
10444 // Declare our module
10445 .module('material.components.fabToolbar', [
10446 'material.core',
10447 'material.components.fabShared',
10448 'material.components.fabActions'
10449 ])
10450
10451 // Register our directive
10452 .directive('mdFabToolbar', MdFabToolbarDirective)
10453
10454 // Register our custom animations
10455 .animation('.md-fab-toolbar', MdFabToolbarAnimation)
10456
10457 // Register a service for the animation so that we can easily inject it into unit tests
10458 .service('mdFabToolbarAnimation', MdFabToolbarAnimation);
10459
10460 /**
10461 * @ngdoc directive
10462 * @name mdFabToolbar
10463 * @module material.components.fabToolbar
10464 *
10465 * @restrict E
10466 *
10467 * @description
10468 *
10469 * The `<md-fab-toolbar>` directive is used present a toolbar of elements (usually `<md-button>`s)
10470 * for quick access to common actions when a floating action button is activated (via click or
10471 * keyboard navigation).
10472 *
10473 * You may also easily position the trigger by applying one one of the following classes to the
10474 * `<md-fab-toolbar>` element:
10475 * - `md-fab-top-left`
10476 * - `md-fab-top-right`
10477 * - `md-fab-bottom-left`
10478 * - `md-fab-bottom-right`
10479 *
10480 * These CSS classes use `position: absolute`, so you need to ensure that the container element
10481 * also uses `position: absolute` or `position: relative` in order for them to work.
10482 *
10483 * @usage
10484 *
10485 * <hljs lang="html">
10486 * <md-fab-toolbar md-direction='left'>
10487 * <md-fab-trigger>
10488 * <md-button aria-label="Add..."><md-icon icon="/img/icons/plus.svg"></md-icon></md-button>
10489 * </md-fab-trigger>
10490 *
10491 * <md-toolbar>
10492 * <md-fab-actions>
10493 * <md-button aria-label="Add User">
10494 * <md-icon icon="/img/icons/user.svg"></md-icon>
10495 * </md-button>
10496 *
10497 * <md-button aria-label="Add Group">
10498 * <md-icon icon="/img/icons/group.svg"></md-icon>
10499 * </md-button>
10500 * </md-fab-actions>
10501 * </md-toolbar>
10502 * </md-fab-toolbar>
10503 * </hljs>
10504 *
10505 * @param {string} md-direction From which direction you would like the toolbar items to appear
10506 * relative to the trigger element. Supports `left` and `right` directions.
10507 * @param {expression=} md-open Programmatically control whether or not the toolbar is visible.
10508 */
10509 function MdFabToolbarDirective() {
10510 return {
10511 restrict: 'E',
10512 transclude: true,
10513 template: '<div class="md-fab-toolbar-wrapper">' +
10514 ' <div class="md-fab-toolbar-content" ng-transclude></div>' +
10515 '</div>',
10516
10517 scope: {
10518 direction: '@?mdDirection',
10519 isOpen: '=?mdOpen'
10520 },
10521
10522 bindToController: true,
10523 controller: 'MdFabController',
10524 controllerAs: 'vm',
10525
10526 link: link
10527 };
10528
10529 function link(scope, element, attributes) {
10530 // Add the base class for animations
10531 element.addClass('md-fab-toolbar');
10532
10533 // Prepend the background element to the trigger's button
10534 element.find('md-fab-trigger').find('button')
10535 .prepend('<div class="md-fab-toolbar-background"></div>');
10536 }
10537 }
10538
10539 function MdFabToolbarAnimation() {
10540
10541 function runAnimation(element, className, done) {
10542 // If no className was specified, don't do anything
10543 if (!className) {
10544 return;
10545 }
10546
10547 var el = element[0];
10548 var ctrl = element.controller('mdFabToolbar');
10549
10550 // Grab the relevant child elements
10551 var backgroundElement = el.querySelector('.md-fab-toolbar-background');
10552 var triggerElement = el.querySelector('md-fab-trigger button');
10553 var toolbarElement = el.querySelector('md-toolbar');
10554 var iconElement = el.querySelector('md-fab-trigger button md-icon');
10555 var actions = element.find('md-fab-actions').children();
10556
10557 // If we have both elements, use them to position the new background
10558 if (triggerElement && backgroundElement) {
10559 // Get our variables
10560 var color = window.getComputedStyle(triggerElement).getPropertyValue('background-color');
10561 var width = el.offsetWidth;
10562 var height = el.offsetHeight;
10563
10564 // Make it twice as big as it should be since we scale from the center
10565 var scale = 2 * (width / triggerElement.offsetWidth);
10566
10567 // Set some basic styles no matter what animation we're doing
10568 backgroundElement.style.backgroundColor = color;
10569 backgroundElement.style.borderRadius = width + 'px';
10570
10571 // If we're open
10572 if (ctrl.isOpen) {
10573 // Turn on toolbar pointer events when closed
10574 toolbarElement.style.pointerEvents = 'inherit';
10575
10576 backgroundElement.style.width = triggerElement.offsetWidth + 'px';
10577 backgroundElement.style.height = triggerElement.offsetHeight + 'px';
10578 backgroundElement.style.transform = 'scale(' + scale + ')';
10579
10580 // Set the next close animation to have the proper delays
10581 backgroundElement.style.transitionDelay = '0ms';
10582 iconElement && (iconElement.style.transitionDelay = '.3s');
10583
10584 // Apply a transition delay to actions
10585 angular.forEach(actions, function(action, index) {
10586 action.style.transitionDelay = (actions.length - index) * 25 + 'ms';
10587 });
10588 } else {
10589 // Turn off toolbar pointer events when closed
10590 toolbarElement.style.pointerEvents = 'none';
10591
10592 // Scale it back down to the trigger's size
10593 backgroundElement.style.transform = 'scale(1)';
10594
10595 // Reset the position
10596 backgroundElement.style.top = '0';
10597
10598 if (element.hasClass('md-right')) {
10599 backgroundElement.style.left = '0';
10600 backgroundElement.style.right = null;
10601 }
10602
10603 if (element.hasClass('md-left')) {
10604 backgroundElement.style.right = '0';
10605 backgroundElement.style.left = null;
10606 }
10607
10608 // Set the next open animation to have the proper delays
10609 backgroundElement.style.transitionDelay = '200ms';
10610 iconElement && (iconElement.style.transitionDelay = '0ms');
10611
10612 // Apply a transition delay to actions
10613 angular.forEach(actions, function(action, index) {
10614 action.style.transitionDelay = 200 + (index * 25) + 'ms';
10615 });
10616 }
10617 }
10618 }
10619
10620 return {
10621 addClass: function(element, className, done) {
10622 runAnimation(element, className, done);
10623 done();
10624 },
10625
10626 removeClass: function(element, className, done) {
10627 runAnimation(element, className, done);
10628 done();
10629 }
10630 }
10631 }
10632})();
10633
10634})();
10635(function(){
10636"use strict";
10637
10638/**
10639 * @ngdoc module
10640 * @name material.components.gridList
10641 */
10642GridListController.$inject = ["$mdUtil"];
10643GridLayoutFactory.$inject = ["$mdUtil"];
10644GridListDirective.$inject = ["$interpolate", "$mdConstant", "$mdGridLayout", "$mdMedia"];
10645GridTileDirective.$inject = ["$mdMedia"];
10646angular.module('material.components.gridList', ['material.core'])
10647 .directive('mdGridList', GridListDirective)
10648 .directive('mdGridTile', GridTileDirective)
10649 .directive('mdGridTileFooter', GridTileCaptionDirective)
10650 .directive('mdGridTileHeader', GridTileCaptionDirective)
10651 .factory('$mdGridLayout', GridLayoutFactory);
10652
10653/**
10654 * @ngdoc directive
10655 * @name mdGridList
10656 * @module material.components.gridList
10657 * @restrict E
10658 * @description
10659 * Grid lists are an alternative to standard list views. Grid lists are distinct
10660 * from grids used for layouts and other visual presentations.
10661 *
10662 * A grid list is best suited to presenting a homogenous data type, typically
10663 * images, and is optimized for visual comprehension and differentiating between
10664 * like data types.
10665 *
10666 * A grid list is a continuous element consisting of tessellated, regular
10667 * subdivisions called cells that contain tiles (`md-grid-tile`).
10668 *
10669 * <img src="//material-design.storage.googleapis.com/publish/v_2/material_ext_publish/0Bx4BSt6jniD7OVlEaXZ5YmU1Xzg/components_grids_usage2.png"
10670 * style="width: 300px; height: auto; margin-right: 16px;" alt="Concept of grid explained visually">
10671 * <img src="//material-design.storage.googleapis.com/publish/v_2/material_ext_publish/0Bx4BSt6jniD7VGhsOE5idWlJWXM/components_grids_usage3.png"
10672 * style="width: 300px; height: auto;" alt="Grid concepts legend">
10673 *
10674 * Cells are arrayed vertically and horizontally within the grid.
10675 *
10676 * Tiles hold content and can span one or more cells vertically or horizontally.
10677 *
10678 * ### Responsive Attributes
10679 *
10680 * The `md-grid-list` directive supports "responsive" attributes, which allow
10681 * different `md-cols`, `md-gutter` and `md-row-height` values depending on the
10682 * currently matching media query.
10683 *
10684 * In order to set a responsive attribute, first define the fallback value with
10685 * the standard attribute name, then add additional attributes with the
10686 * following convention: `{base-attribute-name}-{media-query-name}="{value}"`
10687 * (ie. `md-cols-lg="8"`)
10688 *
10689 * @param {number} md-cols Number of columns in the grid.
10690 * @param {string} md-row-height One of
10691 * <ul>
10692 * <li>CSS length - Fixed height rows (eg. `8px` or `1rem`)</li>
10693 * <li>`{width}:{height}` - Ratio of width to height (eg.
10694 * `md-row-height="16:9"`)</li>
10695 * <li>`"fit"` - Height will be determined by subdividing the available
10696 * height by the number of rows</li>
10697 * </ul>
10698 * @param {string=} md-gutter The amount of space between tiles in CSS units
10699 * (default 1px)
10700 * @param {expression=} md-on-layout Expression to evaluate after layout. Event
10701 * object is available as `$event`, and contains performance information.
10702 *
10703 * @usage
10704 * Basic:
10705 * <hljs lang="html">
10706 * <md-grid-list md-cols="5" md-gutter="1em" md-row-height="4:3">
10707 * <md-grid-tile></md-grid-tile>
10708 * </md-grid-list>
10709 * </hljs>
10710 *
10711 * Fixed-height rows:
10712 * <hljs lang="html">
10713 * <md-grid-list md-cols="4" md-row-height="200px" ...>
10714 * <md-grid-tile></md-grid-tile>
10715 * </md-grid-list>
10716 * </hljs>
10717 *
10718 * Fit rows:
10719 * <hljs lang="html">
10720 * <md-grid-list md-cols="4" md-row-height="fit" style="height: 400px;" ...>
10721 * <md-grid-tile></md-grid-tile>
10722 * </md-grid-list>
10723 * </hljs>
10724 *
10725 * Using responsive attributes:
10726 * <hljs lang="html">
10727 * <md-grid-list
10728 * md-cols-sm="2"
10729 * md-cols-md="4"
10730 * md-cols-lg="8"
10731 * md-cols-gt-lg="12"
10732 * ...>
10733 * <md-grid-tile></md-grid-tile>
10734 * </md-grid-list>
10735 * </hljs>
10736 */
10737function GridListDirective($interpolate, $mdConstant, $mdGridLayout, $mdMedia) {
10738 return {
10739 restrict: 'E',
10740 controller: GridListController,
10741 scope: {
10742 mdOnLayout: '&'
10743 },
10744 link: postLink
10745 };
10746
10747 function postLink(scope, element, attrs, ctrl) {
10748 element.addClass('_md'); // private md component indicator for styling
10749
10750 // Apply semantics
10751 element.attr('role', 'list');
10752
10753 // Provide the controller with a way to trigger layouts.
10754 ctrl.layoutDelegate = layoutDelegate;
10755
10756 var invalidateLayout = angular.bind(ctrl, ctrl.invalidateLayout),
10757 unwatchAttrs = watchMedia();
10758 scope.$on('$destroy', unwatchMedia);
10759
10760 /**
10761 * Watches for changes in media, invalidating layout as necessary.
10762 */
10763 function watchMedia() {
10764 for (var mediaName in $mdConstant.MEDIA) {
10765 $mdMedia(mediaName); // initialize
10766 $mdMedia.getQuery($mdConstant.MEDIA[mediaName])
10767 .addListener(invalidateLayout);
10768 }
10769 return $mdMedia.watchResponsiveAttributes(
10770 ['md-cols', 'md-row-height', 'md-gutter'], attrs, layoutIfMediaMatch);
10771 }
10772
10773 function unwatchMedia() {
10774 ctrl.layoutDelegate = angular.noop;
10775
10776 unwatchAttrs();
10777 for (var mediaName in $mdConstant.MEDIA) {
10778 $mdMedia.getQuery($mdConstant.MEDIA[mediaName])
10779 .removeListener(invalidateLayout);
10780 }
10781 }
10782
10783 /**
10784 * Performs grid layout if the provided mediaName matches the currently
10785 * active media type.
10786 */
10787 function layoutIfMediaMatch(mediaName) {
10788 if (mediaName == null) {
10789 // TODO(shyndman): It would be nice to only layout if we have
10790 // instances of attributes using this media type
10791 ctrl.invalidateLayout();
10792 } else if ($mdMedia(mediaName)) {
10793 ctrl.invalidateLayout();
10794 }
10795 }
10796
10797 var lastLayoutProps;
10798
10799 /**
10800 * Invokes the layout engine, and uses its results to lay out our
10801 * tile elements.
10802 *
10803 * @param {boolean} tilesInvalidated Whether tiles have been
10804 * added/removed/moved since the last layout. This is to avoid situations
10805 * where tiles are replaced with properties identical to their removed
10806 * counterparts.
10807 */
10808 function layoutDelegate(tilesInvalidated) {
10809 var tiles = getTileElements();
10810 var props = {
10811 tileSpans: getTileSpans(tiles),
10812 colCount: getColumnCount(),
10813 rowMode: getRowMode(),
10814 rowHeight: getRowHeight(),
10815 gutter: getGutter()
10816 };
10817
10818 if (!tilesInvalidated && angular.equals(props, lastLayoutProps)) {
10819 return;
10820 }
10821
10822 var performance =
10823 $mdGridLayout(props.colCount, props.tileSpans, tiles)
10824 .map(function(tilePositions, rowCount) {
10825 return {
10826 grid: {
10827 element: element,
10828 style: getGridStyle(props.colCount, rowCount,
10829 props.gutter, props.rowMode, props.rowHeight)
10830 },
10831 tiles: tilePositions.map(function(ps, i) {
10832 return {
10833 element: angular.element(tiles[i]),
10834 style: getTileStyle(ps.position, ps.spans,
10835 props.colCount, rowCount,
10836 props.gutter, props.rowMode, props.rowHeight)
10837 }
10838 })
10839 }
10840 })
10841 .reflow()
10842 .performance();
10843
10844 // Report layout
10845 scope.mdOnLayout({
10846 $event: {
10847 performance: performance
10848 }
10849 });
10850
10851 lastLayoutProps = props;
10852 }
10853
10854 // Use $interpolate to do some simple string interpolation as a convenience.
10855
10856 var startSymbol = $interpolate.startSymbol();
10857 var endSymbol = $interpolate.endSymbol();
10858
10859 // Returns an expression wrapped in the interpolator's start and end symbols.
10860 function expr(exprStr) {
10861 return startSymbol + exprStr + endSymbol;
10862 }
10863
10864 // The amount of space a single 1x1 tile would take up (either width or height), used as
10865 // a basis for other calculations. This consists of taking the base size percent (as would be
10866 // if evenly dividing the size between cells), and then subtracting the size of one gutter.
10867 // However, since there are no gutters on the edges, each tile only uses a fration
10868 // (gutterShare = numGutters / numCells) of the gutter size. (Imagine having one gutter per
10869 // tile, and then breaking up the extra gutter on the edge evenly among the cells).
10870 var UNIT = $interpolate(expr('share') + '% - (' + expr('gutter') + ' * ' + expr('gutterShare') + ')');
10871
10872 // The horizontal or vertical position of a tile, e.g., the 'top' or 'left' property value.
10873 // The position comes the size of a 1x1 tile plus gutter for each previous tile in the
10874 // row/column (offset).
10875 var POSITION = $interpolate('calc((' + expr('unit') + ' + ' + expr('gutter') + ') * ' + expr('offset') + ')');
10876
10877 // The actual size of a tile, e.g., width or height, taking rowSpan or colSpan into account.
10878 // This is computed by multiplying the base unit by the rowSpan/colSpan, and then adding back
10879 // in the space that the gutter would normally have used (which was already accounted for in
10880 // the base unit calculation).
10881 var DIMENSION = $interpolate('calc((' + expr('unit') + ') * ' + expr('span') + ' + (' + expr('span') + ' - 1) * ' + expr('gutter') + ')');
10882
10883 /**
10884 * Gets the styles applied to a tile element described by the given parameters.
10885 * @param {{row: number, col: number}} position The row and column indices of the tile.
10886 * @param {{row: number, col: number}} spans The rowSpan and colSpan of the tile.
10887 * @param {number} colCount The number of columns.
10888 * @param {number} rowCount The number of rows.
10889 * @param {string} gutter The amount of space between tiles. This will be something like
10890 * '5px' or '2em'.
10891 * @param {string} rowMode The row height mode. Can be one of:
10892 * 'fixed': all rows have a fixed size, given by rowHeight,
10893 * 'ratio': row height defined as a ratio to width, or
10894 * 'fit': fit to the grid-list element height, divinding evenly among rows.
10895 * @param {string|number} rowHeight The height of a row. This is only used for 'fixed' mode and
10896 * for 'ratio' mode. For 'ratio' mode, this is the *ratio* of width-to-height (e.g., 0.75).
10897 * @returns {Object} Map of CSS properties to be applied to the style element. Will define
10898 * values for top, left, width, height, marginTop, and paddingTop.
10899 */
10900 function getTileStyle(position, spans, colCount, rowCount, gutter, rowMode, rowHeight) {
10901 // TODO(shyndman): There are style caching opportunities here.
10902
10903 // Percent of the available horizontal space that one column takes up.
10904 var hShare = (1 / colCount) * 100;
10905
10906 // Fraction of the gutter size that each column takes up.
10907 var hGutterShare = (colCount - 1) / colCount;
10908
10909 // Base horizontal size of a column.
10910 var hUnit = UNIT({share: hShare, gutterShare: hGutterShare, gutter: gutter});
10911
10912 // The width and horizontal position of each tile is always calculated the same way, but the
10913 // height and vertical position depends on the rowMode.
10914 var style = {
10915 left: POSITION({ unit: hUnit, offset: position.col, gutter: gutter }),
10916 width: DIMENSION({ unit: hUnit, span: spans.col, gutter: gutter }),
10917 // resets
10918 paddingTop: '',
10919 marginTop: '',
10920 top: '',
10921 height: ''
10922 };
10923
10924 switch (rowMode) {
10925 case 'fixed':
10926 // In fixed mode, simply use the given rowHeight.
10927 style.top = POSITION({ unit: rowHeight, offset: position.row, gutter: gutter });
10928 style.height = DIMENSION({ unit: rowHeight, span: spans.row, gutter: gutter });
10929 break;
10930
10931 case 'ratio':
10932 // Percent of the available vertical space that one row takes up. Here, rowHeight holds
10933 // the ratio value. For example, if the width:height ratio is 4:3, rowHeight = 1.333.
10934 var vShare = hShare / rowHeight;
10935
10936 // Base veritcal size of a row.
10937 var vUnit = UNIT({ share: vShare, gutterShare: hGutterShare, gutter: gutter });
10938
10939 // padidngTop and marginTop are used to maintain the given aspect ratio, as
10940 // a percentage-based value for these properties is applied to the *width* of the
10941 // containing block. See http://www.w3.org/TR/CSS2/box.html#margin-properties
10942 style.paddingTop = DIMENSION({ unit: vUnit, span: spans.row, gutter: gutter});
10943 style.marginTop = POSITION({ unit: vUnit, offset: position.row, gutter: gutter });
10944 break;
10945
10946 case 'fit':
10947 // Fraction of the gutter size that each column takes up.
10948 var vGutterShare = (rowCount - 1) / rowCount;
10949
10950 // Percent of the available vertical space that one row takes up.
10951 var vShare = (1 / rowCount) * 100;
10952
10953 // Base vertical size of a row.
10954 var vUnit = UNIT({share: vShare, gutterShare: vGutterShare, gutter: gutter});
10955
10956 style.top = POSITION({unit: vUnit, offset: position.row, gutter: gutter});
10957 style.height = DIMENSION({unit: vUnit, span: spans.row, gutter: gutter});
10958 break;
10959 }
10960
10961 return style;
10962 }
10963
10964 function getGridStyle(colCount, rowCount, gutter, rowMode, rowHeight) {
10965 var style = {};
10966
10967 switch(rowMode) {
10968 case 'fixed':
10969 style.height = DIMENSION({ unit: rowHeight, span: rowCount, gutter: gutter });
10970 style.paddingBottom = '';
10971 break;
10972
10973 case 'ratio':
10974 // rowHeight is width / height
10975 var hGutterShare = colCount === 1 ? 0 : (colCount - 1) / colCount,
10976 hShare = (1 / colCount) * 100,
10977 vShare = hShare * (1 / rowHeight),
10978 vUnit = UNIT({ share: vShare, gutterShare: hGutterShare, gutter: gutter });
10979
10980 style.height = '';
10981 style.paddingBottom = DIMENSION({ unit: vUnit, span: rowCount, gutter: gutter});
10982 break;
10983
10984 case 'fit':
10985 // noop, as the height is user set
10986 break;
10987 }
10988
10989 return style;
10990 }
10991
10992 function getTileElements() {
10993 return [].filter.call(element.children(), function(ele) {
10994 return ele.tagName == 'MD-GRID-TILE' && !ele.$$mdDestroyed;
10995 });
10996 }
10997
10998 /**
10999 * Gets an array of objects containing the rowspan and colspan for each tile.
11000 * @returns {Array<{row: number, col: number}>}
11001 */
11002 function getTileSpans(tileElements) {
11003 return [].map.call(tileElements, function(ele) {
11004 var ctrl = angular.element(ele).controller('mdGridTile');
11005 return {
11006 row: parseInt(
11007 $mdMedia.getResponsiveAttribute(ctrl.$attrs, 'md-rowspan'), 10) || 1,
11008 col: parseInt(
11009 $mdMedia.getResponsiveAttribute(ctrl.$attrs, 'md-colspan'), 10) || 1
11010 };
11011 });
11012 }
11013
11014 function getColumnCount() {
11015 var colCount = parseInt($mdMedia.getResponsiveAttribute(attrs, 'md-cols'), 10);
11016 if (isNaN(colCount)) {
11017 throw 'md-grid-list: md-cols attribute was not found, or contained a non-numeric value';
11018 }
11019 return colCount;
11020 }
11021
11022 function getGutter() {
11023 return applyDefaultUnit($mdMedia.getResponsiveAttribute(attrs, 'md-gutter') || 1);
11024 }
11025
11026 function getRowHeight() {
11027 var rowHeight = $mdMedia.getResponsiveAttribute(attrs, 'md-row-height');
11028 if (!rowHeight) {
11029 throw 'md-grid-list: md-row-height attribute was not found';
11030 }
11031
11032 switch (getRowMode()) {
11033 case 'fixed':
11034 return applyDefaultUnit(rowHeight);
11035 case 'ratio':
11036 var whRatio = rowHeight.split(':');
11037 return parseFloat(whRatio[0]) / parseFloat(whRatio[1]);
11038 case 'fit':
11039 return 0; // N/A
11040 }
11041 }
11042
11043 function getRowMode() {
11044 var rowHeight = $mdMedia.getResponsiveAttribute(attrs, 'md-row-height');
11045 if (!rowHeight) {
11046 throw 'md-grid-list: md-row-height attribute was not found';
11047 }
11048
11049 if (rowHeight == 'fit') {
11050 return 'fit';
11051 } else if (rowHeight.indexOf(':') !== -1) {
11052 return 'ratio';
11053 } else {
11054 return 'fixed';
11055 }
11056 }
11057
11058 function applyDefaultUnit(val) {
11059 return /\D$/.test(val) ? val : val + 'px';
11060 }
11061 }
11062}
11063
11064/* @ngInject */
11065function GridListController($mdUtil) {
11066 this.layoutInvalidated = false;
11067 this.tilesInvalidated = false;
11068 this.$timeout_ = $mdUtil.nextTick;
11069 this.layoutDelegate = angular.noop;
11070}
11071
11072GridListController.prototype = {
11073 invalidateTiles: function() {
11074 this.tilesInvalidated = true;
11075 this.invalidateLayout();
11076 },
11077
11078 invalidateLayout: function() {
11079 if (this.layoutInvalidated) {
11080 return;
11081 }
11082 this.layoutInvalidated = true;
11083 this.$timeout_(angular.bind(this, this.layout));
11084 },
11085
11086 layout: function() {
11087 try {
11088 this.layoutDelegate(this.tilesInvalidated);
11089 } finally {
11090 this.layoutInvalidated = false;
11091 this.tilesInvalidated = false;
11092 }
11093 }
11094};
11095
11096
11097/* @ngInject */
11098function GridLayoutFactory($mdUtil) {
11099 var defaultAnimator = GridTileAnimator;
11100
11101 /**
11102 * Set the reflow animator callback
11103 */
11104 GridLayout.animateWith = function(customAnimator) {
11105 defaultAnimator = !angular.isFunction(customAnimator) ? GridTileAnimator : customAnimator;
11106 };
11107
11108 return GridLayout;
11109
11110 /**
11111 * Publish layout function
11112 */
11113 function GridLayout(colCount, tileSpans) {
11114 var self, layoutInfo, gridStyles, layoutTime, mapTime, reflowTime;
11115
11116 layoutTime = $mdUtil.time(function() {
11117 layoutInfo = calculateGridFor(colCount, tileSpans);
11118 });
11119
11120 return self = {
11121
11122 /**
11123 * An array of objects describing each tile's position in the grid.
11124 */
11125 layoutInfo: function() {
11126 return layoutInfo;
11127 },
11128
11129 /**
11130 * Maps grid positioning to an element and a set of styles using the
11131 * provided updateFn.
11132 */
11133 map: function(updateFn) {
11134 mapTime = $mdUtil.time(function() {
11135 var info = self.layoutInfo();
11136 gridStyles = updateFn(info.positioning, info.rowCount);
11137 });
11138 return self;
11139 },
11140
11141 /**
11142 * Default animator simply sets the element.css( <styles> ). An alternate
11143 * animator can be provided as an argument. The function has the following
11144 * signature:
11145 *
11146 * function({grid: {element: JQLite, style: Object}, tiles: Array<{element: JQLite, style: Object}>)
11147 */
11148 reflow: function(animatorFn) {
11149 reflowTime = $mdUtil.time(function() {
11150 var animator = animatorFn || defaultAnimator;
11151 animator(gridStyles.grid, gridStyles.tiles);
11152 });
11153 return self;
11154 },
11155
11156 /**
11157 * Timing for the most recent layout run.
11158 */
11159 performance: function() {
11160 return {
11161 tileCount: tileSpans.length,
11162 layoutTime: layoutTime,
11163 mapTime: mapTime,
11164 reflowTime: reflowTime,
11165 totalTime: layoutTime + mapTime + reflowTime
11166 };
11167 }
11168 };
11169 }
11170
11171 /**
11172 * Default Gridlist animator simple sets the css for each element;
11173 * NOTE: any transitions effects must be manually set in the CSS.
11174 * e.g.
11175 *
11176 * md-grid-tile {
11177 * transition: all 700ms ease-out 50ms;
11178 * }
11179 *
11180 */
11181 function GridTileAnimator(grid, tiles) {
11182 grid.element.css(grid.style);
11183 tiles.forEach(function(t) {
11184 t.element.css(t.style);
11185 })
11186 }
11187
11188 /**
11189 * Calculates the positions of tiles.
11190 *
11191 * The algorithm works as follows:
11192 * An Array<Number> with length colCount (spaceTracker) keeps track of
11193 * available tiling positions, where elements of value 0 represents an
11194 * empty position. Space for a tile is reserved by finding a sequence of
11195 * 0s with length <= than the tile's colspan. When such a space has been
11196 * found, the occupied tile positions are incremented by the tile's
11197 * rowspan value, as these positions have become unavailable for that
11198 * many rows.
11199 *
11200 * If the end of a row has been reached without finding space for the
11201 * tile, spaceTracker's elements are each decremented by 1 to a minimum
11202 * of 0. Rows are searched in this fashion until space is found.
11203 */
11204 function calculateGridFor(colCount, tileSpans) {
11205 var curCol = 0,
11206 curRow = 0,
11207 spaceTracker = newSpaceTracker();
11208
11209 return {
11210 positioning: tileSpans.map(function(spans, i) {
11211 return {
11212 spans: spans,
11213 position: reserveSpace(spans, i)
11214 };
11215 }),
11216 rowCount: curRow + Math.max.apply(Math, spaceTracker)
11217 };
11218
11219 function reserveSpace(spans, i) {
11220 if (spans.col > colCount) {
11221 throw 'md-grid-list: Tile at position ' + i + ' has a colspan ' +
11222 '(' + spans.col + ') that exceeds the column count ' +
11223 '(' + colCount + ')';
11224 }
11225
11226 var start = 0,
11227 end = 0;
11228
11229 // TODO(shyndman): This loop isn't strictly necessary if you can
11230 // determine the minimum number of rows before a space opens up. To do
11231 // this, recognize that you've iterated across an entire row looking for
11232 // space, and if so fast-forward by the minimum rowSpan count. Repeat
11233 // until the required space opens up.
11234 while (end - start < spans.col) {
11235 if (curCol >= colCount) {
11236 nextRow();
11237 continue;
11238 }
11239
11240 start = spaceTracker.indexOf(0, curCol);
11241 if (start === -1 || (end = findEnd(start + 1)) === -1) {
11242 start = end = 0;
11243 nextRow();
11244 continue;
11245 }
11246
11247 curCol = end + 1;
11248 }
11249
11250 adjustRow(start, spans.col, spans.row);
11251 curCol = start + spans.col;
11252
11253 return {
11254 col: start,
11255 row: curRow
11256 };
11257 }
11258
11259 function nextRow() {
11260 curCol = 0;
11261 curRow++;
11262 adjustRow(0, colCount, -1); // Decrement row spans by one
11263 }
11264
11265 function adjustRow(from, cols, by) {
11266 for (var i = from; i < from + cols; i++) {
11267 spaceTracker[i] = Math.max(spaceTracker[i] + by, 0);
11268 }
11269 }
11270
11271 function findEnd(start) {
11272 var i;
11273 for (i = start; i < spaceTracker.length; i++) {
11274 if (spaceTracker[i] !== 0) {
11275 return i;
11276 }
11277 }
11278
11279 if (i === spaceTracker.length) {
11280 return i;
11281 }
11282 }
11283
11284 function newSpaceTracker() {
11285 var tracker = [];
11286 for (var i = 0; i < colCount; i++) {
11287 tracker.push(0);
11288 }
11289 return tracker;
11290 }
11291 }
11292}
11293
11294/**
11295 * @ngdoc directive
11296 * @name mdGridTile
11297 * @module material.components.gridList
11298 * @restrict E
11299 * @description
11300 * Tiles contain the content of an `md-grid-list`. They span one or more grid
11301 * cells vertically or horizontally, and use `md-grid-tile-{footer,header}` to
11302 * display secondary content.
11303 *
11304 * ### Responsive Attributes
11305 *
11306 * The `md-grid-tile` directive supports "responsive" attributes, which allow
11307 * different `md-rowspan` and `md-colspan` values depending on the currently
11308 * matching media query.
11309 *
11310 * In order to set a responsive attribute, first define the fallback value with
11311 * the standard attribute name, then add additional attributes with the
11312 * following convention: `{base-attribute-name}-{media-query-name}="{value}"`
11313 * (ie. `md-colspan-sm="4"`)
11314 *
11315 * @param {number=} md-colspan The number of columns to span (default 1). Cannot
11316 * exceed the number of columns in the grid. Supports interpolation.
11317 * @param {number=} md-rowspan The number of rows to span (default 1). Supports
11318 * interpolation.
11319 *
11320 * @usage
11321 * With header:
11322 * <hljs lang="html">
11323 * <md-grid-tile>
11324 * <md-grid-tile-header>
11325 * <h3>This is a header</h3>
11326 * </md-grid-tile-header>
11327 * </md-grid-tile>
11328 * </hljs>
11329 *
11330 * With footer:
11331 * <hljs lang="html">
11332 * <md-grid-tile>
11333 * <md-grid-tile-footer>
11334 * <h3>This is a footer</h3>
11335 * </md-grid-tile-footer>
11336 * </md-grid-tile>
11337 * </hljs>
11338 *
11339 * Spanning multiple rows/columns:
11340 * <hljs lang="html">
11341 * <md-grid-tile md-colspan="2" md-rowspan="3">
11342 * </md-grid-tile>
11343 * </hljs>
11344 *
11345 * Responsive attributes:
11346 * <hljs lang="html">
11347 * <md-grid-tile md-colspan="1" md-colspan-sm="3" md-colspan-md="5">
11348 * </md-grid-tile>
11349 * </hljs>
11350 */
11351function GridTileDirective($mdMedia) {
11352 return {
11353 restrict: 'E',
11354 require: '^mdGridList',
11355 template: '<figure ng-transclude></figure>',
11356 transclude: true,
11357 scope: {},
11358 // Simple controller that exposes attributes to the grid directive
11359 controller: ["$attrs", function($attrs) {
11360 this.$attrs = $attrs;
11361 }],
11362 link: postLink
11363 };
11364
11365 function postLink(scope, element, attrs, gridCtrl) {
11366 // Apply semantics
11367 element.attr('role', 'listitem');
11368
11369 // If our colspan or rowspan changes, trigger a layout
11370 var unwatchAttrs = $mdMedia.watchResponsiveAttributes(['md-colspan', 'md-rowspan'],
11371 attrs, angular.bind(gridCtrl, gridCtrl.invalidateLayout));
11372
11373 // Tile registration/deregistration
11374 gridCtrl.invalidateTiles();
11375 scope.$on('$destroy', function() {
11376 // Mark the tile as destroyed so it is no longer considered in layout,
11377 // even if the DOM element sticks around (like during a leave animation)
11378 element[0].$$mdDestroyed = true;
11379 unwatchAttrs();
11380 gridCtrl.invalidateLayout();
11381 });
11382
11383 if (angular.isDefined(scope.$parent.$index)) {
11384 scope.$watch(function() { return scope.$parent.$index; },
11385 function indexChanged(newIdx, oldIdx) {
11386 if (newIdx === oldIdx) {
11387 return;
11388 }
11389 gridCtrl.invalidateTiles();
11390 });
11391 }
11392 }
11393}
11394
11395
11396function GridTileCaptionDirective() {
11397 return {
11398 template: '<figcaption ng-transclude></figcaption>',
11399 transclude: true
11400 };
11401}
11402
11403})();
11404(function(){
11405"use strict";
11406
11407/**
11408 * @ngdoc module
11409 * @name material.components.icon
11410 * @description
11411 * Icon
11412 */
11413angular.module('material.components.icon', ['material.core']);
11414
11415})();
11416(function(){
11417"use strict";
11418
11419/**
11420 * @ngdoc module
11421 * @name material.components.input
11422 */
11423mdInputContainerDirective.$inject = ["$mdTheming", "$parse"];
11424inputTextareaDirective.$inject = ["$mdUtil", "$window", "$mdAria", "$timeout", "$mdGesture"];
11425mdMaxlengthDirective.$inject = ["$animate", "$mdUtil"];
11426placeholderDirective.$inject = ["$compile"];
11427ngMessageDirective.$inject = ["$mdUtil"];
11428mdSelectOnFocusDirective.$inject = ["$timeout"];
11429mdInputInvalidMessagesAnimation.$inject = ["$$AnimateRunner", "$animateCss", "$mdUtil"];
11430ngMessagesAnimation.$inject = ["$$AnimateRunner", "$animateCss", "$mdUtil"];
11431ngMessageAnimation.$inject = ["$$AnimateRunner", "$animateCss", "$mdUtil"];
11432angular.module('material.components.input', [
11433 'material.core'
11434 ])
11435 .directive('mdInputContainer', mdInputContainerDirective)
11436 .directive('label', labelDirective)
11437 .directive('input', inputTextareaDirective)
11438 .directive('textarea', inputTextareaDirective)
11439 .directive('mdMaxlength', mdMaxlengthDirective)
11440 .directive('placeholder', placeholderDirective)
11441 .directive('ngMessages', ngMessagesDirective)
11442 .directive('ngMessage', ngMessageDirective)
11443 .directive('ngMessageExp', ngMessageDirective)
11444 .directive('mdSelectOnFocus', mdSelectOnFocusDirective)
11445
11446 .animation('.md-input-invalid', mdInputInvalidMessagesAnimation)
11447 .animation('.md-input-messages-animation', ngMessagesAnimation)
11448 .animation('.md-input-message-animation', ngMessageAnimation)
11449
11450 // Register a service for each animation so that we can easily inject them into unit tests
11451 .service('mdInputInvalidAnimation', mdInputInvalidMessagesAnimation)
11452 .service('mdInputMessagesAnimation', ngMessagesAnimation)
11453 .service('mdInputMessageAnimation', ngMessageAnimation);
11454
11455/**
11456 * @ngdoc directive
11457 * @name mdInputContainer
11458 * @module material.components.input
11459 *
11460 * @restrict E
11461 *
11462 * @description
11463 * `<md-input-container>` is the parent of any input or textarea element.
11464 *
11465 * Input and textarea elements will not behave properly unless the md-input-container
11466 * parent is provided.
11467 *
11468 * A single `<md-input-container>` should contain only one `<input>` element, otherwise it will throw an error.
11469 *
11470 * <b>Exception:</b> Hidden inputs (`<input type="hidden" />`) are ignored and will not throw an error, so
11471 * you may combine these with other inputs.
11472 *
11473 * <b>Note:</b> When using `ngMessages` with your input element, make sure the message and container elements
11474 * are *block* elements, otherwise animations applied to the messages will not look as intended. Either use a `div` and
11475 * apply the `ng-message` and `ng-messages` classes respectively, or use the `md-block` class on your element.
11476 *
11477 * @param md-is-error {expression=} When the given expression evaluates to true, the input container
11478 * will go into error state. Defaults to erroring if the input has been touched and is invalid.
11479 * @param md-no-float {boolean=} When present, `placeholder` attributes on the input will not be converted to floating
11480 * labels.
11481 *
11482 * @usage
11483 * <hljs lang="html">
11484 *
11485 * <md-input-container>
11486 * <label>Username</label>
11487 * <input type="text" ng-model="user.name">
11488 * </md-input-container>
11489 *
11490 * <md-input-container>
11491 * <label>Description</label>
11492 * <textarea ng-model="user.description"></textarea>
11493 * </md-input-container>
11494 *
11495 * </hljs>
11496 *
11497 * <h3>When disabling floating labels</h3>
11498 * <hljs lang="html">
11499 *
11500 * <md-input-container md-no-float>
11501 * <input type="text" placeholder="Non-Floating Label">
11502 * </md-input-container>
11503 *
11504 * </hljs>
11505 */
11506function mdInputContainerDirective($mdTheming, $parse) {
11507
11508 ContainerCtrl.$inject = ["$scope", "$element", "$attrs", "$animate"];
11509 var INPUT_TAGS = ['INPUT', 'TEXTAREA', 'SELECT', 'MD-SELECT'];
11510
11511 var LEFT_SELECTORS = INPUT_TAGS.reduce(function(selectors, isel) {
11512 return selectors.concat(['md-icon ~ ' + isel, '.md-icon ~ ' + isel]);
11513 }, []).join(",");
11514
11515 var RIGHT_SELECTORS = INPUT_TAGS.reduce(function(selectors, isel) {
11516 return selectors.concat([isel + ' ~ md-icon', isel + ' ~ .md-icon']);
11517 }, []).join(",");
11518
11519 return {
11520 restrict: 'E',
11521 compile: compile,
11522 controller: ContainerCtrl
11523 };
11524
11525 function compile(tElement) {
11526 // Check for both a left & right icon
11527 var leftIcon = tElement[0].querySelector(LEFT_SELECTORS);
11528 var rightIcon = tElement[0].querySelector(RIGHT_SELECTORS);
11529
11530 if (leftIcon) { tElement.addClass('md-icon-left'); }
11531 if (rightIcon) { tElement.addClass('md-icon-right'); }
11532
11533 return function postLink(scope, element) {
11534 $mdTheming(element);
11535 };
11536 }
11537
11538 function ContainerCtrl($scope, $element, $attrs, $animate) {
11539 var self = this;
11540
11541 self.isErrorGetter = $attrs.mdIsError && $parse($attrs.mdIsError);
11542
11543 self.delegateClick = function() {
11544 self.input.focus();
11545 };
11546 self.element = $element;
11547 self.setFocused = function(isFocused) {
11548 $element.toggleClass('md-input-focused', !!isFocused);
11549 };
11550 self.setHasValue = function(hasValue) {
11551 $element.toggleClass('md-input-has-value', !!hasValue);
11552 };
11553 self.setHasPlaceholder = function(hasPlaceholder) {
11554 $element.toggleClass('md-input-has-placeholder', !!hasPlaceholder);
11555 };
11556 self.setInvalid = function(isInvalid) {
11557 if (isInvalid) {
11558 $animate.addClass($element, 'md-input-invalid');
11559 } else {
11560 $animate.removeClass($element, 'md-input-invalid');
11561 }
11562 };
11563 $scope.$watch(function() {
11564 return self.label && self.input;
11565 }, function(hasLabelAndInput) {
11566 if (hasLabelAndInput && !self.label.attr('for')) {
11567 self.label.attr('for', self.input.attr('id'));
11568 }
11569 });
11570 }
11571}
11572
11573function labelDirective() {
11574 return {
11575 restrict: 'E',
11576 require: '^?mdInputContainer',
11577 link: function(scope, element, attr, containerCtrl) {
11578 if (!containerCtrl || attr.mdNoFloat || element.hasClass('md-container-ignore')) return;
11579
11580 containerCtrl.label = element;
11581 scope.$on('$destroy', function() {
11582 containerCtrl.label = null;
11583 });
11584 }
11585 };
11586}
11587
11588/**
11589 * @ngdoc directive
11590 * @name mdInput
11591 * @restrict E
11592 * @module material.components.input
11593 *
11594 * @description
11595 * You can use any `<input>` or `<textarea>` element as a child of an `<md-input-container>`. This
11596 * allows you to build complex forms for data entry.
11597 *
11598 * When the input is required and uses a floating label, then the label will automatically contain
11599 * an asterisk (`*`).<br/>
11600 * This behavior can be disabled by using the `md-no-asterisk` attribute.
11601 *
11602 * @param {number=} md-maxlength The maximum number of characters allowed in this input. If this is
11603 * specified, a character counter will be shown underneath the input.<br/><br/>
11604 * The purpose of **`md-maxlength`** is exactly to show the max length counter text. If you don't
11605 * want the counter text and only need "plain" validation, you can use the "simple" `ng-maxlength`
11606 * or maxlength attributes.<br/><br/>
11607 * **Note:** Only valid for text/string inputs (not numeric).
11608 *
11609 * @param {string=} aria-label Aria-label is required when no label is present. A warning message
11610 * will be logged in the console if not present.
11611 * @param {string=} placeholder An alternative approach to using aria-label when the label is not
11612 * PRESENT. The placeholder text is copied to the aria-label attribute.
11613 * @param md-no-autogrow {boolean=} When present, textareas will not grow automatically.
11614 * @param md-no-asterisk {boolean=} When present, an asterisk will not be appended to the inputs floating label
11615 * @param md-no-resize {boolean=} Disables the textarea resize handle.
11616 * @param {number=} max-rows The maximum amount of rows for a textarea.
11617 * @param md-detect-hidden {boolean=} When present, textareas will be sized properly when they are
11618 * revealed after being hidden. This is off by default for performance reasons because it
11619 * guarantees a reflow every digest cycle.
11620 *
11621 * @usage
11622 * <hljs lang="html">
11623 * <md-input-container>
11624 * <label>Color</label>
11625 * <input type="text" ng-model="color" required md-maxlength="10">
11626 * </md-input-container>
11627 * </hljs>
11628 *
11629 * <h3>With Errors</h3>
11630 *
11631 * `md-input-container` also supports errors using the standard `ng-messages` directives and
11632 * animates the messages when they become visible using from the `ngEnter`/`ngLeave` events or
11633 * the `ngShow`/`ngHide` events.
11634 *
11635 * By default, the messages will be hidden until the input is in an error state. This is based off
11636 * of the `md-is-error` expression of the `md-input-container`. This gives the user a chance to
11637 * fill out the form before the errors become visible.
11638 *
11639 * <hljs lang="html">
11640 * <form name="colorForm">
11641 * <md-input-container>
11642 * <label>Favorite Color</label>
11643 * <input name="favoriteColor" ng-model="favoriteColor" required>
11644 * <div ng-messages="colorForm.favoriteColor.$error">
11645 * <div ng-message="required">This is required!</div>
11646 * </div>
11647 * </md-input-container>
11648 * </form>
11649 * </hljs>
11650 *
11651 * We automatically disable this auto-hiding functionality if you provide any of the following
11652 * visibility directives on the `ng-messages` container:
11653 *
11654 * - `ng-if`
11655 * - `ng-show`/`ng-hide`
11656 * - `ng-switch-when`/`ng-switch-default`
11657 *
11658 * You can also disable this functionality manually by adding the `md-auto-hide="false"` expression
11659 * to the `ng-messages` container. This may be helpful if you always want to see the error messages
11660 * or if you are building your own visibilty directive.
11661 *
11662 * _<b>Note:</b> The `md-auto-hide` attribute is a static string that is only checked upon
11663 * initialization of the `ng-messages` directive to see if it equals the string `false`._
11664 *
11665 * <hljs lang="html">
11666 * <form name="userForm">
11667 * <md-input-container>
11668 * <label>Last Name</label>
11669 * <input name="lastName" ng-model="lastName" required md-maxlength="10" minlength="4">
11670 * <div ng-messages="userForm.lastName.$error" ng-show="userForm.lastName.$dirty">
11671 * <div ng-message="required">This is required!</div>
11672 * <div ng-message="md-maxlength">That's too long!</div>
11673 * <div ng-message="minlength">That's too short!</div>
11674 * </div>
11675 * </md-input-container>
11676 * <md-input-container>
11677 * <label>Biography</label>
11678 * <textarea name="bio" ng-model="biography" required md-maxlength="150"></textarea>
11679 * <div ng-messages="userForm.bio.$error" ng-show="userForm.bio.$dirty">
11680 * <div ng-message="required">This is required!</div>
11681 * <div ng-message="md-maxlength">That's too long!</div>
11682 * </div>
11683 * </md-input-container>
11684 * <md-input-container>
11685 * <input aria-label='title' ng-model='title'>
11686 * </md-input-container>
11687 * <md-input-container>
11688 * <input placeholder='title' ng-model='title'>
11689 * </md-input-container>
11690 * </form>
11691 * </hljs>
11692 *
11693 * <h3>Notes</h3>
11694 *
11695 * - Requires [ngMessages](https://docs.angularjs.org/api/ngMessages).
11696 * - Behaves like the [AngularJS input directive](https://docs.angularjs.org/api/ng/directive/input).
11697 *
11698 * The `md-input` and `md-input-container` directives use very specific positioning to achieve the
11699 * error animation effects. Therefore, it is *not* advised to use the Layout system inside of the
11700 * `<md-input-container>` tags. Instead, use relative or absolute positioning.
11701 *
11702 *
11703 * <h3>Textarea directive</h3>
11704 * The `textarea` element within a `md-input-container` has the following specific behavior:
11705 * - By default the `textarea` grows as the user types. This can be disabled via the `md-no-autogrow`
11706 * attribute.
11707 * - If a `textarea` has the `rows` attribute, it will treat the `rows` as the minimum height and will
11708 * continue growing as the user types. For example a textarea with `rows="3"` will be 3 lines of text
11709 * high initially. If no rows are specified, the directive defaults to 1.
11710 * - The textarea's height gets set on initialization, as well as while the user is typing. In certain situations
11711 * (e.g. while animating) the directive might have been initialized, before the element got it's final height. In
11712 * those cases, you can trigger a resize manually by broadcasting a `md-resize-textarea` event on the scope.
11713 * - If you wan't a `textarea` to stop growing at a certain point, you can specify the `max-rows` attribute.
11714 * - The textarea's bottom border acts as a handle which users can drag, in order to resize the element vertically.
11715 * Once the user has resized a `textarea`, the autogrowing functionality becomes disabled. If you don't want a
11716 * `textarea` to be resizeable by the user, you can add the `md-no-resize` attribute.
11717 */
11718
11719function inputTextareaDirective($mdUtil, $window, $mdAria, $timeout, $mdGesture) {
11720 return {
11721 restrict: 'E',
11722 require: ['^?mdInputContainer', '?ngModel', '?^form'],
11723 link: postLink
11724 };
11725
11726 function postLink(scope, element, attr, ctrls) {
11727
11728 var containerCtrl = ctrls[0];
11729 var hasNgModel = !!ctrls[1];
11730 var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel();
11731 var parentForm = ctrls[2];
11732 var isReadonly = angular.isDefined(attr.readonly);
11733 var mdNoAsterisk = $mdUtil.parseAttributeBoolean(attr.mdNoAsterisk);
11734 var tagName = element[0].tagName.toLowerCase();
11735
11736
11737 if (!containerCtrl) return;
11738 if (attr.type === 'hidden') {
11739 element.attr('aria-hidden', 'true');
11740 return;
11741 } else if (containerCtrl.input) {
11742 if (containerCtrl.input[0].contains(element[0])) {
11743 return;
11744 } else {
11745 throw new Error("<md-input-container> can only have *one* <input>, <textarea> or <md-select> child element!");
11746 }
11747 }
11748 containerCtrl.input = element;
11749
11750 setupAttributeWatchers();
11751
11752 // Add an error spacer div after our input to provide space for the char counter and any ng-messages
11753 var errorsSpacer = angular.element('<div class="md-errors-spacer">');
11754 element.after(errorsSpacer);
11755
11756 if (!containerCtrl.label) {
11757 $mdAria.expect(element, 'aria-label', attr.placeholder);
11758 }
11759
11760 element.addClass('md-input');
11761 if (!element.attr('id')) {
11762 element.attr('id', 'input_' + $mdUtil.nextUid());
11763 }
11764
11765 // This works around a Webkit issue where number inputs, placed in a flexbox, that have
11766 // a `min` and `max` will collapse to about 1/3 of their proper width. Please check #7349
11767 // for more info. Also note that we don't override the `step` if the user has specified it,
11768 // in order to prevent some unexpected behaviour.
11769 if (tagName === 'input' && attr.type === 'number' && attr.min && attr.max && !attr.step) {
11770 element.attr('step', 'any');
11771 } else if (tagName === 'textarea') {
11772 setupTextarea();
11773 }
11774
11775 // If the input doesn't have an ngModel, it may have a static value. For that case,
11776 // we have to do one initial check to determine if the container should be in the
11777 // "has a value" state.
11778 if (!hasNgModel) {
11779 inputCheckValue();
11780 }
11781
11782 var isErrorGetter = containerCtrl.isErrorGetter || function() {
11783 return ngModelCtrl.$invalid && (ngModelCtrl.$touched || (parentForm && parentForm.$submitted));
11784 };
11785
11786 scope.$watch(isErrorGetter, containerCtrl.setInvalid);
11787
11788 // When the developer uses the ngValue directive for the input, we have to observe the attribute, because
11789 // Angular's ngValue directive is just setting the `value` attribute.
11790 if (attr.ngValue) {
11791 attr.$observe('value', inputCheckValue);
11792 }
11793
11794 ngModelCtrl.$parsers.push(ngModelPipelineCheckValue);
11795 ngModelCtrl.$formatters.push(ngModelPipelineCheckValue);
11796
11797 element.on('input', inputCheckValue);
11798
11799 if (!isReadonly) {
11800 element
11801 .on('focus', function(ev) {
11802 $mdUtil.nextTick(function() {
11803 containerCtrl.setFocused(true);
11804 });
11805 })
11806 .on('blur', function(ev) {
11807 $mdUtil.nextTick(function() {
11808 containerCtrl.setFocused(false);
11809 inputCheckValue();
11810 });
11811 });
11812 }
11813
11814 scope.$on('$destroy', function() {
11815 containerCtrl.setFocused(false);
11816 containerCtrl.setHasValue(false);
11817 containerCtrl.input = null;
11818 });
11819
11820 /** Gets run through ngModel's pipeline and set the `has-value` class on the container. */
11821 function ngModelPipelineCheckValue(arg) {
11822 containerCtrl.setHasValue(!ngModelCtrl.$isEmpty(arg));
11823 return arg;
11824 }
11825
11826 function setupAttributeWatchers() {
11827 if (containerCtrl.label) {
11828 attr.$observe('required', function (value) {
11829 // We don't need to parse the required value, it's always a boolean because of angular's
11830 // required directive.
11831 containerCtrl.label.toggleClass('md-required', value && !mdNoAsterisk);
11832 });
11833 }
11834 }
11835
11836 function inputCheckValue() {
11837 // An input's value counts if its length > 0,
11838 // or if the input's validity state says it has bad input (eg string in a number input)
11839 containerCtrl.setHasValue(element.val().length > 0 || (element[0].validity || {}).badInput);
11840 }
11841
11842 function setupTextarea() {
11843 var isAutogrowing = !attr.hasOwnProperty('mdNoAutogrow');
11844
11845 attachResizeHandle();
11846
11847 if (!isAutogrowing) return;
11848
11849 // Can't check if height was or not explicity set,
11850 // so rows attribute will take precedence if present
11851 var minRows = attr.hasOwnProperty('rows') ? parseInt(attr.rows) : NaN;
11852 var maxRows = attr.hasOwnProperty('maxRows') ? parseInt(attr.maxRows) : NaN;
11853 var scopeResizeListener = scope.$on('md-resize-textarea', growTextarea);
11854 var lineHeight = null;
11855 var node = element[0];
11856
11857 // This timeout is necessary, because the browser needs a little bit
11858 // of time to calculate the `clientHeight` and `scrollHeight`.
11859 $timeout(function() {
11860 $mdUtil.nextTick(growTextarea);
11861 }, 10, false);
11862
11863 // We could leverage ngModel's $parsers here, however it
11864 // isn't reliable, because Angular trims the input by default,
11865 // which means that growTextarea won't fire when newlines and
11866 // spaces are added.
11867 element.on('input', growTextarea);
11868
11869 // We should still use the $formatters, because they fire when
11870 // the value was changed from outside the textarea.
11871 if (hasNgModel) {
11872 ngModelCtrl.$formatters.push(formattersListener);
11873 }
11874
11875 if (!minRows) {
11876 element.attr('rows', 1);
11877 }
11878
11879 angular.element($window).on('resize', growTextarea);
11880 scope.$on('$destroy', disableAutogrow);
11881
11882 function growTextarea() {
11883 // temporarily disables element's flex so its height 'runs free'
11884 element
11885 .attr('rows', 1)
11886 .css('height', 'auto')
11887 .addClass('md-no-flex');
11888
11889 var height = getHeight();
11890
11891 if (!lineHeight) {
11892 // offsetHeight includes padding which can throw off our value
11893 var originalPadding = element[0].style.padding || '';
11894 lineHeight = element.css('padding', 0).prop('offsetHeight');
11895 element[0].style.padding = originalPadding;
11896 }
11897
11898 if (minRows && lineHeight) {
11899 height = Math.max(height, lineHeight * minRows);
11900 }
11901
11902 if (maxRows && lineHeight) {
11903 var maxHeight = lineHeight * maxRows;
11904
11905 if (maxHeight < height) {
11906 element.attr('md-no-autogrow', '');
11907 height = maxHeight;
11908 } else {
11909 element.removeAttr('md-no-autogrow');
11910 }
11911 }
11912
11913 if (lineHeight) {
11914 element.attr('rows', Math.round(height / lineHeight));
11915 }
11916
11917 element
11918 .css('height', height + 'px')
11919 .removeClass('md-no-flex');
11920 }
11921
11922 function getHeight() {
11923 var offsetHeight = node.offsetHeight;
11924 var line = node.scrollHeight - offsetHeight;
11925 return offsetHeight + Math.max(line, 0);
11926 }
11927
11928 function formattersListener(value) {
11929 $mdUtil.nextTick(growTextarea);
11930 return value;
11931 }
11932
11933 function disableAutogrow() {
11934 if (!isAutogrowing) return;
11935
11936 isAutogrowing = false;
11937 angular.element($window).off('resize', growTextarea);
11938 scopeResizeListener && scopeResizeListener();
11939 element
11940 .attr('md-no-autogrow', '')
11941 .off('input', growTextarea);
11942
11943 if (hasNgModel) {
11944 var listenerIndex = ngModelCtrl.$formatters.indexOf(formattersListener);
11945
11946 if (listenerIndex > -1) {
11947 ngModelCtrl.$formatters.splice(listenerIndex, 1);
11948 }
11949 }
11950 }
11951
11952 function attachResizeHandle() {
11953 if (attr.hasOwnProperty('mdNoResize')) return;
11954
11955 var handle = angular.element('<div class="md-resize-handle"></div>');
11956 var isDragging = false;
11957 var dragStart = null;
11958 var startHeight = 0;
11959 var container = containerCtrl.element;
11960 var dragGestureHandler = $mdGesture.register(handle, 'drag', { horizontal: false });
11961
11962
11963 element.wrap('<div class="md-resize-wrapper">').after(handle);
11964 handle.on('mousedown', onMouseDown);
11965
11966 container
11967 .on('$md.dragstart', onDragStart)
11968 .on('$md.drag', onDrag)
11969 .on('$md.dragend', onDragEnd);
11970
11971 scope.$on('$destroy', function() {
11972 handle
11973 .off('mousedown', onMouseDown)
11974 .remove();
11975
11976 container
11977 .off('$md.dragstart', onDragStart)
11978 .off('$md.drag', onDrag)
11979 .off('$md.dragend', onDragEnd);
11980
11981 dragGestureHandler();
11982 handle = null;
11983 container = null;
11984 dragGestureHandler = null;
11985 });
11986
11987 function onMouseDown(ev) {
11988 ev.preventDefault();
11989 isDragging = true;
11990 dragStart = ev.clientY;
11991 startHeight = parseFloat(element.css('height')) || element.prop('offsetHeight');
11992 }
11993
11994 function onDragStart(ev) {
11995 if (!isDragging) return;
11996 ev.preventDefault();
11997 disableAutogrow();
11998 container.addClass('md-input-resized');
11999 }
12000
12001 function onDrag(ev) {
12002 if (!isDragging) return;
12003 element.css('height', startHeight + (ev.pointer.y - dragStart) - $mdUtil.scrollTop() + 'px');
12004 }
12005
12006 function onDragEnd(ev) {
12007 if (!isDragging) return;
12008 isDragging = false;
12009 container.removeClass('md-input-resized');
12010 }
12011 }
12012
12013 // Attach a watcher to detect when the textarea gets shown.
12014 if (attr.hasOwnProperty('mdDetectHidden')) {
12015
12016 var handleHiddenChange = function() {
12017 var wasHidden = false;
12018
12019 return function() {
12020 var isHidden = node.offsetHeight === 0;
12021
12022 if (isHidden === false && wasHidden === true) {
12023 growTextarea();
12024 }
12025
12026 wasHidden = isHidden;
12027 };
12028 }();
12029
12030 // Check every digest cycle whether the visibility of the textarea has changed.
12031 // Queue up to run after the digest cycle is complete.
12032 scope.$watch(function() {
12033 $mdUtil.nextTick(handleHiddenChange, false);
12034 return true;
12035 });
12036 }
12037 }
12038 }
12039}
12040
12041function mdMaxlengthDirective($animate, $mdUtil) {
12042 return {
12043 restrict: 'A',
12044 require: ['ngModel', '^mdInputContainer'],
12045 link: postLink
12046 };
12047
12048 function postLink(scope, element, attr, ctrls) {
12049 var maxlength;
12050 var ngModelCtrl = ctrls[0];
12051 var containerCtrl = ctrls[1];
12052 var charCountEl, errorsSpacer;
12053
12054 // Wait until the next tick to ensure that the input has setup the errors spacer where we will
12055 // append our counter
12056 $mdUtil.nextTick(function() {
12057 errorsSpacer = angular.element(containerCtrl.element[0].querySelector('.md-errors-spacer'));
12058 charCountEl = angular.element('<div class="md-char-counter">');
12059
12060 // Append our character counter inside the errors spacer
12061 errorsSpacer.append(charCountEl);
12062
12063 // Stop model from trimming. This makes it so whitespace
12064 // over the maxlength still counts as invalid.
12065 attr.$set('ngTrim', 'false');
12066
12067 ngModelCtrl.$formatters.push(renderCharCount);
12068 ngModelCtrl.$viewChangeListeners.push(renderCharCount);
12069 element.on('input keydown keyup', function() {
12070 renderCharCount(); //make sure it's called with no args
12071 });
12072
12073 scope.$watch(attr.mdMaxlength, function(value) {
12074 maxlength = value;
12075 if (angular.isNumber(value) && value > 0) {
12076 if (!charCountEl.parent().length) {
12077 $animate.enter(charCountEl, errorsSpacer);
12078 }
12079 renderCharCount();
12080 } else {
12081 $animate.leave(charCountEl);
12082 }
12083 });
12084
12085 ngModelCtrl.$validators['md-maxlength'] = function(modelValue, viewValue) {
12086 if (!angular.isNumber(maxlength) || maxlength < 0) {
12087 return true;
12088 }
12089 return ( modelValue || element.val() || viewValue || '' ).length <= maxlength;
12090 };
12091 });
12092
12093 function renderCharCount(value) {
12094 // If we have not been appended to the body yet; do not render
12095 if (!charCountEl.parent) {
12096 return value;
12097 }
12098
12099 // Force the value into a string since it may be a number,
12100 // which does not have a length property.
12101 charCountEl.text(String(element.val() || value || '').length + ' / ' + maxlength);
12102 return value;
12103 }
12104 }
12105}
12106
12107function placeholderDirective($compile) {
12108 return {
12109 restrict: 'A',
12110 require: '^^?mdInputContainer',
12111 priority: 200,
12112 link: {
12113 // Note that we need to do this in the pre-link, as opposed to the post link, if we want to
12114 // support data bindings in the placeholder. This is necessary, because we have a case where
12115 // we transfer the placeholder value to the `<label>` and we remove it from the original `<input>`.
12116 // If we did this in the post-link, Angular would have set up the observers already and would be
12117 // re-adding the attribute, even though we removed it from the element.
12118 pre: preLink
12119 }
12120 };
12121
12122 function preLink(scope, element, attr, inputContainer) {
12123 // If there is no input container, just return
12124 if (!inputContainer) return;
12125
12126 var label = inputContainer.element.find('label');
12127 var noFloat = inputContainer.element.attr('md-no-float');
12128
12129 // If we have a label, or they specify the md-no-float attribute, just return
12130 if ((label && label.length) || noFloat === '' || scope.$eval(noFloat)) {
12131 // Add a placeholder class so we can target it in the CSS
12132 inputContainer.setHasPlaceholder(true);
12133 return;
12134 }
12135
12136 // md-select handles placeholders on it's own
12137 if (element[0].nodeName != 'MD-SELECT') {
12138 // Move the placeholder expression to the label
12139 var newLabel = angular.element('<label ng-click="delegateClick()" tabindex="-1">' + attr.placeholder + '</label>');
12140
12141 // Note that we unset it via `attr`, in order to get Angular
12142 // to remove any observers that it might have set up. Otherwise
12143 // the attribute will be added on the next digest.
12144 attr.$set('placeholder', null);
12145
12146 // We need to compile the label manually in case it has any bindings.
12147 // A gotcha here is that we first add the element to the DOM and we compile
12148 // it later. This is necessary, because if we compile the element beforehand,
12149 // it won't be able to find the `mdInputContainer` controller.
12150 inputContainer.element
12151 .addClass('md-icon-float')
12152 .prepend(newLabel);
12153
12154 $compile(newLabel)(scope);
12155 }
12156 }
12157}
12158
12159/**
12160 * @ngdoc directive
12161 * @name mdSelectOnFocus
12162 * @module material.components.input
12163 *
12164 * @restrict A
12165 *
12166 * @description
12167 * The `md-select-on-focus` directive allows you to automatically select the element's input text on focus.
12168 *
12169 * <h3>Notes</h3>
12170 * - The use of `md-select-on-focus` is restricted to `<input>` and `<textarea>` elements.
12171 *
12172 * @usage
12173 * <h3>Using with an Input</h3>
12174 * <hljs lang="html">
12175 *
12176 * <md-input-container>
12177 * <label>Auto Select</label>
12178 * <input type="text" md-select-on-focus>
12179 * </md-input-container>
12180 * </hljs>
12181 *
12182 * <h3>Using with a Textarea</h3>
12183 * <hljs lang="html">
12184 *
12185 * <md-input-container>
12186 * <label>Auto Select</label>
12187 * <textarea md-select-on-focus>This text will be selected on focus.</textarea>
12188 * </md-input-container>
12189 *
12190 * </hljs>
12191 */
12192function mdSelectOnFocusDirective($timeout) {
12193
12194 return {
12195 restrict: 'A',
12196 link: postLink
12197 };
12198
12199 function postLink(scope, element, attr) {
12200 if (element[0].nodeName !== 'INPUT' && element[0].nodeName !== "TEXTAREA") return;
12201
12202 var preventMouseUp = false;
12203
12204 element
12205 .on('focus', onFocus)
12206 .on('mouseup', onMouseUp);
12207
12208 scope.$on('$destroy', function() {
12209 element
12210 .off('focus', onFocus)
12211 .off('mouseup', onMouseUp);
12212 });
12213
12214 function onFocus() {
12215 preventMouseUp = true;
12216
12217 $timeout(function() {
12218 // Use HTMLInputElement#select to fix firefox select issues.
12219 // The debounce is here for Edge's sake, otherwise the selection doesn't work.
12220 element[0].select();
12221
12222 // This should be reset from inside the `focus`, because the event might
12223 // have originated from something different than a click, e.g. a keyboard event.
12224 preventMouseUp = false;
12225 }, 1, false);
12226 }
12227
12228 // Prevents the default action of the first `mouseup` after a focus.
12229 // This is necessary, because browsers fire a `mouseup` right after the element
12230 // has been focused. In some browsers (Firefox in particular) this can clear the
12231 // selection. There are examples of the problem in issue #7487.
12232 function onMouseUp(event) {
12233 if (preventMouseUp) {
12234 event.preventDefault();
12235 }
12236 }
12237 }
12238}
12239
12240var visibilityDirectives = ['ngIf', 'ngShow', 'ngHide', 'ngSwitchWhen', 'ngSwitchDefault'];
12241function ngMessagesDirective() {
12242 return {
12243 restrict: 'EA',
12244 link: postLink,
12245
12246 // This is optional because we don't want target *all* ngMessage instances, just those inside of
12247 // mdInputContainer.
12248 require: '^^?mdInputContainer'
12249 };
12250
12251 function postLink(scope, element, attrs, inputContainer) {
12252 // If we are not a child of an input container, don't do anything
12253 if (!inputContainer) return;
12254
12255 // Add our animation class
12256 element.toggleClass('md-input-messages-animation', true);
12257
12258 // Add our md-auto-hide class to automatically hide/show messages when container is invalid
12259 element.toggleClass('md-auto-hide', true);
12260
12261 // If we see some known visibility directives, remove the md-auto-hide class
12262 if (attrs.mdAutoHide == 'false' || hasVisibiltyDirective(attrs)) {
12263 element.toggleClass('md-auto-hide', false);
12264 }
12265 }
12266
12267 function hasVisibiltyDirective(attrs) {
12268 return visibilityDirectives.some(function(attr) {
12269 return attrs[attr];
12270 });
12271 }
12272}
12273
12274function ngMessageDirective($mdUtil) {
12275 return {
12276 restrict: 'EA',
12277 compile: compile,
12278 priority: 100
12279 };
12280
12281 function compile(tElement) {
12282 if (!isInsideInputContainer(tElement)) {
12283
12284 // When the current element is inside of a document fragment, then we need to check for an input-container
12285 // in the postLink, because the element will be later added to the DOM and is currently just in a temporary
12286 // fragment, which causes the input-container check to fail.
12287 if (isInsideFragment()) {
12288 return function (scope, element) {
12289 if (isInsideInputContainer(element)) {
12290 // Inside of the postLink function, a ngMessage directive will be a comment element, because it's
12291 // currently hidden. To access the shown element, we need to use the element from the compile function.
12292 initMessageElement(tElement);
12293 }
12294 };
12295 }
12296 } else {
12297 initMessageElement(tElement);
12298 }
12299
12300 function isInsideFragment() {
12301 var nextNode = tElement[0];
12302 while (nextNode = nextNode.parentNode) {
12303 if (nextNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
12304 return true;
12305 }
12306 }
12307 return false;
12308 }
12309
12310 function isInsideInputContainer(element) {
12311 return !!$mdUtil.getClosest(element, "md-input-container");
12312 }
12313
12314 function initMessageElement(element) {
12315 // Add our animation class
12316 element.toggleClass('md-input-message-animation', true);
12317 }
12318 }
12319}
12320
12321var $$AnimateRunner, $animateCss, $mdUtil;
12322
12323function mdInputInvalidMessagesAnimation($$AnimateRunner, $animateCss, $mdUtil) {
12324 saveSharedServices($$AnimateRunner, $animateCss, $mdUtil);
12325
12326 return {
12327 addClass: function(element, className, done) {
12328 showInputMessages(element, done);
12329 }
12330
12331 // NOTE: We do not need the removeClass method, because the message ng-leave animation will fire
12332 };
12333}
12334
12335function ngMessagesAnimation($$AnimateRunner, $animateCss, $mdUtil) {
12336 saveSharedServices($$AnimateRunner, $animateCss, $mdUtil);
12337
12338 return {
12339 enter: function(element, done) {
12340 showInputMessages(element, done);
12341 },
12342
12343 leave: function(element, done) {
12344 hideInputMessages(element, done);
12345 },
12346
12347 addClass: function(element, className, done) {
12348 if (className == "ng-hide") {
12349 hideInputMessages(element, done);
12350 } else {
12351 done();
12352 }
12353 },
12354
12355 removeClass: function(element, className, done) {
12356 if (className == "ng-hide") {
12357 showInputMessages(element, done);
12358 } else {
12359 done();
12360 }
12361 }
12362 }
12363}
12364
12365function ngMessageAnimation($$AnimateRunner, $animateCss, $mdUtil) {
12366 saveSharedServices($$AnimateRunner, $animateCss, $mdUtil);
12367
12368 return {
12369 enter: function(element, done) {
12370 var animator = showMessage(element);
12371
12372 animator.start().done(done);
12373 },
12374
12375 leave: function(element, done) {
12376 var animator = hideMessage(element);
12377
12378 animator.start().done(done);
12379 }
12380 }
12381}
12382
12383function showInputMessages(element, done) {
12384 var animators = [], animator;
12385 var messages = getMessagesElement(element);
12386
12387 angular.forEach(messages.children(), function(child) {
12388 animator = showMessage(angular.element(child));
12389
12390 animators.push(animator.start());
12391 });
12392
12393 $$AnimateRunner.all(animators, done);
12394}
12395
12396function hideInputMessages(element, done) {
12397 var animators = [], animator;
12398 var messages = getMessagesElement(element);
12399
12400 angular.forEach(messages.children(), function(child) {
12401 animator = hideMessage(angular.element(child));
12402
12403 animators.push(animator.start());
12404 });
12405
12406 $$AnimateRunner.all(animators, done);
12407}
12408
12409function showMessage(element) {
12410 var height = parseInt(window.getComputedStyle(element[0]).height);
12411 var topMargin = parseInt(window.getComputedStyle(element[0]).marginTop);
12412
12413 var messages = getMessagesElement(element);
12414 var container = getInputElement(element);
12415
12416 // Check to see if the message is already visible so we can skip
12417 var alreadyVisible = (topMargin > -height);
12418
12419 // If we have the md-auto-hide class, the md-input-invalid animation will fire, so we can skip
12420 if (alreadyVisible || (messages.hasClass('md-auto-hide') && !container.hasClass('md-input-invalid'))) {
12421 return $animateCss(element, {});
12422 }
12423
12424 return $animateCss(element, {
12425 event: 'enter',
12426 structural: true,
12427 from: {"opacity": 0, "margin-top": -height + "px"},
12428 to: {"opacity": 1, "margin-top": "0"},
12429 duration: 0.3
12430 });
12431}
12432
12433function hideMessage(element) {
12434 var height = element[0].offsetHeight;
12435 var styles = window.getComputedStyle(element[0]);
12436
12437 // If we are already hidden, just return an empty animation
12438 if (styles.opacity == 0) {
12439 return $animateCss(element, {});
12440 }
12441
12442 // Otherwise, animate
12443 return $animateCss(element, {
12444 event: 'leave',
12445 structural: true,
12446 from: {"opacity": 1, "margin-top": 0},
12447 to: {"opacity": 0, "margin-top": -height + "px"},
12448 duration: 0.3
12449 });
12450}
12451
12452function getInputElement(element) {
12453 var inputContainer = element.controller('mdInputContainer');
12454
12455 return inputContainer.element;
12456}
12457
12458function getMessagesElement(element) {
12459 // If we are a ng-message element, we need to traverse up the DOM tree
12460 if (element.hasClass('md-input-message-animation')) {
12461 return angular.element($mdUtil.getClosest(element, function(node) {
12462 return node.classList.contains('md-input-messages-animation');
12463 }));
12464 }
12465
12466 // Otherwise, we can traverse down
12467 return angular.element(element[0].querySelector('.md-input-messages-animation'));
12468}
12469
12470function saveSharedServices(_$$AnimateRunner_, _$animateCss_, _$mdUtil_) {
12471 $$AnimateRunner = _$$AnimateRunner_;
12472 $animateCss = _$animateCss_;
12473 $mdUtil = _$mdUtil_;
12474}
12475
12476})();
12477(function(){
12478"use strict";
12479
12480/**
12481 * @ngdoc module
12482 * @name material.components.list
12483 * @description
12484 * List module
12485 */
12486MdListController.$inject = ["$scope", "$element", "$mdListInkRipple"];
12487mdListDirective.$inject = ["$mdTheming"];
12488mdListItemDirective.$inject = ["$mdAria", "$mdConstant", "$mdUtil", "$timeout"];
12489angular.module('material.components.list', [
12490 'material.core'
12491])
12492 .controller('MdListController', MdListController)
12493 .directive('mdList', mdListDirective)
12494 .directive('mdListItem', mdListItemDirective);
12495
12496/**
12497 * @ngdoc directive
12498 * @name mdList
12499 * @module material.components.list
12500 *
12501 * @restrict E
12502 *
12503 * @description
12504 * The `<md-list>` directive is a list container for 1..n `<md-list-item>` tags.
12505 *
12506 * @usage
12507 * <hljs lang="html">
12508 * <md-list>
12509 * <md-list-item class="md-2-line" ng-repeat="item in todos">
12510 * <md-checkbox ng-model="item.done"></md-checkbox>
12511 * <div class="md-list-item-text">
12512 * <h3>{{item.title}}</h3>
12513 * <p>{{item.description}}</p>
12514 * </div>
12515 * </md-list-item>
12516 * </md-list>
12517 * </hljs>
12518 */
12519
12520function mdListDirective($mdTheming) {
12521 return {
12522 restrict: 'E',
12523 compile: function(tEl) {
12524 tEl[0].setAttribute('role', 'list');
12525 return $mdTheming;
12526 }
12527 };
12528}
12529/**
12530 * @ngdoc directive
12531 * @name mdListItem
12532 * @module material.components.list
12533 *
12534 * @restrict E
12535 *
12536 * @description
12537 * A `md-list-item` element can be used to represent some information in a row.<br/>
12538 *
12539 * @usage
12540 * ### Single Row Item
12541 * <hljs lang="html">
12542 * <md-list-item>
12543 * <span>Single Row Item</span>
12544 * </md-list-item>
12545 * </hljs>
12546 *
12547 * ### Multiple Lines
12548 * By using the following markup, you will be able to have two lines inside of one `md-list-item`.
12549 *
12550 * <hljs lang="html">
12551 * <md-list-item class="md-2-line">
12552 * <div class="md-list-item-text" layout="column">
12553 * <p>First Line</p>
12554 * <p>Second Line</p>
12555 * </div>
12556 * </md-list-item>
12557 * </hljs>
12558 *
12559 * It is also possible to have three lines inside of one list item.
12560 *
12561 * <hljs lang="html">
12562 * <md-list-item class="md-3-line">
12563 * <div class="md-list-item-text" layout="column">
12564 * <p>First Line</p>
12565 * <p>Second Line</p>
12566 * <p>Third Line</p>
12567 * </div>
12568 * </md-list-item>
12569 * </hljs>
12570 *
12571 * ### Secondary Items
12572 * Secondary items are elements which will be aligned at the end of the `md-list-item`.
12573 *
12574 * <hljs lang="html">
12575 * <md-list-item>
12576 * <span>Single Row Item</span>
12577 * <md-button class="md-secondary">
12578 * Secondary Button
12579 * </md-button>
12580 * </md-list-item>
12581 * </hljs>
12582 *
12583 * It also possible to have multiple secondary items inside of one `md-list-item`.
12584 *
12585 * <hljs lang="html">
12586 * <md-list-item>
12587 * <span>Single Row Item</span>
12588 * <md-button class="md-secondary">First Button</md-button>
12589 * <md-button class="md-secondary">Second Button</md-button>
12590 * </md-list-item>
12591 * </hljs>
12592 *
12593 * ### Proxy Item
12594 * Proxies are elements, which will execute their specific action on click<br/>
12595 * Currently supported proxy items are
12596 * - `md-checkbox` (Toggle)
12597 * - `md-switch` (Toggle)
12598 * - `md-menu` (Open)
12599 *
12600 * This means, when using a supported proxy item inside of `md-list-item`, the list item will
12601 * become clickable and executes the associated action of the proxy element on click.
12602 *
12603 * <hljs lang="html">
12604 * <md-list-item>
12605 * <span>First Line</span>
12606 * <md-checkbox class="md-secondary"></md-checkbox>
12607 * </md-list-item>
12608 * </hljs>
12609 *
12610 * The `md-checkbox` element will be automatically detected as a proxy element and will toggle on click.
12611 *
12612 * <hljs lang="html">
12613 * <md-list-item>
12614 * <span>First Line</span>
12615 * <md-switch class="md-secondary"></md-switch>
12616 * </md-list-item>
12617 * </hljs>
12618 *
12619 * The recognized `md-switch` will toggle its state, when the user clicks on the `md-list-item`.
12620 *
12621 * It is also possible to have a `md-menu` inside of a `md-list-item`.
12622 * <hljs lang="html">
12623 * <md-list-item>
12624 * <p>Click anywhere to fire the secondary action</p>
12625 * <md-menu class="md-secondary">
12626 * <md-button class="md-icon-button">
12627 * <md-icon md-svg-icon="communication:message"></md-icon>
12628 * </md-button>
12629 * <md-menu-content width="4">
12630 * <md-menu-item>
12631 * <md-button>
12632 * Redial
12633 * </md-button>
12634 * </md-menu-item>
12635 * <md-menu-item>
12636 * <md-button>
12637 * Check voicemail
12638 * </md-button>
12639 * </md-menu-item>
12640 * <md-menu-divider></md-menu-divider>
12641 * <md-menu-item>
12642 * <md-button>
12643 * Notifications
12644 * </md-button>
12645 * </md-menu-item>
12646 * </md-menu-content>
12647 * </md-menu>
12648 * </md-list-item>
12649 * </hljs>
12650 *
12651 * The menu will automatically open, when the users clicks on the `md-list-item`.<br/>
12652 *
12653 * If the developer didn't specify any position mode on the menu, the `md-list-item` will automatically detect the
12654 * position mode and applies it to the `md-menu`.
12655 *
12656 * ### Avatars
12657 * Sometimes you may want to have some avatars inside of the `md-list-item `.<br/>
12658 * You are able to create a optimized icon for the list item, by applying the `.md-avatar` class on the `<img>` element.
12659 *
12660 * <hljs lang="html">
12661 * <md-list-item>
12662 * <img src="my-avatar.png" class="md-avatar">
12663 * <span>Alan Turing</span>
12664 * </hljs>
12665 *
12666 * When using `<md-icon>` for an avater, you have to use the `.md-avatar-icon` class.
12667 * <hljs lang="html">
12668 * <md-list-item>
12669 * <md-icon class="md-avatar-icon" md-svg-icon="avatars:timothy"></md-icon>
12670 * <span>Timothy Kopra</span>
12671 * </md-list-item>
12672 * </hljs>
12673 *
12674 * In cases, you have a `md-list-item`, which doesn't have any avatar,
12675 * but you want to align it with the other avatar items, you have to use the `.md-offset` class.
12676 *
12677 * <hljs lang="html">
12678 * <md-list-item class="md-offset">
12679 * <span>Jon Doe</span>
12680 * </md-list-item>
12681 * </hljs>
12682 *
12683 * ### DOM modification
12684 * The `md-list-item` component automatically detects if the list item should be clickable.
12685 *
12686 * ---
12687 * If the `md-list-item` is clickable, we wrap all content inside of a `<div>` and create
12688 * an overlaying button, which will will execute the given actions (like `ng-href`, `ng-click`)
12689 *
12690 * We create an overlaying button, instead of wrapping all content inside of the button,
12691 * because otherwise some elements may not be clickable inside of the button.
12692 *
12693 * ---
12694 * When using a secondary item inside of your list item, the `md-list-item` component will automatically create
12695 * a secondary container at the end of the `md-list-item`, which contains all secondary items.
12696 *
12697 * The secondary item container is not static, because otherwise the overflow will not work properly on the
12698 * list item.
12699 *
12700 */
12701function mdListItemDirective($mdAria, $mdConstant, $mdUtil, $timeout) {
12702 var proxiedTypes = ['md-checkbox', 'md-switch', 'md-menu'];
12703 return {
12704 restrict: 'E',
12705 controller: 'MdListController',
12706 compile: function(tEl, tAttrs) {
12707
12708 // Check for proxy controls (no ng-click on parent, and a control inside)
12709 var secondaryItems = tEl[0].querySelectorAll('.md-secondary');
12710 var hasProxiedElement;
12711 var proxyElement;
12712 var itemContainer = tEl;
12713
12714 tEl[0].setAttribute('role', 'listitem');
12715
12716 if (tAttrs.ngClick || tAttrs.ngDblclick || tAttrs.ngHref || tAttrs.href || tAttrs.uiSref || tAttrs.ngAttrUiSref) {
12717 wrapIn('button');
12718 } else {
12719 for (var i = 0, type; type = proxiedTypes[i]; ++i) {
12720 if (proxyElement = tEl[0].querySelector(type)) {
12721 hasProxiedElement = true;
12722 break;
12723 }
12724 }
12725 if (hasProxiedElement) {
12726 wrapIn('div');
12727 } else if (!tEl[0].querySelector('md-button:not(.md-secondary):not(.md-exclude)')) {
12728 tEl.addClass('md-no-proxy');
12729 }
12730 }
12731
12732 wrapSecondaryItems();
12733 setupToggleAria();
12734
12735 if (hasProxiedElement && proxyElement.nodeName === "MD-MENU") {
12736 setupProxiedMenu();
12737 }
12738
12739 function setupToggleAria() {
12740 var toggleTypes = ['md-switch', 'md-checkbox'];
12741 var toggle;
12742
12743 for (var i = 0, toggleType; toggleType = toggleTypes[i]; ++i) {
12744 if (toggle = tEl.find(toggleType)[0]) {
12745 if (!toggle.hasAttribute('aria-label')) {
12746 var p = tEl.find('p')[0];
12747 if (!p) return;
12748 toggle.setAttribute('aria-label', 'Toggle ' + p.textContent);
12749 }
12750 }
12751 }
12752 }
12753
12754 function setupProxiedMenu() {
12755 var menuEl = angular.element(proxyElement);
12756
12757 var isEndAligned = menuEl.parent().hasClass('md-secondary-container') ||
12758 proxyElement.parentNode.firstElementChild !== proxyElement;
12759
12760 var xAxisPosition = 'left';
12761
12762 if (isEndAligned) {
12763 // When the proxy item is aligned at the end of the list, we have to set the origin to the end.
12764 xAxisPosition = 'right';
12765 }
12766
12767 // Set the position mode / origin of the proxied menu.
12768 if (!menuEl.attr('md-position-mode')) {
12769 menuEl.attr('md-position-mode', xAxisPosition + ' target');
12770 }
12771
12772 // Apply menu open binding to menu button
12773 var menuOpenButton = menuEl.children().eq(0);
12774 if (!hasClickEvent(menuOpenButton[0])) {
12775 menuOpenButton.attr('ng-click', '$mdOpenMenu($event)');
12776 }
12777
12778 if (!menuOpenButton.attr('aria-label')) {
12779 menuOpenButton.attr('aria-label', 'Open List Menu');
12780 }
12781 }
12782
12783 function wrapIn(type) {
12784 if (type == 'div') {
12785 itemContainer = angular.element('<div class="md-no-style md-list-item-inner">');
12786 itemContainer.append(tEl.contents());
12787 tEl.addClass('md-proxy-focus');
12788 } else {
12789 // Element which holds the default list-item content.
12790 itemContainer = angular.element(
12791 '<div class="md-button md-no-style">'+
12792 ' <div class="md-list-item-inner"></div>'+
12793 '</div>'
12794 );
12795
12796 // Button which shows ripple and executes primary action.
12797 var buttonWrap = angular.element(
12798 '<md-button class="md-no-style"></md-button>'
12799 );
12800
12801 buttonWrap[0].setAttribute('aria-label', tEl[0].textContent);
12802
12803 copyAttributes(tEl[0], buttonWrap[0]);
12804
12805 // We allow developers to specify the `md-no-focus` class, to disable the focus style
12806 // on the button executor. Once more classes should be forwarded, we should probably make the
12807 // class forward more generic.
12808 if (tEl.hasClass('md-no-focus')) {
12809 buttonWrap.addClass('md-no-focus');
12810 }
12811
12812 // Append the button wrap before our list-item content, because it will overlay in relative.
12813 itemContainer.prepend(buttonWrap);
12814 itemContainer.children().eq(1).append(tEl.contents());
12815
12816 tEl.addClass('_md-button-wrap');
12817 }
12818
12819 tEl[0].setAttribute('tabindex', '-1');
12820 tEl.append(itemContainer);
12821 }
12822
12823 function wrapSecondaryItems() {
12824 var secondaryItemsWrapper = angular.element('<div class="md-secondary-container">');
12825
12826 angular.forEach(secondaryItems, function(secondaryItem) {
12827 wrapSecondaryItem(secondaryItem, secondaryItemsWrapper);
12828 });
12829
12830 itemContainer.append(secondaryItemsWrapper);
12831 }
12832
12833 function wrapSecondaryItem(secondaryItem, container) {
12834 // If the current secondary item is not a button, but contains a ng-click attribute,
12835 // the secondary item will be automatically wrapped inside of a button.
12836 if (secondaryItem && !isButton(secondaryItem) && secondaryItem.hasAttribute('ng-click')) {
12837
12838 $mdAria.expect(secondaryItem, 'aria-label');
12839 var buttonWrapper = angular.element('<md-button class="md-secondary md-icon-button">');
12840
12841 // Copy the attributes from the secondary item to the generated button.
12842 // We also support some additional attributes from the secondary item,
12843 // because some developers may use a ngIf, ngHide, ngShow on their item.
12844 copyAttributes(secondaryItem, buttonWrapper[0], ['ng-if', 'ng-hide', 'ng-show']);
12845
12846 secondaryItem.setAttribute('tabindex', '-1');
12847 buttonWrapper.append(secondaryItem);
12848
12849 secondaryItem = buttonWrapper[0];
12850 }
12851
12852 if (secondaryItem && (!hasClickEvent(secondaryItem) || (!tAttrs.ngClick && isProxiedElement(secondaryItem)))) {
12853 // In this case we remove the secondary class, so we can identify it later, when we searching for the
12854 // proxy items.
12855 angular.element(secondaryItem).removeClass('md-secondary');
12856 }
12857
12858 tEl.addClass('md-with-secondary');
12859 container.append(secondaryItem);
12860 }
12861
12862 /**
12863 * Copies attributes from a source element to the destination element
12864 * By default the function will copy the most necessary attributes, supported
12865 * by the button executor for clickable list items.
12866 * @param source Element with the specified attributes
12867 * @param destination Element which will retrieve the attributes
12868 * @param extraAttrs Additional attributes, which will be copied over.
12869 */
12870 function copyAttributes(source, destination, extraAttrs) {
12871 var copiedAttrs = $mdUtil.prefixer([
12872 'ng-if', 'ng-click', 'ng-dblclick', 'aria-label', 'ng-disabled', 'ui-sref',
12873 'href', 'ng-href', 'target', 'ng-attr-ui-sref', 'ui-sref-opts'
12874 ]);
12875
12876 if (extraAttrs) {
12877 copiedAttrs = copiedAttrs.concat($mdUtil.prefixer(extraAttrs));
12878 }
12879
12880 angular.forEach(copiedAttrs, function(attr) {
12881 if (source.hasAttribute(attr)) {
12882 destination.setAttribute(attr, source.getAttribute(attr));
12883 source.removeAttribute(attr);
12884 }
12885 });
12886 }
12887
12888 function isProxiedElement(el) {
12889 return proxiedTypes.indexOf(el.nodeName.toLowerCase()) != -1;
12890 }
12891
12892 function isButton(el) {
12893 var nodeName = el.nodeName.toUpperCase();
12894
12895 return nodeName == "MD-BUTTON" || nodeName == "BUTTON";
12896 }
12897
12898 function hasClickEvent (element) {
12899 var attr = element.attributes;
12900 for (var i = 0; i < attr.length; i++) {
12901 if (tAttrs.$normalize(attr[i].name) === 'ngClick') return true;
12902 }
12903 return false;
12904 }
12905
12906 return postLink;
12907
12908 function postLink($scope, $element, $attr, ctrl) {
12909 $element.addClass('_md'); // private md component indicator for styling
12910
12911 var proxies = [],
12912 firstElement = $element[0].firstElementChild,
12913 isButtonWrap = $element.hasClass('_md-button-wrap'),
12914 clickChild = isButtonWrap ? firstElement.firstElementChild : firstElement,
12915 hasClick = clickChild && hasClickEvent(clickChild);
12916
12917 computeProxies();
12918 computeClickable();
12919
12920 if ($element.hasClass('md-proxy-focus') && proxies.length) {
12921 angular.forEach(proxies, function(proxy) {
12922 proxy = angular.element(proxy);
12923
12924 $scope.mouseActive = false;
12925 proxy.on('mousedown', function() {
12926 $scope.mouseActive = true;
12927 $timeout(function(){
12928 $scope.mouseActive = false;
12929 }, 100);
12930 })
12931 .on('focus', function() {
12932 if ($scope.mouseActive === false) { $element.addClass('md-focused'); }
12933 proxy.on('blur', function proxyOnBlur() {
12934 $element.removeClass('md-focused');
12935 proxy.off('blur', proxyOnBlur);
12936 });
12937 });
12938 });
12939 }
12940
12941
12942 function computeProxies() {
12943 if (firstElement && firstElement.children && !hasClick) {
12944
12945 angular.forEach(proxiedTypes, function(type) {
12946
12947 // All elements which are not capable for being used a proxy have the .md-secondary class
12948 // applied. These items had been sorted out in the secondary wrap function.
12949 angular.forEach(firstElement.querySelectorAll(type + ':not(.md-secondary)'), function(child) {
12950 proxies.push(child);
12951 });
12952 });
12953
12954 }
12955 }
12956
12957 function computeClickable() {
12958 if (proxies.length == 1 || hasClick) {
12959 $element.addClass('md-clickable');
12960
12961 if (!hasClick) {
12962 ctrl.attachRipple($scope, angular.element($element[0].querySelector('.md-no-style')));
12963 }
12964 }
12965 }
12966
12967 function isEventFromControl(event) {
12968 var forbiddenControls = ['md-slider'];
12969
12970 // If there is no path property in the event, then we can assume that the event was not bubbled.
12971 if (!event.path) {
12972 return forbiddenControls.indexOf(event.target.tagName.toLowerCase()) !== -1;
12973 }
12974
12975 // We iterate the event path up and check for a possible component.
12976 // Our maximum index to search, is the list item root.
12977 var maxPath = event.path.indexOf($element.children()[0]);
12978
12979 for (var i = 0; i < maxPath; i++) {
12980 if (forbiddenControls.indexOf(event.path[i].tagName.toLowerCase()) !== -1) {
12981 return true;
12982 }
12983 }
12984 }
12985
12986 var clickChildKeypressListener = function(e) {
12987 if (e.target.nodeName != 'INPUT' && e.target.nodeName != 'TEXTAREA' && !e.target.isContentEditable) {
12988 var keyCode = e.which || e.keyCode;
12989 if (keyCode == $mdConstant.KEY_CODE.SPACE) {
12990 if (clickChild) {
12991 clickChild.click();
12992 e.preventDefault();
12993 e.stopPropagation();
12994 }
12995 }
12996 }
12997 };
12998
12999 if (!hasClick && !proxies.length) {
13000 clickChild && clickChild.addEventListener('keypress', clickChildKeypressListener);
13001 }
13002
13003 $element.off('click');
13004 $element.off('keypress');
13005
13006 if (proxies.length == 1 && clickChild) {
13007 $element.children().eq(0).on('click', function(e) {
13008 // When the event is coming from an control and it should not trigger the proxied element
13009 // then we are skipping.
13010 if (isEventFromControl(e)) return;
13011
13012 var parentButton = $mdUtil.getClosest(e.target, 'BUTTON');
13013 if (!parentButton && clickChild.contains(e.target)) {
13014 angular.forEach(proxies, function(proxy) {
13015 if (e.target !== proxy && !proxy.contains(e.target)) {
13016 if (proxy.nodeName === 'MD-MENU') {
13017 proxy = proxy.children[0];
13018 }
13019 angular.element(proxy).triggerHandler('click');
13020 }
13021 });
13022 }
13023 });
13024 }
13025
13026 $scope.$on('$destroy', function () {
13027 clickChild && clickChild.removeEventListener('keypress', clickChildKeypressListener);
13028 });
13029 }
13030 }
13031 };
13032}
13033
13034/*
13035 * @private
13036 * @ngdoc controller
13037 * @name MdListController
13038 * @module material.components.list
13039 *
13040 */
13041function MdListController($scope, $element, $mdListInkRipple) {
13042 var ctrl = this;
13043 ctrl.attachRipple = attachRipple;
13044
13045 function attachRipple (scope, element) {
13046 var options = {};
13047 $mdListInkRipple.attach(scope, element, options);
13048 }
13049}
13050
13051})();
13052(function(){
13053"use strict";
13054
13055/**
13056 * @ngdoc module
13057 * @name material.components.menu
13058 */
13059
13060angular.module('material.components.menu', [
13061 'material.core',
13062 'material.components.backdrop'
13063]);
13064
13065})();
13066(function(){
13067"use strict";
13068
13069/**
13070 * @ngdoc module
13071 * @name material.components.menu-bar
13072 */
13073
13074angular.module('material.components.menuBar', [
13075 'material.core',
13076 'material.components.icon',
13077 'material.components.menu'
13078]);
13079
13080})();
13081(function(){
13082"use strict";
13083
13084/**
13085 * @ngdoc module
13086 * @name material.components.navBar
13087 */
13088
13089
13090MdNavBarController.$inject = ["$element", "$scope", "$timeout", "$mdConstant"];
13091MdNavItem.$inject = ["$$rAF"];
13092MdNavItemController.$inject = ["$element"];
13093MdNavBar.$inject = ["$mdAria", "$mdTheming"];
13094angular.module('material.components.navBar', ['material.core'])
13095 .controller('MdNavBarController', MdNavBarController)
13096 .directive('mdNavBar', MdNavBar)
13097 .controller('MdNavItemController', MdNavItemController)
13098 .directive('mdNavItem', MdNavItem);
13099
13100
13101/*****************************************************************************
13102 * PUBLIC DOCUMENTATION *
13103 *****************************************************************************/
13104/**
13105 * @ngdoc directive
13106 * @name mdNavBar
13107 * @module material.components.navBar
13108 *
13109 * @restrict E
13110 *
13111 * @description
13112 * The `<md-nav-bar>` directive renders a list of material tabs that can be used
13113 * for top-level page navigation. Unlike `<md-tabs>`, it has no concept of a tab
13114 * body and no bar pagination.
13115 *
13116 * Because it deals with page navigation, certain routing concepts are built-in.
13117 * Route changes via via ng-href, ui-sref, or ng-click events are supported.
13118 * Alternatively, the user could simply watch currentNavItem for changes.
13119 *
13120 * Accessibility functionality is implemented as a site navigator with a
13121 * listbox, according to
13122 * https://www.w3.org/TR/wai-aria-practices/#Site_Navigator_Tabbed_Style
13123 *
13124 * @param {string=} mdSelectedNavItem The name of the current tab; this must
13125 * match the name attribute of `<md-nav-item>`
13126 * @param {string=} navBarAriaLabel An aria-label for the nav-bar
13127 *
13128 * @usage
13129 * <hljs lang="html">
13130 * <md-nav-bar md-selected-nav-item="currentNavItem">
13131 * <md-nav-item md-nav-click="goto('page1')" name="page1">Page One</md-nav-item>
13132 * <md-nav-item md-nav-sref="app.page2" name="page2">Page Two</md-nav-item>
13133 * <md-nav-item md-nav-href="#page3" name="page3">Page Three</md-nav-item>
13134 * </md-nav-bar>
13135 *</hljs>
13136 * <hljs lang="js">
13137 * (function() {
13138 * ‘use strict’;
13139 *
13140 * $rootScope.$on('$routeChangeSuccess', function(event, current) {
13141 * $scope.currentLink = getCurrentLinkFromRoute(current);
13142 * });
13143 * });
13144 * </hljs>
13145 */
13146
13147/*****************************************************************************
13148 * mdNavItem
13149 *****************************************************************************/
13150/**
13151 * @ngdoc directive
13152 * @name mdNavItem
13153 * @module material.components.navBar
13154 *
13155 * @restrict E
13156 *
13157 * @description
13158 * `<md-nav-item>` describes a page navigation link within the `<md-nav-bar>`
13159 * component. It renders an md-button as the actual link.
13160 *
13161 * Exactly one of the mdNavClick, mdNavHref, mdNavSref attributes are required to be
13162 * specified.
13163 *
13164 * @param {Function=} mdNavClick Function which will be called when the
13165 * link is clicked to change the page. Renders as an `ng-click`.
13166 * @param {string=} mdNavHref url to transition to when this link is clicked.
13167 * Renders as an `ng-href`.
13168 * @param {string=} mdNavSref Ui-router state to transition to when this link is
13169 * clicked. Renders as a `ui-sref`.
13170 * @param {string=} name The name of this link. Used by the nav bar to know
13171 * which link is currently selected.
13172 *
13173 * @usage
13174 * See `<md-nav-bar>` for usage.
13175 */
13176
13177
13178/*****************************************************************************
13179 * IMPLEMENTATION *
13180 *****************************************************************************/
13181
13182function MdNavBar($mdAria, $mdTheming) {
13183 return {
13184 restrict: 'E',
13185 transclude: true,
13186 controller: MdNavBarController,
13187 controllerAs: 'ctrl',
13188 bindToController: true,
13189 scope: {
13190 'mdSelectedNavItem': '=?',
13191 'navBarAriaLabel': '@?',
13192 },
13193 template:
13194 '<div class="md-nav-bar">' +
13195 '<nav role="navigation">' +
13196 '<ul class="_md-nav-bar-list" ng-transclude role="listbox"' +
13197 'tabindex="0"' +
13198 'ng-focus="ctrl.onFocus()"' +
13199 'ng-blur="ctrl.onBlur()"' +
13200 'ng-keydown="ctrl.onKeydown($event)"' +
13201 'aria-label="{{ctrl.navBarAriaLabel}}">' +
13202 '</ul>' +
13203 '</nav>' +
13204 '<md-nav-ink-bar></md-nav-ink-bar>' +
13205 '</div>',
13206 link: function(scope, element, attrs, ctrl) {
13207 $mdTheming(element);
13208 if (!ctrl.navBarAriaLabel) {
13209 $mdAria.expectAsync(element, 'aria-label', angular.noop);
13210 }
13211 },
13212 };
13213}
13214
13215/**
13216 * Controller for the nav-bar component.
13217 *
13218 * Accessibility functionality is implemented as a site navigator with a
13219 * listbox, according to
13220 * https://www.w3.org/TR/wai-aria-practices/#Site_Navigator_Tabbed_Style
13221 * @param {!angular.JQLite} $element
13222 * @param {!angular.Scope} $scope
13223 * @param {!angular.Timeout} $timeout
13224 * @param {!Object} $mdConstant
13225 * @constructor
13226 * @final
13227 * @ngInject
13228 */
13229function MdNavBarController($element, $scope, $timeout, $mdConstant) {
13230 // Injected variables
13231 /** @private @const {!angular.Timeout} */
13232 this._$timeout = $timeout;
13233
13234 /** @private @const {!angular.Scope} */
13235 this._$scope = $scope;
13236
13237 /** @private @const {!Object} */
13238 this._$mdConstant = $mdConstant;
13239
13240 // Data-bound variables.
13241 /** @type {string} */
13242 this.mdSelectedNavItem;
13243
13244 /** @type {string} */
13245 this.navBarAriaLabel;
13246
13247 // State variables.
13248
13249 /** @type {?angular.JQLite} */
13250 this._navBarEl = $element[0];
13251
13252 /** @type {?angular.JQLite} */
13253 this._inkbar;
13254
13255 var self = this;
13256 // need to wait for transcluded content to be available
13257 var deregisterTabWatch = this._$scope.$watch(function() {
13258 return self._navBarEl.querySelectorAll('._md-nav-button').length;
13259 },
13260 function(newLength) {
13261 if (newLength > 0) {
13262 self._initTabs();
13263 deregisterTabWatch();
13264 }
13265 });
13266}
13267
13268
13269
13270/**
13271 * Initializes the tab components once they exist.
13272 * @private
13273 */
13274MdNavBarController.prototype._initTabs = function() {
13275 this._inkbar = angular.element(this._navBarEl.getElementsByTagName('md-nav-ink-bar')[0]);
13276
13277 var self = this;
13278 this._$timeout(function() {
13279 self._updateTabs(self.mdSelectedNavItem, undefined);
13280 });
13281
13282 this._$scope.$watch('ctrl.mdSelectedNavItem', function(newValue, oldValue) {
13283 // Wait a digest before update tabs for products doing
13284 // anything dynamic in the template.
13285 self._$timeout(function() {
13286 self._updateTabs(newValue, oldValue);
13287 });
13288 });
13289};
13290
13291/**
13292 * Set the current tab to be selected.
13293 * @param {string|undefined} newValue New current tab name.
13294 * @param {string|undefined} oldValue Previous tab name.
13295 * @private
13296 */
13297MdNavBarController.prototype._updateTabs = function(newValue, oldValue) {
13298 var self = this;
13299 var tabs = this._getTabs();
13300 var oldIndex = -1;
13301 var newIndex = -1;
13302 var newTab = this._getTabByName(newValue);
13303 var oldTab = this._getTabByName(oldValue);
13304
13305 if (oldTab) {
13306 oldTab.setSelected(false);
13307 oldIndex = tabs.indexOf(oldTab);
13308 }
13309
13310 if (newTab) {
13311 newTab.setSelected(true);
13312 newIndex = tabs.indexOf(newTab);
13313 }
13314
13315 this._$timeout(function() {
13316 self._updateInkBarStyles(newTab, newIndex, oldIndex);
13317 });
13318};
13319
13320/**
13321 * Repositions the ink bar to the selected tab.
13322 * @private
13323 */
13324MdNavBarController.prototype._updateInkBarStyles = function(tab, newIndex, oldIndex) {
13325 this._inkbar.toggleClass('_md-left', newIndex < oldIndex)
13326 .toggleClass('_md-right', newIndex > oldIndex);
13327
13328 this._inkbar.css({display: newIndex < 0 ? 'none' : ''});
13329
13330 if(tab){
13331 var tabEl = tab.getButtonEl();
13332 var left = tabEl.offsetLeft;
13333
13334 this._inkbar.css({left: left + 'px', width: tabEl.offsetWidth + 'px'});
13335 }
13336};
13337
13338/**
13339 * Returns an array of the current tabs.
13340 * @return {!Array<!NavItemController>}
13341 * @private
13342 */
13343MdNavBarController.prototype._getTabs = function() {
13344 var linkArray = Array.prototype.slice.call(
13345 this._navBarEl.querySelectorAll('.md-nav-item'));
13346 return linkArray.map(function(el) {
13347 return angular.element(el).controller('mdNavItem')
13348 });
13349};
13350
13351/**
13352 * Returns the tab with the specified name.
13353 * @param {string} name The name of the tab, found in its name attribute.
13354 * @return {!NavItemController|undefined}
13355 * @private
13356 */
13357MdNavBarController.prototype._getTabByName = function(name) {
13358 return this._findTab(function(tab) {
13359 return tab.getName() == name;
13360 });
13361};
13362
13363/**
13364 * Returns the selected tab.
13365 * @return {!NavItemController|undefined}
13366 * @private
13367 */
13368MdNavBarController.prototype._getSelectedTab = function() {
13369 return this._findTab(function(tab) {
13370 return tab.isSelected()
13371 });
13372};
13373
13374/**
13375 * Returns the focused tab.
13376 * @return {!NavItemController|undefined}
13377 */
13378MdNavBarController.prototype.getFocusedTab = function() {
13379 return this._findTab(function(tab) {
13380 return tab.hasFocus()
13381 });
13382};
13383
13384/**
13385 * Find a tab that matches the specified function.
13386 * @private
13387 */
13388MdNavBarController.prototype._findTab = function(fn) {
13389 var tabs = this._getTabs();
13390 for (var i = 0; i < tabs.length; i++) {
13391 if (fn(tabs[i])) {
13392 return tabs[i];
13393 }
13394 }
13395
13396 return null;
13397};
13398
13399/**
13400 * Direct focus to the selected tab when focus enters the nav bar.
13401 */
13402MdNavBarController.prototype.onFocus = function() {
13403 var tab = this._getSelectedTab();
13404 if (tab) {
13405 tab.setFocused(true);
13406 }
13407};
13408
13409/**
13410 * Clear tab focus when focus leaves the nav bar.
13411 */
13412MdNavBarController.prototype.onBlur = function() {
13413 var tab = this.getFocusedTab();
13414 if (tab) {
13415 tab.setFocused(false);
13416 }
13417};
13418
13419/**
13420 * Move focus from oldTab to newTab.
13421 * @param {!NavItemController} oldTab
13422 * @param {!NavItemController} newTab
13423 * @private
13424 */
13425MdNavBarController.prototype._moveFocus = function(oldTab, newTab) {
13426 oldTab.setFocused(false);
13427 newTab.setFocused(true);
13428};
13429
13430/**
13431 * Responds to keypress events.
13432 * @param {!Event} e
13433 */
13434MdNavBarController.prototype.onKeydown = function(e) {
13435 var keyCodes = this._$mdConstant.KEY_CODE;
13436 var tabs = this._getTabs();
13437 var focusedTab = this.getFocusedTab();
13438 if (!focusedTab) return;
13439
13440 var focusedTabIndex = tabs.indexOf(focusedTab);
13441
13442 // use arrow keys to navigate between tabs
13443 switch (e.keyCode) {
13444 case keyCodes.UP_ARROW:
13445 case keyCodes.LEFT_ARROW:
13446 if (focusedTabIndex > 0) {
13447 this._moveFocus(focusedTab, tabs[focusedTabIndex - 1]);
13448 }
13449 break;
13450 case keyCodes.DOWN_ARROW:
13451 case keyCodes.RIGHT_ARROW:
13452 if (focusedTabIndex < tabs.length - 1) {
13453 this._moveFocus(focusedTab, tabs[focusedTabIndex + 1]);
13454 }
13455 break;
13456 case keyCodes.SPACE:
13457 case keyCodes.ENTER:
13458 // timeout to avoid a "digest already in progress" console error
13459 this._$timeout(function() {
13460 focusedTab.getButtonEl().click();
13461 });
13462 break;
13463 }
13464};
13465
13466/**
13467 * @ngInject
13468 */
13469function MdNavItem($$rAF) {
13470 return {
13471 restrict: 'E',
13472 require: ['mdNavItem', '^mdNavBar'],
13473 controller: MdNavItemController,
13474 bindToController: true,
13475 controllerAs: 'ctrl',
13476 replace: true,
13477 transclude: true,
13478 template:
13479 '<li class="md-nav-item" role="option" aria-selected="{{ctrl.isSelected()}}">' +
13480 '<md-button ng-if="ctrl.mdNavSref" class="_md-nav-button md-accent"' +
13481 'ng-class="ctrl.getNgClassMap()"' +
13482 'tabindex="-1"' +
13483 'ui-sref="{{ctrl.mdNavSref}}">' +
13484 '<span ng-transclude class="_md-nav-button-text"></span>' +
13485 '</md-button>' +
13486 '<md-button ng-if="ctrl.mdNavHref" class="_md-nav-button md-accent"' +
13487 'ng-class="ctrl.getNgClassMap()"' +
13488 'tabindex="-1"' +
13489 'ng-href="{{ctrl.mdNavHref}}">' +
13490 '<span ng-transclude class="_md-nav-button-text"></span>' +
13491 '</md-button>' +
13492 '<md-button ng-if="ctrl.mdNavClick" class="_md-nav-button md-accent"' +
13493 'ng-class="ctrl.getNgClassMap()"' +
13494 'tabindex="-1"' +
13495 'ng-click="ctrl.mdNavClick()">' +
13496 '<span ng-transclude class="_md-nav-button-text"></span>' +
13497 '</md-button>' +
13498 '</li>',
13499 scope: {
13500 'mdNavClick': '&?',
13501 'mdNavHref': '@?',
13502 'mdNavSref': '@?',
13503 'name': '@',
13504 },
13505 link: function(scope, element, attrs, controllers) {
13506 var mdNavItem = controllers[0];
13507 var mdNavBar = controllers[1];
13508
13509 // When accessing the element's contents synchronously, they
13510 // may not be defined yet because of transclusion. There is a higher chance
13511 // that it will be accessible if we wait one frame.
13512 $$rAF(function() {
13513 if (!mdNavItem.name) {
13514 mdNavItem.name = angular.element(element[0].querySelector('._md-nav-button-text'))
13515 .text().trim();
13516 }
13517
13518 var navButton = angular.element(element[0].querySelector('._md-nav-button'));
13519 navButton.on('click', function() {
13520 mdNavBar.mdSelectedNavItem = mdNavItem.name;
13521 scope.$apply();
13522 });
13523 });
13524 }
13525 };
13526}
13527
13528/**
13529 * Controller for the nav-item component.
13530 * @param {!angular.JQLite} $element
13531 * @constructor
13532 * @final
13533 * @ngInject
13534 */
13535function MdNavItemController($element) {
13536
13537 /** @private @const {!angular.JQLite} */
13538 this._$element = $element;
13539
13540 // Data-bound variables
13541 /** @const {?Function} */
13542 this.mdNavClick;
13543 /** @const {?string} */
13544 this.mdNavHref;
13545 /** @const {?string} */
13546 this.name;
13547
13548 // State variables
13549 /** @private {boolean} */
13550 this._selected = false;
13551
13552 /** @private {boolean} */
13553 this._focused = false;
13554
13555 var hasNavClick = !!($element.attr('md-nav-click'));
13556 var hasNavHref = !!($element.attr('md-nav-href'));
13557 var hasNavSref = !!($element.attr('md-nav-sref'));
13558
13559 // Cannot specify more than one nav attribute
13560 if ((hasNavClick ? 1:0) + (hasNavHref ? 1:0) + (hasNavSref ? 1:0) > 1) {
13561 throw Error(
13562 'Must specify exactly one of md-nav-click, md-nav-href, ' +
13563 'md-nav-sref for nav-item directive');
13564 }
13565}
13566
13567/**
13568 * Returns a map of class names and values for use by ng-class.
13569 * @return {!Object<string,boolean>}
13570 */
13571MdNavItemController.prototype.getNgClassMap = function() {
13572 return {
13573 'md-active': this._selected,
13574 'md-primary': this._selected,
13575 'md-unselected': !this._selected,
13576 'md-focused': this._focused,
13577 };
13578};
13579
13580/**
13581 * Get the name attribute of the tab.
13582 * @return {string}
13583 */
13584MdNavItemController.prototype.getName = function() {
13585 return this.name;
13586};
13587
13588/**
13589 * Get the button element associated with the tab.
13590 * @return {!Element}
13591 */
13592MdNavItemController.prototype.getButtonEl = function() {
13593 return this._$element[0].querySelector('._md-nav-button');
13594};
13595
13596/**
13597 * Set the selected state of the tab.
13598 * @param {boolean} isSelected
13599 */
13600MdNavItemController.prototype.setSelected = function(isSelected) {
13601 this._selected = isSelected;
13602};
13603
13604/**
13605 * @return {boolean}
13606 */
13607MdNavItemController.prototype.isSelected = function() {
13608 return this._selected;
13609};
13610
13611/**
13612 * Set the focused state of the tab.
13613 * @param {boolean} isFocused
13614 */
13615MdNavItemController.prototype.setFocused = function(isFocused) {
13616 this._focused = isFocused;
13617};
13618
13619/**
13620 * @return {boolean}
13621 */
13622MdNavItemController.prototype.hasFocus = function() {
13623 return this._focused;
13624};
13625
13626})();
13627(function(){
13628"use strict";
13629
13630/**
13631 * @ngdoc module
13632 * @name material.components.panel
13633 */
13634MdPanelService.$inject = ["$rootElement", "$rootScope", "$injector", "$window"];
13635angular
13636 .module('material.components.panel', [
13637 'material.core',
13638 'material.components.backdrop'
13639 ])
13640 .service('$mdPanel', MdPanelService);
13641
13642
13643/*****************************************************************************
13644 * PUBLIC DOCUMENTATION *
13645 *****************************************************************************/
13646
13647/**
13648 * @ngdoc service
13649 * @name $mdPanel
13650 * @module material.components.panel
13651 *
13652 * @description
13653 * `$mdPanel` is a robust, low-level service for creating floating panels on
13654 * the screen. It can be used to implement tooltips, dialogs, pop-ups, etc.
13655 *
13656 * @usage
13657 * <hljs lang="js">
13658 * (function(angular, undefined) {
13659 * ‘use strict’;
13660 *
13661 * angular
13662 * .module('demoApp', ['ngMaterial'])
13663 * .controller('DemoDialogController', DialogController);
13664 *
13665 * var panelRef;
13666 *
13667 * function showPanel($event) {
13668 * var panelPosition = $mdPanel.newPanelPosition()
13669 * .absolute()
13670 * .top('50%')
13671 * .left('50%');
13672 *
13673 * var panelAnimation = $mdPanel.newPanelAnimation()
13674 * .targetEvent($event)
13675 * .defaultAnimation('md-panel-animate-fly')
13676 * .closeTo('.show-button');
13677 *
13678 * var config = {
13679 * attachTo: angular.element(document.body),
13680 * controller: DialogController,
13681 * controllerAs: 'ctrl',
13682 * position: panelPosition,
13683 * animation: panelAnimation,
13684 * targetEvent: $event,
13685 * templateUrl: 'dialog-template.html',
13686 * clickOutsideToClose: true,
13687 * escapeToClose: true,
13688 * focusOnOpen: true
13689 * }
13690 *
13691 * $mdPanel.open(config)
13692 * .then(function(result) {
13693 * panelRef = result;
13694 * });
13695 * }
13696 *
13697 * function DialogController(MdPanelRef, toppings) {
13698 * var toppings;
13699 *
13700 * function closeDialog() {
13701 * MdPanelRef && MdPanelRef.close();
13702 * }
13703 * }
13704 * })(angular);
13705 * </hljs>
13706 */
13707
13708/**
13709 * @ngdoc method
13710 * @name $mdPanel#create
13711 * @description
13712 * Creates a panel with the specified options.
13713 *
13714 * @param config {!Object=} Specific configuration object that may contain the
13715 * following properties:
13716 *
13717 * - `id` - `{string=}`: An ID to track the panel by. When an ID is provided,
13718 * the created panel is added to a tracked panels object. Any subsequent
13719 * requests made to create a panel with that ID are ignored. This is useful
13720 * in having the panel service not open multiple panels from the same user
13721 * interaction when there is no backdrop and events are propagated. Defaults
13722 * to an arbitrary string that is not tracked.
13723 * - `template` - `{string=}`: HTML template to show in the panel. This
13724 * **must** be trusted HTML with respect to Angular’s
13725 * [$sce service](https://docs.angularjs.org/api/ng/service/$sce).
13726 * - `templateUrl` - `{string=}`: The URL that will be used as the content of
13727 * the panel.
13728 * - `controller` - `{(function|string)=}`: The controller to associate with
13729 * the panel. The controller can inject a reference to the returned
13730 * panelRef, which allows the panel to be closed, hidden, and shown. Any
13731 * fields passed in through locals or resolve will be bound to the
13732 * controller.
13733 * - `controllerAs` - `{string=}`: An alias to assign the controller to on
13734 * the scope.
13735 * - `bindToController` - `{boolean=}`: Binds locals to the controller
13736 * instead of passing them in. Defaults to true, as this is a best
13737 * practice.
13738 * - `locals` - `{Object=}`: An object containing key/value pairs. The keys
13739 * will be used as names of values to inject into the controller. For
13740 * example, `locals: {three: 3}` would inject `three` into the controller,
13741 * with the value 3.
13742 * - `resolve` - `{Object=}`: Similar to locals, except it takes promises as
13743 * values. The panel will not open until all of the promises resolve.
13744 * - `attachTo` - `{(string|!angular.JQLite|!Element)=}`: The element to
13745 * attach the panel to. Defaults to appending to the root element of the
13746 * application.
13747 * - `propagateContainerEvents` - `{boolean=}`: Whether pointer or touch
13748 * events should be allowed to propagate 'go through' the container, aka the
13749 * wrapper, of the panel. Defaults to false.
13750 * - `panelClass` - `{string=}`: A css class to apply to the panel element.
13751 * This class should define any borders, box-shadow, etc. for the panel.
13752 * - `zIndex` - `{number=}`: The z-index to place the panel at.
13753 * Defaults to 80.
13754 * - `position` - `{MdPanelPosition=}`: An MdPanelPosition object that
13755 * specifies the alignment of the panel. For more information, see
13756 * `MdPanelPosition`.
13757 * - `clickOutsideToClose` - `{boolean=}`: Whether the user can click
13758 * outside the panel to close it. Defaults to false.
13759 * - `escapeToClose` - `{boolean=}`: Whether the user can press escape to
13760 * close the panel. Defaults to false.
13761 * - `trapFocus` - `{boolean=}`: Whether focus should be trapped within the
13762 * panel. If `trapFocus` is true, the user will not be able to interact
13763 * with the rest of the page until the panel is dismissed. Defaults to
13764 * false.
13765 * - `focusOnOpen` - `{boolean=}`: An option to override focus behavior on
13766 * open. Only disable if focusing some other way, as focus management is
13767 * required for panels to be accessible. Defaults to true.
13768 * - `fullscreen` - `{boolean=}`: Whether the panel should be full screen.
13769 * Applies the class `._md-panel-fullscreen` to the panel on open. Defaults
13770 * to false.
13771 * - `animation` - `{MdPanelAnimation=}`: An MdPanelAnimation object that
13772 * specifies the animation of the panel. For more information, see
13773 * `MdPanelAnimation`.
13774 * - `hasBackdrop` - `{boolean=}`: Whether there should be an opaque backdrop
13775 * behind the panel. Defaults to false.
13776 * - `disableParentScroll` - `{boolean=}`: Whether the user can scroll the
13777 * page behind the panel. Defaults to false.
13778 * - `onDomAdded` - `{function=}`: Callback function used to announce when
13779 * the panel is added to the DOM.
13780 * - `onOpenComplete` - `{function=}`: Callback function used to announce
13781 * when the open() action is finished.
13782 * - `onRemoving` - `{function=}`: Callback function used to announce the
13783 * close/hide() action is starting.
13784 * - `onDomRemoved` - `{function=}`: Callback function used to announce when
13785 * the panel is removed from the DOM.
13786 * - `origin` - `{(string|!angular.JQLite|!Element)=}`: The element to focus
13787 * on when the panel closes. This is commonly the element which triggered
13788 * the opening of the panel. If you do not use `origin`, you need to control
13789 * the focus manually.
13790 *
13791 * @returns {!MdPanelRef} panelRef
13792 */
13793
13794
13795/**
13796 * @ngdoc method
13797 * @name $mdPanel#open
13798 * @description
13799 * Calls the create method above, then opens the panel. This is a shortcut for
13800 * creating and then calling open manually. If custom methods need to be
13801 * called when the panel is added to the DOM or opened, do not use this method.
13802 * Instead create the panel, chain promises on the domAdded and openComplete
13803 * methods, and call open from the returned panelRef.
13804 *
13805 * @param {!Object=} config Specific configuration object that may contain
13806 * the properties defined in `$mdPanel.create`.
13807 * @returns {!angular.$q.Promise<!MdPanelRef>} panelRef A promise that resolves
13808 * to an instance of the panel.
13809 */
13810
13811
13812/**
13813 * @ngdoc method
13814 * @name $mdPanel#newPanelPosition
13815 * @description
13816 * Returns a new instance of the MdPanelPosition object. Use this to create
13817 * the position config object.
13818 *
13819 * @returns {!MdPanelPosition} panelPosition
13820 */
13821
13822
13823/**
13824 * @ngdoc method
13825 * @name $mdPanel#newPanelAnimation
13826 * @description
13827 * Returns a new instance of the MdPanelAnimation object. Use this to create
13828 * the animation config object.
13829 *
13830 * @returns {!MdPanelAnimation} panelAnimation
13831 */
13832
13833
13834/*****************************************************************************
13835 * MdPanelRef *
13836 *****************************************************************************/
13837
13838
13839/**
13840 * @ngdoc type
13841 * @name MdPanelRef
13842 * @module material.components.panel
13843 * @description
13844 * A reference to a created panel. This reference contains a unique id for the
13845 * panel, along with the following properties:
13846 *
13847 * - `id` - `{string}`: The unique id for the panel. This id is used to track
13848 * when a panel was interacted with.
13849 * - `config` - `{!Object=}`: The entire config object that was used in
13850 * create.
13851 * - `isAttached` - `{boolean}`: Whether the panel is attached to the DOM.
13852 * Visibility to the user does not factor into isAttached.
13853 * - `panelContainer` - `{angular.JQLite}`: The wrapper element containing the
13854 * panel. This property is added in order to have access to the `addClass`,
13855 * `removeClass`, `toggleClass`, etc methods.
13856 * - `panelEl` - `{angular.JQLite}`: The panel element. This property is added
13857 * in order to have access to the `addClass`, `removeClass`, `toggleClass`,
13858 * etc methods.
13859 */
13860
13861/**
13862 * @ngdoc method
13863 * @name MdPanelRef#open
13864 * @description
13865 * Attaches and shows the panel.
13866 *
13867 * @returns {!angular.$q.Promise} A promise that is resolved when the panel is
13868 * opened.
13869 */
13870
13871/**
13872 * @ngdoc method
13873 * @name MdPanelRef#close
13874 * @description
13875 * Hides and detaches the panel. Note that this will **not** destroy the panel.
13876 * If you don't intend on using the panel again, call the {@link #destroy
13877 * destroy} method afterwards.
13878 *
13879 * @returns {!angular.$q.Promise} A promise that is resolved when the panel is
13880 * closed.
13881 */
13882
13883/**
13884 * @ngdoc method
13885 * @name MdPanelRef#attach
13886 * @description
13887 * Create the panel elements and attach them to the DOM. The panel will be
13888 * hidden by default.
13889 *
13890 * @returns {!angular.$q.Promise} A promise that is resolved when the panel is
13891 * attached.
13892 */
13893
13894/**
13895 * @ngdoc method
13896 * @name MdPanelRef#detach
13897 * @description
13898 * Removes the panel from the DOM. This will NOT hide the panel before removing
13899 * it.
13900 *
13901 * @returns {!angular.$q.Promise} A promise that is resolved when the panel is
13902 * detached.
13903 */
13904
13905/**
13906 * @ngdoc method
13907 * @name MdPanelRef#show
13908 * @description
13909 * Shows the panel.
13910 *
13911 * @returns {!angular.$q.Promise} A promise that is resolved when the panel has
13912 * shown and animations are completed.
13913 */
13914
13915/**
13916 * @ngdoc method
13917 * @name MdPanelRef#hide
13918 * @description
13919 * Hides the panel.
13920 *
13921 * @returns {!angular.$q.Promise} A promise that is resolved when the panel has
13922 * hidden and animations are completed.
13923 */
13924
13925/**
13926 * @ngdoc method
13927 * @name MdPanelRef#destroy
13928 * @description
13929 * Destroys the panel. The panel cannot be opened again after this is called.
13930 */
13931
13932/**
13933 * @ngdoc method
13934 * @name MdPanelRef#addClass
13935 * @deprecated
13936 * This method is in the process of being deprecated in favor of using the panel
13937 * and container JQLite elements that are referenced in the MdPanelRef object.
13938 * Full deprecation is scheduled for material 1.2.
13939 * @description
13940 * Adds a class to the panel. DO NOT use this hide/show the panel.
13941 *
13942 * @param {string} newClass class to be added.
13943 * @param {boolean} toElement Whether or not to add the class to the panel
13944 * element instead of the container.
13945 */
13946
13947/**
13948 * @ngdoc method
13949 * @name MdPanelRef#removeClass
13950 * @deprecated
13951 * This method is in the process of being deprecated in favor of using the panel
13952 * and container JQLite elements that are referenced in the MdPanelRef object.
13953 * Full deprecation is scheduled for material 1.2.
13954 * @description
13955 * Removes a class from the panel. DO NOT use this to hide/show the panel.
13956 *
13957 * @param {string} oldClass Class to be removed.
13958 * @param {boolean} fromElement Whether or not to remove the class from the
13959 * panel element instead of the container.
13960 */
13961
13962/**
13963 * @ngdoc method
13964 * @name MdPanelRef#toggleClass
13965 * @deprecated
13966 * This method is in the process of being deprecated in favor of using the panel
13967 * and container JQLite elements that are referenced in the MdPanelRef object.
13968 * Full deprecation is scheduled for material 1.2.
13969 * @description
13970 * Toggles a class on the panel. DO NOT use this to hide/show the panel.
13971 *
13972 * @param {string} toggleClass Class to be toggled.
13973 * @param {boolean} onElement Whether or not to remove the class from the panel
13974 * element instead of the container.
13975 */
13976
13977/**
13978 * @ngdoc method
13979 * @name MdPanelRef#updatePosition
13980 * @description
13981 * Updates the position configuration of a panel. Use this to update the
13982 * position of a panel that is open, without having to close and re-open the
13983 * panel.
13984 *
13985 * @param {!MdPanelPosition} position
13986 */
13987
13988
13989/*****************************************************************************
13990 * MdPanelPosition *
13991 *****************************************************************************/
13992
13993
13994/**
13995 * @ngdoc type
13996 * @name MdPanelPosition
13997 * @module material.components.panel
13998 * @description
13999 *
14000 * Object for configuring the position of the panel.
14001 *
14002 * @usage
14003 *
14004 * #### Centering the panel
14005 *
14006 * <hljs lang="js">
14007 * new MdPanelPosition().absolute().center();
14008 * </hljs>
14009 *
14010 * #### Overlapping the panel with an element
14011 *
14012 * <hljs lang="js">
14013 * new MdPanelPosition()
14014 * .relativeTo(someElement)
14015 * .addPanelPosition(
14016 * $mdPanel.xPosition.ALIGN_START,
14017 * $mdPanel.yPosition.ALIGN_TOPS
14018 * );
14019 * </hljs>
14020 *
14021 * #### Aligning the panel with the bottom of an element
14022 *
14023 * <hljs lang="js">
14024 * new MdPanelPosition()
14025 * .relativeTo(someElement)
14026 * .addPanelPosition($mdPanel.xPosition.CENTER, $mdPanel.yPosition.BELOW);
14027 * </hljs>
14028 */
14029
14030/**
14031 * @ngdoc method
14032 * @name MdPanelPosition#absolute
14033 * @description
14034 * Positions the panel absolutely relative to the parent element. If the parent
14035 * is document.body, this is equivalent to positioning the panel absolutely
14036 * within the viewport.
14037 *
14038 * @returns {!MdPanelPosition}
14039 */
14040
14041/**
14042 * @ngdoc method
14043 * @name MdPanelPosition#relativeTo
14044 * @description
14045 * Positions the panel relative to a specific element.
14046 *
14047 * @param {string|!Element|!angular.JQLite} element Query selector, DOM element,
14048 * or angular element to position the panel with respect to.
14049 * @returns {!MdPanelPosition}
14050 */
14051
14052/**
14053 * @ngdoc method
14054 * @name MdPanelPosition#top
14055 * @description
14056 * Sets the value of `top` for the panel. Clears any previously set vertical
14057 * position.
14058 *
14059 * @param {string=} top Value of `top`. Defaults to '0'.
14060 * @returns {!MdPanelPosition}
14061 */
14062
14063/**
14064 * @ngdoc method
14065 * @name MdPanelPosition#bottom
14066 * @description
14067 * Sets the value of `bottom` for the panel. Clears any previously set vertical
14068 * position.
14069 *
14070 * @param {string=} bottom Value of `bottom`. Defaults to '0'.
14071 * @returns {!MdPanelPosition}
14072 */
14073
14074/**
14075 * @ngdoc method
14076 * @name MdPanelPosition#start
14077 * @description
14078 * Sets the panel to the start of the page - `left` if `ltr` or `right` for
14079 * `rtl`. Clears any previously set horizontal position.
14080 *
14081 * @param {string=} start Value of position. Defaults to '0'.
14082 * @returns {!MdPanelPosition}
14083 */
14084
14085/**
14086 * @ngdoc method
14087 * @name MdPanelPosition#end
14088 * @description
14089 * Sets the panel to the end of the page - `right` if `ltr` or `left` for `rtl`.
14090 * Clears any previously set horizontal position.
14091 *
14092 * @param {string=} end Value of position. Defaults to '0'.
14093 * @returns {!MdPanelPosition}
14094 */
14095
14096/**
14097 * @ngdoc method
14098 * @name MdPanelPosition#left
14099 * @description
14100 * Sets the value of `left` for the panel. Clears any previously set
14101 * horizontal position.
14102 *
14103 * @param {string=} left Value of `left`. Defaults to '0'.
14104 * @returns {!MdPanelPosition}
14105 */
14106
14107/**
14108 * @ngdoc method
14109 * @name MdPanelPosition#right
14110 * @description
14111 * Sets the value of `right` for the panel. Clears any previously set
14112 * horizontal position.
14113 *
14114 * @param {string=} right Value of `right`. Defaults to '0'.
14115 * @returns {!MdPanelPosition}
14116 */
14117
14118/**
14119 * @ngdoc method
14120 * @name MdPanelPosition#centerHorizontally
14121 * @description
14122 * Centers the panel horizontally in the viewport. Clears any previously set
14123 * horizontal position.
14124 *
14125 * @returns {!MdPanelPosition}
14126 */
14127
14128/**
14129 * @ngdoc method
14130 * @name MdPanelPosition#centerVertically
14131 * @description
14132 * Centers the panel vertically in the viewport. Clears any previously set
14133 * vertical position.
14134 *
14135 * @returns {!MdPanelPosition}
14136 */
14137
14138/**
14139 * @ngdoc method
14140 * @name MdPanelPosition#center
14141 * @description
14142 * Centers the panel horizontally and vertically in the viewport. This is
14143 * equivalent to calling both `centerHorizontally` and `centerVertically`.
14144 * Clears any previously set horizontal and vertical positions.
14145 *
14146 * @returns {!MdPanelPosition}
14147 */
14148
14149/**
14150 * @ngdoc method
14151 * @name MdPanelPosition#addPanelPosition
14152 * @description
14153 * Sets the x and y position for the panel relative to another element. Can be
14154 * called multiple times to specify an ordered list of panel positions. The
14155 * first position which allows the panel to be completely on-screen will be
14156 * chosen; the last position will be chose whether it is on-screen or not.
14157 *
14158 * xPosition must be one of the following values available on
14159 * $mdPanel.xPosition:
14160 *
14161 * CENTER | ALIGN_START | ALIGN_END | OFFSET_START | OFFSET_END
14162 *
14163 * *************
14164 * * *
14165 * * PANEL *
14166 * * *
14167 * *************
14168 * A B C D E
14169 *
14170 * A: OFFSET_START (for LTR displays)
14171 * B: ALIGN_START (for LTR displays)
14172 * C: CENTER
14173 * D: ALIGN_END (for LTR displays)
14174 * E: OFFSET_END (for LTR displays)
14175 *
14176 * yPosition must be one of the following values available on
14177 * $mdPanel.yPosition:
14178 *
14179 * CENTER | ALIGN_TOPS | ALIGN_BOTTOMS | ABOVE | BELOW
14180 *
14181 * F
14182 * G *************
14183 * * *
14184 * H * PANEL *
14185 * * *
14186 * I *************
14187 * J
14188 *
14189 * F: BELOW
14190 * G: ALIGN_TOPS
14191 * H: CENTER
14192 * I: ALIGN_BOTTOMS
14193 * J: ABOVE
14194 *
14195 * @param {string} xPosition
14196 * @param {string} yPosition
14197 * @returns {!MdPanelPosition}
14198 */
14199
14200/**
14201 * @ngdoc method
14202 * @name MdPanelPosition#withOffsetX
14203 * @description
14204 * Sets the value of the offset in the x-direction.
14205 *
14206 * @param {string} offsetX
14207 * @returns {!MdPanelPosition}
14208 */
14209
14210/**
14211 * @ngdoc method
14212 * @name MdPanelPosition#withOffsetY
14213 * @description
14214 * Sets the value of the offset in the y-direction.
14215 *
14216 * @param {string} offsetY
14217 * @returns {!MdPanelPosition}
14218 */
14219
14220
14221/*****************************************************************************
14222 * MdPanelAnimation *
14223 *****************************************************************************/
14224
14225
14226/**
14227 * @ngdoc object
14228 * @name MdPanelAnimation
14229 * @description
14230 * Animation configuration object. To use, create an MdPanelAnimation with the
14231 * desired properties, then pass the object as part of $mdPanel creation.
14232 *
14233 * Example:
14234 *
14235 * var panelAnimation = new MdPanelAnimation()
14236 * .openFrom(myButtonEl)
14237 * .closeTo('.my-button')
14238 * .withAnimation($mdPanel.animation.SCALE);
14239 *
14240 * $mdPanel.create({
14241 * animation: panelAnimation
14242 * });
14243 */
14244
14245/**
14246 * @ngdoc method
14247 * @name MdPanelAnimation#openFrom
14248 * @description
14249 * Specifies where to start the open animation. `openFrom` accepts a
14250 * click event object, query selector, DOM element, or a Rect object that
14251 * is used to determine the bounds. When passed a click event, the location
14252 * of the click will be used as the position to start the animation.
14253 *
14254 * @param {string|!Element|!Event|{top: number, left: number}}
14255 * @returns {!MdPanelAnimation}
14256 */
14257
14258/**
14259 * @ngdoc method
14260 * @name MdPanelAnimation#closeTo
14261 * @description
14262 * Specifies where to animate the panel close. `closeTo` accepts a
14263 * query selector, DOM element, or a Rect object that is used to determine
14264 * the bounds.
14265 *
14266 * @param {string|!Element|{top: number, left: number}}
14267 * @returns {!MdPanelAnimation}
14268 */
14269
14270/**
14271 * @ngdoc method
14272 * @name MdPanelAnimation#withAnimation
14273 * @description
14274 * Specifies the animation class.
14275 *
14276 * There are several default animations that can be used:
14277 * ($mdPanel.animation)
14278 * SLIDE: The panel slides in and out from the specified
14279 * elements. It will not fade in or out.
14280 * SCALE: The panel scales in and out. Slide and fade are
14281 * included in this animation.
14282 * FADE: The panel fades in and out.
14283 *
14284 * Custom classes will by default fade in and out unless
14285 * "transition: opacity 1ms" is added to the to custom class.
14286 *
14287 * @param {string|{open: string, close: string}} cssClass
14288 * @returns {!MdPanelAnimation}
14289 */
14290
14291
14292/*****************************************************************************
14293 * IMPLEMENTATION *
14294 *****************************************************************************/
14295
14296
14297// Default z-index for the panel.
14298var defaultZIndex = 80;
14299var MD_PANEL_HIDDEN = '_md-panel-hidden';
14300
14301var FOCUS_TRAP_TEMPLATE = angular.element(
14302 '<div class="_md-panel-focus-trap" tabindex="0"></div>');
14303
14304
14305/**
14306 * A service that is used for controlling/displaying panels on the screen.
14307 * @param {!angular.JQLite} $rootElement
14308 * @param {!angular.Scope} $rootScope
14309 * @param {!angular.$injector} $injector
14310 * @param {!angular.$window} $window
14311 * @final @constructor @ngInject
14312 */
14313function MdPanelService($rootElement, $rootScope, $injector, $window) {
14314 /**
14315 * Default config options for the panel.
14316 * Anything angular related needs to be done later. Therefore
14317 * scope: $rootScope.$new(true),
14318 * attachTo: $rootElement,
14319 * are added later.
14320 * @private {!Object}
14321 */
14322 this._defaultConfigOptions = {
14323 bindToController: true,
14324 clickOutsideToClose: false,
14325 disableParentScroll: false,
14326 escapeToClose: false,
14327 focusOnOpen: true,
14328 fullscreen: false,
14329 hasBackdrop: false,
14330 propagateContainerEvents: false,
14331 transformTemplate: angular.bind(this, this._wrapTemplate),
14332 trapFocus: false,
14333 zIndex: defaultZIndex
14334 };
14335
14336 /** @private {!Object} */
14337 this._config = {};
14338
14339 /** @private @const */
14340 this._$rootElement = $rootElement;
14341
14342 /** @private @const */
14343 this._$rootScope = $rootScope;
14344
14345 /** @private @const */
14346 this._$injector = $injector;
14347
14348 /** @private @const */
14349 this._$window = $window;
14350
14351 /** @private {!Object<string, !MdPanelRef>} */
14352 this._trackedPanels = {};
14353
14354 /**
14355 * Default animations that can be used within the panel.
14356 * @type {enum}
14357 */
14358 this.animation = MdPanelAnimation.animation;
14359
14360 /**
14361 * Possible values of xPosition for positioning the panel relative to
14362 * another element.
14363 * @type {enum}
14364 */
14365 this.xPosition = MdPanelPosition.xPosition;
14366
14367 /**
14368 * Possible values of yPosition for positioning the panel relative to
14369 * another element.
14370 * @type {enum}
14371 */
14372 this.yPosition = MdPanelPosition.yPosition;
14373}
14374
14375
14376/**
14377 * Creates a panel with the specified options.
14378 * @param {!Object=} config Configuration object for the panel.
14379 * @returns {!MdPanelRef}
14380 */
14381MdPanelService.prototype.create = function(config) {
14382 config = config || {};
14383
14384 // If the passed-in config contains an ID and the ID is within _trackedPanels,
14385 // return the tracked panel.
14386 if (angular.isDefined(config.id) && this._trackedPanels[config.id]) {
14387 return this._trackedPanels[config.id];
14388 }
14389
14390 // If no ID is set within the passed-in config, then create an arbitrary ID.
14391 this._config = {
14392 id: config.id || 'panel_' + this._$injector.get('$mdUtil').nextUid(),
14393 scope: this._$rootScope.$new(true),
14394 attachTo: this._$rootElement
14395 };
14396 angular.extend(this._config, this._defaultConfigOptions, config);
14397
14398 var panelRef = new MdPanelRef(this._config, this._$injector);
14399 this._trackedPanels[config.id] = panelRef;
14400
14401 return panelRef;
14402};
14403
14404
14405/**
14406 * Creates and opens a panel with the specified options.
14407 * @param {!Object=} config Configuration object for the panel.
14408 * @returns {!angular.$q.Promise<!MdPanelRef>} The panel created from create.
14409 */
14410MdPanelService.prototype.open = function(config) {
14411 var panelRef = this.create(config);
14412 return panelRef.open().then(function() {
14413 return panelRef;
14414 });
14415};
14416
14417
14418/**
14419 * Returns a new instance of the MdPanelPosition. Use this to create the
14420 * positioning object.
14421 * @returns {!MdPanelPosition}
14422 */
14423MdPanelService.prototype.newPanelPosition = function() {
14424 return new MdPanelPosition(this._$injector);
14425};
14426
14427
14428/**
14429 * Returns a new instance of the MdPanelAnimation. Use this to create the
14430 * animation object.
14431 * @returns {!MdPanelAnimation}
14432 */
14433MdPanelService.prototype.newPanelAnimation = function() {
14434 return new MdPanelAnimation(this._$injector);
14435};
14436
14437
14438/**
14439 * Wraps the users template in two elements, md-panel-outer-wrapper, which
14440 * covers the entire attachTo element, and md-panel, which contains only the
14441 * template. This allows the panel control over positioning, animations,
14442 * and similar properties.
14443 * @param {string} origTemplate The original template.
14444 * @returns {string} The wrapped template.
14445 * @private
14446 */
14447MdPanelService.prototype._wrapTemplate = function(origTemplate) {
14448 var template = origTemplate || '';
14449
14450 // The panel should be initially rendered offscreen so we can calculate
14451 // height and width for positioning.
14452 return '' +
14453 '<div class="md-panel-outer-wrapper">' +
14454 ' <div class="md-panel" style="left: -9999px;">' + template + '</div>' +
14455 '</div>';
14456};
14457
14458
14459/*****************************************************************************
14460 * MdPanelRef *
14461 *****************************************************************************/
14462
14463
14464/**
14465 * A reference to a created panel. This reference contains a unique id for the
14466 * panel, along with properties/functions used to control the panel.
14467 * @param {!Object} config
14468 * @param {!angular.$injector} $injector
14469 * @final @constructor
14470 */
14471function MdPanelRef(config, $injector) {
14472 // Injected variables.
14473 /** @private @const {!angular.$q} */
14474 this._$q = $injector.get('$q');
14475
14476 /** @private @const {!angular.$mdCompiler} */
14477 this._$mdCompiler = $injector.get('$mdCompiler');
14478
14479 /** @private @const {!angular.$mdConstant} */
14480 this._$mdConstant = $injector.get('$mdConstant');
14481
14482 /** @private @const {!angular.$mdUtil} */
14483 this._$mdUtil = $injector.get('$mdUtil');
14484
14485 /** @private @const {!angular.Scope} */
14486 this._$rootScope = $injector.get('$rootScope');
14487
14488 /** @private @const {!angular.$animate} */
14489 this._$animate = $injector.get('$animate');
14490
14491 /** @private @const {!MdPanelRef} */
14492 this._$mdPanel = $injector.get('$mdPanel');
14493
14494 /** @private @const {!angular.$log} */
14495 this._$log = $injector.get('$log');
14496
14497 /** @private @const {!angular.$window} */
14498 this._$window = $injector.get('$window');
14499
14500 /** @private @const {!Function} */
14501 this._$$rAF = $injector.get('$$rAF');
14502
14503 // Public variables.
14504 /**
14505 * Unique id for the panelRef.
14506 * @type {string}
14507 */
14508 this.id = config.id;
14509
14510 /** @type {!Object} */
14511 this.config = config;
14512
14513 /** @type {!angular.JQLite|undefined} */
14514 this.panelContainer;
14515
14516 /** @type {!angular.JQLite|undefined} */
14517 this.panelEl;
14518
14519 /**
14520 * Whether the panel is attached. This is synchronous. When attach is called,
14521 * isAttached is set to true. When detach is called, isAttached is set to
14522 * false.
14523 * @type {boolean}
14524 */
14525 this.isAttached = false;
14526
14527 // Private variables.
14528 /** @private {Array<function()>} */
14529 this._removeListeners = [];
14530
14531 /** @private {!angular.JQLite|undefined} */
14532 this._topFocusTrap;
14533
14534 /** @private {!angular.JQLite|undefined} */
14535 this._bottomFocusTrap;
14536
14537 /** @private {!$mdPanel|undefined} */
14538 this._backdropRef;
14539
14540 /** @private {Function?} */
14541 this._restoreScroll = null;
14542}
14543
14544
14545/**
14546 * Opens an already created and configured panel. If the panel is already
14547 * visible, does nothing.
14548 * @returns {!angular.$q.Promise<!MdPanelRef>} A promise that is resolved when
14549 * the panel is opened and animations finish.
14550 */
14551MdPanelRef.prototype.open = function() {
14552 var self = this;
14553 return this._$q(function(resolve, reject) {
14554 var done = self._done(resolve, self);
14555 var show = self._simpleBind(self.show, self);
14556
14557 self.attach()
14558 .then(show)
14559 .then(done)
14560 .catch(reject);
14561 });
14562};
14563
14564
14565/**
14566 * Closes the panel.
14567 * @returns {!angular.$q.Promise<!MdPanelRef>} A promise that is resolved when
14568 * the panel is closed and animations finish.
14569 */
14570MdPanelRef.prototype.close = function() {
14571 var self = this;
14572
14573 return this._$q(function(resolve, reject) {
14574 var done = self._done(resolve, self);
14575 var detach = self._simpleBind(self.detach, self);
14576
14577 self.hide()
14578 .then(detach)
14579 .then(done)
14580 .catch(reject);
14581 });
14582};
14583
14584
14585/**
14586 * Attaches the panel. The panel will be hidden afterwards.
14587 * @returns {!angular.$q.Promise<!MdPanelRef>} A promise that is resolved when
14588 * the panel is attached.
14589 */
14590MdPanelRef.prototype.attach = function() {
14591 if (this.isAttached && this.panelEl) {
14592 return this._$q.when(this);
14593 }
14594
14595 var self = this;
14596 return this._$q(function(resolve, reject) {
14597 var done = self._done(resolve, self);
14598 var onDomAdded = self.config['onDomAdded'] || angular.noop;
14599 var addListeners = function(response) {
14600 self.isAttached = true;
14601 self._addEventListeners();
14602 return response;
14603 };
14604
14605 self._$q.all([
14606 self._createBackdrop(),
14607 self._createPanel()
14608 .then(addListeners)
14609 .catch(reject)
14610 ]).then(onDomAdded)
14611 .then(done)
14612 .catch(reject);
14613 });
14614};
14615
14616
14617/**
14618 * Only detaches the panel. Will NOT hide the panel first.
14619 * @returns {!angular.$q.Promise<!MdPanelRef>} A promise that is resolved when
14620 * the panel is detached.
14621 */
14622MdPanelRef.prototype.detach = function() {
14623 if (!this.isAttached) {
14624 return this._$q.when(this);
14625 }
14626
14627 var self = this;
14628 var onDomRemoved = self.config['onDomRemoved'] || angular.noop;
14629
14630 var detachFn = function() {
14631 self._removeEventListeners();
14632
14633 // Remove the focus traps that we added earlier for keeping focus within
14634 // the panel.
14635 if (self._topFocusTrap && self._topFocusTrap.parentNode) {
14636 self._topFocusTrap.parentNode.removeChild(self._topFocusTrap);
14637 }
14638
14639 if (self._bottomFocusTrap && self._bottomFocusTrap.parentNode) {
14640 self._bottomFocusTrap.parentNode.removeChild(self._bottomFocusTrap);
14641 }
14642
14643 self.panelContainer.remove();
14644 self.isAttached = false;
14645 return self._$q.when(self);
14646 };
14647
14648 if (this._restoreScroll) {
14649 this._restoreScroll();
14650 this._restoreScroll = null;
14651 }
14652
14653 return this._$q(function(resolve, reject) {
14654 var done = self._done(resolve, self);
14655
14656 self._$q.all([
14657 detachFn(),
14658 self._backdropRef ? self._backdropRef.detach() : true
14659 ]).then(onDomRemoved)
14660 .then(done)
14661 .catch(reject);
14662 });
14663};
14664
14665
14666/**
14667 * Destroys the panel. The Panel cannot be opened again after this.
14668 */
14669MdPanelRef.prototype.destroy = function() {
14670 this.config.scope.$destroy();
14671 this.config.locals = null;
14672};
14673
14674
14675/**
14676 * Shows the panel.
14677 * @returns {!angular.$q.Promise<!MdPanelRef>} A promise that is resolved when
14678 * the panel has shown and animations finish.
14679 */
14680MdPanelRef.prototype.show = function() {
14681 if (!this.panelContainer) {
14682 return this._$q(function(resolve, reject) {
14683 reject('Panel does not exist yet. Call open() or attach().');
14684 });
14685 }
14686
14687 if (!this.panelContainer.hasClass(MD_PANEL_HIDDEN)) {
14688 return this._$q.when(this);
14689 }
14690
14691 var self = this;
14692 var animatePromise = function() {
14693 self.panelContainer.removeClass(MD_PANEL_HIDDEN);
14694 return self._animateOpen();
14695 };
14696
14697 return this._$q(function(resolve, reject) {
14698 var done = self._done(resolve, self);
14699 var onOpenComplete = self.config['onOpenComplete'] || angular.noop;
14700
14701 self._$q.all([
14702 self._backdropRef ? self._backdropRef.show() : self,
14703 animatePromise().then(function() { self._focusOnOpen(); }, reject)
14704 ]).then(onOpenComplete)
14705 .then(done)
14706 .catch(reject);
14707 });
14708};
14709
14710
14711/**
14712 * Hides the panel.
14713 * @returns {!angular.$q.Promise<!MdPanelRef>} A promise that is resolved when
14714 * the panel has hidden and animations finish.
14715 */
14716MdPanelRef.prototype.hide = function() {
14717 if (!this.panelContainer) {
14718 return this._$q(function(resolve, reject) {
14719 reject('Panel does not exist yet. Call open() or attach().');
14720 });
14721 }
14722
14723 if (this.panelContainer.hasClass(MD_PANEL_HIDDEN)) {
14724 return this._$q.when(this);
14725 }
14726
14727 var self = this;
14728
14729 return this._$q(function(resolve, reject) {
14730 var done = self._done(resolve, self);
14731 var onRemoving = self.config['onRemoving'] || angular.noop;
14732
14733 var focusOnOrigin = function() {
14734 var origin = self.config['origin'];
14735 if (origin) {
14736 getElement(origin).focus();
14737 }
14738 };
14739
14740 var hidePanel = function() {
14741 self.panelContainer.addClass(MD_PANEL_HIDDEN);
14742 };
14743
14744 self._$q.all([
14745 self._backdropRef ? self._backdropRef.hide() : self,
14746 self._animateClose()
14747 .then(onRemoving)
14748 .then(hidePanel)
14749 .then(focusOnOrigin)
14750 .catch(reject)
14751 ]).then(done, reject);
14752 });
14753};
14754
14755
14756/**
14757 * Add a class to the panel. DO NOT use this to hide/show the panel.
14758 * @deprecated
14759 * This method is in the process of being deprecated in favor of using the panel
14760 * and container JQLite elements that are referenced in the MdPanelRef object.
14761 * Full deprecation is scheduled for material 1.2.
14762 *
14763 * @param {string} newClass Class to be added.
14764 * @param {boolean} toElement Whether or not to add the class to the panel
14765 * element instead of the container.
14766 */
14767MdPanelRef.prototype.addClass = function(newClass, toElement) {
14768 this._$log.warn(
14769 'The addClass method is in the process of being deprecated. ' +
14770 'Full deprecation is scheduled for the Angular Material 1.2 release. ' +
14771 'To achieve the same results, use the panelContainer or panelEl ' +
14772 'JQLite elements that are referenced in MdPanelRef.');
14773
14774 if (!this.panelContainer) {
14775 throw new Error('Panel does not exist yet. Call open() or attach().');
14776 }
14777
14778 if (!toElement && !this.panelContainer.hasClass(newClass)) {
14779 this.panelContainer.addClass(newClass);
14780 } else if (toElement && !this.panelEl.hasClass(newClass)) {
14781 this.panelEl.addClass(newClass);
14782 }
14783};
14784
14785
14786/**
14787 * Remove a class from the panel. DO NOT use this to hide/show the panel.
14788 * @deprecated
14789 * This method is in the process of being deprecated in favor of using the panel
14790 * and container JQLite elements that are referenced in the MdPanelRef object.
14791 * Full deprecation is scheduled for material 1.2.
14792 *
14793 * @param {string} oldClass Class to be removed.
14794 * @param {boolean} fromElement Whether or not to remove the class from the
14795 * panel element instead of the container.
14796 */
14797MdPanelRef.prototype.removeClass = function(oldClass, fromElement) {
14798 this._$log.warn(
14799 'The removeClass method is in the process of being deprecated. ' +
14800 'Full deprecation is scheduled for the Angular Material 1.2 release. ' +
14801 'To achieve the same results, use the panelContainer or panelEl ' +
14802 'JQLite elements that are referenced in MdPanelRef.');
14803
14804 if (!this.panelContainer) {
14805 throw new Error('Panel does not exist yet. Call open() or attach().');
14806 }
14807
14808 if (!fromElement && this.panelContainer.hasClass(oldClass)) {
14809 this.panelContainer.removeClass(oldClass);
14810 } else if (fromElement && this.panelEl.hasClass(oldClass)) {
14811 this.panelEl.removeClass(oldClass);
14812 }
14813};
14814
14815
14816/**
14817 * Toggle a class on the panel. DO NOT use this to hide/show the panel.
14818 * @deprecated
14819 * This method is in the process of being deprecated in favor of using the panel
14820 * and container JQLite elements that are referenced in the MdPanelRef object.
14821 * Full deprecation is scheduled for material 1.2.
14822 *
14823 * @param {string} toggleClass The class to toggle.
14824 * @param {boolean} onElement Whether or not to toggle the class on the panel
14825 * element instead of the container.
14826 */
14827MdPanelRef.prototype.toggleClass = function(toggleClass, onElement) {
14828 this._$log.warn(
14829 'The toggleClass method is in the process of being deprecated. ' +
14830 'Full deprecation is scheduled for the Angular Material 1.2 release. ' +
14831 'To achieve the same results, use the panelContainer or panelEl ' +
14832 'JQLite elements that are referenced in MdPanelRef.');
14833
14834 if (!this.panelContainer) {
14835 throw new Error('Panel does not exist yet. Call open() or attach().');
14836 }
14837
14838 if (!onElement) {
14839 this.panelContainer.toggleClass(toggleClass);
14840 } else {
14841 this.panelEl.toggleClass(toggleClass);
14842 }
14843};
14844
14845
14846/**
14847 * Creates a panel and adds it to the dom.
14848 * @returns {!angular.$q.Promise} A promise that is resolved when the panel is
14849 * created.
14850 * @private
14851 */
14852MdPanelRef.prototype._createPanel = function() {
14853 var self = this;
14854
14855 return this._$q(function(resolve, reject) {
14856 if (!self.config.locals) {
14857 self.config.locals = {};
14858 }
14859
14860 self.config.locals.mdPanelRef = self;
14861 self._$mdCompiler.compile(self.config)
14862 .then(function(compileData) {
14863 self.panelContainer = compileData.link(self.config['scope']);
14864 getElement(self.config['attachTo']).append(self.panelContainer);
14865
14866 if (self.config['disableParentScroll']) {
14867 self._restoreScroll = self._$mdUtil.disableScrollAround(
14868 null,
14869 self.panelContainer,
14870 { disableScrollMask: true }
14871 );
14872 }
14873
14874 self.panelEl = angular.element(
14875 self.panelContainer[0].querySelector('.md-panel'));
14876
14877 // Add a custom CSS class to the panel element.
14878 if (self.config['panelClass']) {
14879 self.panelEl.addClass(self.config['panelClass']);
14880 }
14881
14882 // Handle click and touch events for the panel container.
14883 if (self.config['propagateContainerEvents']) {
14884 self.panelContainer.css('pointer-events', 'none');
14885 }
14886
14887 // Panel may be outside the $rootElement, tell ngAnimate to animate
14888 // regardless.
14889 if (self._$animate.pin) {
14890 self._$animate.pin(self.panelContainer,
14891 getElement(self.config['attachTo']));
14892 }
14893
14894 self._configureTrapFocus();
14895 self._addStyles().then(function() {
14896 resolve(self);
14897 }, reject);
14898 }, reject);
14899 });
14900};
14901
14902
14903/**
14904 * Adds the styles for the panel, such as positioning and z-index.
14905 * @returns {!angular.$q.Promise<!MdPanelRef>}
14906 * @private
14907 */
14908MdPanelRef.prototype._addStyles = function() {
14909 var self = this;
14910 return this._$q(function(resolve) {
14911 self.panelContainer.css('z-index', self.config['zIndex']);
14912 self.panelEl.css('z-index', self.config['zIndex'] + 1);
14913
14914 var hideAndResolve = function() {
14915 // Remove left: -9999px and add hidden class.
14916 self.panelEl.css('left', '');
14917 self.panelContainer.addClass(MD_PANEL_HIDDEN);
14918 resolve(self);
14919 };
14920
14921 if (self.config['fullscreen']) {
14922 self.panelEl.addClass('_md-panel-fullscreen');
14923 hideAndResolve();
14924 return; // Don't setup positioning.
14925 }
14926
14927 var positionConfig = self.config['position'];
14928 if (!positionConfig) {
14929 hideAndResolve();
14930 return; // Don't setup positioning.
14931 }
14932
14933 // Wait for angular to finish processing the template, then position it
14934 // correctly. This is necessary so that the panel will have a defined height
14935 // and width.
14936 self._$rootScope['$$postDigest'](function() {
14937 self._updatePosition(true);
14938 resolve(self);
14939 });
14940 });
14941};
14942
14943
14944/**
14945 * Updates the position configuration of a panel
14946 * @param {!MdPanelPosition} position
14947 */
14948MdPanelRef.prototype.updatePosition = function(position) {
14949 if (!this.panelContainer) {
14950 throw new Error('Panel does not exist yet. Call open() or attach().');
14951 }
14952
14953 this.config['position'] = position;
14954 this._updatePosition();
14955};
14956
14957
14958/**
14959 * Calculates and updates the position of the panel.
14960 * @param {boolean=} init
14961 * @private
14962 */
14963MdPanelRef.prototype._updatePosition = function(init) {
14964 var positionConfig = this.config['position'];
14965
14966 if (positionConfig) {
14967 positionConfig._setPanelPosition(this.panelEl);
14968
14969 // Hide the panel now that position is known.
14970 if (init) {
14971 this.panelContainer.addClass(MD_PANEL_HIDDEN);
14972 }
14973
14974 this.panelEl.css(
14975 MdPanelPosition.absPosition.TOP,
14976 positionConfig.getTop()
14977 );
14978 this.panelEl.css(
14979 MdPanelPosition.absPosition.BOTTOM,
14980 positionConfig.getBottom()
14981 );
14982 this.panelEl.css(
14983 MdPanelPosition.absPosition.LEFT,
14984 positionConfig.getLeft()
14985 );
14986 this.panelEl.css(
14987 MdPanelPosition.absPosition.RIGHT,
14988 positionConfig.getRight()
14989 );
14990
14991 // Use the vendor prefixed version of transform.
14992 var prefixedTransform = this._$mdConstant.CSS.TRANSFORM;
14993 this.panelEl.css(prefixedTransform, positionConfig.getTransform());
14994 }
14995};
14996
14997
14998/**
14999 * Focuses on the panel or the first focus target.
15000 * @private
15001 */
15002MdPanelRef.prototype._focusOnOpen = function() {
15003 if (this.config['focusOnOpen']) {
15004 // Wait for the template to finish rendering to guarantee md-autofocus has
15005 // finished adding the class md-autofocus, otherwise the focusable element
15006 // isn't available to focus.
15007 var self = this;
15008 this._$rootScope['$$postDigest'](function() {
15009 var target = self._$mdUtil.findFocusTarget(self.panelEl) ||
15010 self.panelEl;
15011 target.focus();
15012 });
15013 }
15014};
15015
15016
15017/**
15018 * Shows the backdrop.
15019 * @returns {!angular.$q.Promise} A promise that is resolved when the backdrop
15020 * is created and attached.
15021 * @private
15022 */
15023MdPanelRef.prototype._createBackdrop = function() {
15024 if (this.config.hasBackdrop) {
15025 if (!this._backdropRef) {
15026 var backdropAnimation = this._$mdPanel.newPanelAnimation()
15027 .openFrom(this.config.attachTo)
15028 .withAnimation({
15029 open: '_md-opaque-enter',
15030 close: '_md-opaque-leave'
15031 });
15032 var backdropConfig = {
15033 animation: backdropAnimation,
15034 attachTo: this.config.attachTo,
15035 focusOnOpen: false,
15036 panelClass: '_md-panel-backdrop',
15037 zIndex: this.config.zIndex - 1
15038 };
15039 this._backdropRef = this._$mdPanel.create(backdropConfig);
15040 }
15041 if (!this._backdropRef.isAttached) {
15042 return this._backdropRef.attach();
15043 }
15044 }
15045};
15046
15047
15048/**
15049 * Listen for escape keys and outside clicks to auto close.
15050 * @private
15051 */
15052MdPanelRef.prototype._addEventListeners = function() {
15053 this._configureEscapeToClose();
15054 this._configureClickOutsideToClose();
15055 this._configureScrollListener();
15056};
15057
15058
15059/**
15060 * Remove event listeners added in _addEventListeners.
15061 * @private
15062 */
15063MdPanelRef.prototype._removeEventListeners = function() {
15064 this._removeListeners && this._removeListeners.forEach(function(removeFn) {
15065 removeFn();
15066 });
15067 this._removeListeners = [];
15068};
15069
15070
15071/**
15072 * Setup the escapeToClose event listeners.
15073 * @private
15074 */
15075MdPanelRef.prototype._configureEscapeToClose = function() {
15076 if (this.config['escapeToClose']) {
15077 var parentTarget = getElement(this.config['attachTo']);
15078 var self = this;
15079
15080 var keyHandlerFn = function(ev) {
15081 if (ev.keyCode === self._$mdConstant.KEY_CODE.ESCAPE) {
15082 ev.stopPropagation();
15083 ev.preventDefault();
15084
15085 self.close();
15086 }
15087 };
15088
15089 // Add keydown listeners
15090 this.panelContainer.on('keydown', keyHandlerFn);
15091 parentTarget.on('keydown', keyHandlerFn);
15092
15093 // Queue remove listeners function
15094 this._removeListeners.push(function() {
15095 self.panelContainer.off('keydown', keyHandlerFn);
15096 parentTarget.off('keydown', keyHandlerFn);
15097 });
15098 }
15099};
15100
15101
15102/**
15103 * Setup the clickOutsideToClose event listeners.
15104 * @private
15105 */
15106MdPanelRef.prototype._configureClickOutsideToClose = function() {
15107 if (this.config['clickOutsideToClose']) {
15108 var target = this.panelContainer;
15109 var sourceElem;
15110
15111 // Keep track of the element on which the mouse originally went down
15112 // so that we can only close the backdrop when the 'click' started on it.
15113 // A simple 'click' handler does not work,
15114 // it sets the target object as the element the mouse went down on.
15115 var mousedownHandler = function(ev) {
15116 sourceElem = ev.target;
15117 };
15118
15119 // We check if our original element and the target is the backdrop
15120 // because if the original was the backdrop and the target was inside the
15121 // panel we don't want to panel to close.
15122 var self = this;
15123 var mouseupHandler = function(ev) {
15124 if (sourceElem === target[0] && ev.target === target[0]) {
15125 ev.stopPropagation();
15126 ev.preventDefault();
15127
15128 self.close();
15129 }
15130 };
15131
15132 // Add listeners
15133 target.on('mousedown', mousedownHandler);
15134 target.on('mouseup', mouseupHandler);
15135
15136 // Queue remove listeners function
15137 this._removeListeners.push(function() {
15138 target.off('mousedown', mousedownHandler);
15139 target.off('mouseup', mouseupHandler);
15140 });
15141 }
15142};
15143
15144
15145/**
15146 * Configures the listeners for updating the panel position on scroll.
15147 * @private
15148*/
15149MdPanelRef.prototype._configureScrollListener = function() {
15150 var updatePosition = angular.bind(this, this._updatePosition);
15151 var debouncedUpdatePosition = this._$$rAF.throttle(updatePosition);
15152 var self = this;
15153
15154 var onScroll = function() {
15155 if (!self.config['disableParentScroll']) {
15156 debouncedUpdatePosition();
15157 }
15158 };
15159
15160 // Add listeners.
15161 this._$window.addEventListener('scroll', onScroll, true);
15162
15163 // Queue remove listeners function.
15164 this._removeListeners.push(function() {
15165 self._$window.removeEventListener('scroll', onScroll, true);
15166 });
15167};
15168
15169
15170/**
15171 * Setup the focus traps. These traps will wrap focus when tabbing past the
15172 * panel. When shift-tabbing, the focus will stick in place.
15173 * @private
15174 */
15175MdPanelRef.prototype._configureTrapFocus = function() {
15176 // Focus doesn't remain instead of the panel without this.
15177 this.panelEl.attr('tabIndex', '-1');
15178 if (this.config['trapFocus']) {
15179 var element = this.panelEl;
15180 // Set up elements before and after the panel to capture focus and
15181 // redirect back into the panel.
15182 this._topFocusTrap = FOCUS_TRAP_TEMPLATE.clone()[0];
15183 this._bottomFocusTrap = FOCUS_TRAP_TEMPLATE.clone()[0];
15184
15185 // When focus is about to move out of the panel, we want to intercept it
15186 // and redirect it back to the panel element.
15187 var focusHandler = function() {
15188 element.focus();
15189 };
15190 this._topFocusTrap.addEventListener('focus', focusHandler);
15191 this._bottomFocusTrap.addEventListener('focus', focusHandler);
15192
15193 // Queue remove listeners function
15194 this._removeListeners.push(this._simpleBind(function() {
15195 this._topFocusTrap.removeEventListener('focus', focusHandler);
15196 this._bottomFocusTrap.removeEventListener('focus', focusHandler);
15197 }, this));
15198
15199 // The top focus trap inserted immediately before the md-panel element (as
15200 // a sibling). The bottom focus trap inserted immediately after the
15201 // md-panel element (as a sibling).
15202 element[0].parentNode.insertBefore(this._topFocusTrap, element[0]);
15203 element.after(this._bottomFocusTrap);
15204 }
15205};
15206
15207
15208/**
15209 * Animate the panel opening.
15210 * @returns {!angular.$q.Promise} A promise that is resolved when the panel has
15211 * animated open.
15212 * @private
15213 */
15214MdPanelRef.prototype._animateOpen = function() {
15215 this.panelContainer.addClass('md-panel-is-showing');
15216 var animationConfig = this.config['animation'];
15217 if (!animationConfig) {
15218 // Promise is in progress, return it.
15219 this.panelContainer.addClass('_md-panel-shown');
15220 return this._$q.when(this);
15221 }
15222
15223 var self = this;
15224 return this._$q(function(resolve) {
15225 var done = self._done(resolve, self);
15226 var warnAndOpen = function() {
15227 self._$log.warn(
15228 'MdPanel Animations failed. Showing panel without animating.');
15229 done();
15230 };
15231
15232 animationConfig.animateOpen(self.panelEl)
15233 .then(done, warnAndOpen);
15234 });
15235};
15236
15237
15238/**
15239 * Animate the panel closing.
15240 * @returns {!angular.$q.Promise} A promise that is resolved when the panel has
15241 * animated closed.
15242 * @private
15243 */
15244MdPanelRef.prototype._animateClose = function() {
15245 var animationConfig = this.config['animation'];
15246 if (!animationConfig) {
15247 this.panelContainer.removeClass('md-panel-is-showing');
15248 this.panelContainer.removeClass('_md-panel-shown');
15249 return this._$q.when(this);
15250 }
15251
15252 var self = this;
15253 return this._$q(function(resolve) {
15254 var done = function() {
15255 self.panelContainer.removeClass('md-panel-is-showing');
15256 resolve(self);
15257 };
15258 var warnAndClose = function() {
15259 self._$log.warn(
15260 'MdPanel Animations failed. Hiding panel without animating.');
15261 done();
15262 };
15263
15264 animationConfig.animateClose(self.panelEl)
15265 .then(done, warnAndClose);
15266 });
15267};
15268
15269
15270/**
15271 * Faster, more basic than angular.bind
15272 * http://jsperf.com/angular-bind-vs-custom-vs-native
15273 * @param {function} callback
15274 * @param {!Object} self
15275 * @return {function} Callback function with a bound self.
15276 */
15277MdPanelRef.prototype._simpleBind = function(callback, self) {
15278 return function(value) {
15279 return callback.apply(self, value);
15280 };
15281};
15282
15283
15284/**
15285 * @param {function} callback
15286 * @param {!Object} self
15287 * @return {function} Callback function with a self param.
15288 */
15289MdPanelRef.prototype._done = function(callback, self) {
15290 return function() {
15291 callback(self);
15292 };
15293};
15294
15295
15296/*****************************************************************************
15297 * MdPanelPosition *
15298 *****************************************************************************/
15299
15300
15301/**
15302 * Position configuration object. To use, create an MdPanelPosition with the
15303 * desired properties, then pass the object as part of $mdPanel creation.
15304 *
15305 * Example:
15306 *
15307 * var panelPosition = new MdPanelPosition()
15308 * .relativeTo(myButtonEl)
15309 * .addPanelPosition(
15310 * $mdPanel.xPosition.CENTER,
15311 * $mdPanel.yPosition.ALIGN_TOPS
15312 * );
15313 *
15314 * $mdPanel.create({
15315 * position: panelPosition
15316 * });
15317 *
15318 * @param {!angular.$injector} $injector
15319 * @final @constructor
15320 */
15321function MdPanelPosition($injector) {
15322 /** @private @const {!angular.$window} */
15323 this._$window = $injector.get('$window');
15324
15325 /** @private {boolean} */
15326 this._isRTL = $injector.get('$mdUtil').bidi() === 'rtl';
15327
15328 /** @private {boolean} */
15329 this._absolute = false;
15330
15331 /** @private {!angular.JQLite} */
15332 this._relativeToEl;
15333
15334 /** @private {string} */
15335 this._top = '';
15336
15337 /** @private {string} */
15338 this._bottom = '';
15339
15340 /** @private {string} */
15341 this._left = '';
15342
15343 /** @private {string} */
15344 this._right = '';
15345
15346 /** @private {!Array<string>} */
15347 this._translateX = [];
15348
15349 /** @private {!Array<string>} */
15350 this._translateY = [];
15351
15352 /** @private {!Array<{x:string, y:string}>} */
15353 this._positions = [];
15354
15355 /** @private {?{x:string, y:string}} */
15356 this._actualPosition;
15357}
15358
15359
15360/**
15361 * Possible values of xPosition.
15362 * @enum {string}
15363 */
15364MdPanelPosition.xPosition = {
15365 CENTER: 'center',
15366 ALIGN_START: 'align-start',
15367 ALIGN_END: 'align-end',
15368 OFFSET_START: 'offset-start',
15369 OFFSET_END: 'offset-end'
15370};
15371
15372
15373/**
15374 * Possible values of yPosition.
15375 * @enum {string}
15376 */
15377MdPanelPosition.yPosition = {
15378 CENTER: 'center',
15379 ALIGN_TOPS: 'align-tops',
15380 ALIGN_BOTTOMS: 'align-bottoms',
15381 ABOVE: 'above',
15382 BELOW: 'below'
15383};
15384
15385
15386/**
15387 * Possible values of absolute position.
15388 * @enum {string}
15389 */
15390MdPanelPosition.absPosition = {
15391 TOP: 'top',
15392 RIGHT: 'right',
15393 BOTTOM: 'bottom',
15394 LEFT: 'left'
15395};
15396
15397
15398/**
15399 * Sets absolute positioning for the panel.
15400 * @return {!MdPanelPosition}
15401 */
15402MdPanelPosition.prototype.absolute = function() {
15403 this._absolute = true;
15404 return this;
15405};
15406
15407/**
15408 * Sets the value of a position for the panel. Clears any previously set
15409 * position.
15410 * @param {string} position Position to set
15411 * @param {string=} value Value of the position. Defaults to '0'.
15412 * @returns {!MdPanelPosition}
15413 * @private
15414 */
15415MdPanelPosition.prototype._setPosition = function(position, value) {
15416 if (position === MdPanelPosition.absPosition.RIGHT ||
15417 position === MdPanelPosition.absPosition.LEFT) {
15418 this._left = this._right = '';
15419 } else if (
15420 position === MdPanelPosition.absPosition.BOTTOM ||
15421 position === MdPanelPosition.absPosition.TOP) {
15422 this._top = this._bottom = '';
15423 } else {
15424 var positions = Object.keys(MdPanelPosition.absPosition).join()
15425 .toLowerCase();
15426
15427 throw new Error('Position must be one of ' + positions + '.');
15428 }
15429
15430 this['_' + position] = angular.isString(value) ? value : '0';
15431
15432 return this;
15433};
15434
15435
15436/**
15437 * Sets the value of `top` for the panel. Clears any previously set vertical
15438 * position.
15439 * @param {string=} top Value of `top`. Defaults to '0'.
15440 * @returns {!MdPanelPosition}
15441 */
15442MdPanelPosition.prototype.top = function(top) {
15443 return this._setPosition(MdPanelPosition.absPosition.TOP, top);
15444};
15445
15446
15447/**
15448 * Sets the value of `bottom` for the panel. Clears any previously set vertical
15449 * position.
15450 * @param {string=} bottom Value of `bottom`. Defaults to '0'.
15451 * @returns {!MdPanelPosition}
15452 */
15453MdPanelPosition.prototype.bottom = function(bottom) {
15454 return this._setPosition(MdPanelPosition.absPosition.BOTTOM, bottom);
15455};
15456
15457
15458/**
15459 * Sets the panel to the start of the page - `left` if `ltr` or `right` for
15460 * `rtl`. Clears any previously set horizontal position.
15461 * @param {string=} start Value of position. Defaults to '0'.
15462 * @returns {!MdPanelPosition}
15463 */
15464MdPanelPosition.prototype.start = function(start) {
15465 var position = this._isRTL ? MdPanelPosition.absPosition.RIGHT : MdPanelPosition.absPosition.LEFT;
15466 return this._setPosition(position, start);
15467};
15468
15469
15470/**
15471 * Sets the panel to the end of the page - `right` if `ltr` or `left` for `rtl`.
15472 * Clears any previously set horizontal position.
15473 * @param {string=} end Value of position. Defaults to '0'.
15474 * @returns {!MdPanelPosition}
15475 */
15476MdPanelPosition.prototype.end = function(end) {
15477 var position = this._isRTL ? MdPanelPosition.absPosition.LEFT : MdPanelPosition.absPosition.RIGHT;
15478 return this._setPosition(position, end);
15479};
15480
15481
15482/**
15483 * Sets the value of `left` for the panel. Clears any previously set
15484 * horizontal position.
15485 * @param {string=} left Value of `left`. Defaults to '0'.
15486 * @returns {!MdPanelPosition}
15487 */
15488MdPanelPosition.prototype.left = function(left) {
15489 return this._setPosition(MdPanelPosition.absPosition.LEFT, left);
15490};
15491
15492
15493/**
15494 * Sets the value of `right` for the panel. Clears any previously set
15495 * horizontal position.
15496 * @param {string=} right Value of `right`. Defaults to '0'.
15497 * @returns {!MdPanelPosition}
15498*/
15499MdPanelPosition.prototype.right = function(right) {
15500 return this._setPosition(MdPanelPosition.absPosition.RIGHT, right);
15501};
15502
15503
15504/**
15505 * Centers the panel horizontally in the viewport. Clears any previously set
15506 * horizontal position.
15507 * @returns {!MdPanelPosition}
15508 */
15509MdPanelPosition.prototype.centerHorizontally = function() {
15510 this._left = '50%';
15511 this._right = '';
15512 this._translateX = ['-50%'];
15513 return this;
15514};
15515
15516
15517/**
15518 * Centers the panel vertically in the viewport. Clears any previously set
15519 * vertical position.
15520 * @returns {!MdPanelPosition}
15521 */
15522MdPanelPosition.prototype.centerVertically = function() {
15523 this._top = '50%';
15524 this._bottom = '';
15525 this._translateY = ['-50%'];
15526 return this;
15527};
15528
15529
15530/**
15531 * Centers the panel horizontally and vertically in the viewport. This is
15532 * equivalent to calling both `centerHorizontally` and `centerVertically`.
15533 * Clears any previously set horizontal and vertical positions.
15534 * @returns {!MdPanelPosition}
15535 */
15536MdPanelPosition.prototype.center = function() {
15537 return this.centerHorizontally().centerVertically();
15538};
15539
15540
15541/**
15542 * Sets element for relative positioning.
15543 * @param {string|!Element|!angular.JQLite} element Query selector, DOM element,
15544 * or angular element to set the panel relative to.
15545 * @returns {!MdPanelPosition}
15546 */
15547MdPanelPosition.prototype.relativeTo = function(element) {
15548 this._absolute = false;
15549 this._relativeToEl = getElement(element);
15550 return this;
15551};
15552
15553
15554/**
15555 * Sets the x and y positions for the panel relative to another element.
15556 * @param {string} xPosition must be one of the MdPanelPosition.xPosition
15557 * values.
15558 * @param {string} yPosition must be one of the MdPanelPosition.yPosition
15559 * values.
15560 * @returns {!MdPanelPosition}
15561 */
15562MdPanelPosition.prototype.addPanelPosition = function(xPosition, yPosition) {
15563 if (!this._relativeToEl) {
15564 throw new Error('addPanelPosition can only be used with relative ' +
15565 'positioning. Set relativeTo first.');
15566 }
15567
15568 this._validateXPosition(xPosition);
15569 this._validateYPosition(yPosition);
15570
15571 this._positions.push({
15572 x: xPosition,
15573 y: yPosition,
15574 });
15575 return this;
15576};
15577
15578
15579/**
15580 * Ensures that yPosition is a valid position name. Throw an exception if not.
15581 * @param {string} yPosition
15582 */
15583MdPanelPosition.prototype._validateYPosition = function(yPosition) {
15584 // empty is ok
15585 if (yPosition == null) {
15586 return;
15587 }
15588
15589 var positionKeys = Object.keys(MdPanelPosition.yPosition);
15590 var positionValues = [];
15591 for (var key, i = 0; key = positionKeys[i]; i++) {
15592 var position = MdPanelPosition.yPosition[key];
15593 positionValues.push(position);
15594
15595 if (position === yPosition) {
15596 return;
15597 }
15598 }
15599
15600 throw new Error('Panel y position only accepts the following values:\n' +
15601 positionValues.join(' | '));
15602};
15603
15604
15605/**
15606 * Ensures that xPosition is a valid position name. Throw an exception if not.
15607 * @param {string} xPosition
15608 */
15609MdPanelPosition.prototype._validateXPosition = function(xPosition) {
15610 // empty is ok
15611 if (xPosition == null) {
15612 return;
15613 }
15614
15615 var positionKeys = Object.keys(MdPanelPosition.xPosition);
15616 var positionValues = [];
15617 for (var key, i = 0; key = positionKeys[i]; i++) {
15618 var position = MdPanelPosition.xPosition[key];
15619 positionValues.push(position);
15620 if (position === xPosition) {
15621 return;
15622 }
15623 }
15624
15625 throw new Error('Panel x Position only accepts the following values:\n' +
15626 positionValues.join(' | '));
15627};
15628
15629
15630/**
15631 * Sets the value of the offset in the x-direction. This will add to any
15632 * previously set offsets.
15633 * @param {string} offsetX
15634 * @returns {!MdPanelPosition}
15635 */
15636MdPanelPosition.prototype.withOffsetX = function(offsetX) {
15637 this._translateX.push(offsetX);
15638 return this;
15639};
15640
15641
15642/**
15643 * Sets the value of the offset in the y-direction. This will add to any
15644 * previously set offsets.
15645 * @param {string} offsetY
15646 * @returns {!MdPanelPosition}
15647 */
15648MdPanelPosition.prototype.withOffsetY = function(offsetY) {
15649 this._translateY.push(offsetY);
15650 return this;
15651};
15652
15653
15654/**
15655 * Gets the value of `top` for the panel.
15656 * @returns {string}
15657 */
15658MdPanelPosition.prototype.getTop = function() {
15659 return this._top;
15660};
15661
15662
15663/**
15664 * Gets the value of `bottom` for the panel.
15665 * @returns {string}
15666 */
15667MdPanelPosition.prototype.getBottom = function() {
15668 return this._bottom;
15669};
15670
15671
15672/**
15673 * Gets the value of `left` for the panel.
15674 * @returns {string}
15675 */
15676MdPanelPosition.prototype.getLeft = function() {
15677 return this._left;
15678};
15679
15680
15681/**
15682 * Gets the value of `right` for the panel.
15683 * @returns {string}
15684 */
15685MdPanelPosition.prototype.getRight = function() {
15686 return this._right;
15687};
15688
15689
15690/**
15691 * Gets the value of `transform` for the panel.
15692 * @returns {string}
15693 */
15694MdPanelPosition.prototype.getTransform = function() {
15695 var translateX = this._reduceTranslateValues('translateX', this._translateX);
15696 var translateY = this._reduceTranslateValues('translateY', this._translateY);
15697
15698 // It's important to trim the result, because the browser will ignore the set
15699 // operation if the string contains only whitespace.
15700 return (translateX + ' ' + translateY).trim();
15701};
15702
15703/**
15704 * True if the panel is completely on-screen with this positioning; false
15705 * otherwise.
15706 * @param {!angular.JQLite} panelEl
15707 * @return {boolean}
15708 */
15709MdPanelPosition.prototype._isOnscreen = function(panelEl) {
15710 // this works because we always use fixed positioning for the panel,
15711 // which is relative to the viewport.
15712 // TODO(gmoothart): take into account _translateX and _translateY to the
15713 // extent feasible.
15714
15715 var left = parseInt(this.getLeft());
15716 var top = parseInt(this.getTop());
15717 var right = left + panelEl[0].offsetWidth;
15718 var bottom = top + panelEl[0].offsetHeight;
15719
15720 return (left >= 0) &&
15721 (top >= 0) &&
15722 (bottom <= this._$window.innerHeight) &&
15723 (right <= this._$window.innerWidth);
15724};
15725
15726
15727/**
15728 * Gets the first x/y position that can fit on-screen.
15729 * @returns {{x: string, y: string}}
15730 */
15731MdPanelPosition.prototype.getActualPosition = function() {
15732 return this._actualPosition;
15733};
15734
15735
15736/**
15737 * Reduces a list of translate values to a string that can be used within
15738 * transform.
15739 * @param {string} translateFn
15740 * @param {!Array<string>} values
15741 * @returns {string}
15742 * @private
15743 */
15744MdPanelPosition.prototype._reduceTranslateValues =
15745 function(translateFn, values) {
15746 return values.map(function(translation) {
15747 return translateFn + '(' + translation + ')';
15748 }).join(' ');
15749 };
15750
15751
15752/**
15753 * Sets the panel position based on the created panel element and best x/y
15754 * positioning.
15755 * @param {!angular.JQLite} panelEl
15756 * @private
15757 */
15758MdPanelPosition.prototype._setPanelPosition = function(panelEl) {
15759 // Only calculate the position if necessary.
15760 if (this._absolute) {
15761 return;
15762 }
15763
15764 if (this._actualPosition) {
15765 this._calculatePanelPosition(panelEl, this._actualPosition);
15766 return;
15767 }
15768
15769 for (var i = 0; i < this._positions.length; i++) {
15770 this._actualPosition = this._positions[i];
15771 this._calculatePanelPosition(panelEl, this._actualPosition);
15772 if (this._isOnscreen(panelEl)) {
15773 break;
15774 }
15775 }
15776};
15777
15778
15779/**
15780 * Switches between 'start' and 'end'.
15781 * @param {string} position Horizontal position of the panel
15782 * @returns {string} Reversed position
15783 * @private
15784 */
15785MdPanelPosition.prototype._reverseXPosition = function(position) {
15786 if (position === MdPanelPosition.xPosition.CENTER) {
15787 return;
15788 }
15789
15790 var start = 'start';
15791 var end = 'end';
15792
15793 return position.indexOf(start) > -1 ? position.replace(start, end) : position.replace(end, start);
15794};
15795
15796
15797/**
15798 * Handles horizontal positioning in rtl or ltr environments.
15799 * @param {string} position Horizontal position of the panel
15800 * @returns {string} The correct position according the page direction
15801 * @private
15802 */
15803MdPanelPosition.prototype._bidi = function(position) {
15804 return this._isRTL ? this._reverseXPosition(position) : position;
15805};
15806
15807
15808/**
15809 * Calculates the panel position based on the created panel element and the
15810 * provided positioning.
15811 * @param {!angular.JQLite} panelEl
15812 * @param {!{x:string, y:string}} position
15813 * @private
15814 */
15815MdPanelPosition.prototype._calculatePanelPosition = function(panelEl, position) {
15816
15817 var panelBounds = panelEl[0].getBoundingClientRect();
15818 var panelWidth = panelBounds.width;
15819 var panelHeight = panelBounds.height;
15820
15821 var targetBounds = this._relativeToEl[0].getBoundingClientRect();
15822
15823 var targetLeft = targetBounds.left;
15824 var targetRight = targetBounds.right;
15825 var targetWidth = targetBounds.width;
15826
15827 switch (this._bidi(position.x)) {
15828 case MdPanelPosition.xPosition.OFFSET_START:
15829 this._left = targetLeft - panelWidth + 'px';
15830 break;
15831 case MdPanelPosition.xPosition.ALIGN_END:
15832 this._left = targetRight - panelWidth + 'px';
15833 break;
15834 case MdPanelPosition.xPosition.CENTER:
15835 var left = targetLeft + (0.5 * targetWidth) - (0.5 * panelWidth);
15836 this._left = left + 'px';
15837 break;
15838 case MdPanelPosition.xPosition.ALIGN_START:
15839 this._left = targetLeft + 'px';
15840 break;
15841 case MdPanelPosition.xPosition.OFFSET_END:
15842 this._left = targetRight + 'px';
15843 break;
15844 }
15845
15846 var targetTop = targetBounds.top;
15847 var targetBottom = targetBounds.bottom;
15848 var targetHeight = targetBounds.height;
15849
15850 switch (position.y) {
15851 case MdPanelPosition.yPosition.ABOVE:
15852 this._top = targetTop - panelHeight + 'px';
15853 break;
15854 case MdPanelPosition.yPosition.ALIGN_BOTTOMS:
15855 this._top = targetBottom - panelHeight + 'px';
15856 break;
15857 case MdPanelPosition.yPosition.CENTER:
15858 var top = targetTop + (0.5 * targetHeight) - (0.5 * panelHeight);
15859 this._top = top + 'px';
15860 break;
15861 case MdPanelPosition.yPosition.ALIGN_TOPS:
15862 this._top = targetTop + 'px';
15863 break;
15864 case MdPanelPosition.yPosition.BELOW:
15865 this._top = targetBottom + 'px';
15866 break;
15867 }
15868};
15869
15870
15871/*****************************************************************************
15872 * MdPanelAnimation *
15873 *****************************************************************************/
15874
15875
15876/**
15877 * Animation configuration object. To use, create an MdPanelAnimation with the
15878 * desired properties, then pass the object as part of $mdPanel creation.
15879 *
15880 * Example:
15881 *
15882 * var panelAnimation = new MdPanelAnimation()
15883 * .openFrom(myButtonEl)
15884 * .closeTo('.my-button')
15885 * .withAnimation($mdPanel.animation.SCALE);
15886 *
15887 * $mdPanel.create({
15888 * animation: panelAnimation
15889 * });
15890 *
15891 * @param {!angular.$injector} $injector
15892 * @final @constructor
15893 */
15894function MdPanelAnimation($injector) {
15895 /** @private @const {!angular.$mdUtil} */
15896 this._$mdUtil = $injector.get('$mdUtil');
15897
15898 /**
15899 * @private {{element: !angular.JQLite|undefined, bounds: !DOMRect}|
15900 * undefined}
15901 */
15902 this._openFrom;
15903
15904 /**
15905 * @private {{element: !angular.JQLite|undefined, bounds: !DOMRect}|
15906 * undefined}
15907 */
15908 this._closeTo;
15909
15910 /** @private {string|{open: string, close: string}} */
15911 this._animationClass = '';
15912}
15913
15914
15915/**
15916 * Possible default animations.
15917 * @enum {string}
15918 */
15919MdPanelAnimation.animation = {
15920 SLIDE: 'md-panel-animate-slide',
15921 SCALE: 'md-panel-animate-scale',
15922 FADE: 'md-panel-animate-fade'
15923};
15924
15925
15926/**
15927 * Specifies where to start the open animation. `openFrom` accepts a
15928 * click event object, query selector, DOM element, or a Rect object that
15929 * is used to determine the bounds. When passed a click event, the location
15930 * of the click will be used as the position to start the animation.
15931 * @param {string|!Element|!Event|{top: number, left: number}} openFrom
15932 * @returns {!MdPanelAnimation}
15933 */
15934MdPanelAnimation.prototype.openFrom = function(openFrom) {
15935 // Check if 'openFrom' is an Event.
15936 openFrom = openFrom.target ? openFrom.target : openFrom;
15937
15938 this._openFrom = this._getPanelAnimationTarget(openFrom);
15939
15940 if (!this._closeTo) {
15941 this._closeTo = this._openFrom;
15942 }
15943 return this;
15944};
15945
15946
15947/**
15948 * Specifies where to animate the panel close. `closeTo` accepts a
15949 * query selector, DOM element, or a Rect object that is used to determine
15950 * the bounds.
15951 * @param {string|!Element|{top: number, left: number}} closeTo
15952 * @returns {!MdPanelAnimation}
15953 */
15954MdPanelAnimation.prototype.closeTo = function(closeTo) {
15955 this._closeTo = this._getPanelAnimationTarget(closeTo);
15956 return this;
15957};
15958
15959
15960/**
15961 * Returns the element and bounds for the animation target.
15962 * @param {string|!Element|{top: number, left: number}} location
15963 * @returns {{element: !angular.JQLite|undefined, bounds: !DOMRect}}
15964 * @private
15965 */
15966MdPanelAnimation.prototype._getPanelAnimationTarget = function(location) {
15967 if (angular.isDefined(location.top) || angular.isDefined(location.left)) {
15968 return {
15969 element: undefined,
15970 bounds: {
15971 top: location.top || 0,
15972 left: location.left || 0
15973 }
15974 };
15975 } else {
15976 return this._getBoundingClientRect(getElement(location));
15977 }
15978};
15979
15980
15981/**
15982 * Specifies the animation class.
15983 *
15984 * There are several default animations that can be used:
15985 * (MdPanelAnimation.animation)
15986 * SLIDE: The panel slides in and out from the specified
15987 * elements.
15988 * SCALE: The panel scales in and out.
15989 * FADE: The panel fades in and out.
15990 *
15991 * @param {string|{open: string, close: string}} cssClass
15992 * @returns {!MdPanelAnimation}
15993 */
15994MdPanelAnimation.prototype.withAnimation = function(cssClass) {
15995 this._animationClass = cssClass;
15996 return this;
15997};
15998
15999
16000/**
16001 * Animate the panel open.
16002 * @param {!angular.JQLite} panelEl
16003 * @returns {!angular.$q.Promise} A promise that is resolved when the open
16004 * animation is complete.
16005 */
16006MdPanelAnimation.prototype.animateOpen = function(panelEl) {
16007 var animator = this._$mdUtil.dom.animator;
16008
16009 this._fixBounds(panelEl);
16010 var animationOptions = {};
16011
16012 // Include the panel transformations when calculating the animations.
16013 var panelTransform = panelEl[0].style.transform || '';
16014
16015 var openFrom = animator.toTransformCss(panelTransform);
16016 var openTo = animator.toTransformCss(panelTransform);
16017
16018 switch (this._animationClass) {
16019 case MdPanelAnimation.animation.SLIDE:
16020 // Slide should start with opacity: 1.
16021 panelEl.css('opacity', '1');
16022
16023 animationOptions = {
16024 transitionInClass: '_md-panel-animate-enter'
16025 };
16026
16027 var openSlide = animator.calculateSlideToOrigin(
16028 panelEl, this._openFrom) || '';
16029 openFrom = animator.toTransformCss(openSlide + ' ' + panelTransform);
16030 break;
16031
16032 case MdPanelAnimation.animation.SCALE:
16033 animationOptions = {
16034 transitionInClass: '_md-panel-animate-enter'
16035 };
16036
16037 var openScale = animator.calculateZoomToOrigin(
16038 panelEl, this._openFrom) || '';
16039 openFrom = animator.toTransformCss(openScale + ' ' + panelTransform);
16040 break;
16041
16042 case MdPanelAnimation.animation.FADE:
16043 animationOptions = {
16044 transitionInClass: '_md-panel-animate-enter'
16045 };
16046 break;
16047
16048 default:
16049 if (angular.isString(this._animationClass)) {
16050 animationOptions = {
16051 transitionInClass: this._animationClass
16052 };
16053 } else {
16054 animationOptions = {
16055 transitionInClass: this._animationClass['open'],
16056 transitionOutClass: this._animationClass['close'],
16057 };
16058 }
16059 }
16060
16061 return animator
16062 .translate3d(panelEl, openFrom, openTo, animationOptions);
16063};
16064
16065
16066/**
16067 * Animate the panel close.
16068 * @param {!angular.JQLite} panelEl
16069 * @returns {!angular.$q.Promise} A promise that resolves when the close
16070 * animation is complete.
16071 */
16072MdPanelAnimation.prototype.animateClose = function(panelEl) {
16073 var animator = this._$mdUtil.dom.animator;
16074 var reverseAnimationOptions = {};
16075
16076 // Include the panel transformations when calculating the animations.
16077 var panelTransform = panelEl[0].style.transform || '';
16078
16079 var closeFrom = animator.toTransformCss(panelTransform);
16080 var closeTo = animator.toTransformCss(panelTransform);
16081
16082 switch (this._animationClass) {
16083 case MdPanelAnimation.animation.SLIDE:
16084 // Slide should start with opacity: 1.
16085 panelEl.css('opacity', '1');
16086 reverseAnimationOptions = {
16087 transitionInClass: '_md-panel-animate-leave'
16088 };
16089
16090 var closeSlide = animator.calculateSlideToOrigin(
16091 panelEl, this._closeTo) || '';
16092 closeTo = animator.toTransformCss(closeSlide + ' ' + panelTransform);
16093 break;
16094
16095 case MdPanelAnimation.animation.SCALE:
16096 reverseAnimationOptions = {
16097 transitionInClass: '_md-panel-animate-scale-out _md-panel-animate-leave'
16098 };
16099
16100 var closeScale = animator.calculateZoomToOrigin(
16101 panelEl, this._closeTo) || '';
16102 closeTo = animator.toTransformCss(closeScale + ' ' + panelTransform);
16103 break;
16104
16105 case MdPanelAnimation.animation.FADE:
16106 reverseAnimationOptions = {
16107 transitionInClass: '_md-panel-animate-fade-out _md-panel-animate-leave'
16108 };
16109 break;
16110
16111 default:
16112 if (angular.isString(this._animationClass)) {
16113 reverseAnimationOptions = {
16114 transitionOutClass: this._animationClass
16115 };
16116 } else {
16117 reverseAnimationOptions = {
16118 transitionInClass: this._animationClass['close'],
16119 transitionOutClass: this._animationClass['open']
16120 };
16121 }
16122 }
16123
16124 return animator
16125 .translate3d(panelEl, closeFrom, closeTo, reverseAnimationOptions);
16126};
16127
16128
16129/**
16130 * Set the height and width to match the panel if not provided.
16131 * @param {!angular.JQLite} panelEl
16132 * @private
16133 */
16134MdPanelAnimation.prototype._fixBounds = function(panelEl) {
16135 var panelWidth = panelEl[0].offsetWidth;
16136 var panelHeight = panelEl[0].offsetHeight;
16137
16138 if (this._openFrom && this._openFrom.bounds.height == null) {
16139 this._openFrom.bounds.height = panelHeight;
16140 }
16141 if (this._openFrom && this._openFrom.bounds.width == null) {
16142 this._openFrom.bounds.width = panelWidth;
16143 }
16144 if (this._closeTo && this._closeTo.bounds.height == null) {
16145 this._closeTo.bounds.height = panelHeight;
16146 }
16147 if (this._closeTo && this._closeTo.bounds.width == null) {
16148 this._closeTo.bounds.width = panelWidth;
16149 }
16150};
16151
16152
16153/**
16154 * Identify the bounding RECT for the target element.
16155 * @param {!angular.JQLite} element
16156 * @returns {{element: !angular.JQLite|undefined, bounds: !DOMRect}}
16157 * @private
16158 */
16159MdPanelAnimation.prototype._getBoundingClientRect = function(element) {
16160 if (element instanceof angular.element) {
16161 return {
16162 element: element,
16163 bounds: element[0].getBoundingClientRect()
16164 };
16165 }
16166};
16167
16168
16169/*****************************************************************************
16170 * Util Methods *
16171 *****************************************************************************/
16172
16173/**
16174 * Returns the angular element associated with a css selector or element.
16175 * @param el {string|!angular.JQLite|!Element}
16176 * @returns {!angular.JQLite}
16177 */
16178function getElement(el) {
16179 var queryResult = angular.isString(el) ?
16180 document.querySelector(el) : el;
16181 return angular.element(queryResult);
16182}
16183
16184})();
16185(function(){
16186"use strict";
16187
16188/**
16189 * @ngdoc module
16190 * @name material.components.progressCircular
16191 * @description Module for a circular progressbar
16192 */
16193
16194angular.module('material.components.progressCircular', ['material.core']);
16195
16196})();
16197(function(){
16198"use strict";
16199
16200/**
16201 * @ngdoc module
16202 * @name material.components.progressLinear
16203 * @description Linear Progress module!
16204 */
16205MdProgressLinearDirective.$inject = ["$mdTheming", "$mdUtil", "$log"];
16206angular.module('material.components.progressLinear', [
16207 'material.core'
16208])
16209 .directive('mdProgressLinear', MdProgressLinearDirective);
16210
16211/**
16212 * @ngdoc directive
16213 * @name mdProgressLinear
16214 * @module material.components.progressLinear
16215 * @restrict E
16216 *
16217 * @description
16218 * The linear progress directive is used to make loading content
16219 * in your app as delightful and painless as possible by minimizing
16220 * the amount of visual change a user sees before they can view
16221 * and interact with content.
16222 *
16223 * Each operation should only be represented by one activity indicator
16224 * For example: one refresh operation should not display both a
16225 * refresh bar and an activity circle.
16226 *
16227 * For operations where the percentage of the operation completed
16228 * can be determined, use a determinate indicator. They give users
16229 * a quick sense of how long an operation will take.
16230 *
16231 * For operations where the user is asked to wait a moment while
16232 * something finishes up, and it’s not necessary to expose what's
16233 * happening behind the scenes and how long it will take, use an
16234 * indeterminate indicator.
16235 *
16236 * @param {string} md-mode Select from one of four modes: determinate, indeterminate, buffer or query.
16237 *
16238 * Note: if the `md-mode` value is set as undefined or specified as 1 of the four (4) valid modes, then `indeterminate`
16239 * will be auto-applied as the mode.
16240 *
16241 * Note: if not configured, the `md-mode="indeterminate"` will be auto injected as an attribute. If `value=""` is also specified, however,
16242 * then `md-mode="determinate"` would be auto-injected instead.
16243 * @param {number=} value In determinate and buffer modes, this number represents the percentage of the primary progress bar. Default: 0
16244 * @param {number=} md-buffer-value In the buffer mode, this number represents the percentage of the secondary progress bar. Default: 0
16245 * @param {boolean=} ng-disabled Determines whether to disable the progress element.
16246 *
16247 * @usage
16248 * <hljs lang="html">
16249 * <md-progress-linear md-mode="determinate" value="..."></md-progress-linear>
16250 *
16251 * <md-progress-linear md-mode="determinate" ng-value="..."></md-progress-linear>
16252 *
16253 * <md-progress-linear md-mode="indeterminate"></md-progress-linear>
16254 *
16255 * <md-progress-linear md-mode="buffer" value="..." md-buffer-value="..."></md-progress-linear>
16256 *
16257 * <md-progress-linear md-mode="query"></md-progress-linear>
16258 * </hljs>
16259 */
16260function MdProgressLinearDirective($mdTheming, $mdUtil, $log) {
16261 var MODE_DETERMINATE = "determinate";
16262 var MODE_INDETERMINATE = "indeterminate";
16263 var MODE_BUFFER = "buffer";
16264 var MODE_QUERY = "query";
16265 var DISABLED_CLASS = "_md-progress-linear-disabled";
16266
16267 return {
16268 restrict: 'E',
16269 template: '<div class="md-container">' +
16270 '<div class="md-dashed"></div>' +
16271 '<div class="md-bar md-bar1"></div>' +
16272 '<div class="md-bar md-bar2"></div>' +
16273 '</div>',
16274 compile: compile
16275 };
16276
16277 function compile(tElement, tAttrs, transclude) {
16278 tElement.attr('aria-valuemin', 0);
16279 tElement.attr('aria-valuemax', 100);
16280 tElement.attr('role', 'progressbar');
16281
16282 return postLink;
16283 }
16284 function postLink(scope, element, attr) {
16285 $mdTheming(element);
16286
16287 var lastMode;
16288 var isDisabled = attr.hasOwnProperty('disabled');
16289 var toVendorCSS = $mdUtil.dom.animator.toCss;
16290 var bar1 = angular.element(element[0].querySelector('.md-bar1'));
16291 var bar2 = angular.element(element[0].querySelector('.md-bar2'));
16292 var container = angular.element(element[0].querySelector('.md-container'));
16293
16294 element
16295 .attr('md-mode', mode())
16296 .toggleClass(DISABLED_CLASS, isDisabled);
16297
16298 validateMode();
16299 watchAttributes();
16300
16301 /**
16302 * Watch the value, md-buffer-value, and md-mode attributes
16303 */
16304 function watchAttributes() {
16305 attr.$observe('value', function(value) {
16306 var percentValue = clamp(value);
16307 element.attr('aria-valuenow', percentValue);
16308
16309 if (mode() != MODE_QUERY) animateIndicator(bar2, percentValue);
16310 });
16311
16312 attr.$observe('mdBufferValue', function(value) {
16313 animateIndicator(bar1, clamp(value));
16314 });
16315
16316 attr.$observe('disabled', function(value) {
16317 if (value === true || value === false) {
16318 isDisabled = !!value;
16319 } else {
16320 isDisabled = angular.isDefined(value);
16321 }
16322
16323 element.toggleClass(DISABLED_CLASS, isDisabled);
16324 container.toggleClass(lastMode, !isDisabled);
16325 });
16326
16327 attr.$observe('mdMode', function(mode) {
16328 if (lastMode) container.removeClass( lastMode );
16329
16330 switch( mode ) {
16331 case MODE_QUERY:
16332 case MODE_BUFFER:
16333 case MODE_DETERMINATE:
16334 case MODE_INDETERMINATE:
16335 container.addClass( lastMode = "md-mode-" + mode );
16336 break;
16337 default:
16338 container.addClass( lastMode = "md-mode-" + MODE_INDETERMINATE );
16339 break;
16340 }
16341 });
16342 }
16343
16344 /**
16345 * Auto-defaults the mode to either `determinate` or `indeterminate` mode; if not specified
16346 */
16347 function validateMode() {
16348 if ( angular.isUndefined(attr.mdMode) ) {
16349 var hasValue = angular.isDefined(attr.value);
16350 var mode = hasValue ? MODE_DETERMINATE : MODE_INDETERMINATE;
16351 var info = "Auto-adding the missing md-mode='{0}' to the ProgressLinear element";
16352
16353 //$log.debug( $mdUtil.supplant(info, [mode]) );
16354
16355 element.attr("md-mode", mode);
16356 attr.mdMode = mode;
16357 }
16358 }
16359
16360 /**
16361 * Is the md-mode a valid option?
16362 */
16363 function mode() {
16364 var value = (attr.mdMode || "").trim();
16365 if ( value ) {
16366 switch(value) {
16367 case MODE_DETERMINATE:
16368 case MODE_INDETERMINATE:
16369 case MODE_BUFFER:
16370 case MODE_QUERY:
16371 break;
16372 default:
16373 value = MODE_INDETERMINATE;
16374 break;
16375 }
16376 }
16377 return value;
16378 }
16379
16380 /**
16381 * Manually set CSS to animate the Determinate indicator based on the specified
16382 * percentage value (0-100).
16383 */
16384 function animateIndicator(target, value) {
16385 if ( isDisabled || !mode() ) return;
16386
16387 var to = $mdUtil.supplant("translateX({0}%) scale({1},1)", [ (value-100)/2, value/100 ]);
16388 var styles = toVendorCSS({ transform : to });
16389 angular.element(target).css( styles );
16390 }
16391 }
16392
16393 /**
16394 * Clamps the value to be between 0 and 100.
16395 * @param {number} value The value to clamp.
16396 * @returns {number}
16397 */
16398 function clamp(value) {
16399 return Math.max(0, Math.min(value || 0, 100));
16400 }
16401}
16402
16403
16404})();
16405(function(){
16406"use strict";
16407
16408/**
16409 * @ngdoc module
16410 * @name material.components.radioButton
16411 * @description radioButton module!
16412 */
16413mdRadioGroupDirective.$inject = ["$mdUtil", "$mdConstant", "$mdTheming", "$timeout"];
16414mdRadioButtonDirective.$inject = ["$mdAria", "$mdUtil", "$mdTheming"];
16415angular.module('material.components.radioButton', [
16416 'material.core'
16417])
16418 .directive('mdRadioGroup', mdRadioGroupDirective)
16419 .directive('mdRadioButton', mdRadioButtonDirective);
16420
16421/**
16422 * @ngdoc directive
16423 * @module material.components.radioButton
16424 * @name mdRadioGroup
16425 *
16426 * @restrict E
16427 *
16428 * @description
16429 * The `<md-radio-group>` directive identifies a grouping
16430 * container for the 1..n grouped radio buttons; specified using nested
16431 * `<md-radio-button>` tags.
16432 *
16433 * As per the [material design spec](http://www.google.com/design/spec/style/color.html#color-ui-color-application)
16434 * the radio button is in the accent color by default. The primary color palette may be used with
16435 * the `md-primary` class.
16436 *
16437 * Note: `<md-radio-group>` and `<md-radio-button>` handle tabindex differently
16438 * than the native `<input type='radio'>` controls. Whereas the native controls
16439 * force the user to tab through all the radio buttons, `<md-radio-group>`
16440 * is focusable, and by default the `<md-radio-button>`s are not.
16441 *
16442 * @param {string} ng-model Assignable angular expression to data-bind to.
16443 * @param {boolean=} md-no-ink Use of attribute indicates flag to disable ink ripple effects.
16444 *
16445 * @usage
16446 * <hljs lang="html">
16447 * <md-radio-group ng-model="selected">
16448 *
16449 * <md-radio-button
16450 * ng-repeat="d in colorOptions"
16451 * ng-value="d.value" aria-label="{{ d.label }}">
16452 *
16453 * {{ d.label }}
16454 *
16455 * </md-radio-button>
16456 *
16457 * </md-radio-group>
16458 * </hljs>
16459 *
16460 */
16461function mdRadioGroupDirective($mdUtil, $mdConstant, $mdTheming, $timeout) {
16462 RadioGroupController.prototype = createRadioGroupControllerProto();
16463
16464 return {
16465 restrict: 'E',
16466 controller: ['$element', RadioGroupController],
16467 require: ['mdRadioGroup', '?ngModel'],
16468 link: { pre: linkRadioGroup }
16469 };
16470
16471 function linkRadioGroup(scope, element, attr, ctrls) {
16472 element.addClass('_md'); // private md component indicator for styling
16473 $mdTheming(element);
16474
16475 var rgCtrl = ctrls[0];
16476 var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel();
16477
16478 rgCtrl.init(ngModelCtrl);
16479
16480 scope.mouseActive = false;
16481
16482 element
16483 .attr({
16484 'role': 'radiogroup',
16485 'tabIndex': element.attr('tabindex') || '0'
16486 })
16487 .on('keydown', keydownListener)
16488 .on('mousedown', function(event) {
16489 scope.mouseActive = true;
16490 $timeout(function() {
16491 scope.mouseActive = false;
16492 }, 100);
16493 })
16494 .on('focus', function() {
16495 if(scope.mouseActive === false) {
16496 rgCtrl.$element.addClass('md-focused');
16497 }
16498 })
16499 .on('blur', function() {
16500 rgCtrl.$element.removeClass('md-focused');
16501 });
16502
16503 /**
16504 *
16505 */
16506 function setFocus() {
16507 if (!element.hasClass('md-focused')) { element.addClass('md-focused'); }
16508 }
16509
16510 /**
16511 *
16512 */
16513 function keydownListener(ev) {
16514 var keyCode = ev.which || ev.keyCode;
16515
16516 // Only listen to events that we originated ourselves
16517 // so that we don't trigger on things like arrow keys in
16518 // inputs.
16519
16520 if (keyCode != $mdConstant.KEY_CODE.ENTER &&
16521 ev.currentTarget != ev.target) {
16522 return;
16523 }
16524
16525 switch (keyCode) {
16526 case $mdConstant.KEY_CODE.LEFT_ARROW:
16527 case $mdConstant.KEY_CODE.UP_ARROW:
16528 ev.preventDefault();
16529 rgCtrl.selectPrevious();
16530 setFocus();
16531 break;
16532
16533 case $mdConstant.KEY_CODE.RIGHT_ARROW:
16534 case $mdConstant.KEY_CODE.DOWN_ARROW:
16535 ev.preventDefault();
16536 rgCtrl.selectNext();
16537 setFocus();
16538 break;
16539
16540 case $mdConstant.KEY_CODE.ENTER:
16541 var form = angular.element($mdUtil.getClosest(element[0], 'form'));
16542 if (form.length > 0) {
16543 form.triggerHandler('submit');
16544 }
16545 break;
16546 }
16547
16548 }
16549 }
16550
16551 function RadioGroupController($element) {
16552 this._radioButtonRenderFns = [];
16553 this.$element = $element;
16554 }
16555
16556 function createRadioGroupControllerProto() {
16557 return {
16558 init: function(ngModelCtrl) {
16559 this._ngModelCtrl = ngModelCtrl;
16560 this._ngModelCtrl.$render = angular.bind(this, this.render);
16561 },
16562 add: function(rbRender) {
16563 this._radioButtonRenderFns.push(rbRender);
16564 },
16565 remove: function(rbRender) {
16566 var index = this._radioButtonRenderFns.indexOf(rbRender);
16567 if (index !== -1) {
16568 this._radioButtonRenderFns.splice(index, 1);
16569 }
16570 },
16571 render: function() {
16572 this._radioButtonRenderFns.forEach(function(rbRender) {
16573 rbRender();
16574 });
16575 },
16576 setViewValue: function(value, eventType) {
16577 this._ngModelCtrl.$setViewValue(value, eventType);
16578 // update the other radio buttons as well
16579 this.render();
16580 },
16581 getViewValue: function() {
16582 return this._ngModelCtrl.$viewValue;
16583 },
16584 selectNext: function() {
16585 return changeSelectedButton(this.$element, 1);
16586 },
16587 selectPrevious: function() {
16588 return changeSelectedButton(this.$element, -1);
16589 },
16590 setActiveDescendant: function (radioId) {
16591 this.$element.attr('aria-activedescendant', radioId);
16592 },
16593 isDisabled: function() {
16594 return this.$element[0].hasAttribute('disabled');
16595 }
16596 };
16597 }
16598 /**
16599 * Change the radio group's selected button by a given increment.
16600 * If no button is selected, select the first button.
16601 */
16602 function changeSelectedButton(parent, increment) {
16603 // Coerce all child radio buttons into an array, then wrap then in an iterator
16604 var buttons = $mdUtil.iterator(parent[0].querySelectorAll('md-radio-button'), true);
16605
16606 if (buttons.count()) {
16607 var validate = function (button) {
16608 // If disabled, then NOT valid
16609 return !angular.element(button).attr("disabled");
16610 };
16611
16612 var selected = parent[0].querySelector('md-radio-button.md-checked');
16613 var target = buttons[increment < 0 ? 'previous' : 'next'](selected, validate) || buttons.first();
16614
16615 // Activate radioButton's click listener (triggerHandler won't create a real click event)
16616 angular.element(target).triggerHandler('click');
16617
16618
16619 }
16620 }
16621
16622}
16623
16624/**
16625 * @ngdoc directive
16626 * @module material.components.radioButton
16627 * @name mdRadioButton
16628 *
16629 * @restrict E
16630 *
16631 * @description
16632 * The `<md-radio-button>`directive is the child directive required to be used within `<md-radio-group>` elements.
16633 *
16634 * While similar to the `<input type="radio" ng-model="" value="">` directive,
16635 * the `<md-radio-button>` directive provides ink effects, ARIA support, and
16636 * supports use within named radio groups.
16637 *
16638 * @param {string} ngModel Assignable angular expression to data-bind to.
16639 * @param {string=} ngChange Angular expression to be executed when input changes due to user
16640 * interaction with the input element.
16641 * @param {string} ngValue Angular expression which sets the value to which the expression should
16642 * be set when selected.
16643 * @param {string} value The value to which the expression should be set when selected.
16644 * @param {string=} name Property name of the form under which the control is published.
16645 * @param {string=} aria-label Adds label to radio button for accessibility.
16646 * Defaults to radio button's text. If no text content is available, a warning will be logged.
16647 *
16648 * @usage
16649 * <hljs lang="html">
16650 *
16651 * <md-radio-button value="1" aria-label="Label 1">
16652 * Label 1
16653 * </md-radio-button>
16654 *
16655 * <md-radio-button ng-model="color" ng-value="specialValue" aria-label="Green">
16656 * Green
16657 * </md-radio-button>
16658 *
16659 * </hljs>
16660 *
16661 */
16662function mdRadioButtonDirective($mdAria, $mdUtil, $mdTheming) {
16663
16664 var CHECKED_CSS = 'md-checked';
16665
16666 return {
16667 restrict: 'E',
16668 require: '^mdRadioGroup',
16669 transclude: true,
16670 template: '<div class="md-container" md-ink-ripple md-ink-ripple-checkbox>' +
16671 '<div class="md-off"></div>' +
16672 '<div class="md-on"></div>' +
16673 '</div>' +
16674 '<div ng-transclude class="md-label"></div>',
16675 link: link
16676 };
16677
16678 function link(scope, element, attr, rgCtrl) {
16679 var lastChecked;
16680
16681 $mdTheming(element);
16682 configureAria(element, scope);
16683
16684 initialize();
16685
16686 /**
16687 *
16688 */
16689 function initialize() {
16690 if (!rgCtrl) {
16691 throw 'RadioButton: No RadioGroupController could be found.';
16692 }
16693
16694 rgCtrl.add(render);
16695 attr.$observe('value', render);
16696
16697 element
16698 .on('click', listener)
16699 .on('$destroy', function() {
16700 rgCtrl.remove(render);
16701 });
16702 }
16703
16704 /**
16705 *
16706 */
16707 function listener(ev) {
16708 if (element[0].hasAttribute('disabled') || rgCtrl.isDisabled()) return;
16709
16710 scope.$apply(function() {
16711 rgCtrl.setViewValue(attr.value, ev && ev.type);
16712 });
16713 }
16714
16715 /**
16716 * Add or remove the `.md-checked` class from the RadioButton (and conditionally its parent).
16717 * Update the `aria-activedescendant` attribute.
16718 */
16719 function render() {
16720 var checked = (rgCtrl.getViewValue() == attr.value);
16721 if (checked === lastChecked) {
16722 return;
16723 }
16724
16725 lastChecked = checked;
16726 element.attr('aria-checked', checked);
16727
16728 if (checked) {
16729 markParentAsChecked(true);
16730 element.addClass(CHECKED_CSS);
16731
16732 rgCtrl.setActiveDescendant(element.attr('id'));
16733
16734 } else {
16735 markParentAsChecked(false);
16736 element.removeClass(CHECKED_CSS);
16737 }
16738
16739 /**
16740 * If the radioButton is inside a div, then add class so highlighting will work...
16741 */
16742 function markParentAsChecked(addClass ) {
16743 if ( element.parent()[0].nodeName != "MD-RADIO-GROUP") {
16744 element.parent()[ !!addClass ? 'addClass' : 'removeClass'](CHECKED_CSS);
16745 }
16746
16747 }
16748 }
16749
16750 /**
16751 * Inject ARIA-specific attributes appropriate for each radio button
16752 */
16753 function configureAria( element, scope ){
16754 scope.ariaId = buildAriaID();
16755
16756 element.attr({
16757 'id' : scope.ariaId,
16758 'role' : 'radio',
16759 'aria-checked' : 'false'
16760 });
16761
16762 $mdAria.expectWithText(element, 'aria-label');
16763
16764 /**
16765 * Build a unique ID for each radio button that will be used with aria-activedescendant.
16766 * Preserve existing ID if already specified.
16767 * @returns {*|string}
16768 */
16769 function buildAriaID() {
16770 return attr.id || ( 'radio' + "_" + $mdUtil.nextUid() );
16771 }
16772 }
16773 }
16774}
16775
16776})();
16777(function(){
16778"use strict";
16779
16780/**
16781 * @ngdoc module
16782 * @name material.components.select
16783 */
16784
16785/***************************************************
16786
16787 ### TODO - POST RC1 ###
16788 - [ ] Abstract placement logic in $mdSelect service to $mdMenu service
16789
16790 ***************************************************/
16791
16792SelectDirective.$inject = ["$mdSelect", "$mdUtil", "$mdConstant", "$mdTheming", "$mdAria", "$compile", "$parse"];
16793SelectMenuDirective.$inject = ["$parse", "$mdUtil", "$mdConstant", "$mdTheming"];
16794OptionDirective.$inject = ["$mdButtonInkRipple", "$mdUtil"];
16795SelectProvider.$inject = ["$$interimElementProvider"];
16796var SELECT_EDGE_MARGIN = 8;
16797var selectNextId = 0;
16798var CHECKBOX_SELECTION_INDICATOR =
16799 angular.element('<div class="md-container"><div class="md-icon"></div></div>');
16800
16801angular.module('material.components.select', [
16802 'material.core',
16803 'material.components.backdrop'
16804 ])
16805 .directive('mdSelect', SelectDirective)
16806 .directive('mdSelectMenu', SelectMenuDirective)
16807 .directive('mdOption', OptionDirective)
16808 .directive('mdOptgroup', OptgroupDirective)
16809 .directive('mdSelectHeader', SelectHeaderDirective)
16810 .provider('$mdSelect', SelectProvider);
16811
16812/**
16813 * @ngdoc directive
16814 * @name mdSelect
16815 * @restrict E
16816 * @module material.components.select
16817 *
16818 * @description Displays a select box, bound to an ng-model.
16819 *
16820 * When the select is required and uses a floating label, then the label will automatically contain
16821 * an asterisk (`*`). This behavior can be disabled by using the `md-no-asterisk` attribute.
16822 *
16823 * By default, the select will display with an underline to match other form elements. This can be
16824 * disabled by applying the `md-no-underline` CSS class.
16825 *
16826 * ### Option Params
16827 *
16828 * When applied, `md-option-empty` will mark the option as "empty" allowing the option to clear the
16829 * select and put it back in it's default state. You may supply this attribute on any option you
16830 * wish, however, it is automatically applied to an option whose `value` or `ng-value` are not
16831 * defined.
16832 *
16833 * **Automatically Applied**
16834 *
16835 * - `<md-option>`
16836 * - `<md-option value>`
16837 * - `<md-option value="">`
16838 * - `<md-option ng-value>`
16839 * - `<md-option ng-value="">`
16840 *
16841 * **NOT Automatically Applied**
16842 *
16843 * - `<md-option ng-value="1">`
16844 * - `<md-option ng-value="''">`
16845 * - `<md-option ng-value="undefined">`
16846 * - `<md-option value="undefined">` (this evaluates to the string `"undefined"`)
16847 * - <code ng-non-bindable><md-option ng-value="{{someValueThatMightBeUndefined}}"></code>
16848 *
16849 * **Note:** A value of `undefined` ***is considered a valid value*** (and does not auto-apply this
16850 * attribute) since you may wish this to be your "Not Available" or "None" option.
16851 *
16852 * **Note:** Using the `value` attribute (as opposed to `ng-value`) always evaluates to a string, so
16853 * `value="null"` will require the test `ng-if="myValue != 'null'"` rather than `ng-if="!myValue"`.
16854 *
16855 * @param {expression} ng-model The model!
16856 * @param {boolean=} multiple Whether it's multiple.
16857 * @param {expression=} md-on-close Expression to be evaluated when the select is closed.
16858 * @param {expression=} md-on-open Expression to be evaluated when opening the select.
16859 * Will hide the select options and show a spinner until the evaluated promise resolves.
16860 * @param {expression=} md-selected-text Expression to be evaluated that will return a string
16861 * to be displayed as a placeholder in the select input box when it is closed.
16862 * @param {string=} placeholder Placeholder hint text.
16863 * @param md-no-asterisk {boolean=} When set to true, an asterisk will not be appended to the
16864 * floating label. **Note:** This attribute is only evaluated once; it is not watched.
16865 * @param {string=} aria-label Optional label for accessibility. Only necessary if no placeholder or
16866 * explicit label is present.
16867 * @param {string=} md-container-class Class list to get applied to the `.md-select-menu-container`
16868 * element (for custom styling).
16869 *
16870 * @usage
16871 * With a placeholder (label and aria-label are added dynamically)
16872 * <hljs lang="html">
16873 * <md-input-container>
16874 * <md-select
16875 * ng-model="someModel"
16876 * placeholder="Select a state">
16877 * <md-option ng-value="opt" ng-repeat="opt in neighborhoods2">{{ opt }}</md-option>
16878 * </md-select>
16879 * </md-input-container>
16880 * </hljs>
16881 *
16882 * With an explicit label
16883 * <hljs lang="html">
16884 * <md-input-container>
16885 * <label>State</label>
16886 * <md-select
16887 * ng-model="someModel">
16888 * <md-option ng-value="opt" ng-repeat="opt in neighborhoods2">{{ opt }}</md-option>
16889 * </md-select>
16890 * </md-input-container>
16891 * </hljs>
16892 *
16893 * With a select-header
16894 *
16895 * When a developer needs to put more than just a text label in the
16896 * md-select-menu, they should use the md-select-header.
16897 * The user can put custom HTML inside of the header and style it to their liking.
16898 * One common use case of this would be a sticky search bar.
16899 *
16900 * When using the md-select-header the labels that would previously be added to the
16901 * OptGroupDirective are ignored.
16902 *
16903 * <hljs lang="html">
16904 * <md-input-container>
16905 * <md-select ng-model="someModel">
16906 * <md-select-header>
16907 * <span> Neighborhoods - </span>
16908 * </md-select-header>
16909 * <md-option ng-value="opt" ng-repeat="opt in neighborhoods2">{{ opt }}</md-option>
16910 * </md-select>
16911 * </md-input-container>
16912 * </hljs>
16913 *
16914 * ## Selects and object equality
16915 * When using a `md-select` to pick from a list of objects, it is important to realize how javascript handles
16916 * equality. Consider the following example:
16917 * <hljs lang="js">
16918 * angular.controller('MyCtrl', function($scope) {
16919 * $scope.users = [
16920 * { id: 1, name: 'Bob' },
16921 * { id: 2, name: 'Alice' },
16922 * { id: 3, name: 'Steve' }
16923 * ];
16924 * $scope.selectedUser = { id: 1, name: 'Bob' };
16925 * });
16926 * </hljs>
16927 * <hljs lang="html">
16928 * <div ng-controller="MyCtrl">
16929 * <md-select ng-model="selectedUser">
16930 * <md-option ng-value="user" ng-repeat="user in users">{{ user.name }}</md-option>
16931 * </md-select>
16932 * </div>
16933 * </hljs>
16934 *
16935 * At first one might expect that the select should be populated with "Bob" as the selected user. However,
16936 * this is not true. To determine whether something is selected,
16937 * `ngModelController` is looking at whether `$scope.selectedUser == (any user in $scope.users);`;
16938 *
16939 * Javascript's `==` operator does not check for deep equality (ie. that all properties
16940 * on the object are the same), but instead whether the objects are *the same object in memory*.
16941 * In this case, we have two instances of identical objects, but they exist in memory as unique
16942 * entities. Because of this, the select will have no value populated for a selected user.
16943 *
16944 * To get around this, `ngModelController` provides a `track by` option that allows us to specify a different
16945 * expression which will be used for the equality operator. As such, we can update our `html` to
16946 * make use of this by specifying the `ng-model-options="{trackBy: '$value.id'}"` on the `md-select`
16947 * element. This converts our equality expression to be
16948 * `$scope.selectedUser.id == (any id in $scope.users.map(function(u) { return u.id; }));`
16949 * which results in Bob being selected as desired.
16950 *
16951 * Working HTML:
16952 * <hljs lang="html">
16953 * <div ng-controller="MyCtrl">
16954 * <md-select ng-model="selectedUser" ng-model-options="{trackBy: '$value.id'}">
16955 * <md-option ng-value="user" ng-repeat="user in users">{{ user.name }}</md-option>
16956 * </md-select>
16957 * </div>
16958 * </hljs>
16959 */
16960function SelectDirective($mdSelect, $mdUtil, $mdConstant, $mdTheming, $mdAria, $compile, $parse) {
16961 var keyCodes = $mdConstant.KEY_CODE;
16962 var NAVIGATION_KEYS = [keyCodes.SPACE, keyCodes.ENTER, keyCodes.UP_ARROW, keyCodes.DOWN_ARROW];
16963
16964 return {
16965 restrict: 'E',
16966 require: ['^?mdInputContainer', 'mdSelect', 'ngModel', '?^form'],
16967 compile: compile,
16968 controller: function() {
16969 } // empty placeholder controller to be initialized in link
16970 };
16971
16972 function compile(element, attr) {
16973 // add the select value that will hold our placeholder or selected option value
16974 var valueEl = angular.element('<md-select-value><span></span></md-select-value>');
16975 valueEl.append('<span class="md-select-icon" aria-hidden="true"></span>');
16976 valueEl.addClass('md-select-value');
16977 if (!valueEl[0].hasAttribute('id')) {
16978 valueEl.attr('id', 'select_value_label_' + $mdUtil.nextUid());
16979 }
16980
16981 // There's got to be an md-content inside. If there's not one, let's add it.
16982 if (!element.find('md-content').length) {
16983 element.append(angular.element('<md-content>').append(element.contents()));
16984 }
16985
16986
16987 // Add progress spinner for md-options-loading
16988 if (attr.mdOnOpen) {
16989
16990 // Show progress indicator while loading async
16991 // Use ng-hide for `display:none` so the indicator does not interfere with the options list
16992 element
16993 .find('md-content')
16994 .prepend(angular.element(
16995 '<div>' +
16996 ' <md-progress-circular md-mode="indeterminate" ng-if="$$loadingAsyncDone === false" md-diameter="25px"></md-progress-circular>' +
16997 '</div>'
16998 ));
16999
17000 // Hide list [of item options] while loading async
17001 element
17002 .find('md-option')
17003 .attr('ng-show', '$$loadingAsyncDone');
17004 }
17005
17006 if (attr.name) {
17007 var autofillClone = angular.element('<select class="md-visually-hidden">');
17008 autofillClone.attr({
17009 'name': attr.name,
17010 'aria-hidden': 'true',
17011 'tabindex': '-1'
17012 });
17013 var opts = element.find('md-option');
17014 angular.forEach(opts, function(el) {
17015 var newEl = angular.element('<option>' + el.innerHTML + '</option>');
17016 if (el.hasAttribute('ng-value')) newEl.attr('ng-value', el.getAttribute('ng-value'));
17017 else if (el.hasAttribute('value')) newEl.attr('value', el.getAttribute('value'));
17018 autofillClone.append(newEl);
17019 });
17020
17021 // Adds an extra option that will hold the selected value for the
17022 // cases where the select is a part of a non-angular form. This can be done with a ng-model,
17023 // however if the `md-option` is being `ng-repeat`-ed, Angular seems to insert a similar
17024 // `option` node, but with a value of `? string: <value> ?` which would then get submitted.
17025 // This also goes around having to prepend a dot to the name attribute.
17026 autofillClone.append(
17027 '<option ng-value="' + attr.ngModel + '" selected></option>'
17028 );
17029
17030 element.parent().append(autofillClone);
17031 }
17032
17033 var isMultiple = $mdUtil.parseAttributeBoolean(attr.multiple);
17034
17035 // Use everything that's left inside element.contents() as the contents of the menu
17036 var multipleContent = isMultiple ? 'multiple' : '';
17037 var selectTemplate = '' +
17038 '<div class="md-select-menu-container" aria-hidden="true">' +
17039 '<md-select-menu {0}>{1}</md-select-menu>' +
17040 '</div>';
17041
17042 selectTemplate = $mdUtil.supplant(selectTemplate, [multipleContent, element.html()]);
17043 element.empty().append(valueEl);
17044 element.append(selectTemplate);
17045
17046 if(!attr.tabindex){
17047 attr.$set('tabindex', 0);
17048 }
17049
17050 return function postLink(scope, element, attr, ctrls) {
17051 var untouched = true;
17052 var isDisabled, ariaLabelBase;
17053
17054 var containerCtrl = ctrls[0];
17055 var mdSelectCtrl = ctrls[1];
17056 var ngModelCtrl = ctrls[2];
17057 var formCtrl = ctrls[3];
17058 // grab a reference to the select menu value label
17059 var valueEl = element.find('md-select-value');
17060 var isReadonly = angular.isDefined(attr.readonly);
17061 var disableAsterisk = $mdUtil.parseAttributeBoolean(attr.mdNoAsterisk);
17062
17063 if (disableAsterisk) {
17064 element.addClass('md-no-asterisk');
17065 }
17066
17067 if (containerCtrl) {
17068 var isErrorGetter = containerCtrl.isErrorGetter || function() {
17069 return ngModelCtrl.$invalid && (ngModelCtrl.$touched || (formCtrl && formCtrl.$submitted));
17070 };
17071
17072 if (containerCtrl.input) {
17073 // We ignore inputs that are in the md-select-header (one
17074 // case where this might be useful would be adding as searchbox)
17075 if (element.find('md-select-header').find('input')[0] !== containerCtrl.input[0]) {
17076 throw new Error("<md-input-container> can only have *one* child <input>, <textarea> or <select> element!");
17077 }
17078 }
17079
17080 containerCtrl.input = element;
17081 if (!containerCtrl.label) {
17082 $mdAria.expect(element, 'aria-label', element.attr('placeholder'));
17083 }
17084
17085 scope.$watch(isErrorGetter, containerCtrl.setInvalid);
17086 }
17087
17088 var selectContainer, selectScope, selectMenuCtrl;
17089
17090 findSelectContainer();
17091 $mdTheming(element);
17092
17093 if (formCtrl && angular.isDefined(attr.multiple)) {
17094 $mdUtil.nextTick(function() {
17095 var hasModelValue = ngModelCtrl.$modelValue || ngModelCtrl.$viewValue;
17096 if (hasModelValue) {
17097 formCtrl.$setPristine();
17098 }
17099 });
17100 }
17101
17102 var originalRender = ngModelCtrl.$render;
17103 ngModelCtrl.$render = function() {
17104 originalRender();
17105 syncLabelText();
17106 syncAriaLabel();
17107 inputCheckValue();
17108 };
17109
17110 attr.$observe('placeholder', ngModelCtrl.$render);
17111
17112 if (containerCtrl && containerCtrl.label) {
17113 attr.$observe('required', function (value) {
17114 // Toggle the md-required class on the input containers label, because the input container is automatically
17115 // applying the asterisk indicator on the label.
17116 containerCtrl.label.toggleClass('md-required', value && !disableAsterisk);
17117 });
17118 }
17119
17120 mdSelectCtrl.setLabelText = function(text) {
17121 mdSelectCtrl.setIsPlaceholder(!text);
17122
17123 if (attr.mdSelectedText) {
17124 text = $parse(attr.mdSelectedText)(scope);
17125 } else {
17126 // Use placeholder attribute, otherwise fallback to the md-input-container label
17127 var tmpPlaceholder = attr.placeholder ||
17128 (containerCtrl && containerCtrl.label ? containerCtrl.label.text() : '');
17129 text = text || tmpPlaceholder || '';
17130 }
17131
17132 var target = valueEl.children().eq(0);
17133 target.html(text);
17134 };
17135
17136 mdSelectCtrl.setIsPlaceholder = function(isPlaceholder) {
17137 if (isPlaceholder) {
17138 valueEl.addClass('md-select-placeholder');
17139 if (containerCtrl && containerCtrl.label) {
17140 containerCtrl.label.addClass('md-placeholder');
17141 }
17142 } else {
17143 valueEl.removeClass('md-select-placeholder');
17144 if (containerCtrl && containerCtrl.label) {
17145 containerCtrl.label.removeClass('md-placeholder');
17146 }
17147 }
17148 };
17149
17150 if (!isReadonly) {
17151 element
17152 .on('focus', function(ev) {
17153 // Always focus the container (if we have one) so floating labels and other styles are
17154 // applied properly
17155 containerCtrl && containerCtrl.setFocused(true);
17156 });
17157
17158 // Attach before ngModel's blur listener to stop propagation of blur event
17159 // to prevent from setting $touched.
17160 element.on('blur', function(event) {
17161 if (untouched) {
17162 untouched = false;
17163 if (selectScope._mdSelectIsOpen) {
17164 event.stopImmediatePropagation();
17165 }
17166 }
17167
17168 if (selectScope._mdSelectIsOpen) return;
17169 containerCtrl && containerCtrl.setFocused(false);
17170 inputCheckValue();
17171 });
17172 }
17173
17174 mdSelectCtrl.triggerClose = function() {
17175 $parse(attr.mdOnClose)(scope);
17176 };
17177
17178 scope.$$postDigest(function() {
17179 initAriaLabel();
17180 syncLabelText();
17181 syncAriaLabel();
17182 });
17183
17184 function initAriaLabel() {
17185 var labelText = element.attr('aria-label') || element.attr('placeholder');
17186 if (!labelText && containerCtrl && containerCtrl.label) {
17187 labelText = containerCtrl.label.text();
17188 }
17189 ariaLabelBase = labelText;
17190 $mdAria.expect(element, 'aria-label', labelText);
17191 }
17192
17193 scope.$watch(function() {
17194 return selectMenuCtrl.selectedLabels();
17195 }, syncLabelText);
17196
17197 function syncLabelText() {
17198 if (selectContainer) {
17199 selectMenuCtrl = selectMenuCtrl || selectContainer.find('md-select-menu').controller('mdSelectMenu');
17200 mdSelectCtrl.setLabelText(selectMenuCtrl.selectedLabels());
17201 }
17202 }
17203
17204 function syncAriaLabel() {
17205 if (!ariaLabelBase) return;
17206 var ariaLabels = selectMenuCtrl.selectedLabels({mode: 'aria'});
17207 element.attr('aria-label', ariaLabels.length ? ariaLabelBase + ': ' + ariaLabels : ariaLabelBase);
17208 }
17209
17210 var deregisterWatcher;
17211 attr.$observe('ngMultiple', function(val) {
17212 if (deregisterWatcher) deregisterWatcher();
17213 var parser = $parse(val);
17214 deregisterWatcher = scope.$watch(function() {
17215 return parser(scope);
17216 }, function(multiple, prevVal) {
17217 if (multiple === undefined && prevVal === undefined) return; // assume compiler did a good job
17218 if (multiple) {
17219 element.attr('multiple', 'multiple');
17220 } else {
17221 element.removeAttr('multiple');
17222 }
17223 element.attr('aria-multiselectable', multiple ? 'true' : 'false');
17224 if (selectContainer) {
17225 selectMenuCtrl.setMultiple(multiple);
17226 originalRender = ngModelCtrl.$render;
17227 ngModelCtrl.$render = function() {
17228 originalRender();
17229 syncLabelText();
17230 syncAriaLabel();
17231 inputCheckValue();
17232 };
17233 ngModelCtrl.$render();
17234 }
17235 });
17236 });
17237
17238 attr.$observe('disabled', function(disabled) {
17239 if (angular.isString(disabled)) {
17240 disabled = true;
17241 }
17242 // Prevent click event being registered twice
17243 if (isDisabled !== undefined && isDisabled === disabled) {
17244 return;
17245 }
17246 isDisabled = disabled;
17247 if (disabled) {
17248 element
17249 .attr({'aria-disabled': 'true'})
17250 .removeAttr('tabindex')
17251 .off('click', openSelect)
17252 .off('keydown', handleKeypress);
17253 } else {
17254 element
17255 .attr({'tabindex': attr.tabindex, 'aria-disabled': 'false'})
17256 .on('click', openSelect)
17257 .on('keydown', handleKeypress);
17258 }
17259 });
17260
17261 if (!attr.hasOwnProperty('disabled') && !attr.hasOwnProperty('ngDisabled')) {
17262 element.attr({'aria-disabled': 'false'});
17263 element.on('click', openSelect);
17264 element.on('keydown', handleKeypress);
17265 }
17266
17267 var ariaAttrs = {
17268 role: 'listbox',
17269 'aria-expanded': 'false',
17270 'aria-multiselectable': isMultiple && !attr.ngMultiple ? 'true' : 'false'
17271 };
17272
17273 if (!element[0].hasAttribute('id')) {
17274 ariaAttrs.id = 'select_' + $mdUtil.nextUid();
17275 }
17276
17277 var containerId = 'select_container_' + $mdUtil.nextUid();
17278 selectContainer.attr('id', containerId);
17279 ariaAttrs['aria-owns'] = containerId;
17280 element.attr(ariaAttrs);
17281
17282 scope.$on('$destroy', function() {
17283 $mdSelect
17284 .destroy()
17285 .finally(function() {
17286 if (containerCtrl) {
17287 containerCtrl.setFocused(false);
17288 containerCtrl.setHasValue(false);
17289 containerCtrl.input = null;
17290 }
17291 ngModelCtrl.$setTouched();
17292 });
17293 });
17294
17295
17296
17297 function inputCheckValue() {
17298 // The select counts as having a value if one or more options are selected,
17299 // or if the input's validity state says it has bad input (eg string in a number input)
17300 containerCtrl && containerCtrl.setHasValue(selectMenuCtrl.selectedLabels().length > 0 || (element[0].validity || {}).badInput);
17301 }
17302
17303 function findSelectContainer() {
17304 selectContainer = angular.element(
17305 element[0].querySelector('.md-select-menu-container')
17306 );
17307 selectScope = scope;
17308 if (attr.mdContainerClass) {
17309 var value = selectContainer[0].getAttribute('class') + ' ' + attr.mdContainerClass;
17310 selectContainer[0].setAttribute('class', value);
17311 }
17312 selectMenuCtrl = selectContainer.find('md-select-menu').controller('mdSelectMenu');
17313 selectMenuCtrl.init(ngModelCtrl, attr.ngModel);
17314 element.on('$destroy', function() {
17315 selectContainer.remove();
17316 });
17317 }
17318
17319 function handleKeypress(e) {
17320 if ($mdConstant.isNavigationKey(e)) {
17321 // prevent page scrolling on interaction
17322 e.preventDefault();
17323 openSelect(e);
17324 } else {
17325 if ($mdConstant.isInputKey(e) || $mdConstant.isNumPadKey(e)) {
17326 e.preventDefault();
17327
17328 var node = selectMenuCtrl.optNodeForKeyboardSearch(e);
17329 if (!node || node.hasAttribute('disabled')) return;
17330 var optionCtrl = angular.element(node).controller('mdOption');
17331 if (!selectMenuCtrl.isMultiple) {
17332 selectMenuCtrl.deselect(Object.keys(selectMenuCtrl.selected)[0]);
17333 }
17334 selectMenuCtrl.select(optionCtrl.hashKey, optionCtrl.value);
17335 selectMenuCtrl.refreshViewValue();
17336 }
17337 }
17338 }
17339
17340 function openSelect() {
17341 selectScope._mdSelectIsOpen = true;
17342 element.attr('aria-expanded', 'true');
17343
17344 $mdSelect.show({
17345 scope: selectScope,
17346 preserveScope: true,
17347 skipCompile: true,
17348 element: selectContainer,
17349 target: element[0],
17350 selectCtrl: mdSelectCtrl,
17351 preserveElement: true,
17352 hasBackdrop: true,
17353 loadingAsync: attr.mdOnOpen ? scope.$eval(attr.mdOnOpen) || true : false
17354 }).finally(function() {
17355 selectScope._mdSelectIsOpen = false;
17356 element.focus();
17357 element.attr('aria-expanded', 'false');
17358 ngModelCtrl.$setTouched();
17359 });
17360 }
17361
17362 };
17363 }
17364}
17365
17366function SelectMenuDirective($parse, $mdUtil, $mdConstant, $mdTheming) {
17367 // We want the scope to be set to 'false' so an isolated scope is not created
17368 // which would interfere with the md-select-header's access to the
17369 // parent scope.
17370 SelectMenuController.$inject = ["$scope", "$attrs", "$element"];
17371 return {
17372 restrict: 'E',
17373 require: ['mdSelectMenu'],
17374 scope: false,
17375 controller: SelectMenuController,
17376 link: {pre: preLink}
17377 };
17378
17379 // We use preLink instead of postLink to ensure that the select is initialized before
17380 // its child options run postLink.
17381 function preLink(scope, element, attr, ctrls) {
17382 var selectCtrl = ctrls[0];
17383
17384 element.addClass('_md'); // private md component indicator for styling
17385
17386 $mdTheming(element);
17387 element.on('click', clickListener);
17388 element.on('keypress', keyListener);
17389
17390 function keyListener(e) {
17391 if (e.keyCode == 13 || e.keyCode == 32) {
17392 clickListener(e);
17393 }
17394 }
17395
17396 function clickListener(ev) {
17397 var option = $mdUtil.getClosest(ev.target, 'md-option');
17398 var optionCtrl = option && angular.element(option).data('$mdOptionController');
17399 if (!option || !optionCtrl) return;
17400 if (option.hasAttribute('disabled')) {
17401 ev.stopImmediatePropagation();
17402 return false;
17403 }
17404
17405 var optionHashKey = selectCtrl.hashGetter(optionCtrl.value);
17406 var isSelected = angular.isDefined(selectCtrl.selected[optionHashKey]);
17407
17408 scope.$apply(function() {
17409 if (selectCtrl.isMultiple) {
17410 if (isSelected) {
17411 selectCtrl.deselect(optionHashKey);
17412 } else {
17413 selectCtrl.select(optionHashKey, optionCtrl.value);
17414 }
17415 } else {
17416 if (!isSelected) {
17417 selectCtrl.deselect(Object.keys(selectCtrl.selected)[0]);
17418 selectCtrl.select(optionHashKey, optionCtrl.value);
17419 }
17420 }
17421 selectCtrl.refreshViewValue();
17422 });
17423 }
17424 }
17425
17426 function SelectMenuController($scope, $attrs, $element) {
17427 var self = this;
17428 self.isMultiple = angular.isDefined($attrs.multiple);
17429 // selected is an object with keys matching all of the selected options' hashed values
17430 self.selected = {};
17431 // options is an object with keys matching every option's hash value,
17432 // and values matching every option's controller.
17433 self.options = {};
17434
17435 $scope.$watchCollection(function() {
17436 return self.options;
17437 }, function() {
17438 self.ngModel.$render();
17439 });
17440
17441 var deregisterCollectionWatch;
17442 var defaultIsEmpty;
17443 self.setMultiple = function(isMultiple) {
17444 var ngModel = self.ngModel;
17445 defaultIsEmpty = defaultIsEmpty || ngModel.$isEmpty;
17446
17447 self.isMultiple = isMultiple;
17448 if (deregisterCollectionWatch) deregisterCollectionWatch();
17449
17450 if (self.isMultiple) {
17451 ngModel.$validators['md-multiple'] = validateArray;
17452 ngModel.$render = renderMultiple;
17453
17454 // watchCollection on the model because by default ngModel only watches the model's
17455 // reference. This allowed the developer to also push and pop from their array.
17456 $scope.$watchCollection(self.modelBinding, function(value) {
17457 if (validateArray(value)) renderMultiple(value);
17458 self.ngModel.$setPristine();
17459 });
17460
17461 ngModel.$isEmpty = function(value) {
17462 return !value || value.length === 0;
17463 };
17464 } else {
17465 delete ngModel.$validators['md-multiple'];
17466 ngModel.$render = renderSingular;
17467 }
17468
17469 function validateArray(modelValue, viewValue) {
17470 // If a value is truthy but not an array, reject it.
17471 // If value is undefined/falsy, accept that it's an empty array.
17472 return angular.isArray(modelValue || viewValue || []);
17473 }
17474 };
17475
17476 var searchStr = '';
17477 var clearSearchTimeout, optNodes, optText;
17478 var CLEAR_SEARCH_AFTER = 300;
17479
17480 self.optNodeForKeyboardSearch = function(e) {
17481 clearSearchTimeout && clearTimeout(clearSearchTimeout);
17482 clearSearchTimeout = setTimeout(function() {
17483 clearSearchTimeout = undefined;
17484 searchStr = '';
17485 optText = undefined;
17486 optNodes = undefined;
17487 }, CLEAR_SEARCH_AFTER);
17488
17489 // Support 1-9 on numpad
17490 var keyCode = e.keyCode - ($mdConstant.isNumPadKey(e) ? 48 : 0);
17491
17492 searchStr += String.fromCharCode(keyCode);
17493 var search = new RegExp('^' + searchStr, 'i');
17494 if (!optNodes) {
17495 optNodes = $element.find('md-option');
17496 optText = new Array(optNodes.length);
17497 angular.forEach(optNodes, function(el, i) {
17498 optText[i] = el.textContent.trim();
17499 });
17500 }
17501 for (var i = 0; i < optText.length; ++i) {
17502 if (search.test(optText[i])) {
17503 return optNodes[i];
17504 }
17505 }
17506 };
17507
17508 self.init = function(ngModel, binding) {
17509 self.ngModel = ngModel;
17510 self.modelBinding = binding;
17511
17512 // Setup a more robust version of isEmpty to ensure value is a valid option
17513 self.ngModel.$isEmpty = function($viewValue) {
17514 // We have to transform the viewValue into the hashKey, because otherwise the
17515 // OptionCtrl may not exist. Developers may have specified a trackBy function.
17516 return !self.options[self.hashGetter($viewValue)];
17517 };
17518
17519 // Allow users to provide `ng-model="foo" ng-model-options="{trackBy: 'foo.id'}"` so
17520 // that we can properly compare objects set on the model to the available options
17521 if (ngModel.$options && ngModel.$options.trackBy) {
17522 var trackByLocals = {};
17523 var trackByParsed = $parse(ngModel.$options.trackBy);
17524 self.hashGetter = function(value, valueScope) {
17525 trackByLocals.$value = value;
17526 return trackByParsed(valueScope || $scope, trackByLocals);
17527 };
17528 // If the user doesn't provide a trackBy, we automatically generate an id for every
17529 // value passed in
17530 } else {
17531 self.hashGetter = function getHashValue(value) {
17532 if (angular.isObject(value)) {
17533 return 'object_' + (value.$$mdSelectId || (value.$$mdSelectId = ++selectNextId));
17534 }
17535 return value;
17536 };
17537 }
17538 self.setMultiple(self.isMultiple);
17539 };
17540
17541 self.selectedLabels = function(opts) {
17542 opts = opts || {};
17543 var mode = opts.mode || 'html';
17544 var selectedOptionEls = $mdUtil.nodesToArray($element[0].querySelectorAll('md-option[selected]'));
17545 if (selectedOptionEls.length) {
17546 var mapFn;
17547
17548 if (mode == 'html') {
17549 // Map the given element to its innerHTML string. If the element has a child ripple
17550 // container remove it from the HTML string, before returning the string.
17551 mapFn = function(el) {
17552 // If we do not have a `value` or `ng-value`, assume it is an empty option which clears the select
17553 if (el.hasAttribute('md-option-empty')) {
17554 return '';
17555 }
17556
17557 var html = el.innerHTML;
17558
17559 // Remove the ripple container from the selected option, copying it would cause a CSP violation.
17560 var rippleContainer = el.querySelector('.md-ripple-container');
17561 if (rippleContainer) {
17562 html = html.replace(rippleContainer.outerHTML, '');
17563 }
17564
17565 // Remove the checkbox container, because it will cause the label to wrap inside of the placeholder.
17566 // It should be not displayed inside of the label element.
17567 var checkboxContainer = el.querySelector('.md-container');
17568 if (checkboxContainer) {
17569 html = html.replace(checkboxContainer.outerHTML, '');
17570 }
17571
17572 return html;
17573 };
17574 } else if (mode == 'aria') {
17575 mapFn = function(el) { return el.hasAttribute('aria-label') ? el.getAttribute('aria-label') : el.textContent; };
17576 }
17577 return selectedOptionEls.map(mapFn).join(', ');
17578 } else {
17579 return '';
17580 }
17581 };
17582
17583 self.select = function(hashKey, hashedValue) {
17584 var option = self.options[hashKey];
17585 option && option.setSelected(true);
17586 self.selected[hashKey] = hashedValue;
17587 };
17588 self.deselect = function(hashKey) {
17589 var option = self.options[hashKey];
17590 option && option.setSelected(false);
17591 delete self.selected[hashKey];
17592 };
17593
17594 self.addOption = function(hashKey, optionCtrl) {
17595 if (angular.isDefined(self.options[hashKey])) {
17596 throw new Error('Duplicate md-option values are not allowed in a select. ' +
17597 'Duplicate value "' + optionCtrl.value + '" found.');
17598 }
17599
17600 self.options[hashKey] = optionCtrl;
17601
17602 // If this option's value was already in our ngModel, go ahead and select it.
17603 if (angular.isDefined(self.selected[hashKey])) {
17604 self.select(hashKey, optionCtrl.value);
17605
17606 // When the current $modelValue of the ngModel Controller is using the same hash as
17607 // the current option, which will be added, then we can be sure, that the validation
17608 // of the option has occurred before the option was added properly.
17609 // This means, that we have to manually trigger a new validation of the current option.
17610 if (angular.isDefined(self.ngModel.$modelValue) && self.hashGetter(self.ngModel.$modelValue) === hashKey) {
17611 self.ngModel.$validate();
17612 }
17613
17614 self.refreshViewValue();
17615 }
17616 };
17617 self.removeOption = function(hashKey) {
17618 delete self.options[hashKey];
17619 // Don't deselect an option when it's removed - the user's ngModel should be allowed
17620 // to have values that do not match a currently available option.
17621 };
17622
17623 self.refreshViewValue = function() {
17624 var values = [];
17625 var option;
17626 for (var hashKey in self.selected) {
17627 // If this hashKey has an associated option, push that option's value to the model.
17628 if ((option = self.options[hashKey])) {
17629 values.push(option.value);
17630 } else {
17631 // Otherwise, the given hashKey has no associated option, and we got it
17632 // from an ngModel value at an earlier time. Push the unhashed value of
17633 // this hashKey to the model.
17634 // This allows the developer to put a value in the model that doesn't yet have
17635 // an associated option.
17636 values.push(self.selected[hashKey]);
17637 }
17638 }
17639 var usingTrackBy = self.ngModel.$options && self.ngModel.$options.trackBy;
17640
17641 var newVal = self.isMultiple ? values : values[0];
17642 var prevVal = self.ngModel.$modelValue;
17643
17644 if (usingTrackBy ? !angular.equals(prevVal, newVal) : prevVal != newVal) {
17645 self.ngModel.$setViewValue(newVal);
17646 self.ngModel.$render();
17647 }
17648 };
17649
17650 function renderMultiple() {
17651 var newSelectedValues = self.ngModel.$modelValue || self.ngModel.$viewValue || [];
17652 if (!angular.isArray(newSelectedValues)) return;
17653
17654 var oldSelected = Object.keys(self.selected);
17655
17656 var newSelectedHashes = newSelectedValues.map(self.hashGetter);
17657 var deselected = oldSelected.filter(function(hash) {
17658 return newSelectedHashes.indexOf(hash) === -1;
17659 });
17660
17661 deselected.forEach(self.deselect);
17662 newSelectedHashes.forEach(function(hashKey, i) {
17663 self.select(hashKey, newSelectedValues[i]);
17664 });
17665 }
17666
17667 function renderSingular() {
17668 var value = self.ngModel.$viewValue || self.ngModel.$modelValue;
17669 Object.keys(self.selected).forEach(self.deselect);
17670 self.select(self.hashGetter(value), value);
17671 }
17672 }
17673
17674}
17675
17676function OptionDirective($mdButtonInkRipple, $mdUtil) {
17677
17678 OptionController.$inject = ["$element"];
17679 return {
17680 restrict: 'E',
17681 require: ['mdOption', '^^mdSelectMenu'],
17682 controller: OptionController,
17683 compile: compile
17684 };
17685
17686 function compile(element, attr) {
17687 // Manual transclusion to avoid the extra inner <span> that ng-transclude generates
17688 element.append(angular.element('<div class="md-text">').append(element.contents()));
17689
17690 element.attr('tabindex', attr.tabindex || '0');
17691
17692 if (!hasDefinedValue(attr)) {
17693 element.attr('md-option-empty', '');
17694 }
17695
17696 return postLink;
17697 }
17698
17699 function hasDefinedValue(attr) {
17700 var value = attr.value;
17701 var ngValue = attr.ngValue;
17702
17703 return value || ngValue;
17704 }
17705
17706 function postLink(scope, element, attr, ctrls) {
17707 var optionCtrl = ctrls[0];
17708 var selectCtrl = ctrls[1];
17709
17710 if (selectCtrl.isMultiple) {
17711 element.addClass('md-checkbox-enabled');
17712 element.prepend(CHECKBOX_SELECTION_INDICATOR.clone());
17713 }
17714
17715 if (angular.isDefined(attr.ngValue)) {
17716 scope.$watch(attr.ngValue, setOptionValue);
17717 } else if (angular.isDefined(attr.value)) {
17718 setOptionValue(attr.value);
17719 } else {
17720 scope.$watch(function() {
17721 return element.text().trim();
17722 }, setOptionValue);
17723 }
17724
17725 attr.$observe('disabled', function(disabled) {
17726 if (disabled) {
17727 element.attr('tabindex', '-1');
17728 } else {
17729 element.attr('tabindex', '0');
17730 }
17731 });
17732
17733 scope.$$postDigest(function() {
17734 attr.$observe('selected', function(selected) {
17735 if (!angular.isDefined(selected)) return;
17736 if (typeof selected == 'string') selected = true;
17737 if (selected) {
17738 if (!selectCtrl.isMultiple) {
17739 selectCtrl.deselect(Object.keys(selectCtrl.selected)[0]);
17740 }
17741 selectCtrl.select(optionCtrl.hashKey, optionCtrl.value);
17742 } else {
17743 selectCtrl.deselect(optionCtrl.hashKey);
17744 }
17745 selectCtrl.refreshViewValue();
17746 });
17747 });
17748
17749 $mdButtonInkRipple.attach(scope, element);
17750 configureAria();
17751
17752 function setOptionValue(newValue, oldValue, prevAttempt) {
17753 if (!selectCtrl.hashGetter) {
17754 if (!prevAttempt) {
17755 scope.$$postDigest(function() {
17756 setOptionValue(newValue, oldValue, true);
17757 });
17758 }
17759 return;
17760 }
17761 var oldHashKey = selectCtrl.hashGetter(oldValue, scope);
17762 var newHashKey = selectCtrl.hashGetter(newValue, scope);
17763
17764 optionCtrl.hashKey = newHashKey;
17765 optionCtrl.value = newValue;
17766
17767 selectCtrl.removeOption(oldHashKey, optionCtrl);
17768 selectCtrl.addOption(newHashKey, optionCtrl);
17769 }
17770
17771 scope.$on('$destroy', function() {
17772 selectCtrl.removeOption(optionCtrl.hashKey, optionCtrl);
17773 });
17774
17775 function configureAria() {
17776 var ariaAttrs = {
17777 'role': 'option',
17778 'aria-selected': 'false'
17779 };
17780
17781 if (!element[0].hasAttribute('id')) {
17782 ariaAttrs.id = 'select_option_' + $mdUtil.nextUid();
17783 }
17784 element.attr(ariaAttrs);
17785 }
17786 }
17787
17788 function OptionController($element) {
17789 this.selected = false;
17790 this.setSelected = function(isSelected) {
17791 if (isSelected && !this.selected) {
17792 $element.attr({
17793 'selected': 'selected',
17794 'aria-selected': 'true'
17795 });
17796 } else if (!isSelected && this.selected) {
17797 $element.removeAttr('selected');
17798 $element.attr('aria-selected', 'false');
17799 }
17800 this.selected = isSelected;
17801 };
17802 }
17803
17804}
17805
17806function OptgroupDirective() {
17807 return {
17808 restrict: 'E',
17809 compile: compile
17810 };
17811 function compile(el, attrs) {
17812 // If we have a select header element, we don't want to add the normal label
17813 // header.
17814 if (!hasSelectHeader()) {
17815 setupLabelElement();
17816 }
17817
17818 function hasSelectHeader() {
17819 return el.parent().find('md-select-header').length;
17820 }
17821
17822 function setupLabelElement() {
17823 var labelElement = el.find('label');
17824 if (!labelElement.length) {
17825 labelElement = angular.element('<label>');
17826 el.prepend(labelElement);
17827 }
17828 labelElement.addClass('md-container-ignore');
17829 if (attrs.label) labelElement.text(attrs.label);
17830 }
17831 }
17832}
17833
17834function SelectHeaderDirective() {
17835 return {
17836 restrict: 'E',
17837 };
17838}
17839
17840function SelectProvider($$interimElementProvider) {
17841 selectDefaultOptions.$inject = ["$mdSelect", "$mdConstant", "$mdUtil", "$window", "$q", "$$rAF", "$animateCss", "$animate", "$document"];
17842 return $$interimElementProvider('$mdSelect')
17843 .setDefaults({
17844 methods: ['target'],
17845 options: selectDefaultOptions
17846 });
17847
17848 /* @ngInject */
17849 function selectDefaultOptions($mdSelect, $mdConstant, $mdUtil, $window, $q, $$rAF, $animateCss, $animate, $document) {
17850 var ERROR_TARGET_EXPECTED = "$mdSelect.show() expected a target element in options.target but got '{0}'!";
17851 var animator = $mdUtil.dom.animator;
17852 var keyCodes = $mdConstant.KEY_CODE;
17853
17854 return {
17855 parent: 'body',
17856 themable: true,
17857 onShow: onShow,
17858 onRemove: onRemove,
17859 hasBackdrop: true,
17860 disableParentScroll: true
17861 };
17862
17863 /**
17864 * Interim-element onRemove logic....
17865 */
17866 function onRemove(scope, element, opts) {
17867 opts = opts || { };
17868 opts.cleanupInteraction();
17869 opts.cleanupResizing();
17870 opts.hideBackdrop();
17871
17872 // For navigation $destroy events, do a quick, non-animated removal,
17873 // but for normal closes (from clicks, etc) animate the removal
17874
17875 return (opts.$destroy === true) ? cleanElement() : animateRemoval().then( cleanElement );
17876
17877 /**
17878 * For normal closes (eg clicks), animate the removal.
17879 * For forced closes (like $destroy events from navigation),
17880 * skip the animations
17881 */
17882 function animateRemoval() {
17883 return $animateCss(element, {addClass: 'md-leave'}).start();
17884 }
17885
17886 /**
17887 * Restore the element to a closed state
17888 */
17889 function cleanElement() {
17890
17891 element.removeClass('md-active');
17892 element.attr('aria-hidden', 'true');
17893 element[0].style.display = 'none';
17894
17895 announceClosed(opts);
17896
17897 if (!opts.$destroy && opts.restoreFocus) {
17898 opts.target.focus();
17899 }
17900 }
17901
17902 }
17903
17904 /**
17905 * Interim-element onShow logic....
17906 */
17907 function onShow(scope, element, opts) {
17908
17909 watchAsyncLoad();
17910 sanitizeAndConfigure(scope, opts);
17911
17912 opts.hideBackdrop = showBackdrop(scope, element, opts);
17913
17914 return showDropDown(scope, element, opts)
17915 .then(function(response) {
17916 element.attr('aria-hidden', 'false');
17917 opts.alreadyOpen = true;
17918 opts.cleanupInteraction = activateInteraction();
17919 opts.cleanupResizing = activateResizing();
17920
17921 return response;
17922 }, opts.hideBackdrop);
17923
17924 // ************************************
17925 // Closure Functions
17926 // ************************************
17927
17928 /**
17929 * Attach the select DOM element(s) and animate to the correct positions
17930 * and scalings...
17931 */
17932 function showDropDown(scope, element, opts) {
17933 opts.parent.append(element);
17934
17935 return $q(function(resolve, reject) {
17936
17937 try {
17938
17939 $animateCss(element, {removeClass: 'md-leave', duration: 0})
17940 .start()
17941 .then(positionAndFocusMenu)
17942 .then(resolve);
17943
17944 } catch (e) {
17945 reject(e);
17946 }
17947
17948 });
17949 }
17950
17951 /**
17952 * Initialize container and dropDown menu positions/scale, then animate
17953 * to show... and autoFocus.
17954 */
17955 function positionAndFocusMenu() {
17956 return $q(function(resolve) {
17957 if (opts.isRemoved) return $q.reject(false);
17958
17959 var info = calculateMenuPositions(scope, element, opts);
17960
17961 info.container.element.css(animator.toCss(info.container.styles));
17962 info.dropDown.element.css(animator.toCss(info.dropDown.styles));
17963
17964 $$rAF(function() {
17965 element.addClass('md-active');
17966 info.dropDown.element.css(animator.toCss({transform: ''}));
17967
17968 autoFocus(opts.focusedNode);
17969 resolve();
17970 });
17971
17972 });
17973 }
17974
17975 /**
17976 * Show modal backdrop element...
17977 */
17978 function showBackdrop(scope, element, options) {
17979
17980 // If we are not within a dialog...
17981 if (options.disableParentScroll && !$mdUtil.getClosest(options.target, 'MD-DIALOG')) {
17982 // !! DO this before creating the backdrop; since disableScrollAround()
17983 // configures the scroll offset; which is used by mdBackDrop postLink()
17984 options.restoreScroll = $mdUtil.disableScrollAround(options.element, options.parent);
17985 } else {
17986 options.disableParentScroll = false;
17987 }
17988
17989 if (options.hasBackdrop) {
17990 // Override duration to immediately show invisible backdrop
17991 options.backdrop = $mdUtil.createBackdrop(scope, "md-select-backdrop md-click-catcher");
17992 $animate.enter(options.backdrop, $document[0].body, null, {duration: 0});
17993 }
17994
17995 /**
17996 * Hide modal backdrop element...
17997 */
17998 return function hideBackdrop() {
17999 if (options.backdrop) options.backdrop.remove();
18000 if (options.disableParentScroll) options.restoreScroll();
18001
18002 delete options.restoreScroll;
18003 };
18004 }
18005
18006 /**
18007 *
18008 */
18009 function autoFocus(focusedNode) {
18010 if (focusedNode && !focusedNode.hasAttribute('disabled')) {
18011 focusedNode.focus();
18012 }
18013 }
18014
18015 /**
18016 * Check for valid opts and set some sane defaults
18017 */
18018 function sanitizeAndConfigure(scope, options) {
18019 var selectEl = element.find('md-select-menu');
18020
18021 if (!options.target) {
18022 throw new Error($mdUtil.supplant(ERROR_TARGET_EXPECTED, [options.target]));
18023 }
18024
18025 angular.extend(options, {
18026 isRemoved: false,
18027 target: angular.element(options.target), //make sure it's not a naked dom node
18028 parent: angular.element(options.parent),
18029 selectEl: selectEl,
18030 contentEl: element.find('md-content'),
18031 optionNodes: selectEl[0].getElementsByTagName('md-option')
18032 });
18033 }
18034
18035 /**
18036 * Configure various resize listeners for screen changes
18037 */
18038 function activateResizing() {
18039 var debouncedOnResize = (function(scope, target, options) {
18040
18041 return function() {
18042 if (options.isRemoved) return;
18043
18044 var updates = calculateMenuPositions(scope, target, options);
18045 var container = updates.container;
18046 var dropDown = updates.dropDown;
18047
18048 container.element.css(animator.toCss(container.styles));
18049 dropDown.element.css(animator.toCss(dropDown.styles));
18050 };
18051
18052 })(scope, element, opts);
18053
18054 var window = angular.element($window);
18055 window.on('resize', debouncedOnResize);
18056 window.on('orientationchange', debouncedOnResize);
18057
18058 // Publish deactivation closure...
18059 return function deactivateResizing() {
18060
18061 // Disable resizing handlers
18062 window.off('resize', debouncedOnResize);
18063 window.off('orientationchange', debouncedOnResize);
18064 };
18065 }
18066
18067 /**
18068 * If asynchronously loading, watch and update internal
18069 * '$$loadingAsyncDone' flag
18070 */
18071 function watchAsyncLoad() {
18072 if (opts.loadingAsync && !opts.isRemoved) {
18073 scope.$$loadingAsyncDone = false;
18074
18075 $q.when(opts.loadingAsync)
18076 .then(function() {
18077 scope.$$loadingAsyncDone = true;
18078 delete opts.loadingAsync;
18079 }).then(function() {
18080 $$rAF(positionAndFocusMenu);
18081 });
18082 }
18083 }
18084
18085 /**
18086 *
18087 */
18088 function activateInteraction() {
18089 if (opts.isRemoved) return;
18090
18091 var dropDown = opts.selectEl;
18092 var selectCtrl = dropDown.controller('mdSelectMenu') || {};
18093
18094 element.addClass('md-clickable');
18095
18096 // Close on backdrop click
18097 opts.backdrop && opts.backdrop.on('click', onBackdropClick);
18098
18099 // Escape to close
18100 // Cycling of options, and closing on enter
18101 dropDown.on('keydown', onMenuKeyDown);
18102 dropDown.on('click', checkCloseMenu);
18103
18104 return function cleanupInteraction() {
18105 opts.backdrop && opts.backdrop.off('click', onBackdropClick);
18106 dropDown.off('keydown', onMenuKeyDown);
18107 dropDown.off('click', checkCloseMenu);
18108
18109 element.removeClass('md-clickable');
18110 opts.isRemoved = true;
18111 };
18112
18113 // ************************************
18114 // Closure Functions
18115 // ************************************
18116
18117 function onBackdropClick(e) {
18118 e.preventDefault();
18119 e.stopPropagation();
18120 opts.restoreFocus = false;
18121 $mdUtil.nextTick($mdSelect.hide, true);
18122 }
18123
18124 function onMenuKeyDown(ev) {
18125 ev.preventDefault();
18126 ev.stopPropagation();
18127
18128 switch (ev.keyCode) {
18129 case keyCodes.UP_ARROW:
18130 return focusPrevOption();
18131 case keyCodes.DOWN_ARROW:
18132 return focusNextOption();
18133 case keyCodes.SPACE:
18134 case keyCodes.ENTER:
18135 var option = $mdUtil.getClosest(ev.target, 'md-option');
18136 if (option) {
18137 dropDown.triggerHandler({
18138 type: 'click',
18139 target: option
18140 });
18141 ev.preventDefault();
18142 }
18143 checkCloseMenu(ev);
18144 break;
18145 case keyCodes.TAB:
18146 case keyCodes.ESCAPE:
18147 ev.stopPropagation();
18148 ev.preventDefault();
18149 opts.restoreFocus = true;
18150 $mdUtil.nextTick($mdSelect.hide, true);
18151 break;
18152 default:
18153 if ($mdConstant.isInputKey(ev) || $mdConstant.isNumPadKey(ev)) {
18154 var optNode = dropDown.controller('mdSelectMenu').optNodeForKeyboardSearch(ev);
18155 opts.focusedNode = optNode || opts.focusedNode;
18156 optNode && optNode.focus();
18157 }
18158 }
18159 }
18160
18161 function focusOption(direction) {
18162 var optionsArray = $mdUtil.nodesToArray(opts.optionNodes);
18163 var index = optionsArray.indexOf(opts.focusedNode);
18164
18165 var newOption;
18166
18167 do {
18168 if (index === -1) {
18169 // We lost the previously focused element, reset to first option
18170 index = 0;
18171 } else if (direction === 'next' && index < optionsArray.length - 1) {
18172 index++;
18173 } else if (direction === 'prev' && index > 0) {
18174 index--;
18175 }
18176 newOption = optionsArray[index];
18177 if (newOption.hasAttribute('disabled')) newOption = undefined;
18178 } while (!newOption && index < optionsArray.length - 1 && index > 0);
18179
18180 newOption && newOption.focus();
18181 opts.focusedNode = newOption;
18182 }
18183
18184 function focusNextOption() {
18185 focusOption('next');
18186 }
18187
18188 function focusPrevOption() {
18189 focusOption('prev');
18190 }
18191
18192 function checkCloseMenu(ev) {
18193 if (ev && ( ev.type == 'click') && (ev.currentTarget != dropDown[0])) return;
18194 if ( mouseOnScrollbar() ) return;
18195
18196 var option = $mdUtil.getClosest(ev.target, 'md-option');
18197 if (option && option.hasAttribute && !option.hasAttribute('disabled')) {
18198 ev.preventDefault();
18199 ev.stopPropagation();
18200 if (!selectCtrl.isMultiple) {
18201 opts.restoreFocus = true;
18202
18203 $mdUtil.nextTick(function () {
18204 $mdSelect.hide(selectCtrl.ngModel.$viewValue);
18205 }, true);
18206 }
18207 }
18208 /**
18209 * check if the mouseup event was on a scrollbar
18210 */
18211 function mouseOnScrollbar() {
18212 var clickOnScrollbar = false;
18213 if (ev && (ev.currentTarget.children.length > 0)) {
18214 var child = ev.currentTarget.children[0];
18215 var hasScrollbar = child.scrollHeight > child.clientHeight;
18216 if (hasScrollbar && child.children.length > 0) {
18217 var relPosX = ev.pageX - ev.currentTarget.getBoundingClientRect().left;
18218 if (relPosX > child.querySelector('md-option').offsetWidth)
18219 clickOnScrollbar = true;
18220 }
18221 }
18222 return clickOnScrollbar;
18223 }
18224 }
18225 }
18226
18227 }
18228
18229 /**
18230 * To notify listeners that the Select menu has closed,
18231 * trigger the [optional] user-defined expression
18232 */
18233 function announceClosed(opts) {
18234 var mdSelect = opts.selectCtrl;
18235 if (mdSelect) {
18236 var menuController = opts.selectEl.controller('mdSelectMenu');
18237 mdSelect.setLabelText(menuController ? menuController.selectedLabels() : '');
18238 mdSelect.triggerClose();
18239 }
18240 }
18241
18242
18243 /**
18244 * Calculate the
18245 */
18246 function calculateMenuPositions(scope, element, opts) {
18247 var
18248 containerNode = element[0],
18249 targetNode = opts.target[0].children[0], // target the label
18250 parentNode = $document[0].body,
18251 selectNode = opts.selectEl[0],
18252 contentNode = opts.contentEl[0],
18253 parentRect = parentNode.getBoundingClientRect(),
18254 targetRect = targetNode.getBoundingClientRect(),
18255 shouldOpenAroundTarget = false,
18256 bounds = {
18257 left: parentRect.left + SELECT_EDGE_MARGIN,
18258 top: SELECT_EDGE_MARGIN,
18259 bottom: parentRect.height - SELECT_EDGE_MARGIN,
18260 right: parentRect.width - SELECT_EDGE_MARGIN - ($mdUtil.floatingScrollbars() ? 16 : 0)
18261 },
18262 spaceAvailable = {
18263 top: targetRect.top - bounds.top,
18264 left: targetRect.left - bounds.left,
18265 right: bounds.right - (targetRect.left + targetRect.width),
18266 bottom: bounds.bottom - (targetRect.top + targetRect.height)
18267 },
18268 maxWidth = parentRect.width - SELECT_EDGE_MARGIN * 2,
18269 selectedNode = selectNode.querySelector('md-option[selected]'),
18270 optionNodes = selectNode.getElementsByTagName('md-option'),
18271 optgroupNodes = selectNode.getElementsByTagName('md-optgroup'),
18272 isScrollable = calculateScrollable(element, contentNode),
18273 centeredNode;
18274
18275 var loading = isPromiseLike(opts.loadingAsync);
18276 if (!loading) {
18277 // If a selected node, center around that
18278 if (selectedNode) {
18279 centeredNode = selectedNode;
18280 // If there are option groups, center around the first option group
18281 } else if (optgroupNodes.length) {
18282 centeredNode = optgroupNodes[0];
18283 // Otherwise - if we are not loading async - center around the first optionNode
18284 } else if (optionNodes.length) {
18285 centeredNode = optionNodes[0];
18286 // In case there are no options, center on whatever's in there... (eg progress indicator)
18287 } else {
18288 centeredNode = contentNode.firstElementChild || contentNode;
18289 }
18290 } else {
18291 // If loading, center on progress indicator
18292 centeredNode = contentNode.firstElementChild || contentNode;
18293 }
18294
18295 if (contentNode.offsetWidth > maxWidth) {
18296 contentNode.style['max-width'] = maxWidth + 'px';
18297 } else {
18298 contentNode.style.maxWidth = null;
18299 }
18300 if (shouldOpenAroundTarget) {
18301 contentNode.style['min-width'] = targetRect.width + 'px';
18302 }
18303
18304 // Remove padding before we compute the position of the menu
18305 if (isScrollable) {
18306 selectNode.classList.add('md-overflow');
18307 }
18308
18309 var focusedNode = centeredNode;
18310 if ((focusedNode.tagName || '').toUpperCase() === 'MD-OPTGROUP') {
18311 focusedNode = optionNodes[0] || contentNode.firstElementChild || contentNode;
18312 centeredNode = focusedNode;
18313 }
18314 // Cache for autoFocus()
18315 opts.focusedNode = focusedNode;
18316
18317 // Get the selectMenuRect *after* max-width is possibly set above
18318 containerNode.style.display = 'block';
18319 var selectMenuRect = selectNode.getBoundingClientRect();
18320 var centeredRect = getOffsetRect(centeredNode);
18321
18322 if (centeredNode) {
18323 var centeredStyle = $window.getComputedStyle(centeredNode);
18324 centeredRect.paddingLeft = parseInt(centeredStyle.paddingLeft, 10) || 0;
18325 centeredRect.paddingRight = parseInt(centeredStyle.paddingRight, 10) || 0;
18326 }
18327
18328 if (isScrollable) {
18329 var scrollBuffer = contentNode.offsetHeight / 2;
18330 contentNode.scrollTop = centeredRect.top + centeredRect.height / 2 - scrollBuffer;
18331
18332 if (spaceAvailable.top < scrollBuffer) {
18333 contentNode.scrollTop = Math.min(
18334 centeredRect.top,
18335 contentNode.scrollTop + scrollBuffer - spaceAvailable.top
18336 );
18337 } else if (spaceAvailable.bottom < scrollBuffer) {
18338 contentNode.scrollTop = Math.max(
18339 centeredRect.top + centeredRect.height - selectMenuRect.height,
18340 contentNode.scrollTop - scrollBuffer + spaceAvailable.bottom
18341 );
18342 }
18343 }
18344
18345 var left, top, transformOrigin, minWidth, fontSize;
18346 if (shouldOpenAroundTarget) {
18347 left = targetRect.left;
18348 top = targetRect.top + targetRect.height;
18349 transformOrigin = '50% 0';
18350 if (top + selectMenuRect.height > bounds.bottom) {
18351 top = targetRect.top - selectMenuRect.height;
18352 transformOrigin = '50% 100%';
18353 }
18354 } else {
18355 left = (targetRect.left + centeredRect.left - centeredRect.paddingLeft) + 2;
18356 top = Math.floor(targetRect.top + targetRect.height / 2 - centeredRect.height / 2 -
18357 centeredRect.top + contentNode.scrollTop) + 2;
18358
18359 transformOrigin = (centeredRect.left + targetRect.width / 2) + 'px ' +
18360 (centeredRect.top + centeredRect.height / 2 - contentNode.scrollTop) + 'px 0px';
18361
18362 minWidth = Math.min(targetRect.width + centeredRect.paddingLeft + centeredRect.paddingRight, maxWidth);
18363
18364 fontSize = window.getComputedStyle(targetNode)['font-size'];
18365 }
18366
18367 // Keep left and top within the window
18368 var containerRect = containerNode.getBoundingClientRect();
18369 var scaleX = Math.round(100 * Math.min(targetRect.width / selectMenuRect.width, 1.0)) / 100;
18370 var scaleY = Math.round(100 * Math.min(targetRect.height / selectMenuRect.height, 1.0)) / 100;
18371
18372 return {
18373 container: {
18374 element: angular.element(containerNode),
18375 styles: {
18376 left: Math.floor(clamp(bounds.left, left, bounds.right - containerRect.width)),
18377 top: Math.floor(clamp(bounds.top, top, bounds.bottom - containerRect.height)),
18378 'min-width': minWidth,
18379 'font-size': fontSize
18380 }
18381 },
18382 dropDown: {
18383 element: angular.element(selectNode),
18384 styles: {
18385 transformOrigin: transformOrigin,
18386 transform: !opts.alreadyOpen ? $mdUtil.supplant('scale({0},{1})', [scaleX, scaleY]) : ""
18387 }
18388 }
18389 };
18390
18391 }
18392
18393 }
18394
18395 function isPromiseLike(obj) {
18396 return obj && angular.isFunction(obj.then);
18397 }
18398
18399 function clamp(min, n, max) {
18400 return Math.max(min, Math.min(n, max));
18401 }
18402
18403 function getOffsetRect(node) {
18404 return node ? {
18405 left: node.offsetLeft,
18406 top: node.offsetTop,
18407 width: node.offsetWidth,
18408 height: node.offsetHeight
18409 } : {left: 0, top: 0, width: 0, height: 0};
18410 }
18411
18412 function calculateScrollable(element, contentNode) {
18413 var isScrollable = false;
18414
18415 try {
18416 var oldDisplay = element[0].style.display;
18417
18418 // Set the element's display to block so that this calculation is correct
18419 element[0].style.display = 'block';
18420
18421 isScrollable = contentNode.scrollHeight > contentNode.offsetHeight;
18422
18423 // Reset it back afterwards
18424 element[0].style.display = oldDisplay;
18425 } finally {
18426 // Nothing to do
18427 }
18428 return isScrollable;
18429 }
18430}
18431
18432})();
18433(function(){
18434"use strict";
18435
18436/**
18437 * @ngdoc module
18438 * @name material.components.showHide
18439 */
18440
18441// Add additional handlers to ng-show and ng-hide that notify directives
18442// contained within that they should recompute their size.
18443// These run in addition to Angular's built-in ng-hide and ng-show directives.
18444angular.module('material.components.showHide', [
18445 'material.core'
18446])
18447 .directive('ngShow', createDirective('ngShow', true))
18448 .directive('ngHide', createDirective('ngHide', false));
18449
18450
18451function createDirective(name, targetValue) {
18452 return ['$mdUtil', '$window', function($mdUtil, $window) {
18453 return {
18454 restrict: 'A',
18455 multiElement: true,
18456 link: function($scope, $element, $attr) {
18457 var unregister = $scope.$on('$md-resize-enable', function() {
18458 unregister();
18459
18460 var node = $element[0];
18461 var cachedTransitionStyles = node.nodeType === $window.Node.ELEMENT_NODE ?
18462 $window.getComputedStyle(node) : {};
18463
18464 $scope.$watch($attr[name], function(value) {
18465 if (!!value === targetValue) {
18466 $mdUtil.nextTick(function() {
18467 $scope.$broadcast('$md-resize');
18468 });
18469
18470 var opts = {
18471 cachedTransitionStyles: cachedTransitionStyles
18472 };
18473
18474 $mdUtil.dom.animator.waitTransitionEnd($element, opts).then(function() {
18475 $scope.$broadcast('$md-resize');
18476 });
18477 }
18478 });
18479 });
18480 }
18481 };
18482 }];
18483}
18484
18485})();
18486(function(){
18487"use strict";
18488
18489/**
18490 * @ngdoc module
18491 * @name material.components.sidenav
18492 *
18493 * @description
18494 * A Sidenav QP component.
18495 */
18496SidenavService.$inject = ["$mdComponentRegistry", "$mdUtil", "$q", "$log"];
18497SidenavDirective.$inject = ["$mdMedia", "$mdUtil", "$mdConstant", "$mdTheming", "$animate", "$compile", "$parse", "$log", "$q", "$document"];
18498SidenavController.$inject = ["$scope", "$element", "$attrs", "$mdComponentRegistry", "$q"];
18499angular
18500 .module('material.components.sidenav', [
18501 'material.core',
18502 'material.components.backdrop'
18503 ])
18504 .factory('$mdSidenav', SidenavService )
18505 .directive('mdSidenav', SidenavDirective)
18506 .directive('mdSidenavFocus', SidenavFocusDirective)
18507 .controller('$mdSidenavController', SidenavController);
18508
18509
18510/**
18511 * @ngdoc service
18512 * @name $mdSidenav
18513 * @module material.components.sidenav
18514 *
18515 * @description
18516 * `$mdSidenav` makes it easy to interact with multiple sidenavs
18517 * in an app. When looking up a sidenav instance, you can either look
18518 * it up synchronously or wait for it to be initializied asynchronously.
18519 * This is done by passing the second argument to `$mdSidenav`.
18520 *
18521 * @usage
18522 * <hljs lang="js">
18523 * // Async lookup for sidenav instance; will resolve when the instance is available
18524 * $mdSidenav(componentId, true).then(function(instance) {
18525 * $log.debug( componentId + "is now ready" );
18526 * });
18527 * // Sync lookup for sidenav instance; this will resolve immediately.
18528 * $mdSidenav(componentId).then(function(instance) {
18529 * $log.debug( componentId + "is now ready" );
18530 * });
18531 * // Async toggle the given sidenav;
18532 * // when instance is known ready and lazy lookup is not needed.
18533 * $mdSidenav(componentId)
18534 * .toggle()
18535 * .then(function(){
18536 * $log.debug('toggled');
18537 * });
18538 * // Async open the given sidenav
18539 * $mdSidenav(componentId)
18540 * .open()
18541 * .then(function(){
18542 * $log.debug('opened');
18543 * });
18544 * // Async close the given sidenav
18545 * $mdSidenav(componentId)
18546 * .close()
18547 * .then(function(){
18548 * $log.debug('closed');
18549 * });
18550 * // Sync check to see if the specified sidenav is set to be open
18551 * $mdSidenav(componentId).isOpen();
18552 * // Sync check to whether given sidenav is locked open
18553 * // If this is true, the sidenav will be open regardless of close()
18554 * $mdSidenav(componentId).isLockedOpen();
18555 * // On close callback to handle close, backdrop click or escape key pressed
18556 * // Callback happens BEFORE the close action occurs.
18557 * $mdSidenav(componentId).onClose(function () {
18558 * $log.debug('closing');
18559 * });
18560 * </hljs>
18561 */
18562function SidenavService($mdComponentRegistry, $mdUtil, $q, $log) {
18563 var errorMsg = "SideNav '{0}' is not available! Did you use md-component-id='{0}'?";
18564 var service = {
18565 find : findInstance, // sync - returns proxy API
18566 waitFor : waitForInstance // async - returns promise
18567 };
18568
18569 /**
18570 * Service API that supports three (3) usages:
18571 * $mdSidenav().find("left") // sync (must already exist) or returns undefined
18572 * $mdSidenav("left").toggle(); // sync (must already exist) or returns reject promise;
18573 * $mdSidenav("left",true).then( function(left){ // async returns instance when available
18574 * left.toggle();
18575 * });
18576 */
18577 return function(handle, enableWait) {
18578 if ( angular.isUndefined(handle) ) return service;
18579
18580 var shouldWait = enableWait === true;
18581 var instance = service.find(handle, shouldWait);
18582 return !instance && shouldWait ? service.waitFor(handle) :
18583 !instance && angular.isUndefined(enableWait) ? addLegacyAPI(service, handle) : instance;
18584 };
18585
18586 /**
18587 * For failed instance/handle lookups, older-clients expect an response object with noops
18588 * that include `rejected promise APIs`
18589 */
18590 function addLegacyAPI(service, handle) {
18591 var falseFn = function() { return false; };
18592 var rejectFn = function() {
18593 return $q.when($mdUtil.supplant(errorMsg, [handle || ""]));
18594 };
18595
18596 return angular.extend({
18597 isLockedOpen : falseFn,
18598 isOpen : falseFn,
18599 toggle : rejectFn,
18600 open : rejectFn,
18601 close : rejectFn,
18602 onClose : angular.noop,
18603 then : function(callback) {
18604 return waitForInstance(handle)
18605 .then(callback || angular.noop);
18606 }
18607 }, service);
18608 }
18609 /**
18610 * Synchronously lookup the controller instance for the specified sidNav instance which has been
18611 * registered with the markup `md-component-id`
18612 */
18613 function findInstance(handle, shouldWait) {
18614 var instance = $mdComponentRegistry.get(handle);
18615
18616 if (!instance && !shouldWait) {
18617
18618 // Report missing instance
18619 $log.error( $mdUtil.supplant(errorMsg, [handle || ""]) );
18620
18621 // The component has not registered itself... most like NOT yet created
18622 // return null to indicate that the Sidenav is not in the DOM
18623 return undefined;
18624 }
18625 return instance;
18626 }
18627
18628 /**
18629 * Asynchronously wait for the component instantiation,
18630 * Deferred lookup of component instance using $component registry
18631 */
18632 function waitForInstance(handle) {
18633 return $mdComponentRegistry.when(handle).catch($log.error);
18634 }
18635}
18636/**
18637 * @ngdoc directive
18638 * @name mdSidenavFocus
18639 * @module material.components.sidenav
18640 *
18641 * @restrict A
18642 *
18643 * @description
18644 * `mdSidenavFocus` provides a way to specify the focused element when a sidenav opens.
18645 * This is completely optional, as the sidenav itself is focused by default.
18646 *
18647 * @usage
18648 * <hljs lang="html">
18649 * <md-sidenav>
18650 * <form>
18651 * <md-input-container>
18652 * <label for="testInput">Label</label>
18653 * <input id="testInput" type="text" md-sidenav-focus>
18654 * </md-input-container>
18655 * </form>
18656 * </md-sidenav>
18657 * </hljs>
18658 **/
18659function SidenavFocusDirective() {
18660 return {
18661 restrict: 'A',
18662 require: '^mdSidenav',
18663 link: function(scope, element, attr, sidenavCtrl) {
18664 // @see $mdUtil.findFocusTarget(...)
18665 }
18666 };
18667}
18668/**
18669 * @ngdoc directive
18670 * @name mdSidenav
18671 * @module material.components.sidenav
18672 * @restrict E
18673 *
18674 * @description
18675 *
18676 * A Sidenav component that can be opened and closed programatically.
18677 *
18678 * By default, upon opening it will slide out on top of the main content area.
18679 *
18680 * For keyboard and screen reader accessibility, focus is sent to the sidenav wrapper by default.
18681 * It can be overridden with the `md-autofocus` directive on the child element you want focused.
18682 *
18683 * @usage
18684 * <hljs lang="html">
18685 * <div layout="row" ng-controller="MyController">
18686 * <md-sidenav md-component-id="left" class="md-sidenav-left">
18687 * Left Nav!
18688 * </md-sidenav>
18689 *
18690 * <md-content>
18691 * Center Content
18692 * <md-button ng-click="openLeftMenu()">
18693 * Open Left Menu
18694 * </md-button>
18695 * </md-content>
18696 *
18697 * <md-sidenav md-component-id="right"
18698 * md-is-locked-open="$mdMedia('min-width: 333px')"
18699 * class="md-sidenav-right">
18700 * <form>
18701 * <md-input-container>
18702 * <label for="testInput">Test input</label>
18703 * <input id="testInput" type="text"
18704 * ng-model="data" md-autofocus>
18705 * </md-input-container>
18706 * </form>
18707 * </md-sidenav>
18708 * </div>
18709 * </hljs>
18710 *
18711 * <hljs lang="js">
18712 * var app = angular.module('myApp', ['ngMaterial']);
18713 * app.controller('MyController', function($scope, $mdSidenav) {
18714 * $scope.openLeftMenu = function() {
18715 * $mdSidenav('left').toggle();
18716 * };
18717 * });
18718 * </hljs>
18719 *
18720 * @param {expression=} md-is-open A model bound to whether the sidenav is opened.
18721 * @param {boolean=} md-disable-backdrop When present in the markup, the sidenav will not show a backdrop.
18722 * @param {string=} md-component-id componentId to use with $mdSidenav service.
18723 * @param {expression=} md-is-locked-open When this expression evaluates to true,
18724 * the sidenav 'locks open': it falls into the content's flow instead
18725 * of appearing over it. This overrides the `md-is-open` attribute.
18726 * @param {string=} md-disable-scroll-target Selector, pointing to an element, whose scrolling will
18727 * be disabled when the sidenav is opened. By default this is the sidenav's direct parent.
18728 *
18729* The $mdMedia() service is exposed to the is-locked-open attribute, which
18730 * can be given a media query or one of the `sm`, `gt-sm`, `md`, `gt-md`, `lg` or `gt-lg` presets.
18731 * Examples:
18732 *
18733 * - `<md-sidenav md-is-locked-open="shouldLockOpen"></md-sidenav>`
18734 * - `<md-sidenav md-is-locked-open="$mdMedia('min-width: 1000px')"></md-sidenav>`
18735 * - `<md-sidenav md-is-locked-open="$mdMedia('sm')"></md-sidenav>` (locks open on small screens)
18736 */
18737function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $animate, $compile, $parse, $log, $q, $document) {
18738 return {
18739 restrict: 'E',
18740 scope: {
18741 isOpen: '=?mdIsOpen'
18742 },
18743 controller: '$mdSidenavController',
18744 compile: function(element) {
18745 element.addClass('md-closed');
18746 element.attr('tabIndex', '-1');
18747 return postLink;
18748 }
18749 };
18750
18751 /**
18752 * Directive Post Link function...
18753 */
18754 function postLink(scope, element, attr, sidenavCtrl) {
18755 var lastParentOverFlow;
18756 var backdrop;
18757 var disableScrollTarget = null;
18758 var triggeringElement = null;
18759 var previousContainerStyles;
18760 var promise = $q.when(true);
18761 var isLockedOpenParsed = $parse(attr.mdIsLockedOpen);
18762 var isLocked = function() {
18763 return isLockedOpenParsed(scope.$parent, {
18764 $media: function(arg) {
18765 $log.warn("$media is deprecated for is-locked-open. Use $mdMedia instead.");
18766 return $mdMedia(arg);
18767 },
18768 $mdMedia: $mdMedia
18769 });
18770 };
18771
18772 if (attr.mdDisableScrollTarget) {
18773 disableScrollTarget = $document[0].querySelector(attr.mdDisableScrollTarget);
18774
18775 if (disableScrollTarget) {
18776 disableScrollTarget = angular.element(disableScrollTarget);
18777 } else {
18778 $log.warn($mdUtil.supplant('mdSidenav: couldn\'t find element matching ' +
18779 'selector "{selector}". Falling back to parent.', { selector: attr.mdDisableScrollTarget }));
18780 }
18781 }
18782
18783 if (!disableScrollTarget) {
18784 disableScrollTarget = element.parent();
18785 }
18786
18787 // Only create the backdrop if the backdrop isn't disabled.
18788 if (!attr.hasOwnProperty('mdDisableBackdrop')) {
18789 backdrop = $mdUtil.createBackdrop(scope, "md-sidenav-backdrop md-opaque ng-enter");
18790 }
18791
18792 element.addClass('_md'); // private md component indicator for styling
18793 $mdTheming(element);
18794
18795 // The backdrop should inherit the sidenavs theme,
18796 // because the backdrop will take its parent theme by default.
18797 if ( backdrop ) $mdTheming.inherit(backdrop, element);
18798
18799 element.on('$destroy', function() {
18800 backdrop && backdrop.remove();
18801 sidenavCtrl.destroy();
18802 });
18803
18804 scope.$on('$destroy', function(){
18805 backdrop && backdrop.remove();
18806 });
18807
18808 scope.$watch(isLocked, updateIsLocked);
18809 scope.$watch('isOpen', updateIsOpen);
18810
18811
18812 // Publish special accessor for the Controller instance
18813 sidenavCtrl.$toggleOpen = toggleOpen;
18814
18815 /**
18816 * Toggle the DOM classes to indicate `locked`
18817 * @param isLocked
18818 */
18819 function updateIsLocked(isLocked, oldValue) {
18820 scope.isLockedOpen = isLocked;
18821 if (isLocked === oldValue) {
18822 element.toggleClass('md-locked-open', !!isLocked);
18823 } else {
18824 $animate[isLocked ? 'addClass' : 'removeClass'](element, 'md-locked-open');
18825 }
18826 if (backdrop) {
18827 backdrop.toggleClass('md-locked-open', !!isLocked);
18828 }
18829 }
18830
18831 /**
18832 * Toggle the SideNav view and attach/detach listeners
18833 * @param isOpen
18834 */
18835 function updateIsOpen(isOpen) {
18836 // Support deprecated md-sidenav-focus attribute as fallback
18837 var focusEl = $mdUtil.findFocusTarget(element) || $mdUtil.findFocusTarget(element,'[md-sidenav-focus]') || element;
18838 var parent = element.parent();
18839
18840 parent[isOpen ? 'on' : 'off']('keydown', onKeyDown);
18841 if (backdrop) backdrop[isOpen ? 'on' : 'off']('click', close);
18842
18843 var restorePositioning = updateContainerPositions(parent, isOpen);
18844
18845 if ( isOpen ) {
18846 // Capture upon opening..
18847 triggeringElement = $document[0].activeElement;
18848 }
18849
18850 disableParentScroll(isOpen);
18851
18852 return promise = $q.all([
18853 isOpen && backdrop ? $animate.enter(backdrop, parent) : backdrop ?
18854 $animate.leave(backdrop) : $q.when(true),
18855 $animate[isOpen ? 'removeClass' : 'addClass'](element, 'md-closed')
18856 ]).then(function() {
18857 // Perform focus when animations are ALL done...
18858 if (scope.isOpen) {
18859 focusEl && focusEl.focus();
18860 }
18861
18862 // Restores the positioning on the sidenav and backdrop.
18863 restorePositioning && restorePositioning();
18864 });
18865 }
18866
18867 function updateContainerPositions(parent, willOpen) {
18868 var drawerEl = element[0];
18869 var scrollTop = parent[0].scrollTop;
18870
18871 if (willOpen && scrollTop) {
18872 previousContainerStyles = {
18873 top: drawerEl.style.top,
18874 bottom: drawerEl.style.bottom,
18875 height: drawerEl.style.height
18876 };
18877
18878 // When the parent is scrolled down, then we want to be able to show the sidenav at the current scroll
18879 // position. We're moving the sidenav down to the correct scroll position and apply the height of the
18880 // parent, to increase the performance. Using 100% as height, will impact the performance heavily.
18881 var positionStyle = {
18882 top: scrollTop + 'px',
18883 bottom: 'auto',
18884 height: parent[0].clientHeight + 'px'
18885 };
18886
18887 // Apply the new position styles to the sidenav and backdrop.
18888 element.css(positionStyle);
18889 backdrop.css(positionStyle);
18890 }
18891
18892 // When the sidenav is closing and we have previous defined container styles,
18893 // then we return a restore function, which resets the sidenav and backdrop.
18894 if (!willOpen && previousContainerStyles) {
18895 return function() {
18896 drawerEl.style.top = previousContainerStyles.top;
18897 drawerEl.style.bottom = previousContainerStyles.bottom;
18898 drawerEl.style.height = previousContainerStyles.height;
18899
18900 backdrop[0].style.top = null;
18901 backdrop[0].style.bottom = null;
18902 backdrop[0].style.height = null;
18903
18904 previousContainerStyles = null;
18905 };
18906 }
18907 }
18908
18909 /**
18910 * Prevent parent scrolling (when the SideNav is open)
18911 */
18912 function disableParentScroll(disabled) {
18913 if ( disabled && !lastParentOverFlow ) {
18914 lastParentOverFlow = disableScrollTarget.css('overflow');
18915 disableScrollTarget.css('overflow', 'hidden');
18916 } else if (angular.isDefined(lastParentOverFlow)) {
18917 disableScrollTarget.css('overflow', lastParentOverFlow);
18918 lastParentOverFlow = undefined;
18919 }
18920 }
18921
18922 /**
18923 * Toggle the sideNav view and publish a promise to be resolved when
18924 * the view animation finishes.
18925 *
18926 * @param isOpen
18927 * @returns {*}
18928 */
18929 function toggleOpen( isOpen ) {
18930 if (scope.isOpen == isOpen ) {
18931
18932 return $q.when(true);
18933
18934 } else {
18935 if (scope.isOpen && sidenavCtrl.onCloseCb) sidenavCtrl.onCloseCb();
18936
18937 return $q(function(resolve){
18938 // Toggle value to force an async `updateIsOpen()` to run
18939 scope.isOpen = isOpen;
18940
18941 $mdUtil.nextTick(function() {
18942 // When the current `updateIsOpen()` animation finishes
18943 promise.then(function(result) {
18944
18945 if ( !scope.isOpen ) {
18946 // reset focus to originating element (if available) upon close
18947 triggeringElement && triggeringElement.focus();
18948 triggeringElement = null;
18949 }
18950
18951 resolve(result);
18952 });
18953 });
18954
18955 });
18956
18957 }
18958 }
18959
18960 /**
18961 * Auto-close sideNav when the `escape` key is pressed.
18962 * @param evt
18963 */
18964 function onKeyDown(ev) {
18965 var isEscape = (ev.keyCode === $mdConstant.KEY_CODE.ESCAPE);
18966 return isEscape ? close(ev) : $q.when(true);
18967 }
18968
18969 /**
18970 * With backdrop `clicks` or `escape` key-press, immediately
18971 * apply the CSS close transition... Then notify the controller
18972 * to close() and perform its own actions.
18973 */
18974 function close(ev) {
18975 ev.preventDefault();
18976
18977 return sidenavCtrl.close();
18978 }
18979
18980 }
18981}
18982
18983/*
18984 * @private
18985 * @ngdoc controller
18986 * @name SidenavController
18987 * @module material.components.sidenav
18988 *
18989 */
18990function SidenavController($scope, $element, $attrs, $mdComponentRegistry, $q) {
18991
18992 var self = this;
18993
18994 // Use Default internal method until overridden by directive postLink
18995
18996 // Synchronous getters
18997 self.isOpen = function() { return !!$scope.isOpen; };
18998 self.isLockedOpen = function() { return !!$scope.isLockedOpen; };
18999
19000 // Synchronous setters
19001 self.onClose = function (callback) {
19002 self.onCloseCb = callback;
19003 return self;
19004 };
19005
19006 // Async actions
19007 self.open = function() { return self.$toggleOpen( true ); };
19008 self.close = function() { return self.$toggleOpen( false ); };
19009 self.toggle = function() { return self.$toggleOpen( !$scope.isOpen ); };
19010 self.$toggleOpen = function(value) { return $q.when($scope.isOpen = value); };
19011
19012 self.destroy = $mdComponentRegistry.register(self, $attrs.mdComponentId);
19013}
19014
19015})();
19016(function(){
19017"use strict";
19018
19019 /**
19020 * @ngdoc module
19021 * @name material.components.slider
19022 */
19023SliderDirective.$inject = ["$$rAF", "$window", "$mdAria", "$mdUtil", "$mdConstant", "$mdTheming", "$mdGesture", "$parse", "$log", "$timeout"];
19024 angular.module('material.components.slider', [
19025 'material.core'
19026 ])
19027 .directive('mdSlider', SliderDirective)
19028 .directive('mdSliderContainer', SliderContainerDirective);
19029
19030/**
19031 * @ngdoc directive
19032 * @name mdSliderContainer
19033 * @module material.components.slider
19034 * @restrict E
19035 * @description
19036 * The `<md-slider-container>` contains slider with two other elements.
19037 *
19038 *
19039 * @usage
19040 * <h4>Normal Mode</h4>
19041 * <hljs lang="html">
19042 * </hljs>
19043 */
19044function SliderContainerDirective() {
19045 return {
19046 controller: function () {},
19047 compile: function (elem) {
19048 var slider = elem.find('md-slider');
19049
19050 if (!slider) {
19051 return;
19052 }
19053
19054 var vertical = slider.attr('md-vertical');
19055
19056 if (vertical !== undefined) {
19057 elem.attr('md-vertical', '');
19058 }
19059
19060 if(!slider.attr('flex')) {
19061 slider.attr('flex', '');
19062 }
19063
19064 return function postLink(scope, element, attr, ctrl) {
19065 element.addClass('_md'); // private md component indicator for styling
19066
19067 // We have to manually stop the $watch on ngDisabled because it exists
19068 // on the parent scope, and won't be automatically destroyed when
19069 // the component is destroyed.
19070 function setDisable(value) {
19071 element.children().attr('disabled', value);
19072 element.find('input').attr('disabled', value);
19073 }
19074
19075 var stopDisabledWatch = angular.noop;
19076
19077 if (attr.disabled) {
19078 setDisable(true);
19079 }
19080 else if (attr.ngDisabled) {
19081 stopDisabledWatch = scope.$watch(attr.ngDisabled, function (value) {
19082 setDisable(value);
19083 });
19084 }
19085
19086 scope.$on('$destroy', function () {
19087 stopDisabledWatch();
19088 });
19089
19090 var initialMaxWidth;
19091
19092 ctrl.fitInputWidthToTextLength = function (length) {
19093 var input = element[0].querySelector('md-input-container');
19094
19095 if (input) {
19096 var computedStyle = getComputedStyle(input);
19097 var minWidth = parseInt(computedStyle.minWidth);
19098 var padding = parseInt(computedStyle.padding) * 2;
19099
19100 initialMaxWidth = initialMaxWidth || parseInt(computedStyle.maxWidth);
19101 var newMaxWidth = Math.max(initialMaxWidth, minWidth + padding + (minWidth / 2 * length));
19102
19103 input.style.maxWidth = newMaxWidth + 'px';
19104 }
19105 };
19106 };
19107 }
19108 };
19109}
19110
19111/**
19112 * @ngdoc directive
19113 * @name mdSlider
19114 * @module material.components.slider
19115 * @restrict E
19116 * @description
19117 * The `<md-slider>` component allows the user to choose from a range of
19118 * values.
19119 *
19120 * As per the [material design spec](http://www.google.com/design/spec/style/color.html#color-ui-color-application)
19121 * the slider is in the accent color by default. The primary color palette may be used with
19122 * the `md-primary` class.
19123 *
19124 * It has two modes: 'normal' mode, where the user slides between a wide range
19125 * of values, and 'discrete' mode, where the user slides between only a few
19126 * select values.
19127 *
19128 * To enable discrete mode, add the `md-discrete` attribute to a slider,
19129 * and use the `step` attribute to change the distance between
19130 * values the user is allowed to pick.
19131 *
19132 * @usage
19133 * <h4>Normal Mode</h4>
19134 * <hljs lang="html">
19135 * <md-slider ng-model="myValue" min="5" max="500">
19136 * </md-slider>
19137 * </hljs>
19138 * <h4>Discrete Mode</h4>
19139 * <hljs lang="html">
19140 * <md-slider md-discrete ng-model="myDiscreteValue" step="10" min="10" max="130">
19141 * </md-slider>
19142 * </hljs>
19143 * <h4>Invert Mode</h4>
19144 * <hljs lang="html">
19145 * <md-slider md-invert ng-model="myValue" step="10" min="10" max="130">
19146 * </md-slider>
19147 * </hljs>
19148 *
19149 * @param {boolean=} md-discrete Whether to enable discrete mode.
19150 * @param {boolean=} md-invert Whether to enable invert mode.
19151 * @param {number=} step The distance between values the user is allowed to pick. Default 1.
19152 * @param {number=} min The minimum value the user is allowed to pick. Default 0.
19153 * @param {number=} max The maximum value the user is allowed to pick. Default 100.
19154 * @param {number=} round The amount of numbers after the decimal point, maximum is 6 to prevent scientific notation. Default 3.
19155 */
19156function SliderDirective($$rAF, $window, $mdAria, $mdUtil, $mdConstant, $mdTheming, $mdGesture, $parse, $log, $timeout) {
19157 return {
19158 scope: {},
19159 require: ['?ngModel', '?^mdSliderContainer'],
19160 template:
19161 '<div class="md-slider-wrapper">' +
19162 '<div class="md-slider-content">' +
19163 '<div class="md-track-container">' +
19164 '<div class="md-track"></div>' +
19165 '<div class="md-track md-track-fill"></div>' +
19166 '<div class="md-track-ticks"></div>' +
19167 '</div>' +
19168 '<div class="md-thumb-container">' +
19169 '<div class="md-thumb"></div>' +
19170 '<div class="md-focus-thumb"></div>' +
19171 '<div class="md-focus-ring"></div>' +
19172 '<div class="md-sign">' +
19173 '<span class="md-thumb-text"></span>' +
19174 '</div>' +
19175 '<div class="md-disabled-thumb"></div>' +
19176 '</div>' +
19177 '</div>' +
19178 '</div>',
19179 compile: compile
19180 };
19181
19182 // **********************************************************
19183 // Private Methods
19184 // **********************************************************
19185
19186 function compile (tElement, tAttrs) {
19187 var wrapper = angular.element(tElement[0].getElementsByClassName('md-slider-wrapper'));
19188
19189 var tabIndex = tAttrs.tabindex || 0;
19190 wrapper.attr('tabindex', tabIndex);
19191
19192 if (tAttrs.disabled || tAttrs.ngDisabled) wrapper.attr('tabindex', -1);
19193
19194 wrapper.attr('role', 'slider');
19195
19196 $mdAria.expect(tElement, 'aria-label');
19197
19198 return postLink;
19199 }
19200
19201 function postLink(scope, element, attr, ctrls) {
19202 $mdTheming(element);
19203 var ngModelCtrl = ctrls[0] || {
19204 // Mock ngModelController if it doesn't exist to give us
19205 // the minimum functionality needed
19206 $setViewValue: function(val) {
19207 this.$viewValue = val;
19208 this.$viewChangeListeners.forEach(function(cb) { cb(); });
19209 },
19210 $parsers: [],
19211 $formatters: [],
19212 $viewChangeListeners: []
19213 };
19214
19215 var containerCtrl = ctrls[1];
19216 var container = angular.element($mdUtil.getClosest(element, '_md-slider-container', true));
19217 var isDisabled = attr.ngDisabled ? angular.bind(null, $parse(attr.ngDisabled), scope.$parent) : function () {
19218 return element[0].hasAttribute('disabled');
19219 };
19220
19221 var thumb = angular.element(element[0].querySelector('.md-thumb'));
19222 var thumbText = angular.element(element[0].querySelector('.md-thumb-text'));
19223 var thumbContainer = thumb.parent();
19224 var trackContainer = angular.element(element[0].querySelector('.md-track-container'));
19225 var activeTrack = angular.element(element[0].querySelector('.md-track-fill'));
19226 var tickContainer = angular.element(element[0].querySelector('.md-track-ticks'));
19227 var wrapper = angular.element(element[0].getElementsByClassName('md-slider-wrapper'));
19228 var content = angular.element(element[0].getElementsByClassName('md-slider-content'));
19229 var throttledRefreshDimensions = $mdUtil.throttle(refreshSliderDimensions, 5000);
19230
19231 // Default values, overridable by attrs
19232 var DEFAULT_ROUND = 3;
19233 var vertical = angular.isDefined(attr.mdVertical);
19234 var discrete = angular.isDefined(attr.mdDiscrete);
19235 var invert = angular.isDefined(attr.mdInvert);
19236 angular.isDefined(attr.min) ? attr.$observe('min', updateMin) : updateMin(0);
19237 angular.isDefined(attr.max) ? attr.$observe('max', updateMax) : updateMax(100);
19238 angular.isDefined(attr.step)? attr.$observe('step', updateStep) : updateStep(1);
19239 angular.isDefined(attr.round)? attr.$observe('round', updateRound) : updateRound(DEFAULT_ROUND);
19240
19241 // We have to manually stop the $watch on ngDisabled because it exists
19242 // on the parent scope, and won't be automatically destroyed when
19243 // the component is destroyed.
19244 var stopDisabledWatch = angular.noop;
19245 if (attr.ngDisabled) {
19246 stopDisabledWatch = scope.$parent.$watch(attr.ngDisabled, updateAriaDisabled);
19247 }
19248
19249 $mdGesture.register(wrapper, 'drag', { horizontal: !vertical });
19250
19251 scope.mouseActive = false;
19252
19253 wrapper
19254 .on('keydown', keydownListener)
19255 .on('mousedown', mouseDownListener)
19256 .on('focus', focusListener)
19257 .on('blur', blurListener)
19258 .on('$md.pressdown', onPressDown)
19259 .on('$md.pressup', onPressUp)
19260 .on('$md.dragstart', onDragStart)
19261 .on('$md.drag', onDrag)
19262 .on('$md.dragend', onDragEnd);
19263
19264 // On resize, recalculate the slider's dimensions and re-render
19265 function updateAll() {
19266 refreshSliderDimensions();
19267 ngModelRender();
19268 }
19269 setTimeout(updateAll, 0);
19270
19271 var debouncedUpdateAll = $$rAF.throttle(updateAll);
19272 angular.element($window).on('resize', debouncedUpdateAll);
19273
19274 scope.$on('$destroy', function() {
19275 angular.element($window).off('resize', debouncedUpdateAll);
19276 });
19277
19278 ngModelCtrl.$render = ngModelRender;
19279 ngModelCtrl.$viewChangeListeners.push(ngModelRender);
19280 ngModelCtrl.$formatters.push(minMaxValidator);
19281 ngModelCtrl.$formatters.push(stepValidator);
19282
19283 /**
19284 * Attributes
19285 */
19286 var min;
19287 var max;
19288 var step;
19289 var round;
19290 function updateMin(value) {
19291 min = parseFloat(value);
19292 element.attr('aria-valuemin', value);
19293 updateAll();
19294 }
19295 function updateMax(value) {
19296 max = parseFloat(value);
19297 element.attr('aria-valuemax', value);
19298 updateAll();
19299 }
19300 function updateStep(value) {
19301 step = parseFloat(value);
19302 }
19303 function updateRound(value) {
19304 // Set max round digits to 6, after 6 the input uses scientific notation
19305 round = minMaxValidator(parseInt(value), 0, 6);
19306 }
19307 function updateAriaDisabled() {
19308 element.attr('aria-disabled', !!isDisabled());
19309 }
19310
19311 // Draw the ticks with canvas.
19312 // The alternative to drawing ticks with canvas is to draw one element for each tick,
19313 // which could quickly become a performance bottleneck.
19314 var tickCanvas, tickCtx;
19315 function redrawTicks() {
19316 if (!discrete || isDisabled()) return;
19317 if ( angular.isUndefined(step) ) return;
19318
19319 if ( step <= 0 ) {
19320 var msg = 'Slider step value must be greater than zero when in discrete mode';
19321 $log.error(msg);
19322 throw new Error(msg);
19323 }
19324
19325 var numSteps = Math.floor( (max - min) / step );
19326 if (!tickCanvas) {
19327 tickCanvas = angular.element('<canvas>').css('position', 'absolute');
19328 tickContainer.append(tickCanvas);
19329
19330 tickCtx = tickCanvas[0].getContext('2d');
19331 }
19332
19333 var dimensions = getSliderDimensions();
19334
19335 // If `dimensions` doesn't have height and width it might be the first attempt so we will refresh dimensions
19336 if (dimensions && !dimensions.height && !dimensions.width) {
19337 refreshSliderDimensions();
19338 dimensions = sliderDimensions;
19339 }
19340
19341 tickCanvas[0].width = dimensions.width;
19342 tickCanvas[0].height = dimensions.height;
19343
19344 var distance;
19345 for (var i = 0; i <= numSteps; i++) {
19346 var trackTicksStyle = $window.getComputedStyle(tickContainer[0]);
19347 tickCtx.fillStyle = trackTicksStyle.color || 'black';
19348
19349 distance = Math.floor((vertical ? dimensions.height : dimensions.width) * (i / numSteps));
19350
19351 tickCtx.fillRect(vertical ? 0 : distance - 1,
19352 vertical ? distance - 1 : 0,
19353 vertical ? dimensions.width : 2,
19354 vertical ? 2 : dimensions.height);
19355 }
19356 }
19357
19358 function clearTicks() {
19359 if(tickCanvas && tickCtx) {
19360 var dimensions = getSliderDimensions();
19361 tickCtx.clearRect(0, 0, dimensions.width, dimensions.height);
19362 }
19363 }
19364
19365 /**
19366 * Refreshing Dimensions
19367 */
19368 var sliderDimensions = {};
19369 refreshSliderDimensions();
19370 function refreshSliderDimensions() {
19371 sliderDimensions = trackContainer[0].getBoundingClientRect();
19372 }
19373 function getSliderDimensions() {
19374 throttledRefreshDimensions();
19375 return sliderDimensions;
19376 }
19377
19378 /**
19379 * left/right/up/down arrow listener
19380 */
19381 function keydownListener(ev) {
19382 if (isDisabled()) return;
19383
19384 var changeAmount;
19385 if (vertical ? ev.keyCode === $mdConstant.KEY_CODE.DOWN_ARROW : ev.keyCode === $mdConstant.KEY_CODE.LEFT_ARROW) {
19386 changeAmount = -step;
19387 } else if (vertical ? ev.keyCode === $mdConstant.KEY_CODE.UP_ARROW : ev.keyCode === $mdConstant.KEY_CODE.RIGHT_ARROW) {
19388 changeAmount = step;
19389 }
19390 changeAmount = invert ? -changeAmount : changeAmount;
19391 if (changeAmount) {
19392 if (ev.metaKey || ev.ctrlKey || ev.altKey) {
19393 changeAmount *= 4;
19394 }
19395 ev.preventDefault();
19396 ev.stopPropagation();
19397 scope.$evalAsync(function() {
19398 setModelValue(ngModelCtrl.$viewValue + changeAmount);
19399 });
19400 }
19401 }
19402
19403 function mouseDownListener() {
19404 redrawTicks();
19405
19406 scope.mouseActive = true;
19407 wrapper.removeClass('md-focused');
19408
19409 $timeout(function() {
19410 scope.mouseActive = false;
19411 }, 100);
19412 }
19413
19414 function focusListener() {
19415 if (scope.mouseActive === false) {
19416 wrapper.addClass('md-focused');
19417 }
19418 }
19419
19420 function blurListener() {
19421 wrapper.removeClass('md-focused');
19422 element.removeClass('md-active');
19423 clearTicks();
19424 }
19425
19426 /**
19427 * ngModel setters and validators
19428 */
19429 function setModelValue(value) {
19430 ngModelCtrl.$setViewValue( minMaxValidator(stepValidator(value)) );
19431 }
19432 function ngModelRender() {
19433 if (isNaN(ngModelCtrl.$viewValue)) {
19434 ngModelCtrl.$viewValue = ngModelCtrl.$modelValue;
19435 }
19436
19437 ngModelCtrl.$viewValue = minMaxValidator(ngModelCtrl.$viewValue);
19438
19439 var percent = valueToPercent(ngModelCtrl.$viewValue);
19440 scope.modelValue = ngModelCtrl.$viewValue;
19441 element.attr('aria-valuenow', ngModelCtrl.$viewValue);
19442 setSliderPercent(percent);
19443 thumbText.text( ngModelCtrl.$viewValue );
19444 }
19445
19446 function minMaxValidator(value, minValue, maxValue) {
19447 if (angular.isNumber(value)) {
19448 minValue = angular.isNumber(minValue) ? minValue : min;
19449 maxValue = angular.isNumber(maxValue) ? maxValue : max;
19450
19451 return Math.max(minValue, Math.min(maxValue, value));
19452 }
19453 }
19454
19455 function stepValidator(value) {
19456 if (angular.isNumber(value)) {
19457 var formattedValue = (Math.round((value - min) / step) * step + min);
19458 formattedValue = (Math.round(formattedValue * Math.pow(10, round)) / Math.pow(10, round));
19459
19460 if (containerCtrl && containerCtrl.fitInputWidthToTextLength){
19461 $mdUtil.debounce(function () {
19462 containerCtrl.fitInputWidthToTextLength(formattedValue.toString().length);
19463 }, 100)();
19464 }
19465
19466 return formattedValue;
19467 }
19468 }
19469
19470 /**
19471 * @param percent 0-1
19472 */
19473 function setSliderPercent(percent) {
19474
19475 percent = clamp(percent);
19476
19477 var thumbPosition = (percent * 100) + '%';
19478 var activeTrackPercent = invert ? (1 - percent) * 100 + '%' : thumbPosition;
19479
19480 if (vertical) {
19481 thumbContainer.css('bottom', thumbPosition);
19482 }
19483 else {
19484 $mdUtil.bidiProperty(thumbContainer, 'left', 'right', thumbPosition);
19485 }
19486
19487
19488 activeTrack.css(vertical ? 'height' : 'width', activeTrackPercent);
19489
19490 element.toggleClass((invert ? 'md-max' : 'md-min'), percent === 0);
19491 element.toggleClass((invert ? 'md-min' : 'md-max'), percent === 1);
19492 }
19493
19494 /**
19495 * Slide listeners
19496 */
19497 var isDragging = false;
19498
19499 function onPressDown(ev) {
19500 if (isDisabled()) return;
19501
19502 element.addClass('md-active');
19503 element[0].focus();
19504 refreshSliderDimensions();
19505
19506 var exactVal = percentToValue( positionToPercent( vertical ? ev.pointer.y : ev.pointer.x ));
19507 var closestVal = minMaxValidator( stepValidator(exactVal) );
19508 scope.$apply(function() {
19509 setModelValue( closestVal );
19510 setSliderPercent( valueToPercent(closestVal));
19511 });
19512 }
19513 function onPressUp(ev) {
19514 if (isDisabled()) return;
19515
19516 element.removeClass('md-dragging');
19517
19518 var exactVal = percentToValue( positionToPercent( vertical ? ev.pointer.y : ev.pointer.x ));
19519 var closestVal = minMaxValidator( stepValidator(exactVal) );
19520 scope.$apply(function() {
19521 setModelValue(closestVal);
19522 ngModelRender();
19523 });
19524 }
19525 function onDragStart(ev) {
19526 if (isDisabled()) return;
19527 isDragging = true;
19528
19529 ev.stopPropagation();
19530
19531 element.addClass('md-dragging');
19532 setSliderFromEvent(ev);
19533 }
19534 function onDrag(ev) {
19535 if (!isDragging) return;
19536 ev.stopPropagation();
19537 setSliderFromEvent(ev);
19538 }
19539 function onDragEnd(ev) {
19540 if (!isDragging) return;
19541 ev.stopPropagation();
19542 isDragging = false;
19543 }
19544
19545 function setSliderFromEvent(ev) {
19546 // While panning discrete, update only the
19547 // visual positioning but not the model value.
19548 if ( discrete ) adjustThumbPosition( vertical ? ev.pointer.y : ev.pointer.x );
19549 else doSlide( vertical ? ev.pointer.y : ev.pointer.x );
19550 }
19551
19552 /**
19553 * Slide the UI by changing the model value
19554 * @param x
19555 */
19556 function doSlide( x ) {
19557 scope.$evalAsync( function() {
19558 setModelValue( percentToValue( positionToPercent(x) ));
19559 });
19560 }
19561
19562 /**
19563 * Slide the UI without changing the model (while dragging/panning)
19564 * @param x
19565 */
19566 function adjustThumbPosition( x ) {
19567 var exactVal = percentToValue( positionToPercent( x ));
19568 var closestVal = minMaxValidator( stepValidator(exactVal) );
19569 setSliderPercent( positionToPercent(x) );
19570 thumbText.text( closestVal );
19571 }
19572
19573 /**
19574 * Clamps the value to be between 0 and 1.
19575 * @param {number} value The value to clamp.
19576 * @returns {number}
19577 */
19578 function clamp(value) {
19579 return Math.max(0, Math.min(value || 0, 1));
19580 }
19581
19582 /**
19583 * Convert position on slider to percentage value of offset from beginning...
19584 * @param position
19585 * @returns {number}
19586 */
19587 function positionToPercent( position ) {
19588 var offset = vertical ? sliderDimensions.top : sliderDimensions.left;
19589 var size = vertical ? sliderDimensions.height : sliderDimensions.width;
19590 var calc = (position - offset) / size;
19591
19592 if (!vertical && $mdUtil.bidi() === 'rtl') {
19593 calc = 1 - calc;
19594 }
19595
19596 return Math.max(0, Math.min(1, vertical ? 1 - calc : calc));
19597 }
19598
19599 /**
19600 * Convert percentage offset on slide to equivalent model value
19601 * @param percent
19602 * @returns {*}
19603 */
19604 function percentToValue( percent ) {
19605 var adjustedPercent = invert ? (1 - percent) : percent;
19606 return (min + adjustedPercent * (max - min));
19607 }
19608
19609 function valueToPercent( val ) {
19610 var percent = (val - min) / (max - min);
19611 return invert ? (1 - percent) : percent;
19612 }
19613 }
19614}
19615
19616})();
19617(function(){
19618"use strict";
19619
19620/**
19621 * @ngdoc module
19622 * @name material.components.sticky
19623 * @description
19624 * Sticky effects for md
19625 *
19626 */
19627MdSticky.$inject = ["$mdConstant", "$$rAF", "$mdUtil", "$compile"];
19628angular
19629 .module('material.components.sticky', [
19630 'material.core',
19631 'material.components.content'
19632 ])
19633 .factory('$mdSticky', MdSticky);
19634
19635/**
19636 * @ngdoc service
19637 * @name $mdSticky
19638 * @module material.components.sticky
19639 *
19640 * @description
19641 * The `$mdSticky`service provides a mixin to make elements sticky.
19642 *
19643 * Whenever the current browser supports stickiness natively, the `$mdSticky` service will just
19644 * use the native browser stickiness.
19645 *
19646 * By default the `$mdSticky` service compiles the cloned element, when not specified through the `elementClone`
19647 * parameter, in the same scope as the actual element lives.
19648 *
19649 *
19650 * <h3>Notes</h3>
19651 * When using an element which is containing a compiled directive, which changed its DOM structure during compilation,
19652 * you should compile the clone yourself using the plain template.<br/><br/>
19653 * See the right usage below:
19654 * <hljs lang="js">
19655 * angular.module('myModule')
19656 * .directive('stickySelect', function($mdSticky, $compile) {
19657 * var SELECT_TEMPLATE =
19658 * '<md-select ng-model="selected">' +
19659 * '<md-option>Option 1</md-option>' +
19660 * '</md-select>';
19661 *
19662 * return {
19663 * restrict: 'E',
19664 * replace: true,
19665 * template: SELECT_TEMPLATE,
19666 * link: function(scope,element) {
19667 * $mdSticky(scope, element, $compile(SELECT_TEMPLATE)(scope));
19668 * }
19669 * };
19670 * });
19671 * </hljs>
19672 *
19673 * @usage
19674 * <hljs lang="js">
19675 * angular.module('myModule')
19676 * .directive('stickyText', function($mdSticky, $compile) {
19677 * return {
19678 * restrict: 'E',
19679 * template: '<span>Sticky Text</span>',
19680 * link: function(scope,element) {
19681 * $mdSticky(scope, element);
19682 * }
19683 * };
19684 * });
19685 * </hljs>
19686 *
19687 * @returns A `$mdSticky` function that takes three arguments:
19688 * - `scope`
19689 * - `element`: The element that will be 'sticky'
19690 * - `elementClone`: A clone of the element, that will be shown
19691 * when the user starts scrolling past the original element.
19692 * If not provided, it will use the result of `element.clone()` and compiles it in the given scope.
19693 */
19694function MdSticky($mdConstant, $$rAF, $mdUtil, $compile) {
19695
19696 var browserStickySupport = $mdUtil.checkStickySupport();
19697
19698 /**
19699 * Registers an element as sticky, used internally by directives to register themselves
19700 */
19701 return function registerStickyElement(scope, element, stickyClone) {
19702 var contentCtrl = element.controller('mdContent');
19703 if (!contentCtrl) return;
19704
19705 if (browserStickySupport) {
19706 element.css({
19707 position: browserStickySupport,
19708 top: 0,
19709 'z-index': 2
19710 });
19711 } else {
19712 var $$sticky = contentCtrl.$element.data('$$sticky');
19713 if (!$$sticky) {
19714 $$sticky = setupSticky(contentCtrl);
19715 contentCtrl.$element.data('$$sticky', $$sticky);
19716 }
19717
19718 // Compile our cloned element, when cloned in this service, into the given scope.
19719 var cloneElement = stickyClone || $compile(element.clone())(scope);
19720
19721 var deregister = $$sticky.add(element, cloneElement);
19722 scope.$on('$destroy', deregister);
19723 }
19724 };
19725
19726 function setupSticky(contentCtrl) {
19727 var contentEl = contentCtrl.$element;
19728
19729 // Refresh elements is very expensive, so we use the debounced
19730 // version when possible.
19731 var debouncedRefreshElements = $$rAF.throttle(refreshElements);
19732
19733 // setupAugmentedScrollEvents gives us `$scrollstart` and `$scroll`,
19734 // more reliable than `scroll` on android.
19735 setupAugmentedScrollEvents(contentEl);
19736 contentEl.on('$scrollstart', debouncedRefreshElements);
19737 contentEl.on('$scroll', onScroll);
19738
19739 var self;
19740 return self = {
19741 prev: null,
19742 current: null, //the currently stickied item
19743 next: null,
19744 items: [],
19745 add: add,
19746 refreshElements: refreshElements
19747 };
19748
19749 /***************
19750 * Public
19751 ***************/
19752 // Add an element and its sticky clone to this content's sticky collection
19753 function add(element, stickyClone) {
19754 stickyClone.addClass('md-sticky-clone');
19755
19756 var item = {
19757 element: element,
19758 clone: stickyClone
19759 };
19760 self.items.push(item);
19761
19762 $mdUtil.nextTick(function() {
19763 contentEl.prepend(item.clone);
19764 });
19765
19766 debouncedRefreshElements();
19767
19768 return function remove() {
19769 self.items.forEach(function(item, index) {
19770 if (item.element[0] === element[0]) {
19771 self.items.splice(index, 1);
19772 item.clone.remove();
19773 }
19774 });
19775 debouncedRefreshElements();
19776 };
19777 }
19778
19779 function refreshElements() {
19780 // Sort our collection of elements by their current position in the DOM.
19781 // We need to do this because our elements' order of being added may not
19782 // be the same as their order of display.
19783 self.items.forEach(refreshPosition);
19784 self.items = self.items.sort(function(a, b) {
19785 return a.top < b.top ? -1 : 1;
19786 });
19787
19788 // Find which item in the list should be active,
19789 // based upon the content's current scroll position
19790 var item;
19791 var currentScrollTop = contentEl.prop('scrollTop');
19792 for (var i = self.items.length - 1; i >= 0; i--) {
19793 if (currentScrollTop > self.items[i].top) {
19794 item = self.items[i];
19795 break;
19796 }
19797 }
19798 setCurrentItem(item);
19799 }
19800
19801 /***************
19802 * Private
19803 ***************/
19804
19805 // Find the `top` of an item relative to the content element,
19806 // and also the height.
19807 function refreshPosition(item) {
19808 // Find the top of an item by adding to the offsetHeight until we reach the
19809 // content element.
19810 var current = item.element[0];
19811 item.top = 0;
19812 item.left = 0;
19813 item.right = 0;
19814 while (current && current !== contentEl[0]) {
19815 item.top += current.offsetTop;
19816 item.left += current.offsetLeft;
19817 if ( current.offsetParent ){
19818 item.right += current.offsetParent.offsetWidth - current.offsetWidth - current.offsetLeft; //Compute offsetRight
19819 }
19820 current = current.offsetParent;
19821 }
19822 item.height = item.element.prop('offsetHeight');
19823
19824 var defaultVal = $mdUtil.floatingScrollbars() ? '0' : undefined;
19825 $mdUtil.bidi(item.clone, 'margin-left', item.left, defaultVal);
19826 $mdUtil.bidi(item.clone, 'margin-right', defaultVal, item.right);
19827 }
19828
19829 // As we scroll, push in and select the correct sticky element.
19830 function onScroll() {
19831 var scrollTop = contentEl.prop('scrollTop');
19832 var isScrollingDown = scrollTop > (onScroll.prevScrollTop || 0);
19833
19834 // Store the previous scroll so we know which direction we are scrolling
19835 onScroll.prevScrollTop = scrollTop;
19836
19837 //
19838 // AT TOP (not scrolling)
19839 //
19840 if (scrollTop === 0) {
19841 // If we're at the top, just clear the current item and return
19842 setCurrentItem(null);
19843 return;
19844 }
19845
19846 //
19847 // SCROLLING DOWN (going towards the next item)
19848 //
19849 if (isScrollingDown) {
19850
19851 // If we've scrolled down past the next item's position, sticky it and return
19852 if (self.next && self.next.top <= scrollTop) {
19853 setCurrentItem(self.next);
19854 return;
19855 }
19856
19857 // If the next item is close to the current one, push the current one up out of the way
19858 if (self.current && self.next && self.next.top - scrollTop <= self.next.height) {
19859 translate(self.current, scrollTop + (self.next.top - self.next.height - scrollTop));
19860 return;
19861 }
19862 }
19863
19864 //
19865 // SCROLLING UP (not at the top & not scrolling down; must be scrolling up)
19866 //
19867 if (!isScrollingDown) {
19868
19869 // If we've scrolled up past the previous item's position, sticky it and return
19870 if (self.current && self.prev && scrollTop < self.current.top) {
19871 setCurrentItem(self.prev);
19872 return;
19873 }
19874
19875 // If the next item is close to the current one, pull the current one down into view
19876 if (self.next && self.current && (scrollTop >= (self.next.top - self.current.height))) {
19877 translate(self.current, scrollTop + (self.next.top - scrollTop - self.current.height));
19878 return;
19879 }
19880 }
19881
19882 //
19883 // Otherwise, just move the current item to the proper place (scrolling up or down)
19884 //
19885 if (self.current) {
19886 translate(self.current, scrollTop);
19887 }
19888 }
19889
19890 function setCurrentItem(item) {
19891 if (self.current === item) return;
19892 // Deactivate currently active item
19893 if (self.current) {
19894 translate(self.current, null);
19895 setStickyState(self.current, null);
19896 }
19897
19898 // Activate new item if given
19899 if (item) {
19900 setStickyState(item, 'active');
19901 }
19902
19903 self.current = item;
19904 var index = self.items.indexOf(item);
19905 // If index === -1, index + 1 = 0. It works out.
19906 self.next = self.items[index + 1];
19907 self.prev = self.items[index - 1];
19908 setStickyState(self.next, 'next');
19909 setStickyState(self.prev, 'prev');
19910 }
19911
19912 function setStickyState(item, state) {
19913 if (!item || item.state === state) return;
19914 if (item.state) {
19915 item.clone.attr('sticky-prev-state', item.state);
19916 item.element.attr('sticky-prev-state', item.state);
19917 }
19918 item.clone.attr('sticky-state', state);
19919 item.element.attr('sticky-state', state);
19920 item.state = state;
19921 }
19922
19923 function translate(item, amount) {
19924 if (!item) return;
19925 if (amount === null || amount === undefined) {
19926 if (item.translateY) {
19927 item.translateY = null;
19928 item.clone.css($mdConstant.CSS.TRANSFORM, '');
19929 }
19930 } else {
19931 item.translateY = amount;
19932
19933 $mdUtil.bidi( item.clone, $mdConstant.CSS.TRANSFORM,
19934 'translate3d(' + item.left + 'px,' + amount + 'px,0)',
19935 'translateY(' + amount + 'px)'
19936 );
19937 }
19938 }
19939 }
19940
19941
19942 // Android 4.4 don't accurately give scroll events.
19943 // To fix this problem, we setup a fake scroll event. We say:
19944 // > If a scroll or touchmove event has happened in the last DELAY milliseconds,
19945 // then send a `$scroll` event every animationFrame.
19946 // Additionally, we add $scrollstart and $scrollend events.
19947 function setupAugmentedScrollEvents(element) {
19948 var SCROLL_END_DELAY = 200;
19949 var isScrolling;
19950 var lastScrollTime;
19951 element.on('scroll touchmove', function() {
19952 if (!isScrolling) {
19953 isScrolling = true;
19954 $$rAF.throttle(loopScrollEvent);
19955 element.triggerHandler('$scrollstart');
19956 }
19957 element.triggerHandler('$scroll');
19958 lastScrollTime = +$mdUtil.now();
19959 });
19960
19961 function loopScrollEvent() {
19962 if (+$mdUtil.now() - lastScrollTime > SCROLL_END_DELAY) {
19963 isScrolling = false;
19964 element.triggerHandler('$scrollend');
19965 } else {
19966 element.triggerHandler('$scroll');
19967 $$rAF.throttle(loopScrollEvent);
19968 }
19969 }
19970 }
19971
19972}
19973
19974})();
19975(function(){
19976"use strict";
19977
19978/**
19979 * @ngdoc module
19980 * @name material.components.subheader
19981 * @description
19982 * SubHeader module
19983 *
19984 * Subheaders are special list tiles that delineate distinct sections of a
19985 * list or grid list and are typically related to the current filtering or
19986 * sorting criteria. Subheader tiles are either displayed inline with tiles or
19987 * can be associated with content, for example, in an adjacent column.
19988 *
19989 * Upon scrolling, subheaders remain pinned to the top of the screen and remain
19990 * pinned until pushed on or off screen by the next subheader. @see [Material
19991 * Design Specifications](https://www.google.com/design/spec/components/subheaders.html)
19992 *
19993 * > To improve the visual grouping of content, use the system color for your subheaders.
19994 *
19995 */
19996MdSubheaderDirective.$inject = ["$mdSticky", "$compile", "$mdTheming", "$mdUtil"];
19997angular
19998 .module('material.components.subheader', [
19999 'material.core',
20000 'material.components.sticky'
20001 ])
20002 .directive('mdSubheader', MdSubheaderDirective);
20003
20004/**
20005 * @ngdoc directive
20006 * @name mdSubheader
20007 * @module material.components.subheader
20008 *
20009 * @restrict E
20010 *
20011 * @description
20012 * The `md-subheader` directive creates a sticky subheader for a section.
20013 *
20014 * Developers are able to disable the stickiness of the subheader by using the following markup
20015 *
20016 * <hljs lang="html">
20017 * <md-subheader class="md-no-sticky">Not Sticky</md-subheader>
20018 * </hljs>
20019 *
20020 * ### Notes
20021 * - The `md-subheader` directive uses the <a ng-href="api/service/$mdSticky">$mdSticky</a> service
20022 * to make the subheader sticky.
20023 *
20024 * > Whenever the current browser doesn't support stickiness natively, the subheader
20025 * will be compiled twice to create a sticky clone of the subheader.
20026 *
20027 * @usage
20028 * <hljs lang="html">
20029 * <md-subheader>Online Friends</md-subheader>
20030 * </hljs>
20031 */
20032
20033function MdSubheaderDirective($mdSticky, $compile, $mdTheming, $mdUtil) {
20034 return {
20035 restrict: 'E',
20036 replace: true,
20037 transclude: true,
20038 template: (
20039 '<div class="md-subheader _md">' +
20040 ' <div class="md-subheader-inner">' +
20041 ' <div class="md-subheader-content"></div>' +
20042 ' </div>' +
20043 '</div>'
20044 ),
20045 link: function postLink(scope, element, attr, controllers, transclude) {
20046 $mdTheming(element);
20047 element.addClass('_md');
20048
20049 // Remove the ngRepeat attribute from the root element, because we don't want to compile
20050 // the ngRepeat for the sticky clone again.
20051 $mdUtil.prefixer().removeAttribute(element, 'ng-repeat');
20052
20053 var outerHTML = element[0].outerHTML;
20054
20055 function getContent(el) {
20056 return angular.element(el[0].querySelector('.md-subheader-content'));
20057 }
20058
20059 // Transclude the user-given contents of the subheader
20060 // the conventional way.
20061 transclude(scope, function(clone) {
20062 getContent(element).append(clone);
20063 });
20064
20065 // Create another clone, that uses the outer and inner contents
20066 // of the element, that will be 'stickied' as the user scrolls.
20067 if (!element.hasClass('md-no-sticky')) {
20068 transclude(scope, function(clone) {
20069 // If the user adds an ng-if or ng-repeat directly to the md-subheader element, the
20070 // compiled clone below will only be a comment tag (since they replace their elements with
20071 // a comment) which cannot be properly passed to the $mdSticky; so we wrap it in our own
20072 // DIV to ensure we have something $mdSticky can use
20073 var wrapper = $compile('<div class="md-subheader-wrapper">' + outerHTML + '</div>')(scope);
20074
20075 // Delay initialization until after any `ng-if`/`ng-repeat`/etc has finished before
20076 // attempting to create the clone
20077 $mdUtil.nextTick(function() {
20078 // Append our transcluded clone into the wrapper.
20079 // We don't have to recompile the element again, because the clone is already
20080 // compiled in it's transclusion scope. If we recompile the outerHTML of the new clone, we would lose
20081 // our ngIf's and other previous registered bindings / properties.
20082 getContent(wrapper).append(clone);
20083 });
20084
20085 // Make the element sticky and provide the stickyClone our self, to avoid recompilation of the subheader
20086 // element.
20087 $mdSticky(scope, element, wrapper);
20088 });
20089 }
20090 }
20091 };
20092}
20093
20094})();
20095(function(){
20096"use strict";
20097
20098/**
20099 * @ngdoc module
20100 * @name material.components.swipe
20101 * @description Swipe module!
20102 */
20103/**
20104 * @ngdoc directive
20105 * @module material.components.swipe
20106 * @name mdSwipeLeft
20107 *
20108 * @restrict A
20109 *
20110 * @description
20111 * The md-swipe-left directive allows you to specify custom behavior when an element is swiped
20112 * left.
20113 *
20114 * @usage
20115 * <hljs lang="html">
20116 * <div md-swipe-left="onSwipeLeft()">Swipe me left!</div>
20117 * </hljs>
20118 */
20119/**
20120 * @ngdoc directive
20121 * @module material.components.swipe
20122 * @name mdSwipeRight
20123 *
20124 * @restrict A
20125 *
20126 * @description
20127 * The md-swipe-right directive allows you to specify custom behavior when an element is swiped
20128 * right.
20129 *
20130 * @usage
20131 * <hljs lang="html">
20132 * <div md-swipe-right="onSwipeRight()">Swipe me right!</div>
20133 * </hljs>
20134 */
20135/**
20136 * @ngdoc directive
20137 * @module material.components.swipe
20138 * @name mdSwipeUp
20139 *
20140 * @restrict A
20141 *
20142 * @description
20143 * The md-swipe-up directive allows you to specify custom behavior when an element is swiped
20144 * up.
20145 *
20146 * @usage
20147 * <hljs lang="html">
20148 * <div md-swipe-up="onSwipeUp()">Swipe me up!</div>
20149 * </hljs>
20150 */
20151/**
20152 * @ngdoc directive
20153 * @module material.components.swipe
20154 * @name mdSwipeDown
20155 *
20156 * @restrict A
20157 *
20158 * @description
20159 * The md-swipe-down directive allows you to specify custom behavior when an element is swiped
20160 * down.
20161 *
20162 * @usage
20163 * <hljs lang="html">
20164 * <div md-swipe-down="onSwipDown()">Swipe me down!</div>
20165 * </hljs>
20166 */
20167
20168angular.module('material.components.swipe', ['material.core'])
20169 .directive('mdSwipeLeft', getDirective('SwipeLeft'))
20170 .directive('mdSwipeRight', getDirective('SwipeRight'))
20171 .directive('mdSwipeUp', getDirective('SwipeUp'))
20172 .directive('mdSwipeDown', getDirective('SwipeDown'));
20173
20174function getDirective(name) {
20175 DirectiveFactory.$inject = ["$parse"];
20176 var directiveName = 'md' + name;
20177 var eventName = '$md.' + name.toLowerCase();
20178
20179 return DirectiveFactory;
20180
20181 /* @ngInject */
20182 function DirectiveFactory($parse) {
20183 return { restrict: 'A', link: postLink };
20184 function postLink(scope, element, attr) {
20185 var fn = $parse(attr[directiveName]);
20186 element.on(eventName, function(ev) {
20187 scope.$applyAsync(function() { fn(scope, { $event: ev }); });
20188 });
20189 }
20190 }
20191}
20192
20193
20194
20195})();
20196(function(){
20197"use strict";
20198
20199/**
20200 * @ngdoc module
20201 * @name material.components.switch
20202 */
20203
20204MdSwitch.$inject = ["mdCheckboxDirective", "$mdUtil", "$mdConstant", "$parse", "$$rAF", "$mdGesture", "$timeout"];
20205angular.module('material.components.switch', [
20206 'material.core',
20207 'material.components.checkbox'
20208])
20209 .directive('mdSwitch', MdSwitch);
20210
20211/**
20212 * @ngdoc directive
20213 * @module material.components.switch
20214 * @name mdSwitch
20215 * @restrict E
20216 *
20217 * The switch directive is used very much like the normal [angular checkbox](https://docs.angularjs.org/api/ng/input/input%5Bcheckbox%5D).
20218 *
20219 * As per the [material design spec](http://www.google.com/design/spec/style/color.html#color-ui-color-application)
20220 * the switch is in the accent color by default. The primary color palette may be used with
20221 * the `md-primary` class.
20222 *
20223 * @param {string} ng-model Assignable angular expression to data-bind to.
20224 * @param {string=} name Property name of the form under which the control is published.
20225 * @param {expression=} ng-true-value The value to which the expression should be set when selected.
20226 * @param {expression=} ng-false-value The value to which the expression should be set when not selected.
20227 * @param {string=} ng-change Angular expression to be executed when input changes due to user interaction with the input element.
20228 * @param {expression=} ng-disabled En/Disable based on the expression.
20229 * @param {boolean=} md-no-ink Use of attribute indicates use of ripple ink effects.
20230 * @param {string=} aria-label Publish the button label used by screen-readers for accessibility. Defaults to the switch's text.
20231 *
20232 * @usage
20233 * <hljs lang="html">
20234 * <md-switch ng-model="isActive" aria-label="Finished?">
20235 * Finished ?
20236 * </md-switch>
20237 *
20238 * <md-switch md-no-ink ng-model="hasInk" aria-label="No Ink Effects">
20239 * No Ink Effects
20240 * </md-switch>
20241 *
20242 * <md-switch ng-disabled="true" ng-model="isDisabled" aria-label="Disabled">
20243 * Disabled
20244 * </md-switch>
20245 *
20246 * </hljs>
20247 */
20248function MdSwitch(mdCheckboxDirective, $mdUtil, $mdConstant, $parse, $$rAF, $mdGesture, $timeout) {
20249 var checkboxDirective = mdCheckboxDirective[0];
20250
20251 return {
20252 restrict: 'E',
20253 priority: 210, // Run before ngAria
20254 transclude: true,
20255 template:
20256 '<div class="md-container">' +
20257 '<div class="md-bar"></div>' +
20258 '<div class="md-thumb-container">' +
20259 '<div class="md-thumb" md-ink-ripple md-ink-ripple-checkbox></div>' +
20260 '</div>'+
20261 '</div>' +
20262 '<div ng-transclude class="md-label"></div>',
20263 require: '?ngModel',
20264 compile: mdSwitchCompile
20265 };
20266
20267 function mdSwitchCompile(element, attr) {
20268 var checkboxLink = checkboxDirective.compile(element, attr).post;
20269 // No transition on initial load.
20270 element.addClass('md-dragging');
20271
20272 return function (scope, element, attr, ngModel) {
20273 ngModel = ngModel || $mdUtil.fakeNgModel();
20274
20275 var disabledGetter = null;
20276 if (attr.disabled != null) {
20277 disabledGetter = function() { return true; };
20278 } else if (attr.ngDisabled) {
20279 disabledGetter = $parse(attr.ngDisabled);
20280 }
20281
20282 var thumbContainer = angular.element(element[0].querySelector('.md-thumb-container'));
20283 var switchContainer = angular.element(element[0].querySelector('.md-container'));
20284
20285 // no transition on initial load
20286 $$rAF(function() {
20287 element.removeClass('md-dragging');
20288 });
20289
20290 checkboxLink(scope, element, attr, ngModel);
20291
20292 if (disabledGetter) {
20293 scope.$watch(disabledGetter, function(isDisabled) {
20294 element.attr('tabindex', isDisabled ? -1 : 0);
20295 });
20296 }
20297
20298 // These events are triggered by setup drag
20299 $mdGesture.register(switchContainer, 'drag');
20300 switchContainer
20301 .on('$md.dragstart', onDragStart)
20302 .on('$md.drag', onDrag)
20303 .on('$md.dragend', onDragEnd);
20304
20305 var drag;
20306 function onDragStart(ev) {
20307 // Don't go if the switch is disabled.
20308 if (disabledGetter && disabledGetter(scope)) return;
20309 ev.stopPropagation();
20310
20311 element.addClass('md-dragging');
20312 drag = {width: thumbContainer.prop('offsetWidth')};
20313 }
20314
20315 function onDrag(ev) {
20316 if (!drag) return;
20317 ev.stopPropagation();
20318 ev.srcEvent && ev.srcEvent.preventDefault();
20319
20320 var percent = ev.pointer.distanceX / drag.width;
20321
20322 //if checked, start from right. else, start from left
20323 var translate = ngModel.$viewValue ? 1 + percent : percent;
20324 // Make sure the switch stays inside its bounds, 0-1%
20325 translate = Math.max(0, Math.min(1, translate));
20326
20327 thumbContainer.css($mdConstant.CSS.TRANSFORM, 'translate3d(' + (100*translate) + '%,0,0)');
20328 drag.translate = translate;
20329 }
20330
20331 function onDragEnd(ev) {
20332 if (!drag) return;
20333 ev.stopPropagation();
20334
20335 element.removeClass('md-dragging');
20336 thumbContainer.css($mdConstant.CSS.TRANSFORM, '');
20337
20338 // We changed if there is no distance (this is a click a click),
20339 // or if the drag distance is >50% of the total.
20340 var isChanged = ngModel.$viewValue ? drag.translate < 0.5 : drag.translate > 0.5;
20341 if (isChanged) {
20342 applyModelValue(!ngModel.$viewValue);
20343 }
20344 drag = null;
20345
20346 // Wait for incoming mouse click
20347 scope.skipToggle = true;
20348 $timeout(function() {
20349 scope.skipToggle = false;
20350 }, 1);
20351 }
20352
20353 function applyModelValue(newValue) {
20354 scope.$apply(function() {
20355 ngModel.$setViewValue(newValue);
20356 ngModel.$render();
20357 });
20358 }
20359
20360 };
20361 }
20362
20363
20364}
20365
20366})();
20367(function(){
20368"use strict";
20369
20370/**
20371 * @ngdoc module
20372 * @name material.components.tabs
20373 * @description
20374 *
20375 * Tabs, created with the `<md-tabs>` directive provide *tabbed* navigation with different styles.
20376 * The Tabs component consists of clickable tabs that are aligned horizontally side-by-side.
20377 *
20378 * Features include support for:
20379 *
20380 * - static or dynamic tabs,
20381 * - responsive designs,
20382 * - accessibility support (ARIA),
20383 * - tab pagination,
20384 * - external or internal tab content,
20385 * - focus indicators and arrow-key navigations,
20386 * - programmatic lookup and access to tab controllers, and
20387 * - dynamic transitions through different tab contents.
20388 *
20389 */
20390/*
20391 * @see js folder for tabs implementation
20392 */
20393angular.module('material.components.tabs', [
20394 'material.core',
20395 'material.components.icon'
20396]);
20397
20398})();
20399(function(){
20400"use strict";
20401
20402/**
20403 * @ngdoc module
20404 * @name material.components.toast
20405 * @description
20406 * Toast
20407 */
20408MdToastDirective.$inject = ["$mdToast"];
20409MdToastProvider.$inject = ["$$interimElementProvider"];
20410angular.module('material.components.toast', [
20411 'material.core',
20412 'material.components.button'
20413])
20414 .directive('mdToast', MdToastDirective)
20415 .provider('$mdToast', MdToastProvider);
20416
20417/* @ngInject */
20418function MdToastDirective($mdToast) {
20419 return {
20420 restrict: 'E',
20421 link: function postLink(scope, element) {
20422 element.addClass('_md'); // private md component indicator for styling
20423
20424 // When navigation force destroys an interimElement, then
20425 // listen and $destroy() that interim instance...
20426 scope.$on('$destroy', function() {
20427 $mdToast.destroy();
20428 });
20429 }
20430 };
20431}
20432
20433/**
20434 * @ngdoc service
20435 * @name $mdToast
20436 * @module material.components.toast
20437 *
20438 * @description
20439 * `$mdToast` is a service to build a toast notification on any position
20440 * on the screen with an optional duration, and provides a simple promise API.
20441 *
20442 * The toast will be always positioned at the `bottom`, when the screen size is
20443 * between `600px` and `959px` (`sm` breakpoint)
20444 *
20445 * ## Restrictions on custom toasts
20446 * - The toast's template must have an outer `<md-toast>` element.
20447 * - For a toast action, use element with class `md-action`.
20448 * - Add the class `md-capsule` for curved corners.
20449 *
20450 * ### Custom Presets
20451 * Developers are also able to create their own preset, which can be easily used without repeating
20452 * their options each time.
20453 *
20454 * <hljs lang="js">
20455 * $mdToastProvider.addPreset('testPreset', {
20456 * options: function() {
20457 * return {
20458 * template:
20459 * '<md-toast>' +
20460 * '<div class="md-toast-content">' +
20461 * 'This is a custom preset' +
20462 * '</div>' +
20463 * '</md-toast>',
20464 * controllerAs: 'toast',
20465 * bindToController: true
20466 * };
20467 * }
20468 * });
20469 * </hljs>
20470 *
20471 * After you created your preset at config phase, you can easily access it.
20472 *
20473 * <hljs lang="js">
20474 * $mdToast.show(
20475 * $mdToast.testPreset()
20476 * );
20477 * </hljs>
20478 *
20479 * ## Parent container notes
20480 *
20481 * The toast is positioned using absolute positioning relative to its first non-static parent
20482 * container. Thus, if the requested parent container uses static positioning, we will temporarily
20483 * set its positioning to `relative` while the toast is visible and reset it when the toast is
20484 * hidden.
20485 *
20486 * Because of this, it is usually best to ensure that the parent container has a fixed height and
20487 * prevents scrolling by setting the `overflow: hidden;` style. Since the position is based off of
20488 * the parent's height, the toast may be mispositioned if you allow the parent to scroll.
20489 *
20490 * You can, however, have a scrollable element inside of the container; just make sure the
20491 * container itself does not scroll.
20492 *
20493 * <hljs lang="html">
20494 * <div layout-fill id="toast-container">
20495 * <md-content>
20496 * I can have lots of content and scroll!
20497 * </md-content>
20498 * </div>
20499 * </hljs>
20500 *
20501 * @usage
20502 * <hljs lang="html">
20503 * <div ng-controller="MyController">
20504 * <md-button ng-click="openToast()">
20505 * Open a Toast!
20506 * </md-button>
20507 * </div>
20508 * </hljs>
20509 *
20510 * <hljs lang="js">
20511 * var app = angular.module('app', ['ngMaterial']);
20512 * app.controller('MyController', function($scope, $mdToast) {
20513 * $scope.openToast = function($event) {
20514 * $mdToast.show($mdToast.simple().textContent('Hello!'));
20515 * // Could also do $mdToast.showSimple('Hello');
20516 * };
20517 * });
20518 * </hljs>
20519 */
20520
20521/**
20522 * @ngdoc method
20523 * @name $mdToast#showSimple
20524 *
20525 * @param {string} message The message to display inside the toast
20526 * @description
20527 * Convenience method which builds and shows a simple toast.
20528 *
20529 * @returns {promise} A promise that can be resolved with `$mdToast.hide()` or
20530 * rejected with `$mdToast.cancel()`.
20531 *
20532 */
20533
20534 /**
20535 * @ngdoc method
20536 * @name $mdToast#simple
20537 *
20538 * @description
20539 * Builds a preconfigured toast.
20540 *
20541 * @returns {obj} a `$mdToastPreset` with the following chainable configuration methods.
20542 *
20543 * _**Note:** These configuration methods are provided in addition to the methods provided by
20544 * the `build()` and `show()` methods below._
20545 *
20546 * <table class="md-api-table methods">
20547 * <thead>
20548 * <tr>
20549 * <th>Method</th>
20550 * <th>Description</th>
20551 * </tr>
20552 * </thead>
20553 * <tbody>
20554 * <tr>
20555 * <td>`.textContent(string)`</td>
20556 * <td>Sets the toast content to the specified string</td>
20557 * </tr>
20558 * <tr>
20559 * <td>`.action(string)`</td>
20560 * <td>
20561 * Adds an action button. <br/>
20562 * If clicked, the promise (returned from `show()`)
20563 * will resolve with the value `'ok'`; otherwise, it is resolved with `true` after a `hideDelay`
20564 * timeout
20565 * </td>
20566 * </tr>
20567 * <tr>
20568 * <td>`.highlightAction(boolean)`</td>
20569 * <td>
20570 * Whether or not the action button will have an additional highlight class.<br/>
20571 * By default the `accent` color will be applied to the action button.
20572 * </td>
20573 * </tr>
20574 * <tr>
20575 * <td>`.highlightClass(string)`</td>
20576 * <td>
20577 * If set, the given class will be applied to the highlighted action button.<br/>
20578 * This allows you to specify the highlight color easily. Highlight classes are `md-primary`, `md-warn`
20579 * and `md-accent`
20580 * </td>
20581 * </tr>
20582 * <tr>
20583 * <td>`.capsule(boolean)`</td>
20584 * <td>Whether or not to add the `md-capsule` class to the toast to provide rounded corners</td>
20585 * </tr>
20586 * <tr>
20587 * <td>`.theme(string)`</td>
20588 * <td>Sets the theme on the toast to the requested theme. Default is `$mdThemingProvider`'s default.</td>
20589 * </tr>
20590 * <tr>
20591 * <td>`.toastClass(string)`</td>
20592 * <td>Sets a class on the toast element</td>
20593 * </tr>
20594 * </tbody>
20595 * </table>
20596 *
20597 */
20598
20599/**
20600 * @ngdoc method
20601 * @name $mdToast#updateTextContent
20602 *
20603 * @description
20604 * Updates the content of an existing toast. Useful for updating things like counts, etc.
20605 *
20606 */
20607
20608 /**
20609 * @ngdoc method
20610 * @name $mdToast#build
20611 *
20612 * @description
20613 * Creates a custom `$mdToastPreset` that you can configure.
20614 *
20615 * @returns {obj} a `$mdToastPreset` with the chainable configuration methods for shows' options (see below).
20616 */
20617
20618 /**
20619 * @ngdoc method
20620 * @name $mdToast#show
20621 *
20622 * @description Shows the toast.
20623 *
20624 * @param {object} optionsOrPreset Either provide an `$mdToastPreset` returned from `simple()`
20625 * and `build()`, or an options object with the following properties:
20626 *
20627 * - `templateUrl` - `{string=}`: The url of an html template file that will
20628 * be used as the content of the toast. Restrictions: the template must
20629 * have an outer `md-toast` element.
20630 * - `template` - `{string=}`: Same as templateUrl, except this is an actual
20631 * template string.
20632 * - `autoWrap` - `{boolean=}`: Whether or not to automatically wrap the template content with a
20633 * `<div class="md-toast-content">` if one is not provided. Defaults to true. Can be disabled if you provide a
20634 * custom toast directive.
20635 * - `scope` - `{object=}`: the scope to link the template / controller to. If none is specified, it will create a new child scope.
20636 * This scope will be destroyed when the toast is removed unless `preserveScope` is set to true.
20637 * - `preserveScope` - `{boolean=}`: whether to preserve the scope when the element is removed. Default is false
20638 * - `hideDelay` - `{number=}`: How many milliseconds the toast should stay
20639 * active before automatically closing. Set to 0 or false to have the toast stay open until
20640 * closed manually. Default: 3000.
20641 * - `position` - `{string=}`: Sets the position of the toast. <br/>
20642 * Available: any combination of `'bottom'`, `'left'`, `'top'`, `'right'`, `'end'` and `'start'`.
20643 * The properties `'end'` and `'start'` are dynamic and can be used for RTL support.<br/>
20644 * Default combination: `'bottom left'`.
20645 * - `toastClass` - `{string=}`: A class to set on the toast element.
20646 * - `controller` - `{string=}`: The controller to associate with this toast.
20647 * The controller will be injected the local `$mdToast.hide( )`, which is a function
20648 * used to hide the toast.
20649 * - `locals` - `{string=}`: An object containing key/value pairs. The keys will
20650 * be used as names of values to inject into the controller. For example,
20651 * `locals: {three: 3}` would inject `three` into the controller with the value
20652 * of 3.
20653 * - `bindToController` - `bool`: bind the locals to the controller, instead of passing them in.
20654 * - `resolve` - `{object=}`: Similar to locals, except it takes promises as values
20655 * and the toast will not open until the promises resolve.
20656 * - `controllerAs` - `{string=}`: An alias to assign the controller to on the scope.
20657 * - `parent` - `{element=}`: The element to append the toast to. Defaults to appending
20658 * to the root element of the application.
20659 *
20660 * @returns {promise} A promise that can be resolved with `$mdToast.hide()` or
20661 * rejected with `$mdToast.cancel()`. `$mdToast.hide()` will resolve either with a Boolean
20662 * value == 'true' or the value passed as an argument to `$mdToast.hide()`.
20663 * And `$mdToast.cancel()` will resolve the promise with a Boolean value == 'false'
20664 */
20665
20666/**
20667 * @ngdoc method
20668 * @name $mdToast#hide
20669 *
20670 * @description
20671 * Hide an existing toast and resolve the promise returned from `$mdToast.show()`.
20672 *
20673 * @param {*=} response An argument for the resolved promise.
20674 *
20675 * @returns {promise} a promise that is called when the existing element is removed from the DOM.
20676 * The promise is resolved with either a Boolean value == 'true' or the value passed as the
20677 * argument to `.hide()`.
20678 *
20679 */
20680
20681/**
20682 * @ngdoc method
20683 * @name $mdToast#cancel
20684 *
20685 * @description
20686 * `DEPRECATED` - The promise returned from opening a toast is used only to notify about the closing of the toast.
20687 * As such, there isn't any reason to also allow that promise to be rejected,
20688 * since it's not clear what the difference between resolve and reject would be.
20689 *
20690 * Hide the existing toast and reject the promise returned from
20691 * `$mdToast.show()`.
20692 *
20693 * @param {*=} response An argument for the rejected promise.
20694 *
20695 * @returns {promise} a promise that is called when the existing element is removed from the DOM
20696 * The promise is resolved with a Boolean value == 'false'.
20697 *
20698 */
20699
20700function MdToastProvider($$interimElementProvider) {
20701 // Differentiate promise resolves: hide timeout (value == true) and hide action clicks (value == ok).
20702 toastDefaultOptions.$inject = ["$animate", "$mdToast", "$mdUtil", "$mdMedia"];
20703 var ACTION_RESOLVE = 'ok';
20704
20705 var activeToastContent;
20706 var $mdToast = $$interimElementProvider('$mdToast')
20707 .setDefaults({
20708 methods: ['position', 'hideDelay', 'capsule', 'parent', 'position', 'toastClass'],
20709 options: toastDefaultOptions
20710 })
20711 .addPreset('simple', {
20712 argOption: 'textContent',
20713 methods: ['textContent', 'content', 'action', 'highlightAction', 'highlightClass', 'theme', 'parent' ],
20714 options: /* @ngInject */ ["$mdToast", "$mdTheming", function($mdToast, $mdTheming) {
20715 return {
20716 template:
20717 '<md-toast md-theme="{{ toast.theme }}" ng-class="{\'md-capsule\': toast.capsule}">' +
20718 ' <div class="md-toast-content">' +
20719 ' <span class="md-toast-text" role="alert" aria-relevant="all" aria-atomic="true">' +
20720 ' {{ toast.content }}' +
20721 ' </span>' +
20722 ' <md-button class="md-action" ng-if="toast.action" ng-click="toast.resolve()" ' +
20723 ' ng-class="highlightClasses">' +
20724 ' {{ toast.action }}' +
20725 ' </md-button>' +
20726 ' </div>' +
20727 '</md-toast>',
20728 controller: /* @ngInject */ ["$scope", function mdToastCtrl($scope) {
20729 var self = this;
20730
20731 if (self.highlightAction) {
20732 $scope.highlightClasses = [
20733 'md-highlight',
20734 self.highlightClass
20735 ]
20736 }
20737
20738 $scope.$watch(function() { return activeToastContent; }, function() {
20739 self.content = activeToastContent;
20740 });
20741
20742 this.resolve = function() {
20743 $mdToast.hide( ACTION_RESOLVE );
20744 };
20745 }],
20746 theme: $mdTheming.defaultTheme(),
20747 controllerAs: 'toast',
20748 bindToController: true
20749 };
20750 }]
20751 })
20752 .addMethod('updateTextContent', updateTextContent)
20753 .addMethod('updateContent', updateTextContent);
20754
20755 function updateTextContent(newContent) {
20756 activeToastContent = newContent;
20757 }
20758
20759 return $mdToast;
20760
20761 /* @ngInject */
20762 function toastDefaultOptions($animate, $mdToast, $mdUtil, $mdMedia) {
20763 var SWIPE_EVENTS = '$md.swipeleft $md.swiperight $md.swipeup $md.swipedown';
20764 return {
20765 onShow: onShow,
20766 onRemove: onRemove,
20767 toastClass: '',
20768 position: 'bottom left',
20769 themable: true,
20770 hideDelay: 3000,
20771 autoWrap: true,
20772 transformTemplate: function(template, options) {
20773 var shouldAddWrapper = options.autoWrap && template && !/md-toast-content/g.test(template);
20774
20775 if (shouldAddWrapper) {
20776 // Root element of template will be <md-toast>. We need to wrap all of its content inside of
20777 // of <div class="md-toast-content">. All templates provided here should be static, developer-controlled
20778 // content (meaning we're not attempting to guard against XSS).
20779 var templateRoot = document.createElement('md-template');
20780 templateRoot.innerHTML = template;
20781
20782 // Iterate through all root children, to detect possible md-toast directives.
20783 for (var i = 0; i < templateRoot.children.length; i++) {
20784 if (templateRoot.children[i].nodeName === 'MD-TOAST') {
20785 var wrapper = angular.element('<div class="md-toast-content">');
20786
20787 // Wrap the children of the `md-toast` directive in jqLite, to be able to append multiple
20788 // nodes with the same execution.
20789 wrapper.append(angular.element(templateRoot.children[i].childNodes));
20790
20791 // Append the new wrapped element to the `md-toast` directive.
20792 templateRoot.children[i].appendChild(wrapper[0]);
20793 }
20794 }
20795
20796 // We have to return the innerHTMl, because we do not want to have the `md-template` element to be
20797 // the root element of our interimElement.
20798 return templateRoot.innerHTML;
20799 }
20800
20801 return template || '';
20802 }
20803 };
20804
20805 function onShow(scope, element, options) {
20806 activeToastContent = options.textContent || options.content; // support deprecated #content method
20807
20808 var isSmScreen = !$mdMedia('gt-sm');
20809
20810 element = $mdUtil.extractElementByName(element, 'md-toast', true);
20811 options.element = element;
20812
20813 options.onSwipe = function(ev, gesture) {
20814 //Add the relevant swipe class to the element so it can animate correctly
20815 var swipe = ev.type.replace('$md.','');
20816 var direction = swipe.replace('swipe', '');
20817
20818 // If the swipe direction is down/up but the toast came from top/bottom don't fade away
20819 // Unless the screen is small, then the toast always on bottom
20820 if ((direction === 'down' && options.position.indexOf('top') != -1 && !isSmScreen) ||
20821 (direction === 'up' && (options.position.indexOf('bottom') != -1 || isSmScreen))) {
20822 return;
20823 }
20824
20825 if ((direction === 'left' || direction === 'right') && isSmScreen) {
20826 return;
20827 }
20828
20829 element.addClass('md-' + swipe);
20830 $mdUtil.nextTick($mdToast.cancel);
20831 };
20832 options.openClass = toastOpenClass(options.position);
20833
20834 element.addClass(options.toastClass);
20835
20836 // 'top left' -> 'md-top md-left'
20837 options.parent.addClass(options.openClass);
20838
20839 // static is the default position
20840 if ($mdUtil.hasComputedStyle(options.parent, 'position', 'static')) {
20841 options.parent.css('position', 'relative');
20842 }
20843
20844 element.on(SWIPE_EVENTS, options.onSwipe);
20845 element.addClass(isSmScreen ? 'md-bottom' : options.position.split(' ').map(function(pos) {
20846 return 'md-' + pos;
20847 }).join(' '));
20848
20849 if (options.parent) options.parent.addClass('md-toast-animating');
20850 return $animate.enter(element, options.parent).then(function() {
20851 if (options.parent) options.parent.removeClass('md-toast-animating');
20852 });
20853 }
20854
20855 function onRemove(scope, element, options) {
20856 element.off(SWIPE_EVENTS, options.onSwipe);
20857 if (options.parent) options.parent.addClass('md-toast-animating');
20858 if (options.openClass) options.parent.removeClass(options.openClass);
20859
20860 return ((options.$destroy == true) ? element.remove() : $animate.leave(element))
20861 .then(function () {
20862 if (options.parent) options.parent.removeClass('md-toast-animating');
20863 if ($mdUtil.hasComputedStyle(options.parent, 'position', 'static')) {
20864 options.parent.css('position', '');
20865 }
20866 });
20867 }
20868
20869 function toastOpenClass(position) {
20870 // For mobile, always open full-width on bottom
20871 if (!$mdMedia('gt-xs')) {
20872 return 'md-toast-open-bottom';
20873 }
20874
20875 return 'md-toast-open-' +
20876 (position.indexOf('top') > -1 ? 'top' : 'bottom');
20877 }
20878 }
20879
20880}
20881
20882})();
20883(function(){
20884"use strict";
20885
20886/**
20887 * @ngdoc module
20888 * @name material.components.toolbar
20889 */
20890mdToolbarDirective.$inject = ["$$rAF", "$mdConstant", "$mdUtil", "$mdTheming", "$animate"];
20891angular.module('material.components.toolbar', [
20892 'material.core',
20893 'material.components.content'
20894])
20895 .directive('mdToolbar', mdToolbarDirective);
20896
20897/**
20898 * @ngdoc directive
20899 * @name mdToolbar
20900 * @module material.components.toolbar
20901 * @restrict E
20902 * @description
20903 * `md-toolbar` is used to place a toolbar in your app.
20904 *
20905 * Toolbars are usually used above a content area to display the title of the
20906 * current page, and show relevant action buttons for that page.
20907 *
20908 * You can change the height of the toolbar by adding either the
20909 * `md-medium-tall` or `md-tall` class to the toolbar.
20910 *
20911 * @usage
20912 * <hljs lang="html">
20913 * <div layout="column" layout-fill>
20914 * <md-toolbar>
20915 *
20916 * <div class="md-toolbar-tools">
20917 * <span>My App's Title</span>
20918 *
20919 * <!-- fill up the space between left and right area -->
20920 * <span flex></span>
20921 *
20922 * <md-button>
20923 * Right Bar Button
20924 * </md-button>
20925 * </div>
20926 *
20927 * </md-toolbar>
20928 * <md-content>
20929 * Hello!
20930 * </md-content>
20931 * </div>
20932 * </hljs>
20933 *
20934 * @param {boolean=} md-scroll-shrink Whether the header should shrink away as
20935 * the user scrolls down, and reveal itself as the user scrolls up.
20936 *
20937 * _**Note (1):** for scrollShrink to work, the toolbar must be a sibling of a
20938 * `md-content` element, placed before it. See the scroll shrink demo._
20939 *
20940 * _**Note (2):** The `md-scroll-shrink` attribute is only parsed on component
20941 * initialization, it does not watch for scope changes._
20942 *
20943 *
20944 * @param {number=} md-shrink-speed-factor How much to change the speed of the toolbar's
20945 * shrinking by. For example, if 0.25 is given then the toolbar will shrink
20946 * at one fourth the rate at which the user scrolls down. Default 0.5.
20947 */
20948
20949function mdToolbarDirective($$rAF, $mdConstant, $mdUtil, $mdTheming, $animate) {
20950 var translateY = angular.bind(null, $mdUtil.supplant, 'translate3d(0,{0}px,0)');
20951
20952 return {
20953 template: '',
20954 restrict: 'E',
20955
20956 link: function(scope, element, attr) {
20957
20958 element.addClass('_md'); // private md component indicator for styling
20959 $mdTheming(element);
20960
20961 $mdUtil.nextTick(function () {
20962 element.addClass('_md-toolbar-transitions'); // adding toolbar transitions after digest
20963 }, false);
20964
20965 if (angular.isDefined(attr.mdScrollShrink)) {
20966 setupScrollShrink();
20967 }
20968
20969 function setupScrollShrink() {
20970
20971 var toolbarHeight;
20972 var contentElement;
20973 var disableScrollShrink = angular.noop;
20974
20975 // Current "y" position of scroll
20976 // Store the last scroll top position
20977 var y = 0;
20978 var prevScrollTop = 0;
20979 var shrinkSpeedFactor = attr.mdShrinkSpeedFactor || 0.5;
20980
20981 var debouncedContentScroll = $$rAF.throttle(onContentScroll);
20982 var debouncedUpdateHeight = $mdUtil.debounce(updateToolbarHeight, 5 * 1000);
20983
20984 // Wait for $mdContentLoaded event from mdContent directive.
20985 // If the mdContent element is a sibling of our toolbar, hook it up
20986 // to scroll events.
20987
20988 scope.$on('$mdContentLoaded', onMdContentLoad);
20989
20990 // If the toolbar is used inside an ng-if statement, we may miss the
20991 // $mdContentLoaded event, so we attempt to fake it if we have a
20992 // md-content close enough.
20993
20994 attr.$observe('mdScrollShrink', onChangeScrollShrink);
20995
20996 // If the toolbar has ngShow or ngHide we need to update height immediately as it changed
20997 // and not wait for $mdUtil.debounce to happen
20998
20999 if (attr.ngShow) { scope.$watch(attr.ngShow, updateToolbarHeight); }
21000 if (attr.ngHide) { scope.$watch(attr.ngHide, updateToolbarHeight); }
21001
21002 // If the scope is destroyed (which could happen with ng-if), make sure
21003 // to disable scroll shrinking again
21004
21005 scope.$on('$destroy', disableScrollShrink);
21006
21007 /**
21008 *
21009 */
21010 function onChangeScrollShrink(shrinkWithScroll) {
21011 var closestContent = element.parent().find('md-content');
21012
21013 // If we have a content element, fake the call; this might still fail
21014 // if the content element isn't a sibling of the toolbar
21015
21016 if (!contentElement && closestContent.length) {
21017 onMdContentLoad(null, closestContent);
21018 }
21019
21020 // Evaluate the expression
21021 shrinkWithScroll = scope.$eval(shrinkWithScroll);
21022
21023 // Disable only if the attribute's expression evaluates to false
21024 if (shrinkWithScroll === false) {
21025 disableScrollShrink();
21026 } else {
21027 disableScrollShrink = enableScrollShrink();
21028 }
21029 }
21030
21031 /**
21032 *
21033 */
21034 function onMdContentLoad($event, newContentEl) {
21035 // Toolbar and content must be siblings
21036 if (newContentEl && element.parent()[0] === newContentEl.parent()[0]) {
21037 // unhook old content event listener if exists
21038 if (contentElement) {
21039 contentElement.off('scroll', debouncedContentScroll);
21040 }
21041
21042 contentElement = newContentEl;
21043 disableScrollShrink = enableScrollShrink();
21044 }
21045 }
21046
21047 /**
21048 *
21049 */
21050 function onContentScroll(e) {
21051 var scrollTop = e ? e.target.scrollTop : prevScrollTop;
21052
21053 debouncedUpdateHeight();
21054
21055 y = Math.min(
21056 toolbarHeight / shrinkSpeedFactor,
21057 Math.max(0, y + scrollTop - prevScrollTop)
21058 );
21059
21060 element.css($mdConstant.CSS.TRANSFORM, translateY([-y * shrinkSpeedFactor]));
21061 contentElement.css($mdConstant.CSS.TRANSFORM, translateY([(toolbarHeight - y) * shrinkSpeedFactor]));
21062
21063 prevScrollTop = scrollTop;
21064
21065 $mdUtil.nextTick(function() {
21066 var hasWhiteFrame = element.hasClass('md-whiteframe-z1');
21067
21068 if (hasWhiteFrame && !y) {
21069 $animate.removeClass(element, 'md-whiteframe-z1');
21070 } else if (!hasWhiteFrame && y) {
21071 $animate.addClass(element, 'md-whiteframe-z1');
21072 }
21073 });
21074
21075 }
21076
21077 /**
21078 *
21079 */
21080 function enableScrollShrink() {
21081 if (!contentElement) return angular.noop; // no md-content
21082
21083 contentElement.on('scroll', debouncedContentScroll);
21084 contentElement.attr('scroll-shrink', 'true');
21085
21086 $mdUtil.nextTick(updateToolbarHeight, false);
21087
21088 return function disableScrollShrink() {
21089 contentElement.off('scroll', debouncedContentScroll);
21090 contentElement.attr('scroll-shrink', 'false');
21091
21092 updateToolbarHeight();
21093 };
21094 }
21095
21096 /**
21097 *
21098 */
21099 function updateToolbarHeight() {
21100 toolbarHeight = element.prop('offsetHeight');
21101 // Add a negative margin-top the size of the toolbar to the content el.
21102 // The content will start transformed down the toolbarHeight amount,
21103 // so everything looks normal.
21104 //
21105 // As the user scrolls down, the content will be transformed up slowly
21106 // to put the content underneath where the toolbar was.
21107 var margin = (-toolbarHeight * shrinkSpeedFactor) + 'px';
21108
21109 contentElement.css({
21110 "margin-top": margin,
21111 "margin-bottom": margin
21112 });
21113
21114 onContentScroll();
21115 }
21116
21117 }
21118
21119 }
21120 };
21121
21122}
21123
21124})();
21125(function(){
21126"use strict";
21127
21128/**
21129 * @ngdoc module
21130 * @name material.components.tooltip
21131 */
21132MdTooltipDirective.$inject = ["$timeout", "$window", "$$rAF", "$document", "$mdUtil", "$mdTheming", "$rootElement", "$animate", "$q", "$interpolate"];
21133angular
21134 .module('material.components.tooltip', [ 'material.core' ])
21135 .directive('mdTooltip', MdTooltipDirective);
21136
21137/**
21138 * @ngdoc directive
21139 * @name mdTooltip
21140 * @module material.components.tooltip
21141 * @description
21142 * Tooltips are used to describe elements that are interactive and primarily graphical (not textual).
21143 *
21144 * Place a `<md-tooltip>` as a child of the element it describes.
21145 *
21146 * A tooltip will activate when the user focuses, hovers over, or touches the parent.
21147 *
21148 * @usage
21149 * <hljs lang="html">
21150 * <md-button class="md-fab md-accent" aria-label="Play">
21151 * <md-tooltip>
21152 * Play Music
21153 * </md-tooltip>
21154 * <md-icon icon="img/icons/ic_play_arrow_24px.svg"></md-icon>
21155 * </md-button>
21156 * </hljs>
21157 *
21158 * @param {expression=} md-visible Boolean bound to whether the tooltip is currently visible.
21159 * @param {number=} md-delay How many milliseconds to wait to show the tooltip after the user focuses, hovers, or touches the
21160 * parent. Defaults to 0ms on non-touch devices and 75ms on touch.
21161 * @param {boolean=} md-autohide If present or provided with a boolean value, the tooltip will hide on mouse leave, regardless of focus
21162 * @param {string=} md-direction Which direction would you like the tooltip to go? Supports left, right, top, and bottom. Defaults to bottom.
21163 */
21164function MdTooltipDirective($timeout, $window, $$rAF, $document, $mdUtil, $mdTheming, $rootElement,
21165 $animate, $q, $interpolate) {
21166
21167 var ENTER_EVENTS = 'focus touchstart mouseenter';
21168 var LEAVE_EVENTS = 'blur touchcancel mouseleave';
21169 var SHOW_CLASS = 'md-show';
21170 var TOOLTIP_SHOW_DELAY = 0;
21171 var TOOLTIP_WINDOW_EDGE_SPACE = 8;
21172
21173 return {
21174 restrict: 'E',
21175 transclude: true,
21176 priority: 210, // Before ngAria
21177 template: '<div class="md-content _md" ng-transclude></div>',
21178 scope: {
21179 delay: '=?mdDelay',
21180 visible: '=?mdVisible',
21181 autohide: '=?mdAutohide',
21182 direction: '@?mdDirection' // only expect raw or interpolated string value; not expression
21183 },
21184 compile: function(tElement, tAttr) {
21185 if (!tAttr.mdDirection) {
21186 tAttr.$set('mdDirection', 'bottom');
21187 }
21188
21189 return postLink;
21190 }
21191 };
21192
21193 function postLink(scope, element, attr) {
21194
21195 $mdTheming(element);
21196
21197 var parent = $mdUtil.getParentWithPointerEvents(element),
21198 content = angular.element(element[0].getElementsByClassName('md-content')[0]),
21199 tooltipParent = angular.element(document.body),
21200 showTimeout = null,
21201 debouncedOnResize = $$rAF.throttle(function () { updatePosition(); });
21202
21203 if ($animate.pin) $animate.pin(element, parent);
21204
21205 // Initialize element
21206
21207 setDefaults();
21208 manipulateElement();
21209 bindEvents();
21210
21211 // Default origin transform point is 'center top'
21212 // positionTooltip() is always relative to center top
21213 updateContentOrigin();
21214
21215 configureWatchers();
21216 addAriaLabel();
21217
21218
21219 function setDefaults () {
21220 scope.delay = scope.delay || TOOLTIP_SHOW_DELAY;
21221 }
21222
21223 function updateContentOrigin() {
21224 var origin = 'center top';
21225 switch (scope.direction) {
21226 case 'left' : origin = 'right center'; break;
21227 case 'right' : origin = 'left center'; break;
21228 case 'top' : origin = 'center bottom'; break;
21229 case 'bottom': origin = 'center top'; break;
21230 }
21231 content.css('transform-origin', origin);
21232 }
21233
21234 function onVisibleChanged (isVisible) {
21235 if (isVisible) showTooltip();
21236 else hideTooltip();
21237 }
21238
21239 function configureWatchers () {
21240 if (element[0] && 'MutationObserver' in $window) {
21241 var attributeObserver = new MutationObserver(function(mutations) {
21242 mutations
21243 .forEach(function (mutation) {
21244 if (mutation.attributeName === 'md-visible') {
21245 if (!scope.visibleWatcher)
21246 scope.visibleWatcher = scope.$watch('visible', onVisibleChanged );
21247 }
21248 if (mutation.attributeName === 'md-direction') {
21249 updatePosition(scope.direction);
21250 }
21251 });
21252 });
21253
21254 attributeObserver.observe(element[0], { attributes: true });
21255
21256 // build watcher only if mdVisible is being used
21257 if (attr.hasOwnProperty('mdVisible')) {
21258 scope.visibleWatcher = scope.$watch('visible', onVisibleChanged );
21259 }
21260 } else { // MutationObserver not supported
21261 scope.visibleWatcher = scope.$watch('visible', onVisibleChanged );
21262 scope.$watch('direction', updatePosition );
21263 }
21264
21265 var onElementDestroy = function() {
21266 scope.$destroy();
21267 };
21268
21269 // Clean up if the element or parent was removed via jqLite's .remove.
21270 // A couple of notes:
21271 // - In these cases the scope might not have been destroyed, which is why we
21272 // destroy it manually. An example of this can be having `md-visible="false"` and
21273 // adding tooltips while they're invisible. If `md-visible` becomes true, at some
21274 // point, you'd usually get a lot of inputs.
21275 // - We use `.one`, not `.on`, because this only needs to fire once. If we were
21276 // using `.on`, it would get thrown into an infinite loop.
21277 // - This kicks off the scope's `$destroy` event which finishes the cleanup.
21278 element.one('$destroy', onElementDestroy);
21279 parent.one('$destroy', onElementDestroy);
21280 scope.$on('$destroy', function() {
21281 setVisible(false);
21282 element.remove();
21283 attributeObserver && attributeObserver.disconnect();
21284 });
21285
21286 // Updates the aria-label when the element text changes. This watch
21287 // doesn't need to be set up if the element doesn't have any data
21288 // bindings.
21289 if (element.text().indexOf($interpolate.startSymbol()) > -1) {
21290 scope.$watch(function() {
21291 return element.text().trim();
21292 }, addAriaLabel);
21293 }
21294 }
21295
21296 function addAriaLabel (override) {
21297 if ((override || !parent.attr('aria-label')) && !parent.text().trim()) {
21298 var rawText = override || element.text().trim();
21299 var interpolatedText = $interpolate(rawText)(parent.scope());
21300 parent.attr('aria-label', interpolatedText);
21301 }
21302 }
21303
21304 function manipulateElement () {
21305 element.detach();
21306 element.attr('role', 'tooltip');
21307 }
21308
21309 function bindEvents () {
21310 var mouseActive = false;
21311
21312 // add an mutationObserver when there is support for it
21313 // and the need for it in the form of viable host(parent[0])
21314 if (parent[0] && 'MutationObserver' in $window) {
21315 // use an mutationObserver to tackle #2602
21316 var attributeObserver = new MutationObserver(function(mutations) {
21317 if (mutations.some(function (mutation) {
21318 return (mutation.attributeName === 'disabled' && parent[0].disabled);
21319 })) {
21320 $mdUtil.nextTick(function() {
21321 setVisible(false);
21322 });
21323 }
21324 });
21325
21326 attributeObserver.observe(parent[0], { attributes: true});
21327 }
21328
21329 // Store whether the element was focused when the window loses focus.
21330 var windowBlurHandler = function() {
21331 elementFocusedOnWindowBlur = document.activeElement === parent[0];
21332 };
21333 var elementFocusedOnWindowBlur = false;
21334
21335 function windowScrollHandler() {
21336 setVisible(false);
21337 }
21338
21339 angular.element($window)
21340 .on('blur', windowBlurHandler)
21341 .on('resize', debouncedOnResize);
21342
21343 document.addEventListener('scroll', windowScrollHandler, true);
21344 scope.$on('$destroy', function() {
21345 angular.element($window)
21346 .off('blur', windowBlurHandler)
21347 .off('resize', debouncedOnResize);
21348
21349 parent
21350 .off(ENTER_EVENTS, enterHandler)
21351 .off(LEAVE_EVENTS, leaveHandler)
21352 .off('mousedown', mousedownHandler);
21353
21354 // Trigger the handler in case any the tooltip was still visible.
21355 leaveHandler();
21356 document.removeEventListener('scroll', windowScrollHandler, true);
21357 attributeObserver && attributeObserver.disconnect();
21358 });
21359
21360 var enterHandler = function(e) {
21361 // Prevent the tooltip from showing when the window is receiving focus.
21362 if (e.type === 'focus' && elementFocusedOnWindowBlur) {
21363 elementFocusedOnWindowBlur = false;
21364 } else if (!scope.visible) {
21365 parent.on(LEAVE_EVENTS, leaveHandler);
21366 setVisible(true);
21367
21368 // If the user is on a touch device, we should bind the tap away after
21369 // the `touched` in order to prevent the tooltip being removed immediately.
21370 if (e.type === 'touchstart') {
21371 parent.one('touchend', function() {
21372 $mdUtil.nextTick(function() {
21373 $document.one('touchend', leaveHandler);
21374 }, false);
21375 });
21376 }
21377 }
21378 };
21379 var leaveHandler = function () {
21380 var autohide = scope.hasOwnProperty('autohide') ? scope.autohide : attr.hasOwnProperty('mdAutohide');
21381
21382 if (autohide || mouseActive || $document[0].activeElement !== parent[0]) {
21383 // When a show timeout is currently in progress, then we have to cancel it.
21384 // Otherwise the tooltip will remain showing without focus or hover.
21385 if (showTimeout) {
21386 $timeout.cancel(showTimeout);
21387 setVisible.queued = false;
21388 showTimeout = null;
21389 }
21390
21391 parent.off(LEAVE_EVENTS, leaveHandler);
21392 parent.triggerHandler('blur');
21393 setVisible(false);
21394 }
21395 mouseActive = false;
21396 };
21397 var mousedownHandler = function() {
21398 mouseActive = true;
21399 };
21400
21401 // to avoid `synthetic clicks` we listen to mousedown instead of `click`
21402 parent.on('mousedown', mousedownHandler);
21403 parent.on(ENTER_EVENTS, enterHandler);
21404 }
21405
21406 function setVisible (value) {
21407 // break if passed value is already in queue or there is no queue and passed value is current in the scope
21408 if (setVisible.queued && setVisible.value === !!value || !setVisible.queued && scope.visible === !!value) return;
21409 setVisible.value = !!value;
21410
21411 if (!setVisible.queued) {
21412 if (value) {
21413 setVisible.queued = true;
21414 showTimeout = $timeout(function() {
21415 scope.visible = setVisible.value;
21416 setVisible.queued = false;
21417 showTimeout = null;
21418
21419 if (!scope.visibleWatcher) {
21420 onVisibleChanged(scope.visible);
21421 }
21422 }, scope.delay);
21423 } else {
21424 $mdUtil.nextTick(function() {
21425 scope.visible = false;
21426 if (!scope.visibleWatcher)
21427 onVisibleChanged(false);
21428 });
21429 }
21430 }
21431 }
21432
21433 function showTooltip() {
21434 // Do not show the tooltip if the text is empty.
21435 if (!element[0].textContent.trim()) return;
21436
21437 // Insert the element and position at top left, so we can get the position
21438 // and check if we should display it
21439 element.css({top: 0, left: 0});
21440 tooltipParent.append(element);
21441
21442 // Check if we should display it or not.
21443 // This handles hide-* and show-* along with any user defined css
21444 if ( $mdUtil.hasComputedStyle(element, 'display', 'none')) {
21445 scope.visible = false;
21446 element.detach();
21447 return;
21448 }
21449
21450 updatePosition();
21451
21452 $animate.addClass(content, SHOW_CLASS).then(function() {
21453 element.addClass(SHOW_CLASS);
21454 });
21455 }
21456
21457 function hideTooltip() {
21458 $animate.removeClass(content, SHOW_CLASS).then(function(){
21459 element.removeClass(SHOW_CLASS);
21460 if (!scope.visible) element.detach();
21461 });
21462 }
21463
21464 function updatePosition() {
21465 if ( !scope.visible ) return;
21466
21467 updateContentOrigin();
21468 positionTooltip();
21469 }
21470
21471 function positionTooltip() {
21472 var tipRect = $mdUtil.offsetRect(element, tooltipParent);
21473 var parentRect = $mdUtil.offsetRect(parent, tooltipParent);
21474 var newPosition = getPosition(scope.direction);
21475 var offsetParent = element.prop('offsetParent');
21476
21477 // If the user provided a direction, just nudge the tooltip onto the screen
21478 // Otherwise, recalculate based on 'top' since default is 'bottom'
21479 if (scope.direction) {
21480 newPosition = fitInParent(newPosition);
21481 } else if (offsetParent && newPosition.top > offsetParent.scrollHeight - tipRect.height - TOOLTIP_WINDOW_EDGE_SPACE) {
21482 newPosition = fitInParent(getPosition('top'));
21483 }
21484
21485 element.css({
21486 left: newPosition.left + 'px',
21487 top: newPosition.top + 'px'
21488 });
21489
21490 function fitInParent (pos) {
21491 var newPosition = { left: pos.left, top: pos.top };
21492 newPosition.left = Math.min( newPosition.left, tooltipParent.prop('scrollWidth') - tipRect.width - TOOLTIP_WINDOW_EDGE_SPACE );
21493 newPosition.left = Math.max( newPosition.left, TOOLTIP_WINDOW_EDGE_SPACE );
21494 newPosition.top = Math.min( newPosition.top, tooltipParent.prop('scrollHeight') - tipRect.height - TOOLTIP_WINDOW_EDGE_SPACE );
21495 newPosition.top = Math.max( newPosition.top, TOOLTIP_WINDOW_EDGE_SPACE );
21496 return newPosition;
21497 }
21498
21499 function getPosition (dir) {
21500 return dir === 'left'
21501 ? { left: parentRect.left - tipRect.width - TOOLTIP_WINDOW_EDGE_SPACE,
21502 top: parentRect.top + parentRect.height / 2 - tipRect.height / 2 }
21503 : dir === 'right'
21504 ? { left: parentRect.left + parentRect.width + TOOLTIP_WINDOW_EDGE_SPACE,
21505 top: parentRect.top + parentRect.height / 2 - tipRect.height / 2 }
21506 : dir === 'top'
21507 ? { left: parentRect.left + parentRect.width / 2 - tipRect.width / 2,
21508 top: parentRect.top - tipRect.height - TOOLTIP_WINDOW_EDGE_SPACE }
21509 : { left: parentRect.left + parentRect.width / 2 - tipRect.width / 2,
21510 top: parentRect.top + parentRect.height + TOOLTIP_WINDOW_EDGE_SPACE };
21511 }
21512 }
21513
21514 }
21515
21516}
21517
21518})();
21519(function(){
21520"use strict";
21521
21522/**
21523 * @ngdoc module
21524 * @name material.components.virtualRepeat
21525 */
21526VirtualRepeatContainerController.$inject = ["$$rAF", "$mdUtil", "$parse", "$rootScope", "$window", "$scope", "$element", "$attrs"];
21527VirtualRepeatController.$inject = ["$scope", "$element", "$attrs", "$browser", "$document", "$rootScope", "$$rAF", "$mdUtil"];
21528VirtualRepeatDirective.$inject = ["$parse"];
21529angular.module('material.components.virtualRepeat', [
21530 'material.core',
21531 'material.components.showHide'
21532])
21533.directive('mdVirtualRepeatContainer', VirtualRepeatContainerDirective)
21534.directive('mdVirtualRepeat', VirtualRepeatDirective);
21535
21536
21537/**
21538 * @ngdoc directive
21539 * @name mdVirtualRepeatContainer
21540 * @module material.components.virtualRepeat
21541 * @restrict E
21542 * @description
21543 * `md-virtual-repeat-container` provides the scroll container for md-virtual-repeat.
21544 *
21545 * VirtualRepeat is a limited substitute for ng-repeat that renders only
21546 * enough DOM nodes to fill the container and recycling them as the user scrolls.
21547 *
21548 * Once an element is not visible anymore, the VirtualRepeat recycles it and will reuse it for
21549 * another visible item by replacing the previous dataset with the new one.
21550 *
21551 * ### Common Issues
21552 *
21553 * > When having one-time bindings inside of the view template, the VirtualRepeat will not properly
21554 * > update the bindings for new items, since the view will be recycled.
21555 *
21556 * ### Notes
21557 *
21558 * > The VirtualRepeat is a similar implementation to the Android
21559 * [RecyclerView](https://developer.android.com/reference/android/support/v7/widget/RecyclerView.html)
21560 *
21561 * <!-- This comment forces a break between blockquotes //-->
21562 *
21563 * > Please also review the <a ng-href="api/directive/mdVirtualRepeat">VirtualRepeat</a>
21564 * documentation for more information.
21565 *
21566 *
21567 * @usage
21568 * <hljs lang="html">
21569 *
21570 * <md-virtual-repeat-container md-top-index="topIndex">
21571 * <div md-virtual-repeat="i in items" md-item-size="20">Hello {{i}}!</div>
21572 * </md-virtual-repeat-container>
21573 * </hljs>
21574 *
21575 * @param {number=} md-top-index Binds the index of the item that is at the top of the scroll
21576 * container to $scope. It can both read and set the scroll position.
21577 * @param {boolean=} md-orient-horizontal Whether the container should scroll horizontally
21578 * (defaults to orientation and scrolling vertically).
21579 * @param {boolean=} md-auto-shrink When present, the container will shrink to fit
21580 * the number of items when that number is less than its original size.
21581 * @param {number=} md-auto-shrink-min Minimum number of items that md-auto-shrink
21582 * will shrink to (default: 0).
21583 */
21584function VirtualRepeatContainerDirective() {
21585 return {
21586 controller: VirtualRepeatContainerController,
21587 template: virtualRepeatContainerTemplate,
21588 compile: function virtualRepeatContainerCompile($element, $attrs) {
21589 $element
21590 .addClass('md-virtual-repeat-container')
21591 .addClass($attrs.hasOwnProperty('mdOrientHorizontal')
21592 ? 'md-orient-horizontal'
21593 : 'md-orient-vertical');
21594 }
21595 };
21596}
21597
21598
21599function virtualRepeatContainerTemplate($element) {
21600 return '<div class="md-virtual-repeat-scroller">' +
21601 '<div class="md-virtual-repeat-sizer"></div>' +
21602 '<div class="md-virtual-repeat-offsetter">' +
21603 $element[0].innerHTML +
21604 '</div></div>';
21605}
21606
21607/**
21608 * Maximum size, in pixels, that can be explicitly set to an element. The actual value varies
21609 * between browsers, but IE11 has the very lowest size at a mere 1,533,917px. Ideally we could
21610 * *compute* this value, but Firefox always reports an element to have a size of zero if it
21611 * goes over the max, meaning that we'd have to binary search for the value.
21612 * @const {number}
21613 */
21614var MAX_ELEMENT_SIZE = 1533917;
21615
21616/**
21617 * Number of additional elements to render above and below the visible area inside
21618 * of the virtual repeat container. A higher number results in less flicker when scrolling
21619 * very quickly in Safari, but comes with a higher rendering and dirty-checking cost.
21620 * @const {number}
21621 */
21622var NUM_EXTRA = 3;
21623
21624/** @ngInject */
21625function VirtualRepeatContainerController(
21626 $$rAF, $mdUtil, $parse, $rootScope, $window, $scope, $element, $attrs) {
21627 this.$rootScope = $rootScope;
21628 this.$scope = $scope;
21629 this.$element = $element;
21630 this.$attrs = $attrs;
21631
21632 /** @type {number} The width or height of the container */
21633 this.size = 0;
21634 /** @type {number} The scroll width or height of the scroller */
21635 this.scrollSize = 0;
21636 /** @type {number} The scrollLeft or scrollTop of the scroller */
21637 this.scrollOffset = 0;
21638 /** @type {boolean} Whether the scroller is oriented horizontally */
21639 this.horizontal = this.$attrs.hasOwnProperty('mdOrientHorizontal');
21640 /** @type {!VirtualRepeatController} The repeater inside of this container */
21641 this.repeater = null;
21642 /** @type {boolean} Whether auto-shrink is enabled */
21643 this.autoShrink = this.$attrs.hasOwnProperty('mdAutoShrink');
21644 /** @type {number} Minimum number of items to auto-shrink to */
21645 this.autoShrinkMin = parseInt(this.$attrs.mdAutoShrinkMin, 10) || 0;
21646 /** @type {?number} Original container size when shrank */
21647 this.originalSize = null;
21648 /** @type {number} Amount to offset the total scroll size by. */
21649 this.offsetSize = parseInt(this.$attrs.mdOffsetSize, 10) || 0;
21650 /** @type {?string} height or width element style on the container prior to auto-shrinking. */
21651 this.oldElementSize = null;
21652
21653 if (this.$attrs.mdTopIndex) {
21654 /** @type {function(angular.Scope): number} Binds to topIndex on Angular scope */
21655 this.bindTopIndex = $parse(this.$attrs.mdTopIndex);
21656 /** @type {number} The index of the item that is at the top of the scroll container */
21657 this.topIndex = this.bindTopIndex(this.$scope);
21658
21659 if (!angular.isDefined(this.topIndex)) {
21660 this.topIndex = 0;
21661 this.bindTopIndex.assign(this.$scope, 0);
21662 }
21663
21664 this.$scope.$watch(this.bindTopIndex, angular.bind(this, function(newIndex) {
21665 if (newIndex !== this.topIndex) {
21666 this.scrollToIndex(newIndex);
21667 }
21668 }));
21669 } else {
21670 this.topIndex = 0;
21671 }
21672
21673 this.scroller = $element[0].querySelector('.md-virtual-repeat-scroller');
21674 this.sizer = this.scroller.querySelector('.md-virtual-repeat-sizer');
21675 this.offsetter = this.scroller.querySelector('.md-virtual-repeat-offsetter');
21676
21677 // After the dom stablizes, measure the initial size of the container and
21678 // make a best effort at re-measuring as it changes.
21679 var boundUpdateSize = angular.bind(this, this.updateSize);
21680
21681 $$rAF(angular.bind(this, function() {
21682 boundUpdateSize();
21683
21684 var debouncedUpdateSize = $mdUtil.debounce(boundUpdateSize, 10, null, false);
21685 var jWindow = angular.element($window);
21686
21687 // Make one more attempt to get the size if it is 0.
21688 // This is not by any means a perfect approach, but there's really no
21689 // silver bullet here.
21690 if (!this.size) {
21691 debouncedUpdateSize();
21692 }
21693
21694 jWindow.on('resize', debouncedUpdateSize);
21695 $scope.$on('$destroy', function() {
21696 jWindow.off('resize', debouncedUpdateSize);
21697 });
21698
21699 $scope.$emit('$md-resize-enable');
21700 $scope.$on('$md-resize', boundUpdateSize);
21701 }));
21702}
21703
21704
21705/** Called by the md-virtual-repeat inside of the container at startup. */
21706VirtualRepeatContainerController.prototype.register = function(repeaterCtrl) {
21707 this.repeater = repeaterCtrl;
21708
21709 angular.element(this.scroller)
21710 .on('scroll wheel touchmove touchend', angular.bind(this, this.handleScroll_));
21711};
21712
21713
21714/** @return {boolean} Whether the container is configured for horizontal scrolling. */
21715VirtualRepeatContainerController.prototype.isHorizontal = function() {
21716 return this.horizontal;
21717};
21718
21719
21720/** @return {number} The size (width or height) of the container. */
21721VirtualRepeatContainerController.prototype.getSize = function() {
21722 return this.size;
21723};
21724
21725
21726/**
21727 * Resizes the container.
21728 * @private
21729 * @param {number} size The new size to set.
21730 */
21731VirtualRepeatContainerController.prototype.setSize_ = function(size) {
21732 var dimension = this.getDimensionName_();
21733
21734 this.size = size;
21735 this.$element[0].style[dimension] = size + 'px';
21736};
21737
21738
21739VirtualRepeatContainerController.prototype.unsetSize_ = function() {
21740 this.$element[0].style[this.getDimensionName_()] = this.oldElementSize;
21741 this.oldElementSize = null;
21742};
21743
21744
21745/** Instructs the container to re-measure its size. */
21746VirtualRepeatContainerController.prototype.updateSize = function() {
21747 // If the original size is already determined, we can skip the update.
21748 if (this.originalSize) return;
21749
21750 this.size = this.isHorizontal()
21751 ? this.$element[0].clientWidth
21752 : this.$element[0].clientHeight;
21753
21754 // Recheck the scroll position after updating the size. This resolves
21755 // problems that can result if the scroll position was measured while the
21756 // element was display: none or detached from the document.
21757 this.handleScroll_();
21758
21759 this.repeater && this.repeater.containerUpdated();
21760};
21761
21762
21763/** @return {number} The container's scrollHeight or scrollWidth. */
21764VirtualRepeatContainerController.prototype.getScrollSize = function() {
21765 return this.scrollSize;
21766};
21767
21768
21769VirtualRepeatContainerController.prototype.getDimensionName_ = function() {
21770 return this.isHorizontal() ? 'width' : 'height';
21771};
21772
21773
21774/**
21775 * Sets the scroller element to the specified size.
21776 * @private
21777 * @param {number} size The new size.
21778 */
21779VirtualRepeatContainerController.prototype.sizeScroller_ = function(size) {
21780 var dimension = this.getDimensionName_();
21781 var crossDimension = this.isHorizontal() ? 'height' : 'width';
21782
21783 // Clear any existing dimensions.
21784 this.sizer.innerHTML = '';
21785
21786 // If the size falls within the browser's maximum explicit size for a single element, we can
21787 // set the size and be done. Otherwise, we have to create children that add up the the desired
21788 // size.
21789 if (size < MAX_ELEMENT_SIZE) {
21790 this.sizer.style[dimension] = size + 'px';
21791 } else {
21792 this.sizer.style[dimension] = 'auto';
21793 this.sizer.style[crossDimension] = 'auto';
21794
21795 // Divide the total size we have to render into N max-size pieces.
21796 var numChildren = Math.floor(size / MAX_ELEMENT_SIZE);
21797
21798 // Element template to clone for each max-size piece.
21799 var sizerChild = document.createElement('div');
21800 sizerChild.style[dimension] = MAX_ELEMENT_SIZE + 'px';
21801 sizerChild.style[crossDimension] = '1px';
21802
21803 for (var i = 0; i < numChildren; i++) {
21804 this.sizer.appendChild(sizerChild.cloneNode(false));
21805 }
21806
21807 // Re-use the element template for the remainder.
21808 sizerChild.style[dimension] = (size - (numChildren * MAX_ELEMENT_SIZE)) + 'px';
21809 this.sizer.appendChild(sizerChild);
21810 }
21811};
21812
21813
21814/**
21815 * If auto-shrinking is enabled, shrinks or unshrinks as appropriate.
21816 * @private
21817 * @param {number} size The new size.
21818 */
21819VirtualRepeatContainerController.prototype.autoShrink_ = function(size) {
21820 var shrinkSize = Math.max(size, this.autoShrinkMin * this.repeater.getItemSize());
21821
21822 if (this.autoShrink && shrinkSize !== this.size) {
21823 if (this.oldElementSize === null) {
21824 this.oldElementSize = this.$element[0].style[this.getDimensionName_()];
21825 }
21826
21827 var currentSize = this.originalSize || this.size;
21828
21829 if (!currentSize || shrinkSize < currentSize) {
21830 if (!this.originalSize) {
21831 this.originalSize = this.size;
21832 }
21833
21834 // Now we update the containers size, because shrinking is enabled.
21835 this.setSize_(shrinkSize);
21836 } else if (this.originalSize !== null) {
21837 // Set the size back to our initial size.
21838 this.unsetSize_();
21839
21840 var _originalSize = this.originalSize;
21841 this.originalSize = null;
21842
21843 // We determine the repeaters size again, if the original size was zero.
21844 // The originalSize needs to be null, to be able to determine the size.
21845 if (!_originalSize) this.updateSize();
21846
21847 // Apply the original size or the determined size back to the container, because
21848 // it has been overwritten before, in the shrink block.
21849 this.setSize_(_originalSize || this.size);
21850 }
21851
21852 this.repeater.containerUpdated();
21853 }
21854};
21855
21856
21857/**
21858 * Sets the scrollHeight or scrollWidth. Called by the repeater based on
21859 * its item count and item size.
21860 * @param {number} itemsSize The total size of the items.
21861 */
21862VirtualRepeatContainerController.prototype.setScrollSize = function(itemsSize) {
21863 var size = itemsSize + this.offsetSize;
21864 if (this.scrollSize === size) return;
21865
21866 this.sizeScroller_(size);
21867 this.autoShrink_(size);
21868 this.scrollSize = size;
21869};
21870
21871
21872/** @return {number} The container's current scroll offset. */
21873VirtualRepeatContainerController.prototype.getScrollOffset = function() {
21874 return this.scrollOffset;
21875};
21876
21877/**
21878 * Scrolls to a given scrollTop position.
21879 * @param {number} position
21880 */
21881VirtualRepeatContainerController.prototype.scrollTo = function(position) {
21882 this.scroller[this.isHorizontal() ? 'scrollLeft' : 'scrollTop'] = position;
21883 this.handleScroll_();
21884};
21885
21886/**
21887 * Scrolls the item with the given index to the top of the scroll container.
21888 * @param {number} index
21889 */
21890VirtualRepeatContainerController.prototype.scrollToIndex = function(index) {
21891 var itemSize = this.repeater.getItemSize();
21892 var itemsLength = this.repeater.itemsLength;
21893 if(index > itemsLength) {
21894 index = itemsLength - 1;
21895 }
21896 this.scrollTo(itemSize * index);
21897};
21898
21899VirtualRepeatContainerController.prototype.resetScroll = function() {
21900 this.scrollTo(0);
21901};
21902
21903
21904VirtualRepeatContainerController.prototype.handleScroll_ = function() {
21905 var doc = angular.element(document)[0];
21906 var ltr = doc.dir != 'rtl' && doc.body.dir != 'rtl';
21907 if(!ltr && !this.maxSize) {
21908 this.scroller.scrollLeft = this.scrollSize;
21909 this.maxSize = this.scroller.scrollLeft;
21910 }
21911 var offset = this.isHorizontal() ?
21912 (ltr?this.scroller.scrollLeft : this.maxSize - this.scroller.scrollLeft)
21913 : this.scroller.scrollTop;
21914 if (offset === this.scrollOffset || offset > this.scrollSize - this.size) return;
21915
21916 var itemSize = this.repeater.getItemSize();
21917 if (!itemSize) return;
21918
21919 var numItems = Math.max(0, Math.floor(offset / itemSize) - NUM_EXTRA);
21920
21921 var transform = (this.isHorizontal() ? 'translateX(' : 'translateY(') +
21922 (!this.isHorizontal() || ltr ? (numItems * itemSize) : - (numItems * itemSize)) + 'px)';
21923
21924 this.scrollOffset = offset;
21925 this.offsetter.style.webkitTransform = transform;
21926 this.offsetter.style.transform = transform;
21927
21928 if (this.bindTopIndex) {
21929 var topIndex = Math.floor(offset / itemSize);
21930 if (topIndex !== this.topIndex && topIndex < this.repeater.getItemCount()) {
21931 this.topIndex = topIndex;
21932 this.bindTopIndex.assign(this.$scope, topIndex);
21933 if (!this.$rootScope.$$phase) this.$scope.$digest();
21934 }
21935 }
21936
21937 this.repeater.containerUpdated();
21938};
21939
21940
21941/**
21942 * @ngdoc directive
21943 * @name mdVirtualRepeat
21944 * @module material.components.virtualRepeat
21945 * @restrict A
21946 * @priority 1000
21947 * @description
21948 * `md-virtual-repeat` specifies an element to repeat using virtual scrolling.
21949 *
21950 * Virtual repeat is a limited substitute for ng-repeat that renders only
21951 * enough dom nodes to fill the container and recycling them as the user scrolls.
21952 * Arrays, but not objects are supported for iteration.
21953 * Track by, as alias, and (key, value) syntax are not supported.
21954 *
21955 * > <b>Note:</b> Please also review the
21956 * <a ng-href="api/directive/mdVirtualRepeatContainer">VirtualRepeatContainer</a> documentation
21957 * for more information.
21958 *
21959 * @usage
21960 * <hljs lang="html">
21961 * <md-virtual-repeat-container>
21962 * <div md-virtual-repeat="i in items">Hello {{i}}!</div>
21963 * </md-virtual-repeat-container>
21964 *
21965 * <md-virtual-repeat-container md-orient-horizontal>
21966 * <div md-virtual-repeat="i in items" md-item-size="20">Hello {{i}}!</div>
21967 * </md-virtual-repeat-container>
21968 * </hljs>
21969 *
21970 * @param {number=} md-item-size The height or width of the repeated elements (which must be
21971 * identical for each element). Optional. Will attempt to read the size from the dom if missing,
21972 * but still assumes that all repeated nodes have same height or width.
21973 * @param {string=} md-extra-name Evaluates to an additional name to which the current iterated item
21974 * can be assigned on the repeated scope (needed for use in `md-autocomplete`).
21975 * @param {boolean=} md-on-demand When present, treats the md-virtual-repeat argument as an object
21976 * that can fetch rows rather than an array.
21977 *
21978 * **NOTE:** This object must implement the following interface with two (2) methods:
21979 *
21980 * - `getItemAtIndex: function(index) [object]` The item at that index or null if it is not yet
21981 * loaded (it should start downloading the item in that case).
21982 * - `getLength: function() [number]` The data length to which the repeater container
21983 * should be sized. Ideally, when the count is known, this method should return it.
21984 * Otherwise, return a higher number than the currently loaded items to produce an
21985 * infinite-scroll behavior.
21986 */
21987function VirtualRepeatDirective($parse) {
21988 return {
21989 controller: VirtualRepeatController,
21990 priority: 1000,
21991 require: ['mdVirtualRepeat', '^^mdVirtualRepeatContainer'],
21992 restrict: 'A',
21993 terminal: true,
21994 transclude: 'element',
21995 compile: function VirtualRepeatCompile($element, $attrs) {
21996 var expression = $attrs.mdVirtualRepeat;
21997 var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)\s*$/);
21998 var repeatName = match[1];
21999 var repeatListExpression = $parse(match[2]);
22000 var extraName = $attrs.mdExtraName && $parse($attrs.mdExtraName);
22001
22002 return function VirtualRepeatLink($scope, $element, $attrs, ctrl, $transclude) {
22003 ctrl[0].link_(ctrl[1], $transclude, repeatName, repeatListExpression, extraName);
22004 };
22005 }
22006 };
22007}
22008
22009
22010/** @ngInject */
22011function VirtualRepeatController($scope, $element, $attrs, $browser, $document, $rootScope,
22012 $$rAF, $mdUtil) {
22013 this.$scope = $scope;
22014 this.$element = $element;
22015 this.$attrs = $attrs;
22016 this.$browser = $browser;
22017 this.$document = $document;
22018 this.$rootScope = $rootScope;
22019 this.$$rAF = $$rAF;
22020
22021 /** @type {boolean} Whether we are in on-demand mode. */
22022 this.onDemand = $mdUtil.parseAttributeBoolean($attrs.mdOnDemand);
22023 /** @type {!Function} Backup reference to $browser.$$checkUrlChange */
22024 this.browserCheckUrlChange = $browser.$$checkUrlChange;
22025 /** @type {number} Most recent starting repeat index (based on scroll offset) */
22026 this.newStartIndex = 0;
22027 /** @type {number} Most recent ending repeat index (based on scroll offset) */
22028 this.newEndIndex = 0;
22029 /** @type {number} Most recent end visible index (based on scroll offset) */
22030 this.newVisibleEnd = 0;
22031 /** @type {number} Previous starting repeat index (based on scroll offset) */
22032 this.startIndex = 0;
22033 /** @type {number} Previous ending repeat index (based on scroll offset) */
22034 this.endIndex = 0;
22035 // TODO: measure width/height of first element from dom if not provided.
22036 // getComputedStyle?
22037 /** @type {?number} Height/width of repeated elements. */
22038 this.itemSize = $scope.$eval($attrs.mdItemSize) || null;
22039
22040 /** @type {boolean} Whether this is the first time that items are rendered. */
22041 this.isFirstRender = true;
22042
22043 /**
22044 * @private {boolean} Whether the items in the list are already being updated. Used to prevent
22045 * nested calls to virtualRepeatUpdate_.
22046 */
22047 this.isVirtualRepeatUpdating_ = false;
22048
22049 /** @type {number} Most recently seen length of items. */
22050 this.itemsLength = 0;
22051
22052 /**
22053 * @type {!Function} Unwatch callback for item size (when md-items-size is
22054 * not specified), or angular.noop otherwise.
22055 */
22056 this.unwatchItemSize_ = angular.noop;
22057
22058 /**
22059 * Presently rendered blocks by repeat index.
22060 * @type {Object<number, !VirtualRepeatController.Block}
22061 */
22062 this.blocks = {};
22063 /** @type {Array<!VirtualRepeatController.Block>} A pool of presently unused blocks. */
22064 this.pooledBlocks = [];
22065
22066 $scope.$on('$destroy', angular.bind(this, this.cleanupBlocks_));
22067}
22068
22069
22070/**
22071 * An object representing a repeated item.
22072 * @typedef {{element: !jqLite, new: boolean, scope: !angular.Scope}}
22073 */
22074VirtualRepeatController.Block;
22075
22076
22077/**
22078 * Called at startup by the md-virtual-repeat postLink function.
22079 * @param {!VirtualRepeatContainerController} container The container's controller.
22080 * @param {!Function} transclude The repeated element's bound transclude function.
22081 * @param {string} repeatName The left hand side of the repeat expression, indicating
22082 * the name for each item in the array.
22083 * @param {!Function} repeatListExpression A compiled expression based on the right hand side
22084 * of the repeat expression. Points to the array to repeat over.
22085 * @param {string|undefined} extraName The optional extra repeatName.
22086 */
22087VirtualRepeatController.prototype.link_ =
22088 function(container, transclude, repeatName, repeatListExpression, extraName) {
22089 this.container = container;
22090 this.transclude = transclude;
22091 this.repeatName = repeatName;
22092 this.rawRepeatListExpression = repeatListExpression;
22093 this.extraName = extraName;
22094 this.sized = false;
22095
22096 this.repeatListExpression = angular.bind(this, this.repeatListExpression_);
22097
22098 this.container.register(this);
22099};
22100
22101
22102/** @private Cleans up unused blocks. */
22103VirtualRepeatController.prototype.cleanupBlocks_ = function() {
22104 angular.forEach(this.pooledBlocks, function cleanupBlock(block) {
22105 block.element.remove();
22106 });
22107};
22108
22109
22110/** @private Attempts to set itemSize by measuring a repeated element in the dom */
22111VirtualRepeatController.prototype.readItemSize_ = function() {
22112 if (this.itemSize) {
22113 // itemSize was successfully read in a different asynchronous call.
22114 return;
22115 }
22116
22117 this.items = this.repeatListExpression(this.$scope);
22118 this.parentNode = this.$element[0].parentNode;
22119 var block = this.getBlock_(0);
22120 if (!block.element[0].parentNode) {
22121 this.parentNode.appendChild(block.element[0]);
22122 }
22123
22124 this.itemSize = block.element[0][
22125 this.container.isHorizontal() ? 'offsetWidth' : 'offsetHeight'] || null;
22126
22127 this.blocks[0] = block;
22128 this.poolBlock_(0);
22129
22130 if (this.itemSize) {
22131 this.containerUpdated();
22132 }
22133};
22134
22135
22136/**
22137 * Returns the user-specified repeat list, transforming it into an array-like
22138 * object in the case of infinite scroll/dynamic load mode.
22139 * @param {!angular.Scope} The scope.
22140 * @return {!Array|!Object} An array or array-like object for iteration.
22141 */
22142VirtualRepeatController.prototype.repeatListExpression_ = function(scope) {
22143 var repeatList = this.rawRepeatListExpression(scope);
22144
22145 if (this.onDemand && repeatList) {
22146 var virtualList = new VirtualRepeatModelArrayLike(repeatList);
22147 virtualList.$$includeIndexes(this.newStartIndex, this.newVisibleEnd);
22148 return virtualList;
22149 } else {
22150 return repeatList;
22151 }
22152};
22153
22154
22155/**
22156 * Called by the container. Informs us that the containers scroll or size has
22157 * changed.
22158 */
22159VirtualRepeatController.prototype.containerUpdated = function() {
22160 // If itemSize is unknown, attempt to measure it.
22161 if (!this.itemSize) {
22162 // Make sure to clean up watchers if we can (see #8178)
22163 if(this.unwatchItemSize_ && this.unwatchItemSize_ !== angular.noop){
22164 this.unwatchItemSize_();
22165 }
22166 this.unwatchItemSize_ = this.$scope.$watchCollection(
22167 this.repeatListExpression,
22168 angular.bind(this, function(items) {
22169 if (items && items.length) {
22170 this.readItemSize_();
22171 }
22172 }));
22173 if (!this.$rootScope.$$phase) this.$scope.$digest();
22174
22175 return;
22176 } else if (!this.sized) {
22177 this.items = this.repeatListExpression(this.$scope);
22178 }
22179
22180 if (!this.sized) {
22181 this.unwatchItemSize_();
22182 this.sized = true;
22183 this.$scope.$watchCollection(this.repeatListExpression,
22184 angular.bind(this, function(items, oldItems) {
22185 if (!this.isVirtualRepeatUpdating_) {
22186 this.virtualRepeatUpdate_(items, oldItems);
22187 }
22188 }));
22189 }
22190
22191 this.updateIndexes_();
22192
22193 if (this.newStartIndex !== this.startIndex ||
22194 this.newEndIndex !== this.endIndex ||
22195 this.container.getScrollOffset() > this.container.getScrollSize()) {
22196 if (this.items instanceof VirtualRepeatModelArrayLike) {
22197 this.items.$$includeIndexes(this.newStartIndex, this.newEndIndex);
22198 }
22199 this.virtualRepeatUpdate_(this.items, this.items);
22200 }
22201};
22202
22203
22204/**
22205 * Called by the container. Returns the size of a single repeated item.
22206 * @return {?number} Size of a repeated item.
22207 */
22208VirtualRepeatController.prototype.getItemSize = function() {
22209 return this.itemSize;
22210};
22211
22212
22213/**
22214 * Called by the container. Returns the size of a single repeated item.
22215 * @return {?number} Size of a repeated item.
22216 */
22217VirtualRepeatController.prototype.getItemCount = function() {
22218 return this.itemsLength;
22219};
22220
22221
22222/**
22223 * Updates the order and visible offset of repeated blocks in response to scrolling
22224 * or items updates.
22225 * @private
22226 */
22227VirtualRepeatController.prototype.virtualRepeatUpdate_ = function(items, oldItems) {
22228 this.isVirtualRepeatUpdating_ = true;
22229
22230 var itemsLength = items && items.length || 0;
22231 var lengthChanged = false;
22232
22233 // If the number of items shrank, keep the scroll position.
22234 if (this.items && itemsLength < this.items.length && this.container.getScrollOffset() !== 0) {
22235 this.items = items;
22236 var previousScrollOffset = this.container.getScrollOffset();
22237 this.container.resetScroll();
22238 this.container.scrollTo(previousScrollOffset);
22239 }
22240
22241 if (itemsLength !== this.itemsLength) {
22242 lengthChanged = true;
22243 this.itemsLength = itemsLength;
22244 }
22245
22246 this.items = items;
22247 if (items !== oldItems || lengthChanged) {
22248 this.updateIndexes_();
22249 }
22250
22251 this.parentNode = this.$element[0].parentNode;
22252
22253 if (lengthChanged) {
22254 this.container.setScrollSize(itemsLength * this.itemSize);
22255 }
22256
22257 if (this.isFirstRender) {
22258 this.isFirstRender = false;
22259 var startIndex = this.$attrs.mdStartIndex ?
22260 this.$scope.$eval(this.$attrs.mdStartIndex) :
22261 this.container.topIndex;
22262 this.container.scrollToIndex(startIndex);
22263 }
22264
22265 // Detach and pool any blocks that are no longer in the viewport.
22266 Object.keys(this.blocks).forEach(function(blockIndex) {
22267 var index = parseInt(blockIndex, 10);
22268 if (index < this.newStartIndex || index >= this.newEndIndex) {
22269 this.poolBlock_(index);
22270 }
22271 }, this);
22272
22273 // Add needed blocks.
22274 // For performance reasons, temporarily block browser url checks as we digest
22275 // the restored block scopes ($$checkUrlChange reads window.location to
22276 // check for changes and trigger route change, etc, which we don't need when
22277 // trying to scroll at 60fps).
22278 this.$browser.$$checkUrlChange = angular.noop;
22279
22280 var i, block,
22281 newStartBlocks = [],
22282 newEndBlocks = [];
22283
22284 // Collect blocks at the top.
22285 for (i = this.newStartIndex; i < this.newEndIndex && this.blocks[i] == null; i++) {
22286 block = this.getBlock_(i);
22287 this.updateBlock_(block, i);
22288 newStartBlocks.push(block);
22289 }
22290
22291 // Update blocks that are already rendered.
22292 for (; this.blocks[i] != null; i++) {
22293 this.updateBlock_(this.blocks[i], i);
22294 }
22295 var maxIndex = i - 1;
22296
22297 // Collect blocks at the end.
22298 for (; i < this.newEndIndex; i++) {
22299 block = this.getBlock_(i);
22300 this.updateBlock_(block, i);
22301 newEndBlocks.push(block);
22302 }
22303
22304 // Attach collected blocks to the document.
22305 if (newStartBlocks.length) {
22306 this.parentNode.insertBefore(
22307 this.domFragmentFromBlocks_(newStartBlocks),
22308 this.$element[0].nextSibling);
22309 }
22310 if (newEndBlocks.length) {
22311 this.parentNode.insertBefore(
22312 this.domFragmentFromBlocks_(newEndBlocks),
22313 this.blocks[maxIndex] && this.blocks[maxIndex].element[0].nextSibling);
22314 }
22315
22316 // Restore $$checkUrlChange.
22317 this.$browser.$$checkUrlChange = this.browserCheckUrlChange;
22318
22319 this.startIndex = this.newStartIndex;
22320 this.endIndex = this.newEndIndex;
22321
22322 this.isVirtualRepeatUpdating_ = false;
22323};
22324
22325
22326/**
22327 * @param {number} index Where the block is to be in the repeated list.
22328 * @return {!VirtualRepeatController.Block} A new or pooled block to place at the specified index.
22329 * @private
22330 */
22331VirtualRepeatController.prototype.getBlock_ = function(index) {
22332 if (this.pooledBlocks.length) {
22333 return this.pooledBlocks.pop();
22334 }
22335
22336 var block;
22337 this.transclude(angular.bind(this, function(clone, scope) {
22338 block = {
22339 element: clone,
22340 new: true,
22341 scope: scope
22342 };
22343
22344 this.updateScope_(scope, index);
22345 this.parentNode.appendChild(clone[0]);
22346 }));
22347
22348 return block;
22349};
22350
22351
22352/**
22353 * Updates and if not in a digest cycle, digests the specified block's scope to the data
22354 * at the specified index.
22355 * @param {!VirtualRepeatController.Block} block The block whose scope should be updated.
22356 * @param {number} index The index to set.
22357 * @private
22358 */
22359VirtualRepeatController.prototype.updateBlock_ = function(block, index) {
22360 this.blocks[index] = block;
22361
22362 if (!block.new &&
22363 (block.scope.$index === index && block.scope[this.repeatName] === this.items[index])) {
22364 return;
22365 }
22366 block.new = false;
22367
22368 // Update and digest the block's scope.
22369 this.updateScope_(block.scope, index);
22370
22371 // Perform digest before reattaching the block.
22372 // Any resulting synchronous dom mutations should be much faster as a result.
22373 // This might break some directives, but I'm going to try it for now.
22374 if (!this.$rootScope.$$phase) {
22375 block.scope.$digest();
22376 }
22377};
22378
22379
22380/**
22381 * Updates scope to the data at the specified index.
22382 * @param {!angular.Scope} scope The scope which should be updated.
22383 * @param {number} index The index to set.
22384 * @private
22385 */
22386VirtualRepeatController.prototype.updateScope_ = function(scope, index) {
22387 scope.$index = index;
22388 scope[this.repeatName] = this.items && this.items[index];
22389 if (this.extraName) scope[this.extraName(this.$scope)] = this.items[index];
22390};
22391
22392
22393/**
22394 * Pools the block at the specified index (Pulls its element out of the dom and stores it).
22395 * @param {number} index The index at which the block to pool is stored.
22396 * @private
22397 */
22398VirtualRepeatController.prototype.poolBlock_ = function(index) {
22399 this.pooledBlocks.push(this.blocks[index]);
22400 this.parentNode.removeChild(this.blocks[index].element[0]);
22401 delete this.blocks[index];
22402};
22403
22404
22405/**
22406 * Produces a dom fragment containing the elements from the list of blocks.
22407 * @param {!Array<!VirtualRepeatController.Block>} blocks The blocks whose elements
22408 * should be added to the document fragment.
22409 * @return {DocumentFragment}
22410 * @private
22411 */
22412VirtualRepeatController.prototype.domFragmentFromBlocks_ = function(blocks) {
22413 var fragment = this.$document[0].createDocumentFragment();
22414 blocks.forEach(function(block) {
22415 fragment.appendChild(block.element[0]);
22416 });
22417 return fragment;
22418};
22419
22420
22421/**
22422 * Updates start and end indexes based on length of repeated items and container size.
22423 * @private
22424 */
22425VirtualRepeatController.prototype.updateIndexes_ = function() {
22426 var itemsLength = this.items ? this.items.length : 0;
22427 var containerLength = Math.ceil(this.container.getSize() / this.itemSize);
22428
22429 this.newStartIndex = Math.max(0, Math.min(
22430 itemsLength - containerLength,
22431 Math.floor(this.container.getScrollOffset() / this.itemSize)));
22432 this.newVisibleEnd = this.newStartIndex + containerLength + NUM_EXTRA;
22433 this.newEndIndex = Math.min(itemsLength, this.newVisibleEnd);
22434 this.newStartIndex = Math.max(0, this.newStartIndex - NUM_EXTRA);
22435};
22436
22437/**
22438 * This VirtualRepeatModelArrayLike class enforces the interface requirements
22439 * for infinite scrolling within a mdVirtualRepeatContainer. An object with this
22440 * interface must implement the following interface with two (2) methods:
22441 *
22442 * getItemAtIndex: function(index) -> item at that index or null if it is not yet
22443 * loaded (It should start downloading the item in that case).
22444 *
22445 * getLength: function() -> number The data legnth to which the repeater container
22446 * should be sized. Ideally, when the count is known, this method should return it.
22447 * Otherwise, return a higher number than the currently loaded items to produce an
22448 * infinite-scroll behavior.
22449 *
22450 * @usage
22451 * <hljs lang="html">
22452 * <md-virtual-repeat-container md-orient-horizontal>
22453 * <div md-virtual-repeat="i in items" md-on-demand>
22454 * Hello {{i}}!
22455 * </div>
22456 * </md-virtual-repeat-container>
22457 * </hljs>
22458 *
22459 */
22460function VirtualRepeatModelArrayLike(model) {
22461 if (!angular.isFunction(model.getItemAtIndex) ||
22462 !angular.isFunction(model.getLength)) {
22463 throw Error('When md-on-demand is enabled, the Object passed to md-virtual-repeat must implement ' +
22464 'functions getItemAtIndex() and getLength() ');
22465 }
22466
22467 this.model = model;
22468}
22469
22470
22471VirtualRepeatModelArrayLike.prototype.$$includeIndexes = function(start, end) {
22472 for (var i = start; i < end; i++) {
22473 if (!this.hasOwnProperty(i)) {
22474 this[i] = this.model.getItemAtIndex(i);
22475 }
22476 }
22477 this.length = this.model.getLength();
22478};
22479
22480
22481function abstractMethod() {
22482 throw Error('Non-overridden abstract method called.');
22483}
22484
22485})();
22486(function(){
22487"use strict";
22488
22489/**
22490 * @ngdoc module
22491 * @name material.components.whiteframe
22492 */
22493MdWhiteframeDirective.$inject = ["$log"];
22494angular
22495 .module('material.components.whiteframe', ['material.core'])
22496 .directive('mdWhiteframe', MdWhiteframeDirective);
22497
22498/**
22499 * @ngdoc directive
22500 * @module material.components.whiteframe
22501 * @name mdWhiteframe
22502 *
22503 * @description
22504 * The md-whiteframe directive allows you to apply an elevation shadow to an element.
22505 *
22506 * The attribute values needs to be a number between 1 and 24 or -1.
22507 * When set to -1 no style is applied.
22508 *
22509 * ### Notes
22510 * - If there is no value specified it defaults to 4dp.
22511 * - If the value is not valid it defaults to 4dp.
22512
22513 * @usage
22514 * <hljs lang="html">
22515 * <div md-whiteframe="3">
22516 * <span>Elevation of 3dp</span>
22517 * </div>
22518 * </hljs>
22519 *
22520 * <hljs lang="html">
22521 * <div md-whiteframe="-1">
22522 * <span>No elevation shadow applied</span>
22523 * </div>
22524 * </hljs>
22525 *
22526 * <hljs lang="html">
22527 * <div ng-init="elevation = 5" md-whiteframe="{{elevation}}">
22528 * <span>Elevation of 5dp with an interpolated value</span>
22529 * </div>
22530 * </hljs>
22531 */
22532function MdWhiteframeDirective($log) {
22533 var DISABLE_DP = -1;
22534 var MIN_DP = 1;
22535 var MAX_DP = 24;
22536 var DEFAULT_DP = 4;
22537
22538 return {
22539 link: postLink
22540 };
22541
22542 function postLink(scope, element, attr) {
22543 var oldClass = '';
22544
22545 attr.$observe('mdWhiteframe', function(elevation) {
22546 elevation = parseInt(elevation, 10) || DEFAULT_DP;
22547
22548 if (elevation != DISABLE_DP && (elevation > MAX_DP || elevation < MIN_DP)) {
22549 $log.warn('md-whiteframe attribute value is invalid. It should be a number between ' + MIN_DP + ' and ' + MAX_DP, element[0]);
22550 elevation = DEFAULT_DP;
22551 }
22552
22553 var newClass = elevation == DISABLE_DP ? '' : 'md-whiteframe-' + elevation + 'dp';
22554 attr.$updateClass(newClass, oldClass);
22555 oldClass = newClass;
22556 });
22557 }
22558}
22559
22560
22561})();
22562(function(){
22563"use strict";
22564
22565
22566MdAutocompleteCtrl.$inject = ["$scope", "$element", "$mdUtil", "$mdConstant", "$mdTheming", "$window", "$animate", "$rootElement", "$attrs", "$q", "$log"];angular
22567 .module('material.components.autocomplete')
22568 .controller('MdAutocompleteCtrl', MdAutocompleteCtrl);
22569
22570var ITEM_HEIGHT = 41,
22571 MAX_HEIGHT = 5.5 * ITEM_HEIGHT,
22572 MENU_PADDING = 8,
22573 INPUT_PADDING = 2; // Padding provided by `md-input-container`
22574
22575function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, $window,
22576 $animate, $rootElement, $attrs, $q, $log) {
22577
22578 // Internal Variables.
22579 var ctrl = this,
22580 itemParts = $scope.itemsExpr.split(/ in /i),
22581 itemExpr = itemParts[ 1 ],
22582 elements = null,
22583 cache = {},
22584 noBlur = false,
22585 selectedItemWatchers = [],
22586 hasFocus = false,
22587 lastCount = 0,
22588 fetchesInProgress = 0,
22589 enableWrapScroll = null,
22590 inputModelCtrl = null;
22591
22592 // Public Exported Variables with handlers
22593 defineProperty('hidden', handleHiddenChange, true);
22594
22595 // Public Exported Variables
22596 ctrl.scope = $scope;
22597 ctrl.parent = $scope.$parent;
22598 ctrl.itemName = itemParts[ 0 ];
22599 ctrl.matches = [];
22600 ctrl.loading = false;
22601 ctrl.hidden = true;
22602 ctrl.index = null;
22603 ctrl.messages = [];
22604 ctrl.id = $mdUtil.nextUid();
22605 ctrl.isDisabled = null;
22606 ctrl.isRequired = null;
22607 ctrl.isReadonly = null;
22608 ctrl.hasNotFound = false;
22609
22610 // Public Exported Methods
22611 ctrl.keydown = keydown;
22612 ctrl.blur = blur;
22613 ctrl.focus = focus;
22614 ctrl.clear = clearValue;
22615 ctrl.select = select;
22616 ctrl.listEnter = onListEnter;
22617 ctrl.listLeave = onListLeave;
22618 ctrl.mouseUp = onMouseup;
22619 ctrl.getCurrentDisplayValue = getCurrentDisplayValue;
22620 ctrl.registerSelectedItemWatcher = registerSelectedItemWatcher;
22621 ctrl.unregisterSelectedItemWatcher = unregisterSelectedItemWatcher;
22622 ctrl.notFoundVisible = notFoundVisible;
22623 ctrl.loadingIsVisible = loadingIsVisible;
22624 ctrl.positionDropdown = positionDropdown;
22625
22626 return init();
22627
22628 //-- initialization methods
22629
22630 /**
22631 * Initialize the controller, setup watchers, gather elements
22632 */
22633 function init () {
22634 $mdUtil.initOptionalProperties($scope, $attrs, { searchText: '', selectedItem: null });
22635 $mdTheming($element);
22636 configureWatchers();
22637 $mdUtil.nextTick(function () {
22638
22639 gatherElements();
22640 moveDropdown();
22641
22642 // Forward all focus events to the input element when autofocus is enabled
22643 if ($scope.autofocus) {
22644 $element.on('focus', focusInputElement);
22645 }
22646 });
22647 }
22648
22649 function updateModelValidators() {
22650 if (!$scope.requireMatch || !inputModelCtrl) return;
22651
22652 inputModelCtrl.$setValidity('md-require-match', !!$scope.selectedItem);
22653 }
22654
22655 /**
22656 * Calculates the dropdown's position and applies the new styles to the menu element
22657 * @returns {*}
22658 */
22659 function positionDropdown () {
22660 if (!elements) return $mdUtil.nextTick(positionDropdown, false, $scope);
22661 var hrect = elements.wrap.getBoundingClientRect(),
22662 vrect = elements.snap.getBoundingClientRect(),
22663 root = elements.root.getBoundingClientRect(),
22664 top = vrect.bottom - root.top,
22665 bot = root.bottom - vrect.top,
22666 left = hrect.left - root.left,
22667 width = hrect.width,
22668 offset = getVerticalOffset(),
22669 styles;
22670 // Adjust the width to account for the padding provided by `md-input-container`
22671 if ($attrs.mdFloatingLabel) {
22672 left += INPUT_PADDING;
22673 width -= INPUT_PADDING * 2;
22674 }
22675 styles = {
22676 left: left + 'px',
22677 minWidth: width + 'px',
22678 maxWidth: Math.max(hrect.right - root.left, root.right - hrect.left) - MENU_PADDING + 'px'
22679 };
22680 if (top > bot && root.height - hrect.bottom - MENU_PADDING < MAX_HEIGHT) {
22681 styles.top = 'auto';
22682 styles.bottom = bot + 'px';
22683 styles.maxHeight = Math.min(MAX_HEIGHT, hrect.top - root.top - MENU_PADDING) + 'px';
22684 } else {
22685 styles.top = (top - offset) + 'px';
22686 styles.bottom = 'auto';
22687 styles.maxHeight = Math.min(MAX_HEIGHT, root.bottom + $mdUtil.scrollTop() - hrect.bottom - MENU_PADDING) + 'px';
22688 }
22689
22690 elements.$.scrollContainer.css(styles);
22691 $mdUtil.nextTick(correctHorizontalAlignment, false);
22692
22693 /**
22694 * Calculates the vertical offset for floating label examples to account for ngMessages
22695 * @returns {number}
22696 */
22697 function getVerticalOffset () {
22698 var offset = 0;
22699 var inputContainer = $element.find('md-input-container');
22700 if (inputContainer.length) {
22701 var input = inputContainer.find('input');
22702 offset = inputContainer.prop('offsetHeight');
22703 offset -= input.prop('offsetTop');
22704 offset -= input.prop('offsetHeight');
22705 // add in the height left up top for the floating label text
22706 offset += inputContainer.prop('offsetTop');
22707 }
22708 return offset;
22709 }
22710
22711 /**
22712 * Makes sure that the menu doesn't go off of the screen on either side.
22713 */
22714 function correctHorizontalAlignment () {
22715 var dropdown = elements.scrollContainer.getBoundingClientRect(),
22716 styles = {};
22717 if (dropdown.right > root.right - MENU_PADDING) {
22718 styles.left = (hrect.right - dropdown.width) + 'px';
22719 }
22720 elements.$.scrollContainer.css(styles);
22721 }
22722 }
22723
22724 /**
22725 * Moves the dropdown menu to the body tag in order to avoid z-index and overflow issues.
22726 */
22727 function moveDropdown () {
22728 if (!elements.$.root.length) return;
22729 $mdTheming(elements.$.scrollContainer);
22730 elements.$.scrollContainer.detach();
22731 elements.$.root.append(elements.$.scrollContainer);
22732 if ($animate.pin) $animate.pin(elements.$.scrollContainer, $rootElement);
22733 }
22734
22735 /**
22736 * Sends focus to the input element.
22737 */
22738 function focusInputElement () {
22739 elements.input.focus();
22740 }
22741
22742 /**
22743 * Sets up any watchers used by autocomplete
22744 */
22745 function configureWatchers () {
22746 var wait = parseInt($scope.delay, 10) || 0;
22747 $attrs.$observe('disabled', function (value) { ctrl.isDisabled = $mdUtil.parseAttributeBoolean(value, false); });
22748 $attrs.$observe('required', function (value) { ctrl.isRequired = $mdUtil.parseAttributeBoolean(value, false); });
22749 $attrs.$observe('readonly', function (value) { ctrl.isReadonly = $mdUtil.parseAttributeBoolean(value, false); });
22750 $scope.$watch('searchText', wait ? $mdUtil.debounce(handleSearchText, wait) : handleSearchText);
22751 $scope.$watch('selectedItem', selectedItemChange);
22752 angular.element($window).on('resize', positionDropdown);
22753 $scope.$on('$destroy', cleanup);
22754 }
22755
22756 /**
22757 * Removes any events or leftover elements created by this controller
22758 */
22759 function cleanup () {
22760 if (!ctrl.hidden) {
22761 $mdUtil.enableScrolling();
22762 }
22763
22764 angular.element($window).off('resize', positionDropdown);
22765 if ( elements ){
22766 var items = ['ul', 'scroller', 'scrollContainer', 'input'];
22767 angular.forEach(items, function(key){
22768 elements.$[key].remove();
22769 });
22770 }
22771 }
22772
22773 /**
22774 * Gathers all of the elements needed for this controller
22775 */
22776 function gatherElements () {
22777 elements = {
22778 main: $element[0],
22779 scrollContainer: $element[0].querySelector('.md-virtual-repeat-container'),
22780 scroller: $element[0].querySelector('.md-virtual-repeat-scroller'),
22781 ul: $element.find('ul')[0],
22782 input: $element.find('input')[0],
22783 wrap: $element.find('md-autocomplete-wrap')[0],
22784 root: document.body
22785 };
22786
22787 elements.li = elements.ul.getElementsByTagName('li');
22788 elements.snap = getSnapTarget();
22789 elements.$ = getAngularElements(elements);
22790
22791 inputModelCtrl = elements.$.input.controller('ngModel');
22792 }
22793
22794 /**
22795 * Finds the element that the menu will base its position on
22796 * @returns {*}
22797 */
22798 function getSnapTarget () {
22799 for (var element = $element; element.length; element = element.parent()) {
22800 if (angular.isDefined(element.attr('md-autocomplete-snap'))) return element[ 0 ];
22801 }
22802 return elements.wrap;
22803 }
22804
22805 /**
22806 * Gathers angular-wrapped versions of each element
22807 * @param elements
22808 * @returns {{}}
22809 */
22810 function getAngularElements (elements) {
22811 var obj = {};
22812 for (var key in elements) {
22813 if (elements.hasOwnProperty(key)) obj[ key ] = angular.element(elements[ key ]);
22814 }
22815 return obj;
22816 }
22817
22818 //-- event/change handlers
22819
22820 /**
22821 * Handles changes to the `hidden` property.
22822 * @param hidden
22823 * @param oldHidden
22824 */
22825 function handleHiddenChange (hidden, oldHidden) {
22826 if (!hidden && oldHidden) {
22827 positionDropdown();
22828
22829 if (elements) {
22830 $mdUtil.disableScrollAround(elements.ul);
22831 enableWrapScroll = disableElementScrollEvents(angular.element(elements.wrap));
22832 }
22833 } else if (hidden && !oldHidden) {
22834 $mdUtil.enableScrolling();
22835
22836 if (enableWrapScroll) {
22837 enableWrapScroll();
22838 enableWrapScroll = null;
22839 }
22840 }
22841 }
22842
22843 /**
22844 * Disables scrolling for a specific element
22845 */
22846 function disableElementScrollEvents(element) {
22847
22848 function preventDefault(e) {
22849 e.preventDefault();
22850 }
22851
22852 element.on('wheel', preventDefault);
22853 element.on('touchmove', preventDefault);
22854
22855 return function() {
22856 element.off('wheel', preventDefault);
22857 element.off('touchmove', preventDefault);
22858 };
22859 }
22860
22861 /**
22862 * When the user mouses over the dropdown menu, ignore blur events.
22863 */
22864 function onListEnter () {
22865 noBlur = true;
22866 }
22867
22868 /**
22869 * When the user's mouse leaves the menu, blur events may hide the menu again.
22870 */
22871 function onListLeave () {
22872 if (!hasFocus && !ctrl.hidden) elements.input.focus();
22873 noBlur = false;
22874 ctrl.hidden = shouldHide();
22875 }
22876
22877 /**
22878 * When the mouse button is released, send focus back to the input field.
22879 */
22880 function onMouseup () {
22881 elements.input.focus();
22882 }
22883
22884 /**
22885 * Handles changes to the selected item.
22886 * @param selectedItem
22887 * @param previousSelectedItem
22888 */
22889 function selectedItemChange (selectedItem, previousSelectedItem) {
22890
22891 updateModelValidators();
22892
22893 if (selectedItem) {
22894 getDisplayValue(selectedItem).then(function (val) {
22895 $scope.searchText = val;
22896 handleSelectedItemChange(selectedItem, previousSelectedItem);
22897 });
22898 } else if (previousSelectedItem && $scope.searchText) {
22899 getDisplayValue(previousSelectedItem).then(function(displayValue) {
22900 // Clear the searchText, when the selectedItem is set to null.
22901 // Do not clear the searchText, when the searchText isn't matching with the previous
22902 // selected item.
22903 if (displayValue.toString().toLowerCase() === $scope.searchText.toLowerCase()) {
22904 $scope.searchText = '';
22905 }
22906 });
22907 }
22908
22909 if (selectedItem !== previousSelectedItem) announceItemChange();
22910 }
22911
22912 /**
22913 * Use the user-defined expression to announce changes each time a new item is selected
22914 */
22915 function announceItemChange () {
22916 angular.isFunction($scope.itemChange) && $scope.itemChange(getItemAsNameVal($scope.selectedItem));
22917 }
22918
22919 /**
22920 * Use the user-defined expression to announce changes each time the search text is changed
22921 */
22922 function announceTextChange () {
22923 angular.isFunction($scope.textChange) && $scope.textChange();
22924 }
22925
22926 /**
22927 * Calls any external watchers listening for the selected item. Used in conjunction with
22928 * `registerSelectedItemWatcher`.
22929 * @param selectedItem
22930 * @param previousSelectedItem
22931 */
22932 function handleSelectedItemChange (selectedItem, previousSelectedItem) {
22933 selectedItemWatchers.forEach(function (watcher) { watcher(selectedItem, previousSelectedItem); });
22934 }
22935
22936 /**
22937 * Register a function to be called when the selected item changes.
22938 * @param cb
22939 */
22940 function registerSelectedItemWatcher (cb) {
22941 if (selectedItemWatchers.indexOf(cb) == -1) {
22942 selectedItemWatchers.push(cb);
22943 }
22944 }
22945
22946 /**
22947 * Unregister a function previously registered for selected item changes.
22948 * @param cb
22949 */
22950 function unregisterSelectedItemWatcher (cb) {
22951 var i = selectedItemWatchers.indexOf(cb);
22952 if (i != -1) {
22953 selectedItemWatchers.splice(i, 1);
22954 }
22955 }
22956
22957 /**
22958 * Handles changes to the searchText property.
22959 * @param searchText
22960 * @param previousSearchText
22961 */
22962 function handleSearchText (searchText, previousSearchText) {
22963 ctrl.index = getDefaultIndex();
22964
22965 // do nothing on init
22966 if (searchText === previousSearchText) return;
22967
22968 updateModelValidators();
22969
22970 getDisplayValue($scope.selectedItem).then(function (val) {
22971 // clear selected item if search text no longer matches it
22972 if (searchText !== val) {
22973 $scope.selectedItem = null;
22974
22975 // trigger change event if available
22976 if (searchText !== previousSearchText) announceTextChange();
22977
22978 // cancel results if search text is not long enough
22979 if (!isMinLengthMet()) {
22980 ctrl.matches = [];
22981 setLoading(false);
22982 updateMessages();
22983 } else {
22984 handleQuery();
22985 }
22986 }
22987 });
22988
22989 }
22990
22991 /**
22992 * Handles input blur event, determines if the dropdown should hide.
22993 */
22994 function blur($event) {
22995 hasFocus = false;
22996
22997 if (!noBlur) {
22998 ctrl.hidden = shouldHide();
22999 evalAttr('ngBlur', { $event: $event });
23000 }
23001 }
23002
23003 /**
23004 * Force blur on input element
23005 * @param forceBlur
23006 */
23007 function doBlur(forceBlur) {
23008 if (forceBlur) {
23009 noBlur = false;
23010 hasFocus = false;
23011 }
23012 elements.input.blur();
23013 }
23014
23015 /**
23016 * Handles input focus event, determines if the dropdown should show.
23017 */
23018 function focus($event) {
23019 hasFocus = true;
23020
23021 if (isSearchable() && isMinLengthMet()) {
23022 handleQuery();
23023 }
23024
23025 ctrl.hidden = shouldHide();
23026
23027 evalAttr('ngFocus', { $event: $event });
23028 }
23029
23030 /**
23031 * Handles keyboard input.
23032 * @param event
23033 */
23034 function keydown (event) {
23035 switch (event.keyCode) {
23036 case $mdConstant.KEY_CODE.DOWN_ARROW:
23037 if (ctrl.loading) return;
23038 event.stopPropagation();
23039 event.preventDefault();
23040 ctrl.index = Math.min(ctrl.index + 1, ctrl.matches.length - 1);
23041 updateScroll();
23042 updateMessages();
23043 break;
23044 case $mdConstant.KEY_CODE.UP_ARROW:
23045 if (ctrl.loading) return;
23046 event.stopPropagation();
23047 event.preventDefault();
23048 ctrl.index = ctrl.index < 0 ? ctrl.matches.length - 1 : Math.max(0, ctrl.index - 1);
23049 updateScroll();
23050 updateMessages();
23051 break;
23052 case $mdConstant.KEY_CODE.TAB:
23053 // If we hit tab, assume that we've left the list so it will close
23054 onListLeave();
23055
23056 if (ctrl.hidden || ctrl.loading || ctrl.index < 0 || ctrl.matches.length < 1) return;
23057 select(ctrl.index);
23058 break;
23059 case $mdConstant.KEY_CODE.ENTER:
23060 if (ctrl.hidden || ctrl.loading || ctrl.index < 0 || ctrl.matches.length < 1) return;
23061 if (hasSelection()) return;
23062 event.stopPropagation();
23063 event.preventDefault();
23064 select(ctrl.index);
23065 break;
23066 case $mdConstant.KEY_CODE.ESCAPE:
23067 event.preventDefault(); // Prevent browser from always clearing input
23068 if (!shouldProcessEscape()) return;
23069 event.stopPropagation();
23070
23071 clearSelectedItem();
23072 if ($scope.searchText && hasEscapeOption('clear')) {
23073 clearSearchText();
23074 }
23075
23076 // Manually hide (needed for mdNotFound support)
23077 ctrl.hidden = true;
23078
23079 if (hasEscapeOption('blur')) {
23080 // Force the component to blur if they hit escape
23081 doBlur(true);
23082 }
23083
23084 break;
23085 default:
23086 }
23087 }
23088
23089 //-- getters
23090
23091 /**
23092 * Returns the minimum length needed to display the dropdown.
23093 * @returns {*}
23094 */
23095 function getMinLength () {
23096 return angular.isNumber($scope.minLength) ? $scope.minLength : 1;
23097 }
23098
23099 /**
23100 * Returns the display value for an item.
23101 * @param item
23102 * @returns {*}
23103 */
23104 function getDisplayValue (item) {
23105 return $q.when(getItemText(item) || item).then(function(itemText) {
23106 if (itemText && !angular.isString(itemText)) {
23107 $log.warn('md-autocomplete: Could not resolve display value to a string. ' +
23108 'Please check the `md-item-text` attribute.');
23109 }
23110
23111 return itemText;
23112 });
23113
23114 /**
23115 * Getter function to invoke user-defined expression (in the directive)
23116 * to convert your object to a single string.
23117 */
23118 function getItemText (item) {
23119 return (item && $scope.itemText) ? $scope.itemText(getItemAsNameVal(item)) : null;
23120 }
23121 }
23122
23123 /**
23124 * Returns the locals object for compiling item templates.
23125 * @param item
23126 * @returns {{}}
23127 */
23128 function getItemAsNameVal (item) {
23129 if (!item) return undefined;
23130
23131 var locals = {};
23132 if (ctrl.itemName) locals[ ctrl.itemName ] = item;
23133
23134 return locals;
23135 }
23136
23137 /**
23138 * Returns the default index based on whether or not autoselect is enabled.
23139 * @returns {number}
23140 */
23141 function getDefaultIndex () {
23142 return $scope.autoselect ? 0 : -1;
23143 }
23144
23145 /**
23146 * Sets the loading parameter and updates the hidden state.
23147 * @param value {boolean} Whether or not the component is currently loading.
23148 */
23149 function setLoading(value) {
23150 if (ctrl.loading != value) {
23151 ctrl.loading = value;
23152 }
23153
23154 // Always refresh the hidden variable as something else might have changed
23155 ctrl.hidden = shouldHide();
23156 }
23157
23158 /**
23159 * Determines if the menu should be hidden.
23160 * @returns {boolean}
23161 */
23162 function shouldHide () {
23163 if (!isSearchable()) return true; // Hide when not able to query
23164 else return !shouldShow(); // Hide when the dropdown is not able to show.
23165 }
23166
23167 /**
23168 * Determines whether the autocomplete is able to query within the current state.
23169 * @returns {boolean}
23170 */
23171 function isSearchable() {
23172 if (ctrl.loading && !hasMatches()) return false; // No query when query is in progress.
23173 else if (hasSelection()) return false; // No query if there is already a selection
23174 else if (!hasFocus) return false; // No query if the input does not have focus
23175 return true;
23176 }
23177
23178 /**
23179 * Determines if the escape keydown should be processed
23180 * @returns {boolean}
23181 */
23182 function shouldProcessEscape() {
23183 return hasEscapeOption('blur') || !ctrl.hidden || ctrl.loading || hasEscapeOption('clear') && $scope.searchText;
23184 }
23185
23186 /**
23187 * Determines if an escape option is set
23188 * @returns {boolean}
23189 */
23190 function hasEscapeOption(option) {
23191 return !$scope.escapeOptions || $scope.escapeOptions.toLowerCase().indexOf(option) !== -1;
23192 }
23193
23194 /**
23195 * Determines if the menu should be shown.
23196 * @returns {boolean}
23197 */
23198 function shouldShow() {
23199 return (isMinLengthMet() && hasMatches()) || notFoundVisible();
23200 }
23201
23202 /**
23203 * Returns true if the search text has matches.
23204 * @returns {boolean}
23205 */
23206 function hasMatches() {
23207 return ctrl.matches.length ? true : false;
23208 }
23209
23210 /**
23211 * Returns true if the autocomplete has a valid selection.
23212 * @returns {boolean}
23213 */
23214 function hasSelection() {
23215 return ctrl.scope.selectedItem ? true : false;
23216 }
23217
23218 /**
23219 * Returns true if the loading indicator is, or should be, visible.
23220 * @returns {boolean}
23221 */
23222 function loadingIsVisible() {
23223 return ctrl.loading && !hasSelection();
23224 }
23225
23226 /**
23227 * Returns the display value of the current item.
23228 * @returns {*}
23229 */
23230 function getCurrentDisplayValue () {
23231 return getDisplayValue(ctrl.matches[ ctrl.index ]);
23232 }
23233
23234 /**
23235 * Determines if the minimum length is met by the search text.
23236 * @returns {*}
23237 */
23238 function isMinLengthMet () {
23239 return ($scope.searchText || '').length >= getMinLength();
23240 }
23241
23242 //-- actions
23243
23244 /**
23245 * Defines a public property with a handler and a default value.
23246 * @param key
23247 * @param handler
23248 * @param value
23249 */
23250 function defineProperty (key, handler, value) {
23251 Object.defineProperty(ctrl, key, {
23252 get: function () { return value; },
23253 set: function (newValue) {
23254 var oldValue = value;
23255 value = newValue;
23256 handler(newValue, oldValue);
23257 }
23258 });
23259 }
23260
23261 /**
23262 * Selects the item at the given index.
23263 * @param index
23264 */
23265 function select (index) {
23266 //-- force form to update state for validation
23267 $mdUtil.nextTick(function () {
23268 getDisplayValue(ctrl.matches[ index ]).then(function (val) {
23269 var ngModel = elements.$.input.controller('ngModel');
23270 ngModel.$setViewValue(val);
23271 ngModel.$render();
23272 }).finally(function () {
23273 $scope.selectedItem = ctrl.matches[ index ];
23274 setLoading(false);
23275 });
23276 }, false);
23277 }
23278
23279 /**
23280 * Clears the searchText value and selected item.
23281 */
23282 function clearValue () {
23283 clearSelectedItem();
23284 clearSearchText();
23285 }
23286
23287 /**
23288 * Clears the selected item
23289 */
23290 function clearSelectedItem () {
23291 // Reset our variables
23292 ctrl.index = 0;
23293 ctrl.matches = [];
23294 }
23295
23296 /**
23297 * Clears the searchText value
23298 */
23299 function clearSearchText () {
23300 // Set the loading to true so we don't see flashes of content.
23301 // The flashing will only occur when an async request is running.
23302 // So the loading process will stop when the results had been retrieved.
23303 setLoading(true);
23304
23305 $scope.searchText = '';
23306
23307 // Normally, triggering the change / input event is unnecessary, because the browser detects it properly.
23308 // But some browsers are not detecting it properly, which means that we have to trigger the event.
23309 // Using the `input` is not working properly, because for example IE11 is not supporting the `input` event.
23310 // The `change` event is a good alternative and is supported by all supported browsers.
23311 var eventObj = document.createEvent('CustomEvent');
23312 eventObj.initCustomEvent('change', true, true, { value: '' });
23313 elements.input.dispatchEvent(eventObj);
23314
23315 // For some reason, firing the above event resets the value of $scope.searchText if
23316 // $scope.searchText has a space character at the end, so we blank it one more time and then
23317 // focus.
23318 elements.input.blur();
23319 $scope.searchText = '';
23320 elements.input.focus();
23321 }
23322
23323 /**
23324 * Fetches the results for the provided search text.
23325 * @param searchText
23326 */
23327 function fetchResults (searchText) {
23328 var items = $scope.$parent.$eval(itemExpr),
23329 term = searchText.toLowerCase(),
23330 isList = angular.isArray(items),
23331 isPromise = !!items.then; // Every promise should contain a `then` property
23332
23333 if (isList) onResultsRetrieved(items);
23334 else if (isPromise) handleAsyncResults(items);
23335
23336 function handleAsyncResults(items) {
23337 if ( !items ) return;
23338
23339 items = $q.when(items);
23340 fetchesInProgress++;
23341 setLoading(true);
23342
23343 $mdUtil.nextTick(function () {
23344 items
23345 .then(onResultsRetrieved)
23346 .finally(function(){
23347 if (--fetchesInProgress === 0) {
23348 setLoading(false);
23349 }
23350 });
23351 },true, $scope);
23352 }
23353
23354 function onResultsRetrieved(matches) {
23355 cache[term] = matches;
23356
23357 // Just cache the results if the request is now outdated.
23358 // The request becomes outdated, when the new searchText has changed during the result fetching.
23359 if ((searchText || '') !== ($scope.searchText || '')) {
23360 return;
23361 }
23362
23363 handleResults(matches);
23364 }
23365 }
23366
23367 /**
23368 * Updates the ARIA messages
23369 */
23370 function updateMessages () {
23371 getCurrentDisplayValue().then(function (msg) {
23372 ctrl.messages = [ getCountMessage(), msg ];
23373 });
23374 }
23375
23376 /**
23377 * Returns the ARIA message for how many results match the current query.
23378 * @returns {*}
23379 */
23380 function getCountMessage () {
23381 if (lastCount === ctrl.matches.length) return '';
23382 lastCount = ctrl.matches.length;
23383 switch (ctrl.matches.length) {
23384 case 0:
23385 return 'There are no matches available.';
23386 case 1:
23387 return 'There is 1 match available.';
23388 default:
23389 return 'There are ' + ctrl.matches.length + ' matches available.';
23390 }
23391 }
23392
23393 /**
23394 * Makes sure that the focused element is within view.
23395 */
23396 function updateScroll () {
23397 if (!elements.li[0]) return;
23398 var height = elements.li[0].offsetHeight,
23399 top = height * ctrl.index,
23400 bot = top + height,
23401 hgt = elements.scroller.clientHeight,
23402 scrollTop = elements.scroller.scrollTop;
23403 if (top < scrollTop) {
23404 scrollTo(top);
23405 } else if (bot > scrollTop + hgt) {
23406 scrollTo(bot - hgt);
23407 }
23408 }
23409
23410 function isPromiseFetching() {
23411 return fetchesInProgress !== 0;
23412 }
23413
23414 function scrollTo (offset) {
23415 elements.$.scrollContainer.controller('mdVirtualRepeatContainer').scrollTo(offset);
23416 }
23417
23418 function notFoundVisible () {
23419 var textLength = (ctrl.scope.searchText || '').length;
23420
23421 return ctrl.hasNotFound && !hasMatches() && (!ctrl.loading || isPromiseFetching()) && textLength >= getMinLength() && (hasFocus || noBlur) && !hasSelection();
23422 }
23423
23424 /**
23425 * Starts the query to gather the results for the current searchText. Attempts to return cached
23426 * results first, then forwards the process to `fetchResults` if necessary.
23427 */
23428 function handleQuery () {
23429 var searchText = $scope.searchText || '';
23430 var term = searchText.toLowerCase();
23431
23432 // If caching is enabled and the current searchText is stored in the cache
23433 if (!$scope.noCache && cache[term]) {
23434 // The results should be handled as same as a normal un-cached request does.
23435 handleResults(cache[term]);
23436 } else {
23437 fetchResults(searchText);
23438 }
23439
23440 ctrl.hidden = shouldHide();
23441 }
23442
23443 /**
23444 * Handles the retrieved results by showing them in the autocompletes dropdown.
23445 * @param results Retrieved results
23446 */
23447 function handleResults(results) {
23448 ctrl.matches = results;
23449 ctrl.hidden = shouldHide();
23450
23451 // If loading is in progress, then we'll end the progress. This is needed for example,
23452 // when the `clear` button was clicked, because there we always show the loading process, to prevent flashing.
23453 if (ctrl.loading) setLoading(false);
23454
23455 if ($scope.selectOnMatch) selectItemOnMatch();
23456
23457 updateMessages();
23458 positionDropdown();
23459 }
23460
23461 /**
23462 * If there is only one matching item and the search text matches its display value exactly,
23463 * automatically select that item. Note: This function is only called if the user uses the
23464 * `md-select-on-match` flag.
23465 */
23466 function selectItemOnMatch () {
23467 var searchText = $scope.searchText,
23468 matches = ctrl.matches,
23469 item = matches[ 0 ];
23470 if (matches.length === 1) getDisplayValue(item).then(function (displayValue) {
23471 var isMatching = searchText == displayValue;
23472 if ($scope.matchInsensitive && !isMatching) {
23473 isMatching = searchText.toLowerCase() == displayValue.toLowerCase();
23474 }
23475
23476 if (isMatching) select(0);
23477 });
23478 }
23479
23480 /**
23481 * Evaluates an attribute expression against the parent scope.
23482 * @param {String} attr Name of the attribute to be evaluated.
23483 * @param {Object?} locals Properties to be injected into the evaluation context.
23484 */
23485 function evalAttr(attr, locals) {
23486 if ($attrs[attr]) {
23487 $scope.$parent.$eval($attrs[attr], locals || {});
23488 }
23489 }
23490
23491}
23492
23493})();
23494(function(){
23495"use strict";
23496
23497
23498MdAutocomplete.$inject = ["$$mdSvgRegistry"];angular
23499 .module('material.components.autocomplete')
23500 .directive('mdAutocomplete', MdAutocomplete);
23501
23502/**
23503 * @ngdoc directive
23504 * @name mdAutocomplete
23505 * @module material.components.autocomplete
23506 *
23507 * @description
23508 * `<md-autocomplete>` is a special input component with a drop-down of all possible matches to a
23509 * custom query. This component allows you to provide real-time suggestions as the user types
23510 * in the input area.
23511 *
23512 * To start, you will need to specify the required parameters and provide a template for your
23513 * results. The content inside `md-autocomplete` will be treated as a template.
23514 *
23515 * In more complex cases, you may want to include other content such as a message to display when
23516 * no matches were found. You can do this by wrapping your template in `md-item-template` and
23517 * adding a tag for `md-not-found`. An example of this is shown below.
23518 *
23519 * To reset the displayed value you must clear both values for `md-search-text` and `md-selected-item`.
23520 *
23521 * ### Validation
23522 *
23523 * You can use `ng-messages` to include validation the same way that you would normally validate;
23524 * however, if you want to replicate a standard input with a floating label, you will have to
23525 * do the following:
23526 *
23527 * - Make sure that your template is wrapped in `md-item-template`
23528 * - Add your `ng-messages` code inside of `md-autocomplete`
23529 * - Add your validation properties to `md-autocomplete` (ie. `required`)
23530 * - Add a `name` to `md-autocomplete` (to be used on the generated `input`)
23531 *
23532 * There is an example below of how this should look.
23533 *
23534 * ### Notes
23535 * The `md-autocomplete` uses the the <a ng-href="api/directive/mdVirtualRepeatContainer">VirtualRepeat</a>
23536 * directive for displaying the results inside of the dropdown.<br/>
23537 * > When encountering issues regarding the item template please take a look at the
23538 * <a ng-href="api/directive/mdVirtualRepeatContainer">VirtualRepeatContainer</a> documentation.
23539 *
23540 *
23541 * @param {expression} md-items An expression in the format of `item in items` to iterate over
23542 * matches for your search.
23543 * @param {expression=} md-selected-item-change An expression to be run each time a new item is
23544 * selected
23545 * @param {expression=} md-search-text-change An expression to be run each time the search text
23546 * updates
23547 * @param {expression=} md-search-text A model to bind the search query text to
23548 * @param {object=} md-selected-item A model to bind the selected item to
23549 * @param {expression=} md-item-text An expression that will convert your object to a single string.
23550 * @param {string=} placeholder Placeholder text that will be forwarded to the input.
23551 * @param {boolean=} md-no-cache Disables the internal caching that happens in autocomplete
23552 * @param {boolean=} ng-disabled Determines whether or not to disable the input field
23553 * @param {boolean=} md-require-match When set to true, the autocomplete will add a validator,
23554 * which will evaluate to false, when no item is currently selected.
23555 * @param {number=} md-min-length Specifies the minimum length of text before autocomplete will
23556 * make suggestions
23557 * @param {number=} md-delay Specifies the amount of time (in milliseconds) to wait before looking
23558 * for results
23559 * @param {boolean=} md-autofocus If true, the autocomplete will be automatically focused when a `$mdDialog`,
23560 * `$mdBottomsheet` or `$mdSidenav`, which contains the autocomplete, is opening. <br/><br/>
23561 * Also the autocomplete will immediately focus the input element.
23562 * @param {boolean=} md-no-asterisk When present, asterisk will not be appended to the floating label
23563 * @param {boolean=} md-autoselect If set to true, the first item will be automatically selected
23564 * in the dropdown upon open.
23565 * @param {string=} md-menu-class This will be applied to the dropdown menu for styling
23566 * @param {string=} md-floating-label This will add a floating label to autocomplete and wrap it in
23567 * `md-input-container`
23568 * @param {string=} md-input-name The name attribute given to the input element to be used with
23569 * FormController
23570 * @param {string=} md-select-on-focus When present the inputs text will be automatically selected
23571 * on focus.
23572 * @param {string=} md-input-id An ID to be added to the input element
23573 * @param {number=} md-input-minlength The minimum length for the input's value for validation
23574 * @param {number=} md-input-maxlength The maximum length for the input's value for validation
23575 * @param {boolean=} md-select-on-match When set, autocomplete will automatically select exact
23576 * the item if the search text is an exact match. <br/><br/>
23577 * Exact match means that there is only one match showing up.
23578 * @param {boolean=} md-match-case-insensitive When set and using `md-select-on-match`, autocomplete
23579 * will select on case-insensitive match
23580 * @param {string=} md-escape-options Override escape key logic. Default is `blur clear`.<br/>
23581 * Options: `blur | clear`, `none`
23582 *
23583 * @usage
23584 * ### Basic Example
23585 * <hljs lang="html">
23586 * <md-autocomplete
23587 * md-selected-item="selectedItem"
23588 * md-search-text="searchText"
23589 * md-items="item in getMatches(searchText)"
23590 * md-item-text="item.display">
23591 * <span md-highlight-text="searchText">{{item.display}}</span>
23592 * </md-autocomplete>
23593 * </hljs>
23594 *
23595 * ### Example with "not found" message
23596 * <hljs lang="html">
23597 * <md-autocomplete
23598 * md-selected-item="selectedItem"
23599 * md-search-text="searchText"
23600 * md-items="item in getMatches(searchText)"
23601 * md-item-text="item.display">
23602 * <md-item-template>
23603 * <span md-highlight-text="searchText">{{item.display}}</span>
23604 * </md-item-template>
23605 * <md-not-found>
23606 * No matches found.
23607 * </md-not-found>
23608 * </md-autocomplete>
23609 * </hljs>
23610 *
23611 * In this example, our code utilizes `md-item-template` and `md-not-found` to specify the
23612 * different parts that make up our component.
23613 *
23614 * ### Example with validation
23615 * <hljs lang="html">
23616 * <form name="autocompleteForm">
23617 * <md-autocomplete
23618 * required
23619 * md-input-name="autocomplete"
23620 * md-selected-item="selectedItem"
23621 * md-search-text="searchText"
23622 * md-items="item in getMatches(searchText)"
23623 * md-item-text="item.display">
23624 * <md-item-template>
23625 * <span md-highlight-text="searchText">{{item.display}}</span>
23626 * </md-item-template>
23627 * <div ng-messages="autocompleteForm.autocomplete.$error">
23628 * <div ng-message="required">This field is required</div>
23629 * </div>
23630 * </md-autocomplete>
23631 * </form>
23632 * </hljs>
23633 *
23634 * In this example, our code utilizes `md-item-template` and `ng-messages` to specify
23635 * input validation for the field.
23636 */
23637
23638function MdAutocomplete ($$mdSvgRegistry) {
23639
23640 return {
23641 controller: 'MdAutocompleteCtrl',
23642 controllerAs: '$mdAutocompleteCtrl',
23643 scope: {
23644 inputName: '@mdInputName',
23645 inputMinlength: '@mdInputMinlength',
23646 inputMaxlength: '@mdInputMaxlength',
23647 searchText: '=?mdSearchText',
23648 selectedItem: '=?mdSelectedItem',
23649 itemsExpr: '@mdItems',
23650 itemText: '&mdItemText',
23651 placeholder: '@placeholder',
23652 noCache: '=?mdNoCache',
23653 requireMatch: '=?mdRequireMatch',
23654 selectOnMatch: '=?mdSelectOnMatch',
23655 matchInsensitive: '=?mdMatchCaseInsensitive',
23656 itemChange: '&?mdSelectedItemChange',
23657 textChange: '&?mdSearchTextChange',
23658 minLength: '=?mdMinLength',
23659 delay: '=?mdDelay',
23660 autofocus: '=?mdAutofocus',
23661 floatingLabel: '@?mdFloatingLabel',
23662 autoselect: '=?mdAutoselect',
23663 menuClass: '@?mdMenuClass',
23664 inputId: '@?mdInputId',
23665 escapeOptions: '@?mdEscapeOptions'
23666 },
23667 link: function(scope, element, attrs, controller) {
23668 // Retrieve the state of using a md-not-found template by using our attribute, which will
23669 // be added to the element in the template function.
23670 controller.hasNotFound = !!element.attr('md-has-not-found');
23671 },
23672 template: function (element, attr) {
23673 var noItemsTemplate = getNoItemsTemplate(),
23674 itemTemplate = getItemTemplate(),
23675 leftover = element.html(),
23676 tabindex = attr.tabindex;
23677
23678 // Set our attribute for the link function above which runs later.
23679 // We will set an attribute, because otherwise the stored variables will be trashed when
23680 // removing the element is hidden while retrieving the template. For example when using ngIf.
23681 if (noItemsTemplate) element.attr('md-has-not-found', true);
23682
23683 // Always set our tabindex of the autocomplete directive to -1, because our input
23684 // will hold the actual tabindex.
23685 element.attr('tabindex', '-1');
23686
23687 return '\
23688 <md-autocomplete-wrap\
23689 ng-class="{ \'md-whiteframe-z1\': !floatingLabel, \'md-menu-showing\': !$mdAutocompleteCtrl.hidden }">\
23690 ' + getInputElement() + '\
23691 <md-progress-linear\
23692 class="' + (attr.mdFloatingLabel ? 'md-inline' : '') + '"\
23693 ng-if="$mdAutocompleteCtrl.loadingIsVisible()"\
23694 md-mode="indeterminate"></md-progress-linear>\
23695 <md-virtual-repeat-container\
23696 md-auto-shrink\
23697 md-auto-shrink-min="1"\
23698 ng-mouseenter="$mdAutocompleteCtrl.listEnter()"\
23699 ng-mouseleave="$mdAutocompleteCtrl.listLeave()"\
23700 ng-mouseup="$mdAutocompleteCtrl.mouseUp()"\
23701 ng-hide="$mdAutocompleteCtrl.hidden"\
23702 class="md-autocomplete-suggestions-container md-whiteframe-z1"\
23703 ng-class="{ \'md-not-found\': $mdAutocompleteCtrl.notFoundVisible() }"\
23704 role="presentation">\
23705 <ul class="md-autocomplete-suggestions"\
23706 ng-class="::menuClass"\
23707 id="ul-{{$mdAutocompleteCtrl.id}}">\
23708 <li md-virtual-repeat="item in $mdAutocompleteCtrl.matches"\
23709 ng-class="{ selected: $index === $mdAutocompleteCtrl.index }"\
23710 ng-click="$mdAutocompleteCtrl.select($index)"\
23711 md-extra-name="$mdAutocompleteCtrl.itemName">\
23712 ' + itemTemplate + '\
23713 </li>' + noItemsTemplate + '\
23714 </ul>\
23715 </md-virtual-repeat-container>\
23716 </md-autocomplete-wrap>\
23717 <aria-status\
23718 class="md-visually-hidden"\
23719 role="status"\
23720 aria-live="assertive">\
23721 <p ng-repeat="message in $mdAutocompleteCtrl.messages track by $index" ng-if="message">{{message}}</p>\
23722 </aria-status>';
23723
23724 function getItemTemplate() {
23725 var templateTag = element.find('md-item-template').detach(),
23726 html = templateTag.length ? templateTag.html() : element.html();
23727 if (!templateTag.length) element.empty();
23728 return '<md-autocomplete-parent-scope md-autocomplete-replace>' + html + '</md-autocomplete-parent-scope>';
23729 }
23730
23731 function getNoItemsTemplate() {
23732 var templateTag = element.find('md-not-found').detach(),
23733 template = templateTag.length ? templateTag.html() : '';
23734 return template
23735 ? '<li ng-if="$mdAutocompleteCtrl.notFoundVisible()"\
23736 md-autocomplete-parent-scope>' + template + '</li>'
23737 : '';
23738
23739 }
23740
23741 function getInputElement () {
23742 if (attr.mdFloatingLabel) {
23743 return '\
23744 <md-input-container ng-if="floatingLabel">\
23745 <label>{{floatingLabel}}</label>\
23746 <input type="search"\
23747 ' + (tabindex != null ? 'tabindex="' + tabindex + '"' : '') + '\
23748 id="{{ inputId || \'fl-input-\' + $mdAutocompleteCtrl.id }}"\
23749 name="{{inputName}}"\
23750 autocomplete="off"\
23751 ng-required="$mdAutocompleteCtrl.isRequired"\
23752 ng-readonly="$mdAutocompleteCtrl.isReadonly"\
23753 ng-minlength="inputMinlength"\
23754 ng-maxlength="inputMaxlength"\
23755 ng-disabled="$mdAutocompleteCtrl.isDisabled"\
23756 ng-model="$mdAutocompleteCtrl.scope.searchText"\
23757 ng-model-options="{ allowInvalid: true }"\
23758 ng-keydown="$mdAutocompleteCtrl.keydown($event)"\
23759 ng-blur="$mdAutocompleteCtrl.blur($event)"\
23760 ng-focus="$mdAutocompleteCtrl.focus($event)"\
23761 aria-owns="ul-{{$mdAutocompleteCtrl.id}}"\
23762 ' + (attr.mdNoAsterisk != null ? 'md-no-asterisk="' + attr.mdNoAsterisk + '"' : '') + '\
23763 ' + (attr.mdSelectOnFocus != null ? 'md-select-on-focus=""' : '') + '\
23764 aria-label="{{floatingLabel}}"\
23765 aria-autocomplete="list"\
23766 role="combobox"\
23767 aria-haspopup="true"\
23768 aria-activedescendant=""\
23769 aria-expanded="{{!$mdAutocompleteCtrl.hidden}}"/>\
23770 <div md-autocomplete-parent-scope md-autocomplete-replace>' + leftover + '</div>\
23771 </md-input-container>';
23772 } else {
23773 return '\
23774 <input type="search"\
23775 ' + (tabindex != null ? 'tabindex="' + tabindex + '"' : '') + '\
23776 id="{{ inputId || \'input-\' + $mdAutocompleteCtrl.id }}"\
23777 name="{{inputName}}"\
23778 ng-if="!floatingLabel"\
23779 autocomplete="off"\
23780 ng-required="$mdAutocompleteCtrl.isRequired"\
23781 ng-disabled="$mdAutocompleteCtrl.isDisabled"\
23782 ng-readonly="$mdAutocompleteCtrl.isReadonly"\
23783 ng-model="$mdAutocompleteCtrl.scope.searchText"\
23784 ng-keydown="$mdAutocompleteCtrl.keydown($event)"\
23785 ng-blur="$mdAutocompleteCtrl.blur($event)"\
23786 ng-focus="$mdAutocompleteCtrl.focus($event)"\
23787 placeholder="{{placeholder}}"\
23788 aria-owns="ul-{{$mdAutocompleteCtrl.id}}"\
23789 ' + (attr.mdSelectOnFocus != null ? 'md-select-on-focus=""' : '') + '\
23790 aria-label="{{placeholder}}"\
23791 aria-autocomplete="list"\
23792 role="combobox"\
23793 aria-haspopup="true"\
23794 aria-activedescendant=""\
23795 aria-expanded="{{!$mdAutocompleteCtrl.hidden}}"/>\
23796 <button\
23797 type="button"\
23798 tabindex="-1"\
23799 ng-if="$mdAutocompleteCtrl.scope.searchText && !$mdAutocompleteCtrl.isDisabled"\
23800 ng-click="$mdAutocompleteCtrl.clear($event)">\
23801 <md-icon md-svg-src="' + $$mdSvgRegistry.mdClose + '"></md-icon>\
23802 <span class="md-visually-hidden">Clear</span>\
23803 </button>\
23804 ';
23805 }
23806 }
23807 }
23808 };
23809}
23810
23811})();
23812(function(){
23813"use strict";
23814
23815
23816MdAutocompleteItemScopeDirective.$inject = ["$compile", "$mdUtil"];angular
23817 .module('material.components.autocomplete')
23818 .directive('mdAutocompleteParentScope', MdAutocompleteItemScopeDirective);
23819
23820function MdAutocompleteItemScopeDirective($compile, $mdUtil) {
23821 return {
23822 restrict: 'AE',
23823 compile: compile,
23824 terminal: true,
23825 transclude: 'element'
23826 };
23827
23828 function compile(tElement, tAttr, transclude) {
23829 return function postLink(scope, element, attr) {
23830 var ctrl = scope.$mdAutocompleteCtrl;
23831 var newScope = ctrl.parent.$new();
23832 var itemName = ctrl.itemName;
23833
23834 // Watch for changes to our scope's variables and copy them to the new scope
23835 watchVariable('$index', '$index');
23836 watchVariable('item', itemName);
23837
23838 // Ensure that $digest calls on our scope trigger $digest on newScope.
23839 connectScopes();
23840
23841 // Link the element against newScope.
23842 transclude(newScope, function(clone) {
23843 element.after(clone);
23844 });
23845
23846 /**
23847 * Creates a watcher for variables that are copied from the parent scope
23848 * @param variable
23849 * @param alias
23850 */
23851 function watchVariable(variable, alias) {
23852 newScope[alias] = scope[variable];
23853
23854 scope.$watch(variable, function(value) {
23855 $mdUtil.nextTick(function() {
23856 newScope[alias] = value;
23857 });
23858 });
23859 }
23860
23861 /**
23862 * Creates watchers on scope and newScope that ensure that for any
23863 * $digest of scope, newScope is also $digested.
23864 */
23865 function connectScopes() {
23866 var scopeDigesting = false;
23867 var newScopeDigesting = false;
23868
23869 scope.$watch(function() {
23870 if (newScopeDigesting || scopeDigesting) {
23871 return;
23872 }
23873
23874 scopeDigesting = true;
23875 scope.$$postDigest(function() {
23876 if (!newScopeDigesting) {
23877 newScope.$digest();
23878 }
23879
23880 scopeDigesting = newScopeDigesting = false;
23881 });
23882 });
23883
23884 newScope.$watch(function() {
23885 newScopeDigesting = true;
23886 });
23887 }
23888 };
23889 }
23890}
23891})();
23892(function(){
23893"use strict";
23894
23895
23896MdHighlightCtrl.$inject = ["$scope", "$element", "$attrs"];angular
23897 .module('material.components.autocomplete')
23898 .controller('MdHighlightCtrl', MdHighlightCtrl);
23899
23900function MdHighlightCtrl ($scope, $element, $attrs) {
23901 this.$scope = $scope;
23902 this.$element = $element;
23903 this.$attrs = $attrs;
23904
23905 // Cache the Regex to avoid rebuilding each time.
23906 this.regex = null;
23907}
23908
23909MdHighlightCtrl.prototype.init = function(unsafeTermFn, unsafeContentFn) {
23910
23911 this.flags = this.$attrs.mdHighlightFlags || '';
23912
23913 this.unregisterFn = this.$scope.$watch(function($scope) {
23914 return {
23915 term: unsafeTermFn($scope),
23916 contentText: unsafeContentFn($scope)
23917 };
23918 }.bind(this), this.onRender.bind(this), true);
23919
23920 this.$element.on('$destroy', this.unregisterFn);
23921};
23922
23923/**
23924 * Triggered once a new change has been recognized and the highlighted
23925 * text needs to be updated.
23926 */
23927MdHighlightCtrl.prototype.onRender = function(state, prevState) {
23928
23929 var contentText = state.contentText;
23930
23931 /* Update the regex if it's outdated, because we don't want to rebuilt it constantly. */
23932 if (this.regex === null || state.term !== prevState.term) {
23933 this.regex = this.createRegex(state.term, this.flags);
23934 }
23935
23936 /* If a term is available apply the regex to the content */
23937 if (state.term) {
23938 this.applyRegex(contentText);
23939 } else {
23940 this.$element.text(contentText);
23941 }
23942
23943};
23944
23945/**
23946 * Decomposes the specified text into different tokens (whether match or not).
23947 * Breaking down the string guarantees proper XSS protection due to the native browser
23948 * escaping of unsafe text.
23949 */
23950MdHighlightCtrl.prototype.applyRegex = function(text) {
23951 var tokens = this.resolveTokens(text);
23952
23953 this.$element.empty();
23954
23955 tokens.forEach(function (token) {
23956
23957 if (token.isMatch) {
23958 var tokenEl = angular.element('<span class="highlight">').text(token.text);
23959
23960 this.$element.append(tokenEl);
23961 } else {
23962 this.$element.append(document.createTextNode(token));
23963 }
23964
23965 }.bind(this));
23966
23967};
23968
23969 /**
23970 * Decomposes the specified text into different tokens by running the regex against the text.
23971 */
23972MdHighlightCtrl.prototype.resolveTokens = function(string) {
23973 var tokens = [];
23974 var lastIndex = 0;
23975
23976 // Use replace here, because it supports global and single regular expressions at same time.
23977 string.replace(this.regex, function(match, index) {
23978 appendToken(lastIndex, index);
23979
23980 tokens.push({
23981 text: match,
23982 isMatch: true
23983 });
23984
23985 lastIndex = index + match.length;
23986 });
23987
23988 // Append the missing text as a token.
23989 appendToken(lastIndex);
23990
23991 return tokens;
23992
23993 function appendToken(from, to) {
23994 var targetText = string.slice(from, to);
23995 targetText && tokens.push(targetText);
23996 }
23997};
23998
23999/** Creates a regex for the specified text with the given flags. */
24000MdHighlightCtrl.prototype.createRegex = function(term, flags) {
24001 var startFlag = '', endFlag = '';
24002 var regexTerm = this.sanitizeRegex(term);
24003
24004 if (flags.indexOf('^') >= 0) startFlag = '^';
24005 if (flags.indexOf('$') >= 0) endFlag = '$';
24006
24007 return new RegExp(startFlag + regexTerm + endFlag, flags.replace(/[$\^]/g, ''));
24008};
24009
24010/** Sanitizes a regex by removing all common RegExp identifiers */
24011MdHighlightCtrl.prototype.sanitizeRegex = function(term) {
24012 return term && term.toString().replace(/[\\\^\$\*\+\?\.\(\)\|\{}\[\]]/g, '\\$&');
24013};
24014
24015})();
24016(function(){
24017"use strict";
24018
24019
24020MdHighlight.$inject = ["$interpolate", "$parse"];angular
24021 .module('material.components.autocomplete')
24022 .directive('mdHighlightText', MdHighlight);
24023
24024/**
24025 * @ngdoc directive
24026 * @name mdHighlightText
24027 * @module material.components.autocomplete
24028 *
24029 * @description
24030 * The `md-highlight-text` directive allows you to specify text that should be highlighted within
24031 * an element. Highlighted text will be wrapped in `<span class="highlight"></span>` which can
24032 * be styled through CSS. Please note that child elements may not be used with this directive.
24033 *
24034 * @param {string} md-highlight-text A model to be searched for
24035 * @param {string=} md-highlight-flags A list of flags (loosely based on JavaScript RexExp flags).
24036 * #### **Supported flags**:
24037 * - `g`: Find all matches within the provided text
24038 * - `i`: Ignore case when searching for matches
24039 * - `$`: Only match if the text ends with the search term
24040 * - `^`: Only match if the text begins with the search term
24041 *
24042 * @usage
24043 * <hljs lang="html">
24044 * <input placeholder="Enter a search term..." ng-model="searchTerm" type="text" />
24045 * <ul>
24046 * <li ng-repeat="result in results" md-highlight-text="searchTerm">
24047 * {{result.text}}
24048 * </li>
24049 * </ul>
24050 * </hljs>
24051 */
24052
24053function MdHighlight ($interpolate, $parse) {
24054 return {
24055 terminal: true,
24056 controller: 'MdHighlightCtrl',
24057 compile: function mdHighlightCompile(tElement, tAttr) {
24058 var termExpr = $parse(tAttr.mdHighlightText);
24059 var unsafeContentExpr = $interpolate(tElement.html());
24060
24061 return function mdHighlightLink(scope, element, attr, ctrl) {
24062 ctrl.init(termExpr, unsafeContentExpr);
24063 };
24064 }
24065 };
24066}
24067
24068})();
24069(function(){
24070"use strict";
24071
24072
24073MdChipCtrl.$inject = ["$scope", "$element", "$mdConstant", "$timeout", "$mdUtil"];angular
24074 .module('material.components.chips')
24075 .controller('MdChipCtrl', MdChipCtrl);
24076
24077/**
24078 * Controller for the MdChip component. Responsible for handling keyboard
24079 * events and editting the chip if needed.
24080 *
24081 * @param $scope
24082 * @param $element
24083 * @param $mdConstant
24084 * @param $timeout
24085 * @param $mdUtil
24086 * @constructor
24087 */
24088function MdChipCtrl ($scope, $element, $mdConstant, $timeout, $mdUtil) {
24089 /**
24090 * @type {$scope}
24091 */
24092 this.$scope = $scope;
24093
24094 /**
24095 * @type {$element}
24096 */
24097 this.$element = $element;
24098
24099 /**
24100 * @type {$mdConstant}
24101 */
24102 this.$mdConstant = $mdConstant;
24103
24104 /**
24105 * @type {$timeout}
24106 */
24107 this.$timeout = $timeout;
24108
24109 /**
24110 * @type {$mdUtil}
24111 */
24112 this.$mdUtil = $mdUtil;
24113
24114 /**
24115 * @type {boolean}
24116 */
24117 this.isEditting = false;
24118
24119 /**
24120 * @type {MdChipsCtrl}
24121 */
24122 this.parentController = undefined;
24123
24124 /**
24125 * @type {boolean}
24126 */
24127 this.enableChipEdit = false;
24128}
24129
24130
24131/**
24132 * @param {MdChipsCtrl} controller
24133 */
24134MdChipCtrl.prototype.init = function(controller) {
24135 this.parentController = controller;
24136 this.enableChipEdit = this.parentController.enableChipEdit;
24137
24138 if (this.enableChipEdit) {
24139 this.$element.on('keydown', this.chipKeyDown.bind(this));
24140 this.$element.on('mousedown', this.chipMouseDown.bind(this));
24141 this.getChipContent().addClass('_md-chip-content-edit-is-enabled');
24142 }
24143};
24144
24145
24146/**
24147 * @return {Object}
24148 */
24149MdChipCtrl.prototype.getChipContent = function() {
24150 var chipContents = this.$element[0].getElementsByClassName('md-chip-content');
24151 return angular.element(chipContents[0]);
24152};
24153
24154
24155/**
24156 * @return {Object}
24157 */
24158MdChipCtrl.prototype.getContentElement = function() {
24159 return angular.element(this.getChipContent().children()[0]);
24160};
24161
24162
24163/**
24164 * @return {number}
24165 */
24166MdChipCtrl.prototype.getChipIndex = function() {
24167 return parseInt(this.$element.attr('index'));
24168};
24169
24170
24171/**
24172 * Presents an input element to edit the contents of the chip.
24173 */
24174MdChipCtrl.prototype.goOutOfEditMode = function() {
24175 if (!this.isEditting) return;
24176
24177 this.isEditting = false;
24178 this.$element.removeClass('_md-chip-editing');
24179 this.getChipContent()[0].contentEditable = 'false';
24180 var chipIndex = this.getChipIndex();
24181
24182 var content = this.getContentElement().text();
24183 if (content) {
24184 this.parentController.updateChipContents(
24185 chipIndex,
24186 this.getContentElement().text()
24187 );
24188
24189 this.$mdUtil.nextTick(function() {
24190 if (this.parentController.selectedChip === chipIndex) {
24191 this.parentController.focusChip(chipIndex);
24192 }
24193 }.bind(this));
24194 } else {
24195 this.parentController.removeChipAndFocusInput(chipIndex);
24196 }
24197};
24198
24199
24200/**
24201 * Given an HTML element. Selects contents of it.
24202 * @param node
24203 */
24204MdChipCtrl.prototype.selectNodeContents = function(node) {
24205 var range, selection;
24206 if (document.body.createTextRange) {
24207 range = document.body.createTextRange();
24208 range.moveToElementText(node);
24209 range.select();
24210 } else if (window.getSelection) {
24211 selection = window.getSelection();
24212 range = document.createRange();
24213 range.selectNodeContents(node);
24214 selection.removeAllRanges();
24215 selection.addRange(range);
24216 }
24217};
24218
24219
24220/**
24221 * Presents an input element to edit the contents of the chip.
24222 */
24223MdChipCtrl.prototype.goInEditMode = function() {
24224 this.isEditting = true;
24225 this.$element.addClass('_md-chip-editing');
24226 this.getChipContent()[0].contentEditable = 'true';
24227 this.getChipContent().on('blur', function() {
24228 this.goOutOfEditMode();
24229 }.bind(this));
24230
24231 this.selectNodeContents(this.getChipContent()[0]);
24232};
24233
24234
24235/**
24236 * Handles the keydown event on the chip element. If enable-chip-edit attribute is
24237 * set to true, space or enter keys can trigger going into edit mode. Enter can also
24238 * trigger submitting if the chip is already being edited.
24239 * @param event
24240 */
24241MdChipCtrl.prototype.chipKeyDown = function(event) {
24242 if (!this.isEditting &&
24243 (event.keyCode === this.$mdConstant.KEY_CODE.ENTER ||
24244 event.keyCode === this.$mdConstant.KEY_CODE.SPACE)) {
24245 event.preventDefault();
24246 this.goInEditMode();
24247 } else if (this.isEditting &&
24248 event.keyCode === this.$mdConstant.KEY_CODE.ENTER) {
24249 event.preventDefault();
24250 this.goOutOfEditMode();
24251 }
24252};
24253
24254
24255/**
24256 * Handles the double click event
24257 */
24258MdChipCtrl.prototype.chipMouseDown = function() {
24259 if(this.getChipIndex() == this.parentController.selectedChip &&
24260 this.enableChipEdit &&
24261 !this.isEditting) {
24262 this.goInEditMode();
24263 }
24264};
24265
24266})();
24267(function(){
24268"use strict";
24269
24270
24271MdChip.$inject = ["$mdTheming", "$mdUtil"];angular
24272 .module('material.components.chips')
24273 .directive('mdChip', MdChip);
24274
24275/**
24276 * @ngdoc directive
24277 * @name mdChip
24278 * @module material.components.chips
24279 *
24280 * @description
24281 * `<md-chip>` is a component used within `<md-chips>` and is responsible for rendering individual
24282 * chips.
24283 *
24284 *
24285 * @usage
24286 * <hljs lang="html">
24287 * <md-chip>{{$chip}}</md-chip>
24288 * </hljs>
24289 *
24290 */
24291
24292// This hint text is hidden within a chip but used by screen readers to
24293// inform the user how they can interact with a chip.
24294var DELETE_HINT_TEMPLATE = '\
24295 <span ng-if="!$mdChipsCtrl.readonly" class="md-visually-hidden">\
24296 {{$mdChipsCtrl.deleteHint}}\
24297 </span>';
24298
24299/**
24300 * MDChip Directive Definition
24301 *
24302 * @param $mdTheming
24303 * @param $mdUtil
24304 * @ngInject
24305 */
24306function MdChip($mdTheming, $mdUtil) {
24307 var hintTemplate = $mdUtil.processTemplate(DELETE_HINT_TEMPLATE);
24308
24309 return {
24310 restrict: 'E',
24311 require: ['^?mdChips', 'mdChip'],
24312 compile: compile,
24313 controller: 'MdChipCtrl'
24314 };
24315
24316 function compile(element, attr) {
24317 // Append the delete template
24318 element.append($mdUtil.processTemplate(hintTemplate));
24319
24320 return function postLink(scope, element, attr, ctrls) {
24321 var chipsController = ctrls.shift();
24322 var chipController = ctrls.shift();
24323 $mdTheming(element);
24324
24325 if (chipsController) {
24326 chipController.init(chipsController);
24327
24328 angular
24329 .element(element[0]
24330 .querySelector('.md-chip-content'))
24331 .on('blur', function () {
24332 chipsController.resetSelectedChip();
24333 chipsController.$scope.$applyAsync();
24334 });
24335 }
24336 };
24337 }
24338}
24339
24340})();
24341(function(){
24342"use strict";
24343
24344
24345MdChipRemove.$inject = ["$timeout"];angular
24346 .module('material.components.chips')
24347 .directive('mdChipRemove', MdChipRemove);
24348
24349/**
24350 * @ngdoc directive
24351 * @name mdChipRemove
24352 * @restrict A
24353 * @module material.components.chips
24354 *
24355 * @description
24356 * Designates an element to be used as the delete button for a chip. <br/>
24357 * This element is passed as a child of the `md-chips` element.
24358 *
24359 * The designated button will be just appended to the chip and removes the given chip on click.<br/>
24360 * By default the button is not being styled by the `md-chips` component.
24361 *
24362 * @usage
24363 * <hljs lang="html">
24364 * <md-chips>
24365 * <button md-chip-remove="">
24366 * <md-icon md-svg-icon="md-close"></md-icon>
24367 * </button>
24368 * </md-chips>
24369 * </hljs>
24370 */
24371
24372
24373/**
24374 * MdChipRemove Directive Definition.
24375 *
24376 * @param $timeout
24377 * @returns {{restrict: string, require: string[], link: Function, scope: boolean}}
24378 * @constructor
24379 */
24380function MdChipRemove ($timeout) {
24381 return {
24382 restrict: 'A',
24383 require: '^mdChips',
24384 scope: false,
24385 link: postLink
24386 };
24387
24388 function postLink(scope, element, attr, ctrl) {
24389 element.on('click', function(event) {
24390 scope.$apply(function() {
24391 ctrl.removeChip(scope.$$replacedScope.$index);
24392 });
24393 });
24394
24395 // Child elements aren't available until after a $timeout tick as they are hidden by an
24396 // `ng-if`. see http://goo.gl/zIWfuw
24397 $timeout(function() {
24398 element.attr({ tabindex: -1, 'aria-hidden': true });
24399 element.find('button').attr('tabindex', '-1');
24400 });
24401 }
24402}
24403
24404})();
24405(function(){
24406"use strict";
24407
24408
24409MdChipTransclude.$inject = ["$compile"];angular
24410 .module('material.components.chips')
24411 .directive('mdChipTransclude', MdChipTransclude);
24412
24413function MdChipTransclude ($compile) {
24414 return {
24415 restrict: 'EA',
24416 terminal: true,
24417 link: link,
24418 scope: false
24419 };
24420 function link (scope, element, attr) {
24421 var ctrl = scope.$parent.$mdChipsCtrl,
24422 newScope = ctrl.parent.$new(false, ctrl.parent);
24423 newScope.$$replacedScope = scope;
24424 newScope.$chip = scope.$chip;
24425 newScope.$index = scope.$index;
24426 newScope.$mdChipsCtrl = ctrl;
24427
24428 var newHtml = ctrl.$scope.$eval(attr.mdChipTransclude);
24429
24430 element.html(newHtml);
24431 $compile(element.contents())(newScope);
24432 }
24433}
24434
24435})();
24436(function(){
24437"use strict";
24438
24439
24440MdChipsCtrl.$inject = ["$scope", "$attrs", "$mdConstant", "$log", "$element", "$timeout", "$mdUtil"];angular
24441 .module('material.components.chips')
24442 .controller('MdChipsCtrl', MdChipsCtrl);
24443
24444/**
24445 * Controller for the MdChips component. Responsible for adding to and
24446 * removing from the list of chips, marking chips as selected, and binding to
24447 * the models of various input components.
24448 *
24449 * @param $scope
24450 * @param $attrs
24451 * @param $mdConstant
24452 * @param $log
24453 * @param $element
24454 * @param $timeout
24455 * @param $mdUtil
24456 * @constructor
24457 */
24458function MdChipsCtrl ($scope, $attrs, $mdConstant, $log, $element, $timeout, $mdUtil) {
24459 /** @type {$timeout} **/
24460 this.$timeout = $timeout;
24461
24462 /** @type {Object} */
24463 this.$mdConstant = $mdConstant;
24464
24465 /** @type {angular.$scope} */
24466 this.$scope = $scope;
24467
24468 /** @type {angular.$scope} */
24469 this.parent = $scope.$parent;
24470
24471 /** @type {$log} */
24472 this.$log = $log;
24473
24474 /** @type {$element} */
24475 this.$element = $element;
24476
24477 /** @type {angular.NgModelController} */
24478 this.ngModelCtrl = null;
24479
24480 /** @type {angular.NgModelController} */
24481 this.userInputNgModelCtrl = null;
24482
24483 /** @type {MdAutocompleteCtrl} */
24484 this.autocompleteCtrl = null;
24485
24486 /** @type {Element} */
24487 this.userInputElement = null;
24488
24489 /** @type {Array.<Object>} */
24490 this.items = [];
24491
24492 /** @type {number} */
24493 this.selectedChip = -1;
24494
24495 /** @type {string} */
24496 this.enableChipEdit = $mdUtil.parseAttributeBoolean($attrs.mdEnableChipEdit);
24497
24498 /** @type {string} */
24499 this.addOnBlur = $mdUtil.parseAttributeBoolean($attrs.mdAddOnBlur);
24500
24501 /**
24502 * Hidden hint text for how to delete a chip. Used to give context to screen readers.
24503 * @type {string}
24504 */
24505 this.deleteHint = 'Press delete to remove this chip.';
24506
24507 /**
24508 * Hidden label for the delete button. Used to give context to screen readers.
24509 * @type {string}
24510 */
24511 this.deleteButtonLabel = 'Remove';
24512
24513 /**
24514 * Model used by the input element.
24515 * @type {string}
24516 */
24517 this.chipBuffer = '';
24518
24519 /**
24520 * Whether to use the transformChip expression to transform the chip buffer
24521 * before appending it to the list.
24522 * @type {boolean}
24523 */
24524 this.useTransformChip = false;
24525
24526 /**
24527 * Whether to use the onAdd expression to notify of chip additions.
24528 * @type {boolean}
24529 */
24530 this.useOnAdd = false;
24531
24532 /**
24533 * Whether to use the onRemove expression to notify of chip removals.
24534 * @type {boolean}
24535 */
24536 this.useOnRemove = false;
24537
24538 /**
24539 * Whether to use the onSelect expression to notify the component's user
24540 * after selecting a chip from the list.
24541 * @type {boolean}
24542 */
24543}
24544
24545/**
24546 * Handles the keydown event on the input element: by default <enter> appends
24547 * the buffer to the chip list, while backspace removes the last chip in the
24548 * list if the current buffer is empty.
24549 * @param event
24550 */
24551MdChipsCtrl.prototype.inputKeydown = function(event) {
24552 var chipBuffer = this.getChipBuffer();
24553
24554 // If we have an autocomplete, and it handled the event, we have nothing to do
24555 if (this.autocompleteCtrl && event.isDefaultPrevented && event.isDefaultPrevented()) {
24556 return;
24557 }
24558
24559 if (event.keyCode === this.$mdConstant.KEY_CODE.BACKSPACE) {
24560 // Only select and focus the previous chip, if the current caret position of the
24561 // input element is at the beginning.
24562 if (this.getCursorPosition(event.target) !== 0) {
24563 return;
24564 }
24565
24566 event.preventDefault();
24567 event.stopPropagation();
24568
24569 if (this.items.length) {
24570 this.selectAndFocusChipSafe(this.items.length - 1);
24571 }
24572
24573 return;
24574 }
24575
24576 // By default <enter> appends the buffer to the chip list.
24577 if (!this.separatorKeys || this.separatorKeys.length < 1) {
24578 this.separatorKeys = [this.$mdConstant.KEY_CODE.ENTER];
24579 }
24580
24581 // Support additional separator key codes in an array of `md-separator-keys`.
24582 if (this.separatorKeys.indexOf(event.keyCode) !== -1) {
24583 if ((this.autocompleteCtrl && this.requireMatch) || !chipBuffer) return;
24584 event.preventDefault();
24585
24586 // Only append the chip and reset the chip buffer if the max chips limit isn't reached.
24587 if (this.hasMaxChipsReached()) return;
24588
24589 this.appendChip(chipBuffer.trim());
24590 this.resetChipBuffer();
24591 }
24592};
24593
24594/**
24595 * Returns the cursor position of the specified input element.
24596 * @param element HTMLInputElement
24597 * @returns {Number} Cursor Position of the input.
24598 */
24599MdChipsCtrl.prototype.getCursorPosition = function(element) {
24600 /*
24601 * Figure out whether the current input for the chips buffer is valid for using
24602 * the selectionStart / end property to retrieve the cursor position.
24603 * Some browsers do not allow the use of those attributes, on different input types.
24604 */
24605 try {
24606 if (element.selectionStart === element.selectionEnd) {
24607 return element.selectionStart;
24608 }
24609 } catch (e) {
24610 if (!element.value) {
24611 return 0;
24612 }
24613 }
24614};
24615
24616
24617/**
24618 * Updates the content of the chip at given index
24619 * @param chipIndex
24620 * @param chipContents
24621 */
24622MdChipsCtrl.prototype.updateChipContents = function(chipIndex, chipContents){
24623 if(chipIndex >= 0 && chipIndex < this.items.length) {
24624 this.items[chipIndex] = chipContents;
24625 this.ngModelCtrl.$setDirty();
24626 }
24627};
24628
24629
24630/**
24631 * Returns true if a chip is currently being edited. False otherwise.
24632 * @return {boolean}
24633 */
24634MdChipsCtrl.prototype.isEditingChip = function() {
24635 return !!this.$element[0].getElementsByClassName('_md-chip-editing').length;
24636};
24637
24638
24639MdChipsCtrl.prototype.isRemovable = function() {
24640 // Return false if we have static chips
24641 if (!this.ngModelCtrl) {
24642 return false;
24643 }
24644
24645 return this.readonly ? this.removable :
24646 angular.isDefined(this.removable) ? this.removable : true;
24647};
24648
24649/**
24650 * Handles the keydown event on the chip elements: backspace removes the selected chip, arrow
24651 * keys switch which chips is active
24652 * @param event
24653 */
24654MdChipsCtrl.prototype.chipKeydown = function (event) {
24655 if (this.getChipBuffer()) return;
24656 if (this.isEditingChip()) return;
24657
24658 switch (event.keyCode) {
24659 case this.$mdConstant.KEY_CODE.BACKSPACE:
24660 case this.$mdConstant.KEY_CODE.DELETE:
24661 if (this.selectedChip < 0) return;
24662 event.preventDefault();
24663 // Cancel the delete action only after the event cancel. Otherwise the page will go back.
24664 if (!this.isRemovable()) return;
24665 this.removeAndSelectAdjacentChip(this.selectedChip);
24666 break;
24667 case this.$mdConstant.KEY_CODE.LEFT_ARROW:
24668 event.preventDefault();
24669 if (this.selectedChip < 0) this.selectedChip = this.items.length;
24670 if (this.items.length) this.selectAndFocusChipSafe(this.selectedChip - 1);
24671 break;
24672 case this.$mdConstant.KEY_CODE.RIGHT_ARROW:
24673 event.preventDefault();
24674 this.selectAndFocusChipSafe(this.selectedChip + 1);
24675 break;
24676 case this.$mdConstant.KEY_CODE.ESCAPE:
24677 case this.$mdConstant.KEY_CODE.TAB:
24678 if (this.selectedChip < 0) return;
24679 event.preventDefault();
24680 this.onFocus();
24681 break;
24682 }
24683};
24684
24685/**
24686 * Get the input's placeholder - uses `placeholder` when list is empty and `secondary-placeholder`
24687 * when the list is non-empty. If `secondary-placeholder` is not provided, `placeholder` is used
24688 * always.
24689 */
24690MdChipsCtrl.prototype.getPlaceholder = function() {
24691 // Allow `secondary-placeholder` to be blank.
24692 var useSecondary = (this.items && this.items.length &&
24693 (this.secondaryPlaceholder == '' || this.secondaryPlaceholder));
24694 return useSecondary ? this.secondaryPlaceholder : this.placeholder;
24695};
24696
24697/**
24698 * Removes chip at {@code index} and selects the adjacent chip.
24699 * @param index
24700 */
24701MdChipsCtrl.prototype.removeAndSelectAdjacentChip = function(index) {
24702 var selIndex = this.getAdjacentChipIndex(index);
24703 this.removeChip(index);
24704 this.$timeout(angular.bind(this, function () {
24705 this.selectAndFocusChipSafe(selIndex);
24706 }));
24707};
24708
24709/**
24710 * Sets the selected chip index to -1.
24711 */
24712MdChipsCtrl.prototype.resetSelectedChip = function() {
24713 this.selectedChip = -1;
24714};
24715
24716/**
24717 * Gets the index of an adjacent chip to select after deletion. Adjacency is
24718 * determined as the next chip in the list, unless the target chip is the
24719 * last in the list, then it is the chip immediately preceding the target. If
24720 * there is only one item in the list, -1 is returned (select none).
24721 * The number returned is the index to select AFTER the target has been
24722 * removed.
24723 * If the current chip is not selected, then -1 is returned to select none.
24724 */
24725MdChipsCtrl.prototype.getAdjacentChipIndex = function(index) {
24726 var len = this.items.length - 1;
24727 return (len == 0) ? -1 :
24728 (index == len) ? index -1 : index;
24729};
24730
24731/**
24732 * Append the contents of the buffer to the chip list. This method will first
24733 * call out to the md-transform-chip method, if provided.
24734 *
24735 * @param newChip
24736 */
24737MdChipsCtrl.prototype.appendChip = function(newChip) {
24738 if (this.useTransformChip && this.transformChip) {
24739 var transformedChip = this.transformChip({'$chip': newChip});
24740
24741 // Check to make sure the chip is defined before assigning it, otherwise, we'll just assume
24742 // they want the string version.
24743 if (angular.isDefined(transformedChip)) {
24744 newChip = transformedChip;
24745 }
24746 }
24747
24748 // If items contains an identical object to newChip, do not append
24749 if (angular.isObject(newChip)){
24750 var identical = this.items.some(function(item){
24751 return angular.equals(newChip, item);
24752 });
24753 if (identical) return;
24754 }
24755
24756 // Check for a null (but not undefined), or existing chip and cancel appending
24757 if (newChip == null || this.items.indexOf(newChip) + 1) return;
24758
24759 // Append the new chip onto our list
24760 var index = this.items.push(newChip);
24761
24762 // Update model validation
24763 this.ngModelCtrl.$setDirty();
24764 this.validateModel();
24765
24766 // If they provide the md-on-add attribute, notify them of the chip addition
24767 if (this.useOnAdd && this.onAdd) {
24768 this.onAdd({ '$chip': newChip, '$index': index });
24769 }
24770};
24771
24772/**
24773 * Sets whether to use the md-transform-chip expression. This expression is
24774 * bound to scope and controller in {@code MdChipsDirective} as
24775 * {@code transformChip}. Due to the nature of directive scope bindings, the
24776 * controller cannot know on its own/from the scope whether an expression was
24777 * actually provided.
24778 */
24779MdChipsCtrl.prototype.useTransformChipExpression = function() {
24780 this.useTransformChip = true;
24781};
24782
24783/**
24784 * Sets whether to use the md-on-add expression. This expression is
24785 * bound to scope and controller in {@code MdChipsDirective} as
24786 * {@code onAdd}. Due to the nature of directive scope bindings, the
24787 * controller cannot know on its own/from the scope whether an expression was
24788 * actually provided.
24789 */
24790MdChipsCtrl.prototype.useOnAddExpression = function() {
24791 this.useOnAdd = true;
24792};
24793
24794/**
24795 * Sets whether to use the md-on-remove expression. This expression is
24796 * bound to scope and controller in {@code MdChipsDirective} as
24797 * {@code onRemove}. Due to the nature of directive scope bindings, the
24798 * controller cannot know on its own/from the scope whether an expression was
24799 * actually provided.
24800 */
24801MdChipsCtrl.prototype.useOnRemoveExpression = function() {
24802 this.useOnRemove = true;
24803};
24804
24805/*
24806 * Sets whether to use the md-on-select expression. This expression is
24807 * bound to scope and controller in {@code MdChipsDirective} as
24808 * {@code onSelect}. Due to the nature of directive scope bindings, the
24809 * controller cannot know on its own/from the scope whether an expression was
24810 * actually provided.
24811 */
24812MdChipsCtrl.prototype.useOnSelectExpression = function() {
24813 this.useOnSelect = true;
24814};
24815
24816/**
24817 * Gets the input buffer. The input buffer can be the model bound to the
24818 * default input item {@code this.chipBuffer}, the {@code selectedItem}
24819 * model of an {@code md-autocomplete}, or, through some magic, the model
24820 * bound to any inpput or text area element found within a
24821 * {@code md-input-container} element.
24822 * @return {Object|string}
24823 */
24824MdChipsCtrl.prototype.getChipBuffer = function() {
24825 return !this.userInputElement ? this.chipBuffer :
24826 this.userInputNgModelCtrl ? this.userInputNgModelCtrl.$viewValue :
24827 this.userInputElement[0].value;
24828};
24829
24830/**
24831 * Resets the input buffer for either the internal input or user provided input element.
24832 */
24833MdChipsCtrl.prototype.resetChipBuffer = function() {
24834 if (this.userInputElement) {
24835 if (this.userInputNgModelCtrl) {
24836 this.userInputNgModelCtrl.$setViewValue('');
24837 this.userInputNgModelCtrl.$render();
24838 } else {
24839 this.userInputElement[0].value = '';
24840 }
24841 } else {
24842 this.chipBuffer = '';
24843 }
24844};
24845
24846MdChipsCtrl.prototype.hasMaxChipsReached = function() {
24847 if (angular.isString(this.maxChips)) this.maxChips = parseInt(this.maxChips, 10) || 0;
24848
24849 return this.maxChips > 0 && this.items.length >= this.maxChips;
24850};
24851
24852/**
24853 * Updates the validity properties for the ngModel.
24854 */
24855MdChipsCtrl.prototype.validateModel = function() {
24856 this.ngModelCtrl.$setValidity('md-max-chips', !this.hasMaxChipsReached());
24857};
24858
24859/**
24860 * Removes the chip at the given index.
24861 * @param index
24862 */
24863MdChipsCtrl.prototype.removeChip = function(index) {
24864 var removed = this.items.splice(index, 1);
24865
24866 // Update model validation
24867 this.ngModelCtrl.$setDirty();
24868 this.validateModel();
24869
24870 if (removed && removed.length && this.useOnRemove && this.onRemove) {
24871 this.onRemove({ '$chip': removed[0], '$index': index });
24872 }
24873};
24874
24875MdChipsCtrl.prototype.removeChipAndFocusInput = function (index) {
24876 this.removeChip(index);
24877
24878 if (this.autocompleteCtrl) {
24879 // Always hide the autocomplete dropdown before focusing the autocomplete input.
24880 // Wait for the input to move horizontally, because the chip was removed.
24881 // This can lead to an incorrect dropdown position.
24882 this.autocompleteCtrl.hidden = true;
24883 this.$mdUtil.nextTick(this.onFocus.bind(this));
24884 } else {
24885 this.onFocus();
24886 }
24887
24888};
24889/**
24890 * Selects the chip at `index`,
24891 * @param index
24892 */
24893MdChipsCtrl.prototype.selectAndFocusChipSafe = function(index) {
24894 if (!this.items.length) {
24895 this.selectChip(-1);
24896 this.onFocus();
24897 return;
24898 }
24899 if (index === this.items.length) return this.onFocus();
24900 index = Math.max(index, 0);
24901 index = Math.min(index, this.items.length - 1);
24902 this.selectChip(index);
24903 this.focusChip(index);
24904};
24905
24906/**
24907 * Marks the chip at the given index as selected.
24908 * @param index
24909 */
24910MdChipsCtrl.prototype.selectChip = function(index) {
24911 if (index >= -1 && index <= this.items.length) {
24912 this.selectedChip = index;
24913
24914 // Fire the onSelect if provided
24915 if (this.useOnSelect && this.onSelect) {
24916 this.onSelect({'$chip': this.items[this.selectedChip] });
24917 }
24918 } else {
24919 this.$log.warn('Selected Chip index out of bounds; ignoring.');
24920 }
24921};
24922
24923/**
24924 * Selects the chip at `index` and gives it focus.
24925 * @param index
24926 */
24927MdChipsCtrl.prototype.selectAndFocusChip = function(index) {
24928 this.selectChip(index);
24929 if (index != -1) {
24930 this.focusChip(index);
24931 }
24932};
24933
24934/**
24935 * Call `focus()` on the chip at `index`
24936 */
24937MdChipsCtrl.prototype.focusChip = function(index) {
24938 this.$element[0].querySelector('md-chip[index="' + index + '"] .md-chip-content').focus();
24939};
24940
24941/**
24942 * Configures the required interactions with the ngModel Controller.
24943 * Specifically, set {@code this.items} to the {@code NgModelCtrl#$viewVale}.
24944 * @param ngModelCtrl
24945 */
24946MdChipsCtrl.prototype.configureNgModel = function(ngModelCtrl) {
24947 this.ngModelCtrl = ngModelCtrl;
24948
24949 var self = this;
24950 ngModelCtrl.$render = function() {
24951 // model is updated. do something.
24952 self.items = self.ngModelCtrl.$viewValue;
24953 };
24954};
24955
24956MdChipsCtrl.prototype.onFocus = function () {
24957 var input = this.$element[0].querySelector('input');
24958 input && input.focus();
24959 this.resetSelectedChip();
24960};
24961
24962MdChipsCtrl.prototype.onInputFocus = function () {
24963 this.inputHasFocus = true;
24964 this.resetSelectedChip();
24965};
24966
24967MdChipsCtrl.prototype.onInputBlur = function () {
24968 this.inputHasFocus = false;
24969
24970 var chipBuffer = this.getChipBuffer().trim();
24971
24972 // Update the custom chip validators.
24973 this.validateModel();
24974
24975 var isModelValid = this.ngModelCtrl.$valid;
24976
24977 if (this.userInputNgModelCtrl) {
24978 isModelValid &= this.userInputNgModelCtrl.$valid;
24979 }
24980
24981 // Only append the chip and reset the chip buffer if the chips and input ngModel is valid.
24982 if (this.addOnBlur && chipBuffer && isModelValid) {
24983 this.appendChip(chipBuffer);
24984 this.resetChipBuffer();
24985 }
24986};
24987
24988/**
24989 * Configure event bindings on a user-provided input element.
24990 * @param inputElement
24991 */
24992MdChipsCtrl.prototype.configureUserInput = function(inputElement) {
24993 this.userInputElement = inputElement;
24994
24995 // Find the NgModelCtrl for the input element
24996 var ngModelCtrl = inputElement.controller('ngModel');
24997 // `.controller` will look in the parent as well.
24998 if (ngModelCtrl != this.ngModelCtrl) {
24999 this.userInputNgModelCtrl = ngModelCtrl;
25000 }
25001
25002 var scope = this.$scope;
25003 var ctrl = this;
25004
25005 // Run all of the events using evalAsync because a focus may fire a blur in the same digest loop
25006 var scopeApplyFn = function(event, fn) {
25007 scope.$evalAsync(angular.bind(ctrl, fn, event));
25008 };
25009
25010 // Bind to keydown and focus events of input
25011 inputElement
25012 .attr({ tabindex: 0 })
25013 .on('keydown', function(event) { scopeApplyFn(event, ctrl.inputKeydown) })
25014 .on('focus', function(event) { scopeApplyFn(event, ctrl.onInputFocus) })
25015 .on('blur', function(event) { scopeApplyFn(event, ctrl.onInputBlur) })
25016};
25017
25018MdChipsCtrl.prototype.configureAutocomplete = function(ctrl) {
25019 if (ctrl) {
25020 this.autocompleteCtrl = ctrl;
25021
25022 ctrl.registerSelectedItemWatcher(angular.bind(this, function (item) {
25023 if (item) {
25024 // Only append the chip and reset the chip buffer if the max chips limit isn't reached.
25025 if (this.hasMaxChipsReached()) return;
25026
25027 this.appendChip(item);
25028 this.resetChipBuffer();
25029 }
25030 }));
25031
25032 this.$element.find('input')
25033 .on('focus',angular.bind(this, this.onInputFocus) )
25034 .on('blur', angular.bind(this, this.onInputBlur) );
25035 }
25036};
25037
25038MdChipsCtrl.prototype.hasFocus = function () {
25039 return this.inputHasFocus || this.selectedChip >= 0;
25040};
25041
25042})();
25043(function(){
25044"use strict";
25045
25046
25047 MdChips.$inject = ["$mdTheming", "$mdUtil", "$compile", "$log", "$timeout", "$$mdSvgRegistry"];angular
25048 .module('material.components.chips')
25049 .directive('mdChips', MdChips);
25050
25051 /**
25052 * @ngdoc directive
25053 * @name mdChips
25054 * @module material.components.chips
25055 *
25056 * @description
25057 * `<md-chips>` is an input component for building lists of strings or objects. The list items are
25058 * displayed as 'chips'. This component can make use of an `<input>` element or an
25059 * `<md-autocomplete>` element.
25060 *
25061 * ### Custom templates
25062 * A custom template may be provided to render the content of each chip. This is achieved by
25063 * specifying an `<md-chip-template>` element containing the custom content as a child of
25064 * `<md-chips>`.
25065 *
25066 * Note: Any attributes on
25067 * `<md-chip-template>` will be dropped as only the innerHTML is used for the chip template. The
25068 * variables `$chip` and `$index` are available in the scope of `<md-chip-template>`, representing
25069 * the chip object and its index in the list of chips, respectively.
25070 * To override the chip delete control, include an element (ideally a button) with the attribute
25071 * `md-chip-remove`. A click listener to remove the chip will be added automatically. The element
25072 * is also placed as a sibling to the chip content (on which there are also click listeners) to
25073 * avoid a nested ng-click situation.
25074 *
25075 * <h3> Pending Features </h3>
25076 * <ul style="padding-left:20px;">
25077 *
25078 * <ul>Style
25079 * <li>Colours for hover, press states (ripple?).</li>
25080 * </ul>
25081 *
25082 * <ul>Validation
25083 * <li>allow a validation callback</li>
25084 * <li>hilighting style for invalid chips</li>
25085 * </ul>
25086 *
25087 * <ul>Item mutation
25088 * <li>Support `
25089 * <md-chip-edit>` template, show/hide the edit element on tap/click? double tap/double
25090 * click?
25091 * </li>
25092 * </ul>
25093 *
25094 * <ul>Truncation and Disambiguation (?)
25095 * <li>Truncate chip text where possible, but do not truncate entries such that two are
25096 * indistinguishable.</li>
25097 * </ul>
25098 *
25099 * <ul>Drag and Drop
25100 * <li>Drag and drop chips between related `<md-chips>` elements.
25101 * </li>
25102 * </ul>
25103 * </ul>
25104 *
25105 * <span style="font-size:.8em;text-align:center">
25106 * Warning: This component is a WORK IN PROGRESS. If you use it now,
25107 * it will probably break on you in the future.
25108 * </span>
25109 *
25110 * Sometimes developers want to limit the amount of possible chips.<br/>
25111 * You can specify the maximum amount of chips by using the following markup.
25112 *
25113 * <hljs lang="html">
25114 * <md-chips
25115 * ng-model="myItems"
25116 * placeholder="Add an item"
25117 * md-max-chips="5">
25118 * </md-chips>
25119 * </hljs>
25120 *
25121 * In some cases, you have an autocomplete inside of the `md-chips`.<br/>
25122 * When the maximum amount of chips has been reached, you can also disable the autocomplete selection.<br/>
25123 * Here is an example markup.
25124 *
25125 * <hljs lang="html">
25126 * <md-chips ng-model="myItems" md-max-chips="5">
25127 * <md-autocomplete ng-hide="myItems.length > 5" ...></md-autocomplete>
25128 * </md-chips>
25129 * </hljs>
25130 *
25131 * @param {string=|object=} ng-model A model to bind the list of items to
25132 * @param {string=} placeholder Placeholder text that will be forwarded to the input.
25133 * @param {string=} secondary-placeholder Placeholder text that will be forwarded to the input,
25134 * displayed when there is at least one item in the list
25135 * @param {boolean=} md-removable Enables or disables the deletion of chips through the
25136 * removal icon or the Delete/Backspace key. Defaults to true.
25137 * @param {boolean=} readonly Disables list manipulation (deleting or adding list items), hiding
25138 * the input and delete buttons. If no `ng-model` is provided, the chips will automatically be
25139 * marked as readonly.<br/><br/>
25140 * When `md-removable` is not defined, the `md-remove` behavior will be overwritten and disabled.
25141 * @param {string=} md-enable-chip-edit Set this to "true" to enable editing of chip contents. The user can
25142 * go into edit mode with pressing "space", "enter", or double clicking on the chip. Chip edit is only
25143 * supported for chips with basic template.
25144 * @param {number=} md-max-chips The maximum number of chips allowed to add through user input.
25145 * <br/><br/>The validation property `md-max-chips` can be used when the max chips
25146 * amount is reached.
25147 * @param {boolean=} md-add-on-blur When set to true, remaining text inside of the input will
25148 * be converted into a new chip on blur.
25149 * @param {expression} md-transform-chip An expression of form `myFunction($chip)` that when called
25150 * expects one of the following return values:
25151 * - an object representing the `$chip` input string
25152 * - `undefined` to simply add the `$chip` input string, or
25153 * - `null` to prevent the chip from being appended
25154 * @param {expression=} md-on-add An expression which will be called when a chip has been
25155 * added.
25156 * @param {expression=} md-on-remove An expression which will be called when a chip has been
25157 * removed.
25158 * @param {expression=} md-on-select An expression which will be called when a chip is selected.
25159 * @param {boolean} md-require-match If true, and the chips template contains an autocomplete,
25160 * only allow selection of pre-defined chips (i.e. you cannot add new ones).
25161 * @param {string=} delete-hint A string read by screen readers instructing users that pressing
25162 * the delete key will remove the chip.
25163 * @param {string=} delete-button-label A label for the delete button. Also hidden and read by
25164 * screen readers.
25165 * @param {expression=} md-separator-keys An array of key codes used to separate chips.
25166 *
25167 * @usage
25168 * <hljs lang="html">
25169 * <md-chips
25170 * ng-model="myItems"
25171 * placeholder="Add an item"
25172 * readonly="isReadOnly">
25173 * </md-chips>
25174 * </hljs>
25175 *
25176 * <h3>Validation</h3>
25177 * When using [ngMessages](https://docs.angularjs.org/api/ngMessages), you can show errors based
25178 * on our custom validators.
25179 * <hljs lang="html">
25180 * <form name="userForm">
25181 * <md-chips
25182 * name="fruits"
25183 * ng-model="myItems"
25184 * placeholder="Add an item"
25185 * md-max-chips="5">
25186 * </md-chips>
25187 * <div ng-messages="userForm.fruits.$error" ng-if="userForm.$dirty">
25188 * <div ng-message="md-max-chips">You reached the maximum amount of chips</div>
25189 * </div>
25190 * </form>
25191 * </hljs>
25192 *
25193 */
25194
25195
25196 var MD_CHIPS_TEMPLATE = '\
25197 <md-chips-wrap\
25198 ng-keydown="$mdChipsCtrl.chipKeydown($event)"\
25199 ng-class="{ \'md-focused\': $mdChipsCtrl.hasFocus(), \
25200 \'md-readonly\': !$mdChipsCtrl.ngModelCtrl || $mdChipsCtrl.readonly,\
25201 \'md-removable\': $mdChipsCtrl.isRemovable() }"\
25202 class="md-chips">\
25203 <md-chip ng-repeat="$chip in $mdChipsCtrl.items"\
25204 index="{{$index}}"\
25205 ng-class="{\'md-focused\': $mdChipsCtrl.selectedChip == $index, \'md-readonly\': !$mdChipsCtrl.ngModelCtrl || $mdChipsCtrl.readonly}">\
25206 <div class="md-chip-content"\
25207 tabindex="-1"\
25208 aria-hidden="true"\
25209 ng-click="!$mdChipsCtrl.readonly && $mdChipsCtrl.focusChip($index)"\
25210 ng-focus="!$mdChipsCtrl.readonly && $mdChipsCtrl.selectChip($index)"\
25211 md-chip-transclude="$mdChipsCtrl.chipContentsTemplate"></div>\
25212 <div ng-if="$mdChipsCtrl.isRemovable()"\
25213 class="md-chip-remove-container"\
25214 md-chip-transclude="$mdChipsCtrl.chipRemoveTemplate"></div>\
25215 </md-chip>\
25216 <div class="md-chip-input-container" ng-if="!$mdChipsCtrl.readonly && $mdChipsCtrl.ngModelCtrl">\
25217 <div md-chip-transclude="$mdChipsCtrl.chipInputTemplate"></div>\
25218 </div>\
25219 </md-chips-wrap>';
25220
25221 var CHIP_INPUT_TEMPLATE = '\
25222 <input\
25223 class="md-input"\
25224 tabindex="0"\
25225 placeholder="{{$mdChipsCtrl.getPlaceholder()}}"\
25226 aria-label="{{$mdChipsCtrl.getPlaceholder()}}"\
25227 ng-model="$mdChipsCtrl.chipBuffer"\
25228 ng-focus="$mdChipsCtrl.onInputFocus()"\
25229 ng-blur="$mdChipsCtrl.onInputBlur()"\
25230 ng-keydown="$mdChipsCtrl.inputKeydown($event)">';
25231
25232 var CHIP_DEFAULT_TEMPLATE = '\
25233 <span>{{$chip}}</span>';
25234
25235 var CHIP_REMOVE_TEMPLATE = '\
25236 <button\
25237 class="md-chip-remove"\
25238 ng-if="$mdChipsCtrl.isRemovable()"\
25239 ng-click="$mdChipsCtrl.removeChipAndFocusInput($$replacedScope.$index)"\
25240 type="button"\
25241 aria-hidden="true"\
25242 tabindex="-1">\
25243 <md-icon md-svg-src="{{ $mdChipsCtrl.mdCloseIcon }}"></md-icon>\
25244 <span class="md-visually-hidden">\
25245 {{$mdChipsCtrl.deleteButtonLabel}}\
25246 </span>\
25247 </button>';
25248
25249 /**
25250 * MDChips Directive Definition
25251 */
25252 function MdChips ($mdTheming, $mdUtil, $compile, $log, $timeout, $$mdSvgRegistry) {
25253 // Run our templates through $mdUtil.processTemplate() to allow custom start/end symbols
25254 var templates = getTemplates();
25255
25256 return {
25257 template: function(element, attrs) {
25258 // Clone the element into an attribute. By prepending the attribute
25259 // name with '$', Angular won't write it into the DOM. The cloned
25260 // element propagates to the link function via the attrs argument,
25261 // where various contained-elements can be consumed.
25262 attrs['$mdUserTemplate'] = element.clone();
25263 return templates.chips;
25264 },
25265 require: ['mdChips'],
25266 restrict: 'E',
25267 controller: 'MdChipsCtrl',
25268 controllerAs: '$mdChipsCtrl',
25269 bindToController: true,
25270 compile: compile,
25271 scope: {
25272 readonly: '=readonly',
25273 removable: '=mdRemovable',
25274 placeholder: '@',
25275 secondaryPlaceholder: '@',
25276 maxChips: '@mdMaxChips',
25277 transformChip: '&mdTransformChip',
25278 onAppend: '&mdOnAppend',
25279 onAdd: '&mdOnAdd',
25280 onRemove: '&mdOnRemove',
25281 onSelect: '&mdOnSelect',
25282 deleteHint: '@',
25283 deleteButtonLabel: '@',
25284 separatorKeys: '=?mdSeparatorKeys',
25285 requireMatch: '=?mdRequireMatch'
25286 }
25287 };
25288
25289 /**
25290 * Builds the final template for `md-chips` and returns the postLink function.
25291 *
25292 * Building the template involves 3 key components:
25293 * static chips
25294 * chip template
25295 * input control
25296 *
25297 * If no `ng-model` is provided, only the static chip work needs to be done.
25298 *
25299 * If no user-passed `md-chip-template` exists, the default template is used. This resulting
25300 * template is appended to the chip content element.
25301 *
25302 * The remove button may be overridden by passing an element with an md-chip-remove attribute.
25303 *
25304 * If an `input` or `md-autocomplete` element is provided by the caller, it is set aside for
25305 * transclusion later. The transclusion happens in `postLink` as the parent scope is required.
25306 * If no user input is provided, a default one is appended to the input container node in the
25307 * template.
25308 *
25309 * Static Chips (i.e. `md-chip` elements passed from the caller) are gathered and set aside for
25310 * transclusion in the `postLink` function.
25311 *
25312 *
25313 * @param element
25314 * @param attr
25315 * @returns {Function}
25316 */
25317 function compile(element, attr) {
25318 // Grab the user template from attr and reset the attribute to null.
25319 var userTemplate = attr['$mdUserTemplate'];
25320 attr['$mdUserTemplate'] = null;
25321
25322 var chipTemplate = getTemplateByQuery('md-chips>md-chip-template');
25323
25324 var chipRemoveSelector = $mdUtil
25325 .prefixer()
25326 .buildList('md-chip-remove')
25327 .map(function(attr) {
25328 return 'md-chips>*[' + attr + ']';
25329 })
25330 .join(',');
25331
25332 // Set the chip remove, chip contents and chip input templates. The link function will put
25333 // them on the scope for transclusion later.
25334 var chipRemoveTemplate = getTemplateByQuery(chipRemoveSelector) || templates.remove,
25335 chipContentsTemplate = chipTemplate || templates.default,
25336 chipInputTemplate = getTemplateByQuery('md-chips>md-autocomplete')
25337 || getTemplateByQuery('md-chips>input')
25338 || templates.input,
25339 staticChips = userTemplate.find('md-chip');
25340
25341 // Warn of malformed template. See #2545
25342 if (userTemplate[0].querySelector('md-chip-template>*[md-chip-remove]')) {
25343 $log.warn('invalid placement of md-chip-remove within md-chip-template.');
25344 }
25345
25346 function getTemplateByQuery (query) {
25347 if (!attr.ngModel) return;
25348 var element = userTemplate[0].querySelector(query);
25349 return element && element.outerHTML;
25350 }
25351
25352 /**
25353 * Configures controller and transcludes.
25354 */
25355 return function postLink(scope, element, attrs, controllers) {
25356 $mdUtil.initOptionalProperties(scope, attr);
25357
25358 $mdTheming(element);
25359 var mdChipsCtrl = controllers[0];
25360 if(chipTemplate) {
25361 // Chip editing functionality assumes we are using the default chip template.
25362 mdChipsCtrl.enableChipEdit = false;
25363 }
25364
25365 mdChipsCtrl.chipContentsTemplate = chipContentsTemplate;
25366 mdChipsCtrl.chipRemoveTemplate = chipRemoveTemplate;
25367 mdChipsCtrl.chipInputTemplate = chipInputTemplate;
25368
25369 mdChipsCtrl.mdCloseIcon = $$mdSvgRegistry.mdClose;
25370
25371 element
25372 .attr({ 'aria-hidden': true, tabindex: -1 })
25373 .on('focus', function () { mdChipsCtrl.onFocus(); });
25374
25375 if (attr.ngModel) {
25376 mdChipsCtrl.configureNgModel(element.controller('ngModel'));
25377
25378 // If an `md-transform-chip` attribute was set, tell the controller to use the expression
25379 // before appending chips.
25380 if (attrs.mdTransformChip) mdChipsCtrl.useTransformChipExpression();
25381
25382 // If an `md-on-append` attribute was set, tell the controller to use the expression
25383 // when appending chips.
25384 //
25385 // DEPRECATED: Will remove in official 1.0 release
25386 if (attrs.mdOnAppend) mdChipsCtrl.useOnAppendExpression();
25387
25388 // If an `md-on-add` attribute was set, tell the controller to use the expression
25389 // when adding chips.
25390 if (attrs.mdOnAdd) mdChipsCtrl.useOnAddExpression();
25391
25392 // If an `md-on-remove` attribute was set, tell the controller to use the expression
25393 // when removing chips.
25394 if (attrs.mdOnRemove) mdChipsCtrl.useOnRemoveExpression();
25395
25396 // If an `md-on-select` attribute was set, tell the controller to use the expression
25397 // when selecting chips.
25398 if (attrs.mdOnSelect) mdChipsCtrl.useOnSelectExpression();
25399
25400 // The md-autocomplete and input elements won't be compiled until after this directive
25401 // is complete (due to their nested nature). Wait a tick before looking for them to
25402 // configure the controller.
25403 if (chipInputTemplate != templates.input) {
25404 // The autocomplete will not appear until the readonly attribute is not true (i.e.
25405 // false or undefined), so we have to watch the readonly and then on the next tick
25406 // after the chip transclusion has run, we can configure the autocomplete and user
25407 // input.
25408 scope.$watch('$mdChipsCtrl.readonly', function(readonly) {
25409 if (!readonly) {
25410
25411 $mdUtil.nextTick(function(){
25412
25413 if (chipInputTemplate.indexOf('<md-autocomplete') === 0) {
25414 var autocompleteEl = element.find('md-autocomplete');
25415 mdChipsCtrl.configureAutocomplete(autocompleteEl.controller('mdAutocomplete'));
25416 }
25417
25418 mdChipsCtrl.configureUserInput(element.find('input'));
25419 });
25420 }
25421 });
25422 }
25423
25424 // At the next tick, if we find an input, make sure it has the md-input class
25425 $mdUtil.nextTick(function() {
25426 var input = element.find('input');
25427
25428 input && input.toggleClass('md-input', true);
25429 });
25430 }
25431
25432 // Compile with the parent's scope and prepend any static chips to the wrapper.
25433 if (staticChips.length > 0) {
25434 var compiledStaticChips = $compile(staticChips.clone())(scope.$parent);
25435 $timeout(function() { element.find('md-chips-wrap').prepend(compiledStaticChips); });
25436 }
25437 };
25438 }
25439
25440 function getTemplates() {
25441 return {
25442 chips: $mdUtil.processTemplate(MD_CHIPS_TEMPLATE),
25443 input: $mdUtil.processTemplate(CHIP_INPUT_TEMPLATE),
25444 default: $mdUtil.processTemplate(CHIP_DEFAULT_TEMPLATE),
25445 remove: $mdUtil.processTemplate(CHIP_REMOVE_TEMPLATE)
25446 };
25447 }
25448 }
25449
25450})();
25451(function(){
25452"use strict";
25453
25454angular
25455 .module('material.components.chips')
25456 .controller('MdContactChipsCtrl', MdContactChipsCtrl);
25457
25458
25459
25460/**
25461 * Controller for the MdContactChips component
25462 * @constructor
25463 */
25464function MdContactChipsCtrl () {
25465 /** @type {Object} */
25466 this.selectedItem = null;
25467
25468 /** @type {string} */
25469 this.searchText = '';
25470}
25471
25472
25473MdContactChipsCtrl.prototype.queryContact = function(searchText) {
25474 var results = this.contactQuery({'$query': searchText});
25475 return this.filterSelected ?
25476 results.filter(angular.bind(this, this.filterSelectedContacts)) : results;
25477};
25478
25479
25480MdContactChipsCtrl.prototype.itemName = function(item) {
25481 return item[this.contactName];
25482};
25483
25484
25485MdContactChipsCtrl.prototype.filterSelectedContacts = function(contact) {
25486 return this.contacts.indexOf(contact) == -1;
25487};
25488
25489})();
25490(function(){
25491"use strict";
25492
25493
25494MdContactChips.$inject = ["$mdTheming", "$mdUtil"];angular
25495 .module('material.components.chips')
25496 .directive('mdContactChips', MdContactChips);
25497
25498/**
25499 * @ngdoc directive
25500 * @name mdContactChips
25501 * @module material.components.chips
25502 *
25503 * @description
25504 * `<md-contact-chips>` is an input component based on `md-chips` and makes use of an
25505 * `md-autocomplete` element. The component allows the caller to supply a query expression which
25506 * returns a list of possible contacts. The user can select one of these and add it to the list of
25507 * chips.
25508 *
25509 * You may also use the `md-highlight-text` directive along with its parameters to control the
25510 * appearance of the matched text inside of the contacts' autocomplete popup.
25511 *
25512 * @param {string=|object=} ng-model A model to bind the list of items to
25513 * @param {string=} placeholder Placeholder text that will be forwarded to the input.
25514 * @param {string=} secondary-placeholder Placeholder text that will be forwarded to the input,
25515 * displayed when there is at least on item in the list
25516 * @param {expression} md-contacts An expression expected to return contacts matching the search
25517 * test, `$query`. If this expression involves a promise, a loading bar is displayed while
25518 * waiting for it to resolve.
25519 * @param {string} md-contact-name The field name of the contact object representing the
25520 * contact's name.
25521 * @param {string} md-contact-email The field name of the contact object representing the
25522 * contact's email address.
25523 * @param {string} md-contact-image The field name of the contact object representing the
25524 * contact's image.
25525 *
25526 *
25527 * @param {expression=} filter-selected Whether to filter selected contacts from the list of
25528 * suggestions shown in the autocomplete. This attribute has been removed but may come back.
25529 *
25530 *
25531 *
25532 * @usage
25533 * <hljs lang="html">
25534 * <md-contact-chips
25535 * ng-model="ctrl.contacts"
25536 * md-contacts="ctrl.querySearch($query)"
25537 * md-contact-name="name"
25538 * md-contact-image="image"
25539 * md-contact-email="email"
25540 * placeholder="To">
25541 * </md-contact-chips>
25542 * </hljs>
25543 *
25544 */
25545
25546
25547var MD_CONTACT_CHIPS_TEMPLATE = '\
25548 <md-chips class="md-contact-chips"\
25549 ng-model="$mdContactChipsCtrl.contacts"\
25550 md-require-match="$mdContactChipsCtrl.requireMatch"\
25551 md-autocomplete-snap>\
25552 <md-autocomplete\
25553 md-menu-class="md-contact-chips-suggestions"\
25554 md-selected-item="$mdContactChipsCtrl.selectedItem"\
25555 md-search-text="$mdContactChipsCtrl.searchText"\
25556 md-items="item in $mdContactChipsCtrl.queryContact($mdContactChipsCtrl.searchText)"\
25557 md-item-text="$mdContactChipsCtrl.itemName(item)"\
25558 md-no-cache="true"\
25559 md-autoselect\
25560 placeholder="{{$mdContactChipsCtrl.contacts.length == 0 ?\
25561 $mdContactChipsCtrl.placeholder : $mdContactChipsCtrl.secondaryPlaceholder}}">\
25562 <div class="md-contact-suggestion">\
25563 <img \
25564 ng-src="{{item[$mdContactChipsCtrl.contactImage]}}"\
25565 alt="{{item[$mdContactChipsCtrl.contactName]}}"\
25566 ng-if="item[$mdContactChipsCtrl.contactImage]" />\
25567 <span class="md-contact-name" md-highlight-text="$mdContactChipsCtrl.searchText"\
25568 md-highlight-flags="{{$mdContactChipsCtrl.highlightFlags}}">\
25569 {{item[$mdContactChipsCtrl.contactName]}}\
25570 </span>\
25571 <span class="md-contact-email" >{{item[$mdContactChipsCtrl.contactEmail]}}</span>\
25572 </div>\
25573 </md-autocomplete>\
25574 <md-chip-template>\
25575 <div class="md-contact-avatar">\
25576 <img \
25577 ng-src="{{$chip[$mdContactChipsCtrl.contactImage]}}"\
25578 alt="{{$chip[$mdContactChipsCtrl.contactName]}}"\
25579 ng-if="$chip[$mdContactChipsCtrl.contactImage]" />\
25580 </div>\
25581 <div class="md-contact-name">\
25582 {{$chip[$mdContactChipsCtrl.contactName]}}\
25583 </div>\
25584 </md-chip-template>\
25585 </md-chips>';
25586
25587
25588/**
25589 * MDContactChips Directive Definition
25590 *
25591 * @param $mdTheming
25592 * @returns {*}
25593 * @ngInject
25594 */
25595function MdContactChips($mdTheming, $mdUtil) {
25596 return {
25597 template: function(element, attrs) {
25598 return MD_CONTACT_CHIPS_TEMPLATE;
25599 },
25600 restrict: 'E',
25601 controller: 'MdContactChipsCtrl',
25602 controllerAs: '$mdContactChipsCtrl',
25603 bindToController: true,
25604 compile: compile,
25605 scope: {
25606 contactQuery: '&mdContacts',
25607 placeholder: '@',
25608 secondaryPlaceholder: '@',
25609 contactName: '@mdContactName',
25610 contactImage: '@mdContactImage',
25611 contactEmail: '@mdContactEmail',
25612 contacts: '=ngModel',
25613 requireMatch: '=?mdRequireMatch',
25614 highlightFlags: '@?mdHighlightFlags'
25615 }
25616 };
25617
25618 function compile(element, attr) {
25619 return function postLink(scope, element, attrs, controllers) {
25620
25621 $mdUtil.initOptionalProperties(scope, attr);
25622 $mdTheming(element);
25623
25624 element.attr('tabindex', '-1');
25625 };
25626 }
25627}
25628
25629})();
25630(function(){
25631"use strict";
25632
25633(function() {
25634 'use strict';
25635
25636 /**
25637 * @ngdoc directive
25638 * @name mdCalendar
25639 * @module material.components.datepicker
25640 *
25641 * @param {Date} ng-model The component's model. Should be a Date object.
25642 * @param {Date=} md-min-date Expression representing the minimum date.
25643 * @param {Date=} md-max-date Expression representing the maximum date.
25644 * @param {(function(Date): boolean)=} md-date-filter Function expecting a date and returning a boolean whether it can be selected or not.
25645 *
25646 * @description
25647 * `<md-calendar>` is a component that renders a calendar that can be used to select a date.
25648 * It is a part of the `<md-datepicker` pane, however it can also be used on it's own.
25649 *
25650 * @usage
25651 *
25652 * <hljs lang="html">
25653 * <md-calendar ng-model="birthday"></md-calendar>
25654 * </hljs>
25655 */
25656 CalendarCtrl.$inject = ["$element", "$scope", "$$mdDateUtil", "$mdUtil", "$mdConstant", "$mdTheming", "$$rAF", "$attrs", "$mdDateLocale"];
25657 angular.module('material.components.datepicker')
25658 .directive('mdCalendar', calendarDirective);
25659
25660 // POST RELEASE
25661 // TODO(jelbourn): Mac Cmd + left / right == Home / End
25662 // TODO(jelbourn): Refactor month element creation to use cloneNode (performance).
25663 // TODO(jelbourn): Define virtual scrolling constants (compactness) users can override.
25664 // TODO(jelbourn): Animated month transition on ng-model change (virtual-repeat)
25665 // TODO(jelbourn): Scroll snapping (virtual repeat)
25666 // TODO(jelbourn): Remove superfluous row from short months (virtual-repeat)
25667 // TODO(jelbourn): Month headers stick to top when scrolling.
25668 // TODO(jelbourn): Previous month opacity is lowered when partially scrolled out of view.
25669 // TODO(jelbourn): Support md-calendar standalone on a page (as a tabstop w/ aria-live
25670 // announcement and key handling).
25671 // Read-only calendar (not just date-picker).
25672
25673 function calendarDirective() {
25674 return {
25675 template: function(tElement, tAttr) {
25676 // TODO(crisbeto): This is a workaround that allows the calendar to work, without
25677 // a datepicker, until issue #8585 gets resolved. It can safely be removed
25678 // afterwards. This ensures that the virtual repeater scrolls to the proper place on load by
25679 // deferring the execution until the next digest. It's necessary only if the calendar is used
25680 // without a datepicker, otherwise it's already wrapped in an ngIf.
25681 var extraAttrs = tAttr.hasOwnProperty('ngIf') ? '' : 'ng-if="calendarCtrl.isInitialized"';
25682 var template = '' +
25683 '<div ng-switch="calendarCtrl.currentView" ' + extraAttrs + '>' +
25684 '<md-calendar-year ng-switch-when="year"></md-calendar-year>' +
25685 '<md-calendar-month ng-switch-default></md-calendar-month>' +
25686 '</div>';
25687
25688 return template;
25689 },
25690 scope: {
25691 minDate: '=mdMinDate',
25692 maxDate: '=mdMaxDate',
25693 dateFilter: '=mdDateFilter',
25694 _currentView: '@mdCurrentView'
25695 },
25696 require: ['ngModel', 'mdCalendar'],
25697 controller: CalendarCtrl,
25698 controllerAs: 'calendarCtrl',
25699 bindToController: true,
25700 link: function(scope, element, attrs, controllers) {
25701 var ngModelCtrl = controllers[0];
25702 var mdCalendarCtrl = controllers[1];
25703 mdCalendarCtrl.configureNgModel(ngModelCtrl);
25704 }
25705 };
25706 }
25707
25708 /**
25709 * Occasionally the hideVerticalScrollbar method might read an element's
25710 * width as 0, because it hasn't been laid out yet. This value will be used
25711 * as a fallback, in order to prevent scenarios where the element's width
25712 * would otherwise have been set to 0. This value is the "usual" width of a
25713 * calendar within a floating calendar pane.
25714 */
25715 var FALLBACK_WIDTH = 340;
25716
25717 /** Next identifier for calendar instance. */
25718 var nextUniqueId = 0;
25719
25720 /**
25721 * Controller for the mdCalendar component.
25722 * @ngInject @constructor
25723 */
25724 function CalendarCtrl($element, $scope, $$mdDateUtil, $mdUtil,
25725 $mdConstant, $mdTheming, $$rAF, $attrs, $mdDateLocale) {
25726
25727 $mdTheming($element);
25728
25729 /** @final {!angular.JQLite} */
25730 this.$element = $element;
25731
25732 /** @final {!angular.Scope} */
25733 this.$scope = $scope;
25734
25735 /** @final */
25736 this.dateUtil = $$mdDateUtil;
25737
25738 /** @final */
25739 this.$mdUtil = $mdUtil;
25740
25741 /** @final */
25742 this.keyCode = $mdConstant.KEY_CODE;
25743
25744 /** @final */
25745 this.$$rAF = $$rAF;
25746
25747 /** @final {Date} */
25748 this.today = this.dateUtil.createDateAtMidnight();
25749
25750 /** @type {!angular.NgModelController} */
25751 this.ngModelCtrl = null;
25752
25753 /**
25754 * The currently visible calendar view. Note the prefix on the scope value,
25755 * which is necessary, because the datepicker seems to reset the real one value if the
25756 * calendar is open, but the value on the datepicker's scope is empty.
25757 * @type {String}
25758 */
25759 this.currentView = this._currentView || 'month';
25760
25761 /** @type {String} Class applied to the selected date cell. */
25762 this.SELECTED_DATE_CLASS = 'md-calendar-selected-date';
25763
25764 /** @type {String} Class applied to the cell for today. */
25765 this.TODAY_CLASS = 'md-calendar-date-today';
25766
25767 /** @type {String} Class applied to the focused cell. */
25768 this.FOCUSED_DATE_CLASS = 'md-focus';
25769
25770 /** @final {number} Unique ID for this calendar instance. */
25771 this.id = nextUniqueId++;
25772
25773 /**
25774 * The date that is currently focused or showing in the calendar. This will initially be set
25775 * to the ng-model value if set, otherwise to today. It will be updated as the user navigates
25776 * to other months. The cell corresponding to the displayDate does not necesarily always have
25777 * focus in the document (such as for cases when the user is scrolling the calendar).
25778 * @type {Date}
25779 */
25780 this.displayDate = null;
25781
25782 /**
25783 * The selected date. Keep track of this separately from the ng-model value so that we
25784 * can know, when the ng-model value changes, what the previous value was before it's updated
25785 * in the component's UI.
25786 *
25787 * @type {Date}
25788 */
25789 this.selectedDate = null;
25790
25791 /**
25792 * The first date that can be rendered by the calendar. The default is taken
25793 * from the mdDateLocale provider and is limited by the mdMinDate.
25794 * @type {Date}
25795 */
25796 this.firstRenderableDate = null;
25797
25798 /**
25799 * The last date that can be rendered by the calendar. The default comes
25800 * from the mdDateLocale provider and is limited by the maxDate.
25801 * @type {Date}
25802 */
25803 this.lastRenderableDate = null;
25804
25805 /**
25806 * Used to toggle initialize the root element in the next digest.
25807 * @type {Boolean}
25808 */
25809 this.isInitialized = false;
25810
25811 /**
25812 * Cache for the width of the element without a scrollbar. Used to hide the scrollbar later on
25813 * and to avoid extra reflows when switching between views.
25814 * @type {Number}
25815 */
25816 this.width = 0;
25817
25818 /**
25819 * Caches the width of the scrollbar in order to be used when hiding it and to avoid extra reflows.
25820 * @type {Number}
25821 */
25822 this.scrollbarWidth = 0;
25823
25824 // Unless the user specifies so, the calendar should not be a tab stop.
25825 // This is necessary because ngAria might add a tabindex to anything with an ng-model
25826 // (based on whether or not the user has turned that particular feature on/off).
25827 if (!$attrs.tabindex) {
25828 $element.attr('tabindex', '-1');
25829 }
25830
25831 var boundKeyHandler = angular.bind(this, this.handleKeyEvent);
25832
25833 // Bind the keydown handler to the body, in order to handle cases where the focused
25834 // element gets removed from the DOM and stops propagating click events.
25835 angular.element(document.body).on('keydown', boundKeyHandler);
25836
25837 $scope.$on('$destroy', function() {
25838 angular.element(document.body).off('keydown', boundKeyHandler);
25839 });
25840
25841 if (this.minDate && this.minDate > $mdDateLocale.firstRenderableDate) {
25842 this.firstRenderableDate = this.minDate;
25843 } else {
25844 this.firstRenderableDate = $mdDateLocale.firstRenderableDate;
25845 }
25846
25847 if (this.maxDate && this.maxDate < $mdDateLocale.lastRenderableDate) {
25848 this.lastRenderableDate = this.maxDate;
25849 } else {
25850 this.lastRenderableDate = $mdDateLocale.lastRenderableDate;
25851 }
25852 }
25853
25854 /**
25855 * Sets up the controller's reference to ngModelController.
25856 * @param {!angular.NgModelController} ngModelCtrl
25857 */
25858 CalendarCtrl.prototype.configureNgModel = function(ngModelCtrl) {
25859 var self = this;
25860
25861 self.ngModelCtrl = ngModelCtrl;
25862
25863 self.$mdUtil.nextTick(function() {
25864 self.isInitialized = true;
25865 });
25866
25867 ngModelCtrl.$render = function() {
25868 var value = this.$viewValue;
25869
25870 // Notify the child scopes of any changes.
25871 self.$scope.$broadcast('md-calendar-parent-changed', value);
25872
25873 // Set up the selectedDate if it hasn't been already.
25874 if (!self.selectedDate) {
25875 self.selectedDate = value;
25876 }
25877
25878 // Also set up the displayDate.
25879 if (!self.displayDate) {
25880 self.displayDate = self.selectedDate || self.today;
25881 }
25882 };
25883 };
25884
25885 /**
25886 * Sets the ng-model value for the calendar and emits a change event.
25887 * @param {Date} date
25888 */
25889 CalendarCtrl.prototype.setNgModelValue = function(date) {
25890 var value = this.dateUtil.createDateAtMidnight(date);
25891 this.focus(value);
25892 this.$scope.$emit('md-calendar-change', value);
25893 this.ngModelCtrl.$setViewValue(value);
25894 this.ngModelCtrl.$render();
25895 return value;
25896 };
25897
25898 /**
25899 * Sets the current view that should be visible in the calendar
25900 * @param {string} newView View name to be set.
25901 * @param {number|Date} time Date object or a timestamp for the new display date.
25902 */
25903 CalendarCtrl.prototype.setCurrentView = function(newView, time) {
25904 var self = this;
25905
25906 self.$mdUtil.nextTick(function() {
25907 self.currentView = newView;
25908
25909 if (time) {
25910 self.displayDate = angular.isDate(time) ? time : new Date(time);
25911 }
25912 });
25913 };
25914
25915 /**
25916 * Focus the cell corresponding to the given date.
25917 * @param {Date} date The date to be focused.
25918 */
25919 CalendarCtrl.prototype.focus = function(date) {
25920 if (this.dateUtil.isValidDate(date)) {
25921 var previousFocus = this.$element[0].querySelector('.md-focus');
25922 if (previousFocus) {
25923 previousFocus.classList.remove(this.FOCUSED_DATE_CLASS);
25924 }
25925
25926 var cellId = this.getDateId(date, this.currentView);
25927 var cell = document.getElementById(cellId);
25928 if (cell) {
25929 cell.classList.add(this.FOCUSED_DATE_CLASS);
25930 cell.focus();
25931 this.displayDate = date;
25932 }
25933 } else {
25934 var rootElement = this.$element[0].querySelector('[ng-switch]');
25935
25936 if (rootElement) {
25937 rootElement.focus();
25938 }
25939 }
25940 };
25941
25942 /**
25943 * Normalizes the key event into an action name. The action will be broadcast
25944 * to the child controllers.
25945 * @param {KeyboardEvent} event
25946 * @returns {String} The action that should be taken, or null if the key
25947 * does not match a calendar shortcut.
25948 */
25949 CalendarCtrl.prototype.getActionFromKeyEvent = function(event) {
25950 var keyCode = this.keyCode;
25951
25952 switch (event.which) {
25953 case keyCode.ENTER: return 'select';
25954
25955 case keyCode.RIGHT_ARROW: return 'move-right';
25956 case keyCode.LEFT_ARROW: return 'move-left';
25957
25958 // TODO(crisbeto): Might want to reconsider using metaKey, because it maps
25959 // to the "Windows" key on PC, which opens the start menu or resizes the browser.
25960 case keyCode.DOWN_ARROW: return event.metaKey ? 'move-page-down' : 'move-row-down';
25961 case keyCode.UP_ARROW: return event.metaKey ? 'move-page-up' : 'move-row-up';
25962
25963 case keyCode.PAGE_DOWN: return 'move-page-down';
25964 case keyCode.PAGE_UP: return 'move-page-up';
25965
25966 case keyCode.HOME: return 'start';
25967 case keyCode.END: return 'end';
25968
25969 default: return null;
25970 }
25971 };
25972
25973 /**
25974 * Handles a key event in the calendar with the appropriate action. The action will either
25975 * be to select the focused date or to navigate to focus a new date.
25976 * @param {KeyboardEvent} event
25977 */
25978 CalendarCtrl.prototype.handleKeyEvent = function(event) {
25979 var self = this;
25980
25981 this.$scope.$apply(function() {
25982 // Capture escape and emit back up so that a wrapping component
25983 // (such as a date-picker) can decide to close.
25984 if (event.which == self.keyCode.ESCAPE || event.which == self.keyCode.TAB) {
25985 self.$scope.$emit('md-calendar-close');
25986
25987 if (event.which == self.keyCode.TAB) {
25988 event.preventDefault();
25989 }
25990
25991 return;
25992 }
25993
25994 // Broadcast the action that any child controllers should take.
25995 var action = self.getActionFromKeyEvent(event);
25996 if (action) {
25997 event.preventDefault();
25998 event.stopPropagation();
25999 self.$scope.$broadcast('md-calendar-parent-action', action);
26000 }
26001 });
26002 };
26003
26004 /**
26005 * Hides the vertical scrollbar on the calendar scroller of a child controller by
26006 * setting the width on the calendar scroller and the `overflow: hidden` wrapper
26007 * around the scroller, and then setting a padding-right on the scroller equal
26008 * to the width of the browser's scrollbar.
26009 *
26010 * This will cause a reflow.
26011 *
26012 * @param {object} childCtrl The child controller whose scrollbar should be hidden.
26013 */
26014 CalendarCtrl.prototype.hideVerticalScrollbar = function(childCtrl) {
26015 var self = this;
26016 var element = childCtrl.$element[0];
26017 var scrollMask = element.querySelector('.md-calendar-scroll-mask');
26018
26019 if (self.width > 0) {
26020 setWidth();
26021 } else {
26022 self.$$rAF(function() {
26023 var scroller = childCtrl.calendarScroller;
26024
26025 self.scrollbarWidth = scroller.offsetWidth - scroller.clientWidth;
26026 self.width = element.querySelector('table').offsetWidth;
26027 setWidth();
26028 });
26029 }
26030
26031 function setWidth() {
26032 var width = self.width || FALLBACK_WIDTH;
26033 var scrollbarWidth = self.scrollbarWidth;
26034 var scroller = childCtrl.calendarScroller;
26035
26036 scrollMask.style.width = width + 'px';
26037 scroller.style.width = (width + scrollbarWidth) + 'px';
26038 scroller.style.paddingRight = scrollbarWidth + 'px';
26039 }
26040 };
26041
26042 /**
26043 * Gets an identifier for a date unique to the calendar instance for internal
26044 * purposes. Not to be displayed.
26045 * @param {Date} date The date for which the id is being generated
26046 * @param {string} namespace Namespace for the id. (month, year etc.)
26047 * @returns {string}
26048 */
26049 CalendarCtrl.prototype.getDateId = function(date, namespace) {
26050 if (!namespace) {
26051 throw new Error('A namespace for the date id has to be specified.');
26052 }
26053
26054 return [
26055 'md',
26056 this.id,
26057 namespace,
26058 date.getFullYear(),
26059 date.getMonth(),
26060 date.getDate()
26061 ].join('-');
26062 };
26063
26064 /**
26065 * Util to trigger an extra digest on a parent scope, in order to to ensure that
26066 * any child virtual repeaters have updated. This is necessary, because the virtual
26067 * repeater doesn't update the $index the first time around since the content isn't
26068 * in place yet. The case, in which this is an issue, is when the repeater has less
26069 * than a page of content (e.g. a month or year view has a min or max date).
26070 */
26071 CalendarCtrl.prototype.updateVirtualRepeat = function() {
26072 var scope = this.$scope;
26073 var virtualRepeatResizeListener = scope.$on('$md-resize-enable', function() {
26074 if (!scope.$$phase) {
26075 scope.$apply();
26076 }
26077
26078 virtualRepeatResizeListener();
26079 });
26080 };
26081})();
26082
26083})();
26084(function(){
26085"use strict";
26086
26087(function() {
26088 'use strict';
26089
26090 CalendarMonthCtrl.$inject = ["$element", "$scope", "$animate", "$q", "$$mdDateUtil", "$mdDateLocale"];
26091 angular.module('material.components.datepicker')
26092 .directive('mdCalendarMonth', calendarDirective);
26093
26094 /**
26095 * Height of one calendar month tbody. This must be made known to the virtual-repeat and is
26096 * subsequently used for scrolling to specific months.
26097 */
26098 var TBODY_HEIGHT = 265;
26099
26100 /**
26101 * Height of a calendar month with a single row. This is needed to calculate the offset for
26102 * rendering an extra month in virtual-repeat that only contains one row.
26103 */
26104 var TBODY_SINGLE_ROW_HEIGHT = 45;
26105
26106 /** Private directive that represents a list of months inside the calendar. */
26107 function calendarDirective() {
26108 return {
26109 template:
26110 '<table aria-hidden="true" class="md-calendar-day-header"><thead></thead></table>' +
26111 '<div class="md-calendar-scroll-mask">' +
26112 '<md-virtual-repeat-container class="md-calendar-scroll-container" ' +
26113 'md-offset-size="' + (TBODY_SINGLE_ROW_HEIGHT - TBODY_HEIGHT) + '">' +
26114 '<table role="grid" tabindex="0" class="md-calendar" aria-readonly="true">' +
26115 '<tbody ' +
26116 'md-calendar-month-body ' +
26117 'role="rowgroup" ' +
26118 'md-virtual-repeat="i in monthCtrl.items" ' +
26119 'md-month-offset="$index" ' +
26120 'class="md-calendar-month" ' +
26121 'md-start-index="monthCtrl.getSelectedMonthIndex()" ' +
26122 'md-item-size="' + TBODY_HEIGHT + '"></tbody>' +
26123 '</table>' +
26124 '</md-virtual-repeat-container>' +
26125 '</div>',
26126 require: ['^^mdCalendar', 'mdCalendarMonth'],
26127 controller: CalendarMonthCtrl,
26128 controllerAs: 'monthCtrl',
26129 bindToController: true,
26130 link: function(scope, element, attrs, controllers) {
26131 var calendarCtrl = controllers[0];
26132 var monthCtrl = controllers[1];
26133 monthCtrl.initialize(calendarCtrl);
26134 }
26135 };
26136 }
26137
26138 /**
26139 * Controller for the calendar month component.
26140 * @ngInject @constructor
26141 */
26142 function CalendarMonthCtrl($element, $scope, $animate, $q,
26143 $$mdDateUtil, $mdDateLocale) {
26144
26145 /** @final {!angular.JQLite} */
26146 this.$element = $element;
26147
26148 /** @final {!angular.Scope} */
26149 this.$scope = $scope;
26150
26151 /** @final {!angular.$animate} */
26152 this.$animate = $animate;
26153
26154 /** @final {!angular.$q} */
26155 this.$q = $q;
26156
26157 /** @final */
26158 this.dateUtil = $$mdDateUtil;
26159
26160 /** @final */
26161 this.dateLocale = $mdDateLocale;
26162
26163 /** @final {HTMLElement} */
26164 this.calendarScroller = $element[0].querySelector('.md-virtual-repeat-scroller');
26165
26166 /** @type {boolean} */
26167 this.isInitialized = false;
26168
26169 /** @type {boolean} */
26170 this.isMonthTransitionInProgress = false;
26171
26172 var self = this;
26173
26174 /**
26175 * Handles a click event on a date cell.
26176 * Created here so that every cell can use the same function instance.
26177 * @this {HTMLTableCellElement} The cell that was clicked.
26178 */
26179 this.cellClickHandler = function() {
26180 var timestamp = $$mdDateUtil.getTimestampFromNode(this);
26181 self.$scope.$apply(function() {
26182 self.calendarCtrl.setNgModelValue(timestamp);
26183 });
26184 };
26185
26186 /**
26187 * Handles click events on the month headers. Switches
26188 * the calendar to the year view.
26189 * @this {HTMLTableCellElement} The cell that was clicked.
26190 */
26191 this.headerClickHandler = function() {
26192 self.calendarCtrl.setCurrentView('year', $$mdDateUtil.getTimestampFromNode(this));
26193 };
26194 }
26195
26196 /*** Initialization ***/
26197
26198 /**
26199 * Initialize the controller by saving a reference to the calendar and
26200 * setting up the object that will be iterated by the virtual repeater.
26201 */
26202 CalendarMonthCtrl.prototype.initialize = function(calendarCtrl) {
26203 /**
26204 * Dummy array-like object for virtual-repeat to iterate over. The length is the total
26205 * number of months that can be viewed. We add 2 months: one to include the current month
26206 * and one for the last dummy month.
26207 *
26208 * This is shorter than ideal because of a (potential) Firefox bug
26209 * https://bugzilla.mozilla.org/show_bug.cgi?id=1181658.
26210 */
26211
26212 this.items = {
26213 length: this.dateUtil.getMonthDistance(
26214 calendarCtrl.firstRenderableDate,
26215 calendarCtrl.lastRenderableDate
26216 ) + 2
26217 };
26218
26219 this.calendarCtrl = calendarCtrl;
26220 this.attachScopeListeners();
26221 calendarCtrl.updateVirtualRepeat();
26222
26223 // Fire the initial render, since we might have missed it the first time it fired.
26224 calendarCtrl.ngModelCtrl && calendarCtrl.ngModelCtrl.$render();
26225 };
26226
26227 /**
26228 * Gets the "index" of the currently selected date as it would be in the virtual-repeat.
26229 * @returns {number}
26230 */
26231 CalendarMonthCtrl.prototype.getSelectedMonthIndex = function() {
26232 var calendarCtrl = this.calendarCtrl;
26233
26234 return this.dateUtil.getMonthDistance(
26235 calendarCtrl.firstRenderableDate,
26236 calendarCtrl.displayDate || calendarCtrl.selectedDate || calendarCtrl.today
26237 );
26238 };
26239
26240 /**
26241 * Change the selected date in the calendar (ngModel value has already been changed).
26242 * @param {Date} date
26243 */
26244 CalendarMonthCtrl.prototype.changeSelectedDate = function(date) {
26245 var self = this;
26246 var calendarCtrl = self.calendarCtrl;
26247 var previousSelectedDate = calendarCtrl.selectedDate;
26248 calendarCtrl.selectedDate = date;
26249
26250 this.changeDisplayDate(date).then(function() {
26251 var selectedDateClass = calendarCtrl.SELECTED_DATE_CLASS;
26252 var namespace = 'month';
26253
26254 // Remove the selected class from the previously selected date, if any.
26255 if (previousSelectedDate) {
26256 var prevDateCell = document.getElementById(calendarCtrl.getDateId(previousSelectedDate, namespace));
26257 if (prevDateCell) {
26258 prevDateCell.classList.remove(selectedDateClass);
26259 prevDateCell.setAttribute('aria-selected', 'false');
26260 }
26261 }
26262
26263 // Apply the select class to the new selected date if it is set.
26264 if (date) {
26265 var dateCell = document.getElementById(calendarCtrl.getDateId(date, namespace));
26266 if (dateCell) {
26267 dateCell.classList.add(selectedDateClass);
26268 dateCell.setAttribute('aria-selected', 'true');
26269 }
26270 }
26271 });
26272 };
26273
26274 /**
26275 * Change the date that is being shown in the calendar. If the given date is in a different
26276 * month, the displayed month will be transitioned.
26277 * @param {Date} date
26278 */
26279 CalendarMonthCtrl.prototype.changeDisplayDate = function(date) {
26280 // Initialization is deferred until this function is called because we want to reflect
26281 // the starting value of ngModel.
26282 if (!this.isInitialized) {
26283 this.buildWeekHeader();
26284 this.calendarCtrl.hideVerticalScrollbar(this);
26285 this.isInitialized = true;
26286 return this.$q.when();
26287 }
26288
26289 // If trying to show an invalid date or a transition is in progress, do nothing.
26290 if (!this.dateUtil.isValidDate(date) || this.isMonthTransitionInProgress) {
26291 return this.$q.when();
26292 }
26293
26294 this.isMonthTransitionInProgress = true;
26295 var animationPromise = this.animateDateChange(date);
26296
26297 this.calendarCtrl.displayDate = date;
26298
26299 var self = this;
26300 animationPromise.then(function() {
26301 self.isMonthTransitionInProgress = false;
26302 });
26303
26304 return animationPromise;
26305 };
26306
26307 /**
26308 * Animates the transition from the calendar's current month to the given month.
26309 * @param {Date} date
26310 * @returns {angular.$q.Promise} The animation promise.
26311 */
26312 CalendarMonthCtrl.prototype.animateDateChange = function(date) {
26313 if (this.dateUtil.isValidDate(date)) {
26314 var monthDistance = this.dateUtil.getMonthDistance(this.calendarCtrl.firstRenderableDate, date);
26315 this.calendarScroller.scrollTop = monthDistance * TBODY_HEIGHT;
26316 }
26317
26318 return this.$q.when();
26319 };
26320
26321 /**
26322 * Builds and appends a day-of-the-week header to the calendar.
26323 * This should only need to be called once during initialization.
26324 */
26325 CalendarMonthCtrl.prototype.buildWeekHeader = function() {
26326 var firstDayOfWeek = this.dateLocale.firstDayOfWeek;
26327 var shortDays = this.dateLocale.shortDays;
26328
26329 var row = document.createElement('tr');
26330 for (var i = 0; i < 7; i++) {
26331 var th = document.createElement('th');
26332 th.textContent = shortDays[(i + firstDayOfWeek) % 7];
26333 row.appendChild(th);
26334 }
26335
26336 this.$element.find('thead').append(row);
26337 };
26338
26339 /**
26340 * Attaches listeners for the scope events that are broadcast by the calendar.
26341 */
26342 CalendarMonthCtrl.prototype.attachScopeListeners = function() {
26343 var self = this;
26344
26345 self.$scope.$on('md-calendar-parent-changed', function(event, value) {
26346 self.changeSelectedDate(value);
26347 });
26348
26349 self.$scope.$on('md-calendar-parent-action', angular.bind(this, this.handleKeyEvent));
26350 };
26351
26352 /**
26353 * Handles the month-specific keyboard interactions.
26354 * @param {Object} event Scope event object passed by the calendar.
26355 * @param {String} action Action, corresponding to the key that was pressed.
26356 */
26357 CalendarMonthCtrl.prototype.handleKeyEvent = function(event, action) {
26358 var calendarCtrl = this.calendarCtrl;
26359 var displayDate = calendarCtrl.displayDate;
26360
26361 if (action === 'select') {
26362 calendarCtrl.setNgModelValue(displayDate);
26363 } else {
26364 var date = null;
26365 var dateUtil = this.dateUtil;
26366
26367 switch (action) {
26368 case 'move-right': date = dateUtil.incrementDays(displayDate, 1); break;
26369 case 'move-left': date = dateUtil.incrementDays(displayDate, -1); break;
26370
26371 case 'move-page-down': date = dateUtil.incrementMonths(displayDate, 1); break;
26372 case 'move-page-up': date = dateUtil.incrementMonths(displayDate, -1); break;
26373
26374 case 'move-row-down': date = dateUtil.incrementDays(displayDate, 7); break;
26375 case 'move-row-up': date = dateUtil.incrementDays(displayDate, -7); break;
26376
26377 case 'start': date = dateUtil.getFirstDateOfMonth(displayDate); break;
26378 case 'end': date = dateUtil.getLastDateOfMonth(displayDate); break;
26379 }
26380
26381 if (date) {
26382 date = this.dateUtil.clampDate(date, calendarCtrl.minDate, calendarCtrl.maxDate);
26383
26384 this.changeDisplayDate(date).then(function() {
26385 calendarCtrl.focus(date);
26386 });
26387 }
26388 }
26389 };
26390})();
26391
26392})();
26393(function(){
26394"use strict";
26395
26396(function() {
26397 'use strict';
26398
26399 mdCalendarMonthBodyDirective.$inject = ["$compile", "$$mdSvgRegistry"];
26400 CalendarMonthBodyCtrl.$inject = ["$element", "$$mdDateUtil", "$mdDateLocale"];
26401 angular.module('material.components.datepicker')
26402 .directive('mdCalendarMonthBody', mdCalendarMonthBodyDirective);
26403
26404 /**
26405 * Private directive consumed by md-calendar-month. Having this directive lets the calender use
26406 * md-virtual-repeat and also cleanly separates the month DOM construction functions from
26407 * the rest of the calendar controller logic.
26408 * @ngInject
26409 */
26410 function mdCalendarMonthBodyDirective($compile, $$mdSvgRegistry) {
26411 var ARROW_ICON = $compile('<md-icon md-svg-src="' +
26412 $$mdSvgRegistry.mdTabsArrow + '"></md-icon>')({})[0];
26413
26414 return {
26415 require: ['^^mdCalendar', '^^mdCalendarMonth', 'mdCalendarMonthBody'],
26416 scope: { offset: '=mdMonthOffset' },
26417 controller: CalendarMonthBodyCtrl,
26418 controllerAs: 'mdMonthBodyCtrl',
26419 bindToController: true,
26420 link: function(scope, element, attrs, controllers) {
26421 var calendarCtrl = controllers[0];
26422 var monthCtrl = controllers[1];
26423 var monthBodyCtrl = controllers[2];
26424
26425 monthBodyCtrl.calendarCtrl = calendarCtrl;
26426 monthBodyCtrl.monthCtrl = monthCtrl;
26427 monthBodyCtrl.arrowIcon = ARROW_ICON.cloneNode(true);
26428
26429 // The virtual-repeat re-uses the same DOM elements, so there are only a limited number
26430 // of repeated items that are linked, and then those elements have their bindings updated.
26431 // Since the months are not generated by bindings, we simply regenerate the entire thing
26432 // when the binding (offset) changes.
26433 scope.$watch(function() { return monthBodyCtrl.offset; }, function(offset, oldOffset) {
26434 if (offset !== oldOffset) {
26435 monthBodyCtrl.generateContent();
26436 }
26437 });
26438 }
26439 };
26440 }
26441
26442 /**
26443 * Controller for a single calendar month.
26444 * @ngInject @constructor
26445 */
26446 function CalendarMonthBodyCtrl($element, $$mdDateUtil, $mdDateLocale) {
26447 /** @final {!angular.JQLite} */
26448 this.$element = $element;
26449
26450 /** @final */
26451 this.dateUtil = $$mdDateUtil;
26452
26453 /** @final */
26454 this.dateLocale = $mdDateLocale;
26455
26456 /** @type {Object} Reference to the month view. */
26457 this.monthCtrl = null;
26458
26459 /** @type {Object} Reference to the calendar. */
26460 this.calendarCtrl = null;
26461
26462 /**
26463 * Number of months from the start of the month "items" that the currently rendered month
26464 * occurs. Set via angular data binding.
26465 * @type {number}
26466 */
26467 this.offset = null;
26468
26469 /**
26470 * Date cell to focus after appending the month to the document.
26471 * @type {HTMLElement}
26472 */
26473 this.focusAfterAppend = null;
26474 }
26475
26476 /** Generate and append the content for this month to the directive element. */
26477 CalendarMonthBodyCtrl.prototype.generateContent = function() {
26478 var date = this.dateUtil.incrementMonths(this.calendarCtrl.firstRenderableDate, this.offset);
26479
26480 this.$element
26481 .empty()
26482 .append(this.buildCalendarForMonth(date));
26483
26484 if (this.focusAfterAppend) {
26485 this.focusAfterAppend.classList.add(this.calendarCtrl.FOCUSED_DATE_CLASS);
26486 this.focusAfterAppend.focus();
26487 this.focusAfterAppend = null;
26488 }
26489 };
26490
26491 /**
26492 * Creates a single cell to contain a date in the calendar with all appropriate
26493 * attributes and classes added. If a date is given, the cell content will be set
26494 * based on the date.
26495 * @param {Date=} opt_date
26496 * @returns {HTMLElement}
26497 */
26498 CalendarMonthBodyCtrl.prototype.buildDateCell = function(opt_date) {
26499 var monthCtrl = this.monthCtrl;
26500 var calendarCtrl = this.calendarCtrl;
26501
26502 // TODO(jelbourn): cloneNode is likely a faster way of doing this.
26503 var cell = document.createElement('td');
26504 cell.tabIndex = -1;
26505 cell.classList.add('md-calendar-date');
26506 cell.setAttribute('role', 'gridcell');
26507
26508 if (opt_date) {
26509 cell.setAttribute('tabindex', '-1');
26510 cell.setAttribute('aria-label', this.dateLocale.longDateFormatter(opt_date));
26511 cell.id = calendarCtrl.getDateId(opt_date, 'month');
26512
26513 // Use `data-timestamp` attribute because IE10 does not support the `dataset` property.
26514 cell.setAttribute('data-timestamp', opt_date.getTime());
26515
26516 // TODO(jelourn): Doing these comparisons for class addition during generation might be slow.
26517 // It may be better to finish the construction and then query the node and add the class.
26518 if (this.dateUtil.isSameDay(opt_date, calendarCtrl.today)) {
26519 cell.classList.add(calendarCtrl.TODAY_CLASS);
26520 }
26521
26522 if (this.dateUtil.isValidDate(calendarCtrl.selectedDate) &&
26523 this.dateUtil.isSameDay(opt_date, calendarCtrl.selectedDate)) {
26524 cell.classList.add(calendarCtrl.SELECTED_DATE_CLASS);
26525 cell.setAttribute('aria-selected', 'true');
26526 }
26527
26528 var cellText = this.dateLocale.dates[opt_date.getDate()];
26529
26530 if (this.isDateEnabled(opt_date)) {
26531 // Add a indicator for select, hover, and focus states.
26532 var selectionIndicator = document.createElement('span');
26533 selectionIndicator.classList.add('md-calendar-date-selection-indicator');
26534 selectionIndicator.textContent = cellText;
26535 cell.appendChild(selectionIndicator);
26536 cell.addEventListener('click', monthCtrl.cellClickHandler);
26537
26538 if (calendarCtrl.displayDate && this.dateUtil.isSameDay(opt_date, calendarCtrl.displayDate)) {
26539 this.focusAfterAppend = cell;
26540 }
26541 } else {
26542 cell.classList.add('md-calendar-date-disabled');
26543 cell.textContent = cellText;
26544 }
26545 }
26546
26547 return cell;
26548 };
26549
26550 /**
26551 * Check whether date is in range and enabled
26552 * @param {Date=} opt_date
26553 * @return {boolean} Whether the date is enabled.
26554 */
26555 CalendarMonthBodyCtrl.prototype.isDateEnabled = function(opt_date) {
26556 return this.dateUtil.isDateWithinRange(opt_date,
26557 this.calendarCtrl.minDate, this.calendarCtrl.maxDate) &&
26558 (!angular.isFunction(this.calendarCtrl.dateFilter)
26559 || this.calendarCtrl.dateFilter(opt_date));
26560 };
26561
26562 /**
26563 * Builds a `tr` element for the calendar grid.
26564 * @param rowNumber The week number within the month.
26565 * @returns {HTMLElement}
26566 */
26567 CalendarMonthBodyCtrl.prototype.buildDateRow = function(rowNumber) {
26568 var row = document.createElement('tr');
26569 row.setAttribute('role', 'row');
26570
26571 // Because of an NVDA bug (with Firefox), the row needs an aria-label in order
26572 // to prevent the entire row being read aloud when the user moves between rows.
26573 // See http://community.nvda-project.org/ticket/4643.
26574 row.setAttribute('aria-label', this.dateLocale.weekNumberFormatter(rowNumber));
26575
26576 return row;
26577 };
26578
26579 /**
26580 * Builds the <tbody> content for the given date's month.
26581 * @param {Date=} opt_dateInMonth
26582 * @returns {DocumentFragment} A document fragment containing the <tr> elements.
26583 */
26584 CalendarMonthBodyCtrl.prototype.buildCalendarForMonth = function(opt_dateInMonth) {
26585 var date = this.dateUtil.isValidDate(opt_dateInMonth) ? opt_dateInMonth : new Date();
26586
26587 var firstDayOfMonth = this.dateUtil.getFirstDateOfMonth(date);
26588 var firstDayOfTheWeek = this.getLocaleDay_(firstDayOfMonth);
26589 var numberOfDaysInMonth = this.dateUtil.getNumberOfDaysInMonth(date);
26590
26591 // Store rows for the month in a document fragment so that we can append them all at once.
26592 var monthBody = document.createDocumentFragment();
26593
26594 var rowNumber = 1;
26595 var row = this.buildDateRow(rowNumber);
26596 monthBody.appendChild(row);
26597
26598 // If this is the final month in the list of items, only the first week should render,
26599 // so we should return immediately after the first row is complete and has been
26600 // attached to the body.
26601 var isFinalMonth = this.offset === this.monthCtrl.items.length - 1;
26602
26603 // Add a label for the month. If the month starts on a Sun/Mon/Tues, the month label
26604 // goes on a row above the first of the month. Otherwise, the month label takes up the first
26605 // two cells of the first row.
26606 var blankCellOffset = 0;
26607 var monthLabelCell = document.createElement('td');
26608 var monthLabelCellContent = document.createElement('span');
26609
26610 monthLabelCellContent.textContent = this.dateLocale.monthHeaderFormatter(date);
26611 monthLabelCell.appendChild(monthLabelCellContent);
26612 monthLabelCell.classList.add('md-calendar-month-label');
26613 // If the entire month is after the max date, render the label as a disabled state.
26614 if (this.calendarCtrl.maxDate && firstDayOfMonth > this.calendarCtrl.maxDate) {
26615 monthLabelCell.classList.add('md-calendar-month-label-disabled');
26616 } else {
26617 monthLabelCell.addEventListener('click', this.monthCtrl.headerClickHandler);
26618 monthLabelCell.setAttribute('data-timestamp', firstDayOfMonth.getTime());
26619 monthLabelCell.setAttribute('aria-label', this.dateLocale.monthFormatter(date));
26620 monthLabelCell.appendChild(this.arrowIcon.cloneNode(true));
26621 }
26622
26623 if (firstDayOfTheWeek <= 2) {
26624 monthLabelCell.setAttribute('colspan', '7');
26625
26626 var monthLabelRow = this.buildDateRow();
26627 monthLabelRow.appendChild(monthLabelCell);
26628 monthBody.insertBefore(monthLabelRow, row);
26629
26630 if (isFinalMonth) {
26631 return monthBody;
26632 }
26633 } else {
26634 blankCellOffset = 3;
26635 monthLabelCell.setAttribute('colspan', '3');
26636 row.appendChild(monthLabelCell);
26637 }
26638
26639 // Add a blank cell for each day of the week that occurs before the first of the month.
26640 // For example, if the first day of the month is a Tuesday, add blank cells for Sun and Mon.
26641 // The blankCellOffset is needed in cases where the first N cells are used by the month label.
26642 for (var i = blankCellOffset; i < firstDayOfTheWeek; i++) {
26643 row.appendChild(this.buildDateCell());
26644 }
26645
26646 // Add a cell for each day of the month, keeping track of the day of the week so that
26647 // we know when to start a new row.
26648 var dayOfWeek = firstDayOfTheWeek;
26649 var iterationDate = firstDayOfMonth;
26650 for (var d = 1; d <= numberOfDaysInMonth; d++) {
26651 // If we've reached the end of the week, start a new row.
26652 if (dayOfWeek === 7) {
26653 // We've finished the first row, so we're done if this is the final month.
26654 if (isFinalMonth) {
26655 return monthBody;
26656 }
26657 dayOfWeek = 0;
26658 rowNumber++;
26659 row = this.buildDateRow(rowNumber);
26660 monthBody.appendChild(row);
26661 }
26662
26663 iterationDate.setDate(d);
26664 var cell = this.buildDateCell(iterationDate);
26665 row.appendChild(cell);
26666
26667 dayOfWeek++;
26668 }
26669
26670 // Ensure that the last row of the month has 7 cells.
26671 while (row.childNodes.length < 7) {
26672 row.appendChild(this.buildDateCell());
26673 }
26674
26675 // Ensure that all months have 6 rows. This is necessary for now because the virtual-repeat
26676 // requires that all items have exactly the same height.
26677 while (monthBody.childNodes.length < 6) {
26678 var whitespaceRow = this.buildDateRow();
26679 for (var j = 0; j < 7; j++) {
26680 whitespaceRow.appendChild(this.buildDateCell());
26681 }
26682 monthBody.appendChild(whitespaceRow);
26683 }
26684
26685 return monthBody;
26686 };
26687
26688 /**
26689 * Gets the day-of-the-week index for a date for the current locale.
26690 * @private
26691 * @param {Date} date
26692 * @returns {number} The column index of the date in the calendar.
26693 */
26694 CalendarMonthBodyCtrl.prototype.getLocaleDay_ = function(date) {
26695 return (date.getDay() + (7 - this.dateLocale.firstDayOfWeek)) % 7;
26696 };
26697})();
26698
26699})();
26700(function(){
26701"use strict";
26702
26703(function() {
26704 'use strict';
26705
26706 CalendarYearCtrl.$inject = ["$element", "$scope", "$animate", "$q", "$$mdDateUtil"];
26707 angular.module('material.components.datepicker')
26708 .directive('mdCalendarYear', calendarDirective);
26709
26710 /**
26711 * Height of one calendar year tbody. This must be made known to the virtual-repeat and is
26712 * subsequently used for scrolling to specific years.
26713 */
26714 var TBODY_HEIGHT = 88;
26715
26716 /** Private component, representing a list of years in the calendar. */
26717 function calendarDirective() {
26718 return {
26719 template:
26720 '<div class="md-calendar-scroll-mask">' +
26721 '<md-virtual-repeat-container class="md-calendar-scroll-container">' +
26722 '<table role="grid" tabindex="0" class="md-calendar" aria-readonly="true">' +
26723 '<tbody ' +
26724 'md-calendar-year-body ' +
26725 'role="rowgroup" ' +
26726 'md-virtual-repeat="i in yearCtrl.items" ' +
26727 'md-year-offset="$index" class="md-calendar-year" ' +
26728 'md-start-index="yearCtrl.getFocusedYearIndex()" ' +
26729 'md-item-size="' + TBODY_HEIGHT + '"></tbody>' +
26730 '</table>' +
26731 '</md-virtual-repeat-container>' +
26732 '</div>',
26733 require: ['^^mdCalendar', 'mdCalendarYear'],
26734 controller: CalendarYearCtrl,
26735 controllerAs: 'yearCtrl',
26736 bindToController: true,
26737 link: function(scope, element, attrs, controllers) {
26738 var calendarCtrl = controllers[0];
26739 var yearCtrl = controllers[1];
26740 yearCtrl.initialize(calendarCtrl);
26741 }
26742 };
26743 }
26744
26745 /**
26746 * Controller for the mdCalendar component.
26747 * @ngInject @constructor
26748 */
26749 function CalendarYearCtrl($element, $scope, $animate, $q, $$mdDateUtil) {
26750
26751 /** @final {!angular.JQLite} */
26752 this.$element = $element;
26753
26754 /** @final {!angular.Scope} */
26755 this.$scope = $scope;
26756
26757 /** @final {!angular.$animate} */
26758 this.$animate = $animate;
26759
26760 /** @final {!angular.$q} */
26761 this.$q = $q;
26762
26763 /** @final */
26764 this.dateUtil = $$mdDateUtil;
26765
26766 /** @final {HTMLElement} */
26767 this.calendarScroller = $element[0].querySelector('.md-virtual-repeat-scroller');
26768
26769 /** @type {boolean} */
26770 this.isInitialized = false;
26771
26772 /** @type {boolean} */
26773 this.isMonthTransitionInProgress = false;
26774
26775 var self = this;
26776
26777 /**
26778 * Handles a click event on a date cell.
26779 * Created here so that every cell can use the same function instance.
26780 * @this {HTMLTableCellElement} The cell that was clicked.
26781 */
26782 this.cellClickHandler = function() {
26783 self.calendarCtrl.setCurrentView('month', $$mdDateUtil.getTimestampFromNode(this));
26784 };
26785 }
26786
26787 /**
26788 * Initialize the controller by saving a reference to the calendar and
26789 * setting up the object that will be iterated by the virtual repeater.
26790 */
26791 CalendarYearCtrl.prototype.initialize = function(calendarCtrl) {
26792 /**
26793 * Dummy array-like object for virtual-repeat to iterate over. The length is the total
26794 * number of years that can be viewed. We add 1 extra in order to include the current year.
26795 */
26796 this.items = {
26797 length: this.dateUtil.getYearDistance(
26798 calendarCtrl.firstRenderableDate,
26799 calendarCtrl.lastRenderableDate
26800 ) + 1
26801 };
26802
26803 this.calendarCtrl = calendarCtrl;
26804 this.attachScopeListeners();
26805 calendarCtrl.updateVirtualRepeat();
26806
26807 // Fire the initial render, since we might have missed it the first time it fired.
26808 calendarCtrl.ngModelCtrl && calendarCtrl.ngModelCtrl.$render();
26809 };
26810
26811 /**
26812 * Gets the "index" of the currently selected date as it would be in the virtual-repeat.
26813 * @returns {number}
26814 */
26815 CalendarYearCtrl.prototype.getFocusedYearIndex = function() {
26816 var calendarCtrl = this.calendarCtrl;
26817
26818 return this.dateUtil.getYearDistance(
26819 calendarCtrl.firstRenderableDate,
26820 calendarCtrl.displayDate || calendarCtrl.selectedDate || calendarCtrl.today
26821 );
26822 };
26823
26824 /**
26825 * Change the date that is highlighted in the calendar.
26826 * @param {Date} date
26827 */
26828 CalendarYearCtrl.prototype.changeDate = function(date) {
26829 // Initialization is deferred until this function is called because we want to reflect
26830 // the starting value of ngModel.
26831 if (!this.isInitialized) {
26832 this.calendarCtrl.hideVerticalScrollbar(this);
26833 this.isInitialized = true;
26834 return this.$q.when();
26835 } else if (this.dateUtil.isValidDate(date) && !this.isMonthTransitionInProgress) {
26836 var self = this;
26837 var animationPromise = this.animateDateChange(date);
26838
26839 self.isMonthTransitionInProgress = true;
26840 self.calendarCtrl.displayDate = date;
26841
26842 return animationPromise.then(function() {
26843 self.isMonthTransitionInProgress = false;
26844 });
26845 }
26846 };
26847
26848 /**
26849 * Animates the transition from the calendar's current month to the given month.
26850 * @param {Date} date
26851 * @returns {angular.$q.Promise} The animation promise.
26852 */
26853 CalendarYearCtrl.prototype.animateDateChange = function(date) {
26854 if (this.dateUtil.isValidDate(date)) {
26855 var monthDistance = this.dateUtil.getYearDistance(this.calendarCtrl.firstRenderableDate, date);
26856 this.calendarScroller.scrollTop = monthDistance * TBODY_HEIGHT;
26857 }
26858
26859 return this.$q.when();
26860 };
26861
26862 /**
26863 * Handles the year-view-specific keyboard interactions.
26864 * @param {Object} event Scope event object passed by the calendar.
26865 * @param {String} action Action, corresponding to the key that was pressed.
26866 */
26867 CalendarYearCtrl.prototype.handleKeyEvent = function(event, action) {
26868 var calendarCtrl = this.calendarCtrl;
26869 var displayDate = calendarCtrl.displayDate;
26870
26871 if (action === 'select') {
26872 this.changeDate(displayDate).then(function() {
26873 calendarCtrl.setCurrentView('month', displayDate);
26874 calendarCtrl.focus(displayDate);
26875 });
26876 } else {
26877 var date = null;
26878 var dateUtil = this.dateUtil;
26879
26880 switch (action) {
26881 case 'move-right': date = dateUtil.incrementMonths(displayDate, 1); break;
26882 case 'move-left': date = dateUtil.incrementMonths(displayDate, -1); break;
26883
26884 case 'move-row-down': date = dateUtil.incrementMonths(displayDate, 6); break;
26885 case 'move-row-up': date = dateUtil.incrementMonths(displayDate, -6); break;
26886 }
26887
26888 if (date) {
26889 var min = calendarCtrl.minDate ? dateUtil.getFirstDateOfMonth(calendarCtrl.minDate) : null;
26890 var max = calendarCtrl.maxDate ? dateUtil.getFirstDateOfMonth(calendarCtrl.maxDate) : null;
26891 date = dateUtil.getFirstDateOfMonth(this.dateUtil.clampDate(date, min, max));
26892
26893 this.changeDate(date).then(function() {
26894 calendarCtrl.focus(date);
26895 });
26896 }
26897 }
26898 };
26899
26900 /**
26901 * Attaches listeners for the scope events that are broadcast by the calendar.
26902 */
26903 CalendarYearCtrl.prototype.attachScopeListeners = function() {
26904 var self = this;
26905
26906 self.$scope.$on('md-calendar-parent-changed', function(event, value) {
26907 self.changeDate(value);
26908 });
26909
26910 self.$scope.$on('md-calendar-parent-action', angular.bind(self, self.handleKeyEvent));
26911 };
26912})();
26913
26914})();
26915(function(){
26916"use strict";
26917
26918(function() {
26919 'use strict';
26920
26921 CalendarYearBodyCtrl.$inject = ["$element", "$$mdDateUtil", "$mdDateLocale"];
26922 angular.module('material.components.datepicker')
26923 .directive('mdCalendarYearBody', mdCalendarYearDirective);
26924
26925 /**
26926 * Private component, consumed by the md-calendar-year, which separates the DOM construction logic
26927 * and allows for the year view to use md-virtual-repeat.
26928 */
26929 function mdCalendarYearDirective() {
26930 return {
26931 require: ['^^mdCalendar', '^^mdCalendarYear', 'mdCalendarYearBody'],
26932 scope: { offset: '=mdYearOffset' },
26933 controller: CalendarYearBodyCtrl,
26934 controllerAs: 'mdYearBodyCtrl',
26935 bindToController: true,
26936 link: function(scope, element, attrs, controllers) {
26937 var calendarCtrl = controllers[0];
26938 var yearCtrl = controllers[1];
26939 var yearBodyCtrl = controllers[2];
26940
26941 yearBodyCtrl.calendarCtrl = calendarCtrl;
26942 yearBodyCtrl.yearCtrl = yearCtrl;
26943
26944 scope.$watch(function() { return yearBodyCtrl.offset; }, function(offset, oldOffset) {
26945 if (offset !== oldOffset) {
26946 yearBodyCtrl.generateContent();
26947 }
26948 });
26949 }
26950 };
26951 }
26952
26953 /**
26954 * Controller for a single year.
26955 * @ngInject @constructor
26956 */
26957 function CalendarYearBodyCtrl($element, $$mdDateUtil, $mdDateLocale) {
26958 /** @final {!angular.JQLite} */
26959 this.$element = $element;
26960
26961 /** @final */
26962 this.dateUtil = $$mdDateUtil;
26963
26964 /** @final */
26965 this.dateLocale = $mdDateLocale;
26966
26967 /** @type {Object} Reference to the calendar. */
26968 this.calendarCtrl = null;
26969
26970 /** @type {Object} Reference to the year view. */
26971 this.yearCtrl = null;
26972
26973 /**
26974 * Number of months from the start of the month "items" that the currently rendered month
26975 * occurs. Set via angular data binding.
26976 * @type {number}
26977 */
26978 this.offset = null;
26979
26980 /**
26981 * Date cell to focus after appending the month to the document.
26982 * @type {HTMLElement}
26983 */
26984 this.focusAfterAppend = null;
26985 }
26986
26987 /** Generate and append the content for this year to the directive element. */
26988 CalendarYearBodyCtrl.prototype.generateContent = function() {
26989 var date = this.dateUtil.incrementYears(this.calendarCtrl.firstRenderableDate, this.offset);
26990
26991 this.$element
26992 .empty()
26993 .append(this.buildCalendarForYear(date));
26994
26995 if (this.focusAfterAppend) {
26996 this.focusAfterAppend.classList.add(this.calendarCtrl.FOCUSED_DATE_CLASS);
26997 this.focusAfterAppend.focus();
26998 this.focusAfterAppend = null;
26999 }
27000 };
27001
27002 /**
27003 * Creates a single cell to contain a year in the calendar.
27004 * @param {number} opt_year Four-digit year.
27005 * @param {number} opt_month Zero-indexed month.
27006 * @returns {HTMLElement}
27007 */
27008 CalendarYearBodyCtrl.prototype.buildMonthCell = function(year, month) {
27009 var calendarCtrl = this.calendarCtrl;
27010 var yearCtrl = this.yearCtrl;
27011 var cell = this.buildBlankCell();
27012
27013 // Represent this month/year as a date.
27014 var firstOfMonth = new Date(year, month, 1);
27015 cell.setAttribute('aria-label', this.dateLocale.monthFormatter(firstOfMonth));
27016 cell.id = calendarCtrl.getDateId(firstOfMonth, 'year');
27017
27018 // Use `data-timestamp` attribute because IE10 does not support the `dataset` property.
27019 cell.setAttribute('data-timestamp', firstOfMonth.getTime());
27020
27021 if (this.dateUtil.isSameMonthAndYear(firstOfMonth, calendarCtrl.today)) {
27022 cell.classList.add(calendarCtrl.TODAY_CLASS);
27023 }
27024
27025 if (this.dateUtil.isValidDate(calendarCtrl.selectedDate) &&
27026 this.dateUtil.isSameMonthAndYear(firstOfMonth, calendarCtrl.selectedDate)) {
27027 cell.classList.add(calendarCtrl.SELECTED_DATE_CLASS);
27028 cell.setAttribute('aria-selected', 'true');
27029 }
27030
27031 var cellText = this.dateLocale.shortMonths[month];
27032
27033 if (this.dateUtil.isMonthWithinRange(firstOfMonth,
27034 calendarCtrl.minDate, calendarCtrl.maxDate)) {
27035 var selectionIndicator = document.createElement('span');
27036 selectionIndicator.classList.add('md-calendar-date-selection-indicator');
27037 selectionIndicator.textContent = cellText;
27038 cell.appendChild(selectionIndicator);
27039 cell.addEventListener('click', yearCtrl.cellClickHandler);
27040
27041 if (calendarCtrl.displayDate && this.dateUtil.isSameMonthAndYear(firstOfMonth, calendarCtrl.displayDate)) {
27042 this.focusAfterAppend = cell;
27043 }
27044 } else {
27045 cell.classList.add('md-calendar-date-disabled');
27046 cell.textContent = cellText;
27047 }
27048
27049 return cell;
27050 };
27051
27052 /**
27053 * Builds a blank cell.
27054 * @return {HTMLTableCellElement}
27055 */
27056 CalendarYearBodyCtrl.prototype.buildBlankCell = function() {
27057 var cell = document.createElement('td');
27058 cell.tabIndex = -1;
27059 cell.classList.add('md-calendar-date');
27060 cell.setAttribute('role', 'gridcell');
27061
27062 cell.setAttribute('tabindex', '-1');
27063 return cell;
27064 };
27065
27066 /**
27067 * Builds the <tbody> content for the given year.
27068 * @param {Date} date Date for which the content should be built.
27069 * @returns {DocumentFragment} A document fragment containing the months within the year.
27070 */
27071 CalendarYearBodyCtrl.prototype.buildCalendarForYear = function(date) {
27072 // Store rows for the month in a document fragment so that we can append them all at once.
27073 var year = date.getFullYear();
27074 var yearBody = document.createDocumentFragment();
27075
27076 var monthCell, i;
27077 // First row contains label and Jan-Jun.
27078 var firstRow = document.createElement('tr');
27079 var labelCell = document.createElement('td');
27080 labelCell.className = 'md-calendar-month-label';
27081 labelCell.textContent = year;
27082 firstRow.appendChild(labelCell);
27083
27084 for (i = 0; i < 6; i++) {
27085 firstRow.appendChild(this.buildMonthCell(year, i));
27086 }
27087 yearBody.appendChild(firstRow);
27088
27089 // Second row contains a blank cell and Jul-Dec.
27090 var secondRow = document.createElement('tr');
27091 secondRow.appendChild(this.buildBlankCell());
27092 for (i = 6; i < 12; i++) {
27093 secondRow.appendChild(this.buildMonthCell(year, i));
27094 }
27095 yearBody.appendChild(secondRow);
27096
27097 return yearBody;
27098 };
27099})();
27100
27101})();
27102(function(){
27103"use strict";
27104
27105(function() {
27106 'use strict';
27107
27108 /**
27109 * @ngdoc service
27110 * @name $mdDateLocaleProvider
27111 * @module material.components.datepicker
27112 *
27113 * @description
27114 * The `$mdDateLocaleProvider` is the provider that creates the `$mdDateLocale` service.
27115 * This provider that allows the user to specify messages, formatters, and parsers for date
27116 * internationalization. The `$mdDateLocale` service itself is consumed by Angular Material
27117 * components that deal with dates.
27118 *
27119 * @property {(Array<string>)=} months Array of month names (in order).
27120 * @property {(Array<string>)=} shortMonths Array of abbreviated month names.
27121 * @property {(Array<string>)=} days Array of the days of the week (in order).
27122 * @property {(Array<string>)=} shortDays Array of abbreviated dayes of the week.
27123 * @property {(Array<string>)=} dates Array of dates of the month. Only necessary for locales
27124 * using a numeral system other than [1, 2, 3...].
27125 * @property {(Array<string>)=} firstDayOfWeek The first day of the week. Sunday = 0, Monday = 1,
27126 * etc.
27127 * @property {(function(string): Date)=} parseDate Function to parse a date object from a string.
27128 * @property {(function(Date): string)=} formatDate Function to format a date object to a string.
27129 * @property {(function(Date): string)=} monthHeaderFormatter Function that returns the label for
27130 * a month given a date.
27131 * @property {(function(Date): string)=} monthFormatter Function that returns the full name of a month
27132 * for a giben date.
27133 * @property {(function(number): string)=} weekNumberFormatter Function that returns a label for
27134 * a week given the week number.
27135 * @property {(string)=} msgCalendar Translation of the label "Calendar" for the current locale.
27136 * @property {(string)=} msgOpenCalendar Translation of the button label "Open calendar" for the
27137 * current locale.
27138 * @property {Date=} firstRenderableDate The date from which the datepicker calendar will begin
27139 * rendering. Note that this will be ignored if a minimum date is set. Defaults to January 1st 1880.
27140 * @property {Date=} lastRenderableDate The last date that will be rendered by the datepicker
27141 * calendar. Note that this will be ignored if a maximum date is set. Defaults to January 1st 2130.
27142 *
27143 * @usage
27144 * <hljs lang="js">
27145 * myAppModule.config(function($mdDateLocaleProvider) {
27146 *
27147 * // Example of a French localization.
27148 * $mdDateLocaleProvider.months = ['janvier', 'février', 'mars', ...];
27149 * $mdDateLocaleProvider.shortMonths = ['janv', 'févr', 'mars', ...];
27150 * $mdDateLocaleProvider.days = ['dimanche', 'lundi', 'mardi', ...];
27151 * $mdDateLocaleProvider.shortDays = ['Di', 'Lu', 'Ma', ...];
27152 *
27153 * // Can change week display to start on Monday.
27154 * $mdDateLocaleProvider.firstDayOfWeek = 1;
27155 *
27156 * // Optional.
27157 * $mdDateLocaleProvider.dates = [1, 2, 3, 4, 5, 6, ...];
27158 *
27159 * // Example uses moment.js to parse and format dates.
27160 * $mdDateLocaleProvider.parseDate = function(dateString) {
27161 * var m = moment(dateString, 'L', true);
27162 * return m.isValid() ? m.toDate() : new Date(NaN);
27163 * };
27164 *
27165 * $mdDateLocaleProvider.formatDate = function(date) {
27166 * var m = moment(date);
27167 * return m.isValid() ? m.format('L') : '';
27168 * };
27169 *
27170 * $mdDateLocaleProvider.monthHeaderFormatter = function(date) {
27171 * return myShortMonths[date.getMonth()] + ' ' + date.getFullYear();
27172 * };
27173 *
27174 * // In addition to date display, date components also need localized messages
27175 * // for aria-labels for screen-reader users.
27176 *
27177 * $mdDateLocaleProvider.weekNumberFormatter = function(weekNumber) {
27178 * return 'Semaine ' + weekNumber;
27179 * };
27180 *
27181 * $mdDateLocaleProvider.msgCalendar = 'Calendrier';
27182 * $mdDateLocaleProvider.msgOpenCalendar = 'Ouvrir le calendrier';
27183 *
27184 * // You can also set when your calendar begins and ends.
27185 * $mdDateLocaleProvider.firstRenderableDate = new Date(1776, 6, 4);
27186 * $mdDateLocaleProvider.lastRenderableDate = new Date(2012, 11, 21);
27187 * });
27188 * </hljs>
27189 *
27190 */
27191 angular.module('material.components.datepicker').config(["$provide", function($provide) {
27192 // TODO(jelbourn): Assert provided values are correctly formatted. Need assertions.
27193
27194 /** @constructor */
27195 function DateLocaleProvider() {
27196 /** Array of full month names. E.g., ['January', 'Febuary', ...] */
27197 this.months = null;
27198
27199 /** Array of abbreviated month names. E.g., ['Jan', 'Feb', ...] */
27200 this.shortMonths = null;
27201
27202 /** Array of full day of the week names. E.g., ['Monday', 'Tuesday', ...] */
27203 this.days = null;
27204
27205 /** Array of abbreviated dat of the week names. E.g., ['M', 'T', ...] */
27206 this.shortDays = null;
27207
27208 /** Array of dates of a month (1 - 31). Characters might be different in some locales. */
27209 this.dates = null;
27210
27211 /** Index of the first day of the week. 0 = Sunday, 1 = Monday, etc. */
27212 this.firstDayOfWeek = 0;
27213
27214 /**
27215 * Function that converts the date portion of a Date to a string.
27216 * @type {(function(Date): string)}
27217 */
27218 this.formatDate = null;
27219
27220 /**
27221 * Function that converts a date string to a Date object (the date portion)
27222 * @type {function(string): Date}
27223 */
27224 this.parseDate = null;
27225
27226 /**
27227 * Function that formats a Date into a month header string.
27228 * @type {function(Date): string}
27229 */
27230 this.monthHeaderFormatter = null;
27231
27232 /**
27233 * Function that formats a week number into a label for the week.
27234 * @type {function(number): string}
27235 */
27236 this.weekNumberFormatter = null;
27237
27238 /**
27239 * Function that formats a date into a long aria-label that is read
27240 * when the focused date changes.
27241 * @type {function(Date): string}
27242 */
27243 this.longDateFormatter = null;
27244
27245 /**
27246 * ARIA label for the calendar "dialog" used in the datepicker.
27247 * @type {string}
27248 */
27249 this.msgCalendar = '';
27250
27251 /**
27252 * ARIA label for the datepicker's "Open calendar" buttons.
27253 * @type {string}
27254 */
27255 this.msgOpenCalendar = '';
27256 }
27257
27258 /**
27259 * Factory function that returns an instance of the dateLocale service.
27260 * @ngInject
27261 * @param $locale
27262 * @returns {DateLocale}
27263 */
27264 DateLocaleProvider.prototype.$get = function($locale, $filter) {
27265 /**
27266 * Default date-to-string formatting function.
27267 * @param {!Date} date
27268 * @returns {string}
27269 */
27270 function defaultFormatDate(date) {
27271 if (!date) {
27272 return '';
27273 }
27274
27275 // All of the dates created through ng-material *should* be set to midnight.
27276 // If we encounter a date where the localeTime shows at 11pm instead of midnight,
27277 // we have run into an issue with DST where we need to increment the hour by one:
27278 // var d = new Date(1992, 9, 8, 0, 0, 0);
27279 // d.toLocaleString(); // == "10/7/1992, 11:00:00 PM"
27280 var localeTime = date.toLocaleTimeString();
27281 var formatDate = date;
27282 if (date.getHours() == 0 &&
27283 (localeTime.indexOf('11:') !== -1 || localeTime.indexOf('23:') !== -1)) {
27284 formatDate = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 1, 0, 0);
27285 }
27286
27287 return $filter('date')(formatDate, 'M/d/yyyy');
27288 }
27289
27290 /**
27291 * Default string-to-date parsing function.
27292 * @param {string} dateString
27293 * @returns {!Date}
27294 */
27295 function defaultParseDate(dateString) {
27296 return new Date(dateString);
27297 }
27298
27299 /**
27300 * Default function to determine whether a string makes sense to be
27301 * parsed to a Date object.
27302 *
27303 * This is very permissive and is just a basic sanity check to ensure that
27304 * things like single integers aren't able to be parsed into dates.
27305 * @param {string} dateString
27306 * @returns {boolean}
27307 */
27308 function defaultIsDateComplete(dateString) {
27309 dateString = dateString.trim();
27310
27311 // Looks for three chunks of content (either numbers or text) separated
27312 // by delimiters.
27313 var re = /^(([a-zA-Z]{3,}|[0-9]{1,4})([ \.,]+|[\/\-])){2}([a-zA-Z]{3,}|[0-9]{1,4})$/;
27314 return re.test(dateString);
27315 }
27316
27317 /**
27318 * Default date-to-string formatter to get a month header.
27319 * @param {!Date} date
27320 * @returns {string}
27321 */
27322 function defaultMonthHeaderFormatter(date) {
27323 return service.shortMonths[date.getMonth()] + ' ' + date.getFullYear();
27324 }
27325
27326 /**
27327 * Default formatter for a month.
27328 * @param {!Date} date
27329 * @returns {string}
27330 */
27331 function defaultMonthFormatter(date) {
27332 return service.months[date.getMonth()] + ' ' + date.getFullYear();
27333 }
27334
27335 /**
27336 * Default week number formatter.
27337 * @param number
27338 * @returns {string}
27339 */
27340 function defaultWeekNumberFormatter(number) {
27341 return 'Week ' + number;
27342 }
27343
27344 /**
27345 * Default formatter for date cell aria-labels.
27346 * @param {!Date} date
27347 * @returns {string}
27348 */
27349 function defaultLongDateFormatter(date) {
27350 // Example: 'Thursday June 18 2015'
27351 return [
27352 service.days[date.getDay()],
27353 service.months[date.getMonth()],
27354 service.dates[date.getDate()],
27355 date.getFullYear()
27356 ].join(' ');
27357 }
27358
27359 // The default "short" day strings are the first character of each day,
27360 // e.g., "Monday" => "M".
27361 var defaultShortDays = $locale.DATETIME_FORMATS.SHORTDAY.map(function(day) {
27362 return day.substring(0, 1);
27363 });
27364
27365 // The default dates are simply the numbers 1 through 31.
27366 var defaultDates = Array(32);
27367 for (var i = 1; i <= 31; i++) {
27368 defaultDates[i] = i;
27369 }
27370
27371 // Default ARIA messages are in English (US).
27372 var defaultMsgCalendar = 'Calendar';
27373 var defaultMsgOpenCalendar = 'Open calendar';
27374
27375 // Default start/end dates that are rendered in the calendar.
27376 var defaultFirstRenderableDate = new Date(1880, 0, 1);
27377 var defaultLastRendereableDate = new Date(defaultFirstRenderableDate.getFullYear() + 250, 0, 1);
27378
27379 var service = {
27380 months: this.months || $locale.DATETIME_FORMATS.MONTH,
27381 shortMonths: this.shortMonths || $locale.DATETIME_FORMATS.SHORTMONTH,
27382 days: this.days || $locale.DATETIME_FORMATS.DAY,
27383 shortDays: this.shortDays || defaultShortDays,
27384 dates: this.dates || defaultDates,
27385 firstDayOfWeek: this.firstDayOfWeek || 0,
27386 formatDate: this.formatDate || defaultFormatDate,
27387 parseDate: this.parseDate || defaultParseDate,
27388 isDateComplete: this.isDateComplete || defaultIsDateComplete,
27389 monthHeaderFormatter: this.monthHeaderFormatter || defaultMonthHeaderFormatter,
27390 monthFormatter: this.monthFormatter || defaultMonthFormatter,
27391 weekNumberFormatter: this.weekNumberFormatter || defaultWeekNumberFormatter,
27392 longDateFormatter: this.longDateFormatter || defaultLongDateFormatter,
27393 msgCalendar: this.msgCalendar || defaultMsgCalendar,
27394 msgOpenCalendar: this.msgOpenCalendar || defaultMsgOpenCalendar,
27395 firstRenderableDate: this.firstRenderableDate || defaultFirstRenderableDate,
27396 lastRenderableDate: this.lastRenderableDate || defaultLastRendereableDate
27397 };
27398
27399 return service;
27400 };
27401 DateLocaleProvider.prototype.$get.$inject = ["$locale", "$filter"];
27402
27403 $provide.provider('$mdDateLocale', new DateLocaleProvider());
27404 }]);
27405})();
27406
27407})();
27408(function(){
27409"use strict";
27410
27411(function() {
27412 'use strict';
27413
27414 /**
27415 * Utility for performing date calculations to facilitate operation of the calendar and
27416 * datepicker.
27417 */
27418 angular.module('material.components.datepicker').factory('$$mdDateUtil', function() {
27419 return {
27420 getFirstDateOfMonth: getFirstDateOfMonth,
27421 getNumberOfDaysInMonth: getNumberOfDaysInMonth,
27422 getDateInNextMonth: getDateInNextMonth,
27423 getDateInPreviousMonth: getDateInPreviousMonth,
27424 isInNextMonth: isInNextMonth,
27425 isInPreviousMonth: isInPreviousMonth,
27426 getDateMidpoint: getDateMidpoint,
27427 isSameMonthAndYear: isSameMonthAndYear,
27428 getWeekOfMonth: getWeekOfMonth,
27429 incrementDays: incrementDays,
27430 incrementMonths: incrementMonths,
27431 getLastDateOfMonth: getLastDateOfMonth,
27432 isSameDay: isSameDay,
27433 getMonthDistance: getMonthDistance,
27434 isValidDate: isValidDate,
27435 setDateTimeToMidnight: setDateTimeToMidnight,
27436 createDateAtMidnight: createDateAtMidnight,
27437 isDateWithinRange: isDateWithinRange,
27438 incrementYears: incrementYears,
27439 getYearDistance: getYearDistance,
27440 clampDate: clampDate,
27441 getTimestampFromNode: getTimestampFromNode,
27442 isMonthWithinRange: isMonthWithinRange
27443 };
27444
27445 /**
27446 * Gets the first day of the month for the given date's month.
27447 * @param {Date} date
27448 * @returns {Date}
27449 */
27450 function getFirstDateOfMonth(date) {
27451 return new Date(date.getFullYear(), date.getMonth(), 1);
27452 }
27453
27454 /**
27455 * Gets the number of days in the month for the given date's month.
27456 * @param date
27457 * @returns {number}
27458 */
27459 function getNumberOfDaysInMonth(date) {
27460 return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate();
27461 }
27462
27463 /**
27464 * Get an arbitrary date in the month after the given date's month.
27465 * @param date
27466 * @returns {Date}
27467 */
27468 function getDateInNextMonth(date) {
27469 return new Date(date.getFullYear(), date.getMonth() + 1, 1);
27470 }
27471
27472 /**
27473 * Get an arbitrary date in the month before the given date's month.
27474 * @param date
27475 * @returns {Date}
27476 */
27477 function getDateInPreviousMonth(date) {
27478 return new Date(date.getFullYear(), date.getMonth() - 1, 1);
27479 }
27480
27481 /**
27482 * Gets whether two dates have the same month and year.
27483 * @param {Date} d1
27484 * @param {Date} d2
27485 * @returns {boolean}
27486 */
27487 function isSameMonthAndYear(d1, d2) {
27488 return d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth();
27489 }
27490
27491 /**
27492 * Gets whether two dates are the same day (not not necesarily the same time).
27493 * @param {Date} d1
27494 * @param {Date} d2
27495 * @returns {boolean}
27496 */
27497 function isSameDay(d1, d2) {
27498 return d1.getDate() == d2.getDate() && isSameMonthAndYear(d1, d2);
27499 }
27500
27501 /**
27502 * Gets whether a date is in the month immediately after some date.
27503 * @param {Date} startDate The date from which to compare.
27504 * @param {Date} endDate The date to check.
27505 * @returns {boolean}
27506 */
27507 function isInNextMonth(startDate, endDate) {
27508 var nextMonth = getDateInNextMonth(startDate);
27509 return isSameMonthAndYear(nextMonth, endDate);
27510 }
27511
27512 /**
27513 * Gets whether a date is in the month immediately before some date.
27514 * @param {Date} startDate The date from which to compare.
27515 * @param {Date} endDate The date to check.
27516 * @returns {boolean}
27517 */
27518 function isInPreviousMonth(startDate, endDate) {
27519 var previousMonth = getDateInPreviousMonth(startDate);
27520 return isSameMonthAndYear(endDate, previousMonth);
27521 }
27522
27523 /**
27524 * Gets the midpoint between two dates.
27525 * @param {Date} d1
27526 * @param {Date} d2
27527 * @returns {Date}
27528 */
27529 function getDateMidpoint(d1, d2) {
27530 return createDateAtMidnight((d1.getTime() + d2.getTime()) / 2);
27531 }
27532
27533 /**
27534 * Gets the week of the month that a given date occurs in.
27535 * @param {Date} date
27536 * @returns {number} Index of the week of the month (zero-based).
27537 */
27538 function getWeekOfMonth(date) {
27539 var firstDayOfMonth = getFirstDateOfMonth(date);
27540 return Math.floor((firstDayOfMonth.getDay() + date.getDate() - 1) / 7);
27541 }
27542
27543 /**
27544 * Gets a new date incremented by the given number of days. Number of days can be negative.
27545 * @param {Date} date
27546 * @param {number} numberOfDays
27547 * @returns {Date}
27548 */
27549 function incrementDays(date, numberOfDays) {
27550 return new Date(date.getFullYear(), date.getMonth(), date.getDate() + numberOfDays);
27551 }
27552
27553 /**
27554 * Gets a new date incremented by the given number of months. Number of months can be negative.
27555 * If the date of the given month does not match the target month, the date will be set to the
27556 * last day of the month.
27557 * @param {Date} date
27558 * @param {number} numberOfMonths
27559 * @returns {Date}
27560 */
27561 function incrementMonths(date, numberOfMonths) {
27562 // If the same date in the target month does not actually exist, the Date object will
27563 // automatically advance *another* month by the number of missing days.
27564 // For example, if you try to go from Jan. 30 to Feb. 30, you'll end up on March 2.
27565 // So, we check if the month overflowed and go to the last day of the target month instead.
27566 var dateInTargetMonth = new Date(date.getFullYear(), date.getMonth() + numberOfMonths, 1);
27567 var numberOfDaysInMonth = getNumberOfDaysInMonth(dateInTargetMonth);
27568 if (numberOfDaysInMonth < date.getDate()) {
27569 dateInTargetMonth.setDate(numberOfDaysInMonth);
27570 } else {
27571 dateInTargetMonth.setDate(date.getDate());
27572 }
27573
27574 return dateInTargetMonth;
27575 }
27576
27577 /**
27578 * Get the integer distance between two months. This *only* considers the month and year
27579 * portion of the Date instances.
27580 *
27581 * @param {Date} start
27582 * @param {Date} end
27583 * @returns {number} Number of months between `start` and `end`. If `end` is before `start`
27584 * chronologically, this number will be negative.
27585 */
27586 function getMonthDistance(start, end) {
27587 return (12 * (end.getFullYear() - start.getFullYear())) + (end.getMonth() - start.getMonth());
27588 }
27589
27590 /**
27591 * Gets the last day of the month for the given date.
27592 * @param {Date} date
27593 * @returns {Date}
27594 */
27595 function getLastDateOfMonth(date) {
27596 return new Date(date.getFullYear(), date.getMonth(), getNumberOfDaysInMonth(date));
27597 }
27598
27599 /**
27600 * Checks whether a date is valid.
27601 * @param {Date} date
27602 * @return {boolean} Whether the date is a valid Date.
27603 */
27604 function isValidDate(date) {
27605 return date != null && date.getTime && !isNaN(date.getTime());
27606 }
27607
27608 /**
27609 * Sets a date's time to midnight.
27610 * @param {Date} date
27611 */
27612 function setDateTimeToMidnight(date) {
27613 if (isValidDate(date)) {
27614 date.setHours(0, 0, 0, 0);
27615 }
27616 }
27617
27618 /**
27619 * Creates a date with the time set to midnight.
27620 * Drop-in replacement for two forms of the Date constructor:
27621 * 1. No argument for Date representing now.
27622 * 2. Single-argument value representing number of seconds since Unix Epoch
27623 * or a Date object.
27624 * @param {number|Date=} opt_value
27625 * @return {Date} New date with time set to midnight.
27626 */
27627 function createDateAtMidnight(opt_value) {
27628 var date;
27629 if (angular.isUndefined(opt_value)) {
27630 date = new Date();
27631 } else {
27632 date = new Date(opt_value);
27633 }
27634 setDateTimeToMidnight(date);
27635 return date;
27636 }
27637
27638 /**
27639 * Checks if a date is within a min and max range, ignoring the time component.
27640 * If minDate or maxDate are not dates, they are ignored.
27641 * @param {Date} date
27642 * @param {Date} minDate
27643 * @param {Date} maxDate
27644 */
27645 function isDateWithinRange(date, minDate, maxDate) {
27646 var dateAtMidnight = createDateAtMidnight(date);
27647 var minDateAtMidnight = isValidDate(minDate) ? createDateAtMidnight(minDate) : null;
27648 var maxDateAtMidnight = isValidDate(maxDate) ? createDateAtMidnight(maxDate) : null;
27649 return (!minDateAtMidnight || minDateAtMidnight <= dateAtMidnight) &&
27650 (!maxDateAtMidnight || maxDateAtMidnight >= dateAtMidnight);
27651 }
27652
27653 /**
27654 * Gets a new date incremented by the given number of years. Number of years can be negative.
27655 * See `incrementMonths` for notes on overflow for specific dates.
27656 * @param {Date} date
27657 * @param {number} numberOfYears
27658 * @returns {Date}
27659 */
27660 function incrementYears(date, numberOfYears) {
27661 return incrementMonths(date, numberOfYears * 12);
27662 }
27663
27664 /**
27665 * Get the integer distance between two years. This *only* considers the year portion of the
27666 * Date instances.
27667 *
27668 * @param {Date} start
27669 * @param {Date} end
27670 * @returns {number} Number of months between `start` and `end`. If `end` is before `start`
27671 * chronologically, this number will be negative.
27672 */
27673 function getYearDistance(start, end) {
27674 return end.getFullYear() - start.getFullYear();
27675 }
27676
27677 /**
27678 * Clamps a date between a minimum and a maximum date.
27679 * @param {Date} date Date to be clamped
27680 * @param {Date=} minDate Minimum date
27681 * @param {Date=} maxDate Maximum date
27682 * @return {Date}
27683 */
27684 function clampDate(date, minDate, maxDate) {
27685 var boundDate = date;
27686 if (minDate && date < minDate) {
27687 boundDate = new Date(minDate.getTime());
27688 }
27689 if (maxDate && date > maxDate) {
27690 boundDate = new Date(maxDate.getTime());
27691 }
27692 return boundDate;
27693 }
27694
27695 /**
27696 * Extracts and parses the timestamp from a DOM node.
27697 * @param {HTMLElement} node Node from which the timestamp will be extracted.
27698 * @return {number} Time since epoch.
27699 */
27700 function getTimestampFromNode(node) {
27701 if (node && node.hasAttribute('data-timestamp')) {
27702 return Number(node.getAttribute('data-timestamp'));
27703 }
27704 }
27705
27706 /**
27707 * Checks if a month is within a min and max range, ignoring the date and time components.
27708 * If minDate or maxDate are not dates, they are ignored.
27709 * @param {Date} date
27710 * @param {Date} minDate
27711 * @param {Date} maxDate
27712 */
27713 function isMonthWithinRange(date, minDate, maxDate) {
27714 var month = date.getMonth();
27715 var year = date.getFullYear();
27716
27717 return (!minDate || minDate.getFullYear() < year || minDate.getMonth() <= month) &&
27718 (!maxDate || maxDate.getFullYear() > year || maxDate.getMonth() >= month);
27719 }
27720 });
27721})();
27722
27723})();
27724(function(){
27725"use strict";
27726
27727(function() {
27728 'use strict';
27729
27730 // POST RELEASE
27731 // TODO(jelbourn): Demo that uses moment.js
27732 // TODO(jelbourn): make sure this plays well with validation and ngMessages.
27733 // TODO(jelbourn): calendar pane doesn't open up outside of visible viewport.
27734 // TODO(jelbourn): forward more attributes to the internal input (required, autofocus, etc.)
27735 // TODO(jelbourn): something better for mobile (calendar panel takes up entire screen?)
27736 // TODO(jelbourn): input behavior (masking? auto-complete?)
27737
27738
27739 DatePickerCtrl.$inject = ["$scope", "$element", "$attrs", "$window", "$mdConstant", "$mdTheming", "$mdUtil", "$mdDateLocale", "$$mdDateUtil", "$$rAF", "$mdGesture", "$filter"];
27740 datePickerDirective.$inject = ["$$mdSvgRegistry", "$mdUtil", "$mdAria", "inputDirective"];
27741 angular.module('material.components.datepicker')
27742 .directive('mdDatepicker', datePickerDirective);
27743
27744 /**
27745 * @ngdoc directive
27746 * @name mdDatepicker
27747 * @module material.components.datepicker
27748 *
27749 * @param {Date} ng-model The component's model. Expects a JavaScript Date object.
27750 * @param {Object=} ng-model-options Allows tuning of the way in which `ng-model` is being updated. Also allows
27751 * for a timezone to be specified. <a href="https://docs.angularjs.org/api/ng/directive/ngModelOptions#usage">Read more at the ngModelOptions docs.</a>
27752 * @param {expression=} ng-change Expression evaluated when the model value changes.
27753 * @param {expression=} ng-focus Expression evaluated when the input is focused or the calendar is opened.
27754 * @param {expression=} ng-blur Expression evaluated when focus is removed from the input or the calendar is closed.
27755 * @param {Date=} md-min-date Expression representing a min date (inclusive).
27756 * @param {Date=} md-max-date Expression representing a max date (inclusive).
27757 * @param {(function(Date): boolean)=} md-date-filter Function expecting a date and returning a boolean whether it can be selected or not.
27758 * @param {String=} md-placeholder The date input placeholder value.
27759 * @param {String=} md-open-on-focus When present, the calendar will be opened when the input is focused.
27760 * @param {Boolean=} md-is-open Expression that can be used to open the datepicker's calendar on-demand.
27761 * @param {String=} md-current-view Default open view of the calendar pane. Can be either "month" or "year".
27762 * @param {String=} md-hide-icons Determines which datepicker icons should be hidden. Note that this may cause the
27763 * datepicker to not align properly with other components. **Use at your own risk.** Possible values are:
27764 * * `"all"` - Hides all icons.
27765 * * `"calendar"` - Only hides the calendar icon.
27766 * * `"triangle"` - Only hides the triangle icon.
27767 * @param {boolean=} ng-disabled Whether the datepicker is disabled.
27768 * @param {boolean=} ng-required Whether a value is required for the datepicker.
27769 *
27770 * @description
27771 * `<md-datepicker>` is a component used to select a single date.
27772 * For information on how to configure internationalization for the date picker,
27773 * see `$mdDateLocaleProvider`.
27774 *
27775 * This component supports [ngMessages](https://docs.angularjs.org/api/ngMessages/directive/ngMessages).
27776 * Supported attributes are:
27777 * * `required`: whether a required date is not set.
27778 * * `mindate`: whether the selected date is before the minimum allowed date.
27779 * * `maxdate`: whether the selected date is after the maximum allowed date.
27780 * * `debounceInterval`: ms to delay input processing (since last debounce reset); default value 500ms
27781 *
27782 * @usage
27783 * <hljs lang="html">
27784 * <md-datepicker ng-model="birthday"></md-datepicker>
27785 * </hljs>
27786 *
27787 */
27788
27789 function datePickerDirective($$mdSvgRegistry, $mdUtil, $mdAria, inputDirective) {
27790 return {
27791 template: function(tElement, tAttrs) {
27792 // Buttons are not in the tab order because users can open the calendar via keyboard
27793 // interaction on the text input, and multiple tab stops for one component (picker)
27794 // may be confusing.
27795 var hiddenIcons = tAttrs.mdHideIcons;
27796 var ariaLabelValue = tAttrs.ariaLabel || tAttrs.mdPlaceholder;
27797
27798 var calendarButton = (hiddenIcons === 'all' || hiddenIcons === 'calendar') ? '' :
27799 '<md-button class="md-datepicker-button md-icon-button" type="button" ' +
27800 'tabindex="-1" aria-hidden="true" ' +
27801 'ng-click="ctrl.openCalendarPane($event)">' +
27802 '<md-icon class="md-datepicker-calendar-icon" aria-label="md-calendar" ' +
27803 'md-svg-src="' + $$mdSvgRegistry.mdCalendar + '"></md-icon>' +
27804 '</md-button>';
27805
27806 var triangleButton = (hiddenIcons === 'all' || hiddenIcons === 'triangle') ? '' :
27807 '<md-button type="button" md-no-ink ' +
27808 'class="md-datepicker-triangle-button md-icon-button" ' +
27809 'ng-click="ctrl.openCalendarPane($event)" ' +
27810 'aria-label="{{::ctrl.dateLocale.msgOpenCalendar}}">' +
27811 '<div class="md-datepicker-expand-triangle"></div>' +
27812 '</md-button>';
27813
27814 return calendarButton +
27815 '<div class="md-datepicker-input-container" ng-class="{\'md-datepicker-focused\': ctrl.isFocused}">' +
27816 '<input ' +
27817 (ariaLabelValue ? 'aria-label="' + ariaLabelValue + '" ' : '') +
27818 'class="md-datepicker-input" ' +
27819 'aria-haspopup="true" ' +
27820 'ng-focus="ctrl.setFocused(true)" ' +
27821 'ng-blur="ctrl.setFocused(false)"> ' +
27822 triangleButton +
27823 '</div>' +
27824
27825 // This pane will be detached from here and re-attached to the document body.
27826 '<div class="md-datepicker-calendar-pane md-whiteframe-z1">' +
27827 '<div class="md-datepicker-input-mask">' +
27828 '<div class="md-datepicker-input-mask-opaque"></div>' +
27829 '</div>' +
27830 '<div class="md-datepicker-calendar">' +
27831 '<md-calendar role="dialog" aria-label="{{::ctrl.dateLocale.msgCalendar}}" ' +
27832 'md-current-view="{{::ctrl.currentView}}"' +
27833 'md-min-date="ctrl.minDate"' +
27834 'md-max-date="ctrl.maxDate"' +
27835 'md-date-filter="ctrl.dateFilter"' +
27836 'ng-model="ctrl.date" ng-if="ctrl.isCalendarOpen">' +
27837 '</md-calendar>' +
27838 '</div>' +
27839 '</div>';
27840 },
27841 require: ['ngModel', 'mdDatepicker', '?^mdInputContainer', '?^form'],
27842 scope: {
27843 minDate: '=mdMinDate',
27844 maxDate: '=mdMaxDate',
27845 placeholder: '@mdPlaceholder',
27846 currentView: '@mdCurrentView',
27847 dateFilter: '=mdDateFilter',
27848 isOpen: '=?mdIsOpen',
27849 debounceInterval: '=mdDebounceInterval'
27850 },
27851 controller: DatePickerCtrl,
27852 controllerAs: 'ctrl',
27853 bindToController: true,
27854 link: function(scope, element, attr, controllers) {
27855 var ngModelCtrl = controllers[0];
27856 var mdDatePickerCtrl = controllers[1];
27857 var mdInputContainer = controllers[2];
27858 var parentForm = controllers[3];
27859 var mdNoAsterisk = $mdUtil.parseAttributeBoolean(attr.mdNoAsterisk);
27860
27861 mdDatePickerCtrl.configureNgModel(ngModelCtrl, mdInputContainer, inputDirective);
27862
27863 if (mdInputContainer) {
27864 // We need to move the spacer after the datepicker itself,
27865 // because md-input-container adds it after the
27866 // md-datepicker-input by default. The spacer gets wrapped in a
27867 // div, because it floats and gets aligned next to the datepicker.
27868 // There are easier ways of working around this with CSS (making the
27869 // datepicker 100% wide, change the `display` etc.), however they
27870 // break the alignment with any other form controls.
27871 var spacer = element[0].querySelector('.md-errors-spacer');
27872
27873 if (spacer) {
27874 element.after(angular.element('<div>').append(spacer));
27875 }
27876
27877 mdInputContainer.setHasPlaceholder(attr.mdPlaceholder);
27878 mdInputContainer.input = element;
27879 mdInputContainer.element
27880 .addClass(INPUT_CONTAINER_CLASS)
27881 .toggleClass(HAS_ICON_CLASS, attr.mdHideIcons !== 'calendar' && attr.mdHideIcons !== 'all');
27882
27883 if (!mdInputContainer.label) {
27884 $mdAria.expect(element, 'aria-label', attr.mdPlaceholder);
27885 } else if(!mdNoAsterisk) {
27886 attr.$observe('required', function(value) {
27887 mdInputContainer.label.toggleClass('md-required', !!value);
27888 });
27889 }
27890
27891 scope.$watch(mdInputContainer.isErrorGetter || function() {
27892 return ngModelCtrl.$invalid && (ngModelCtrl.$touched || (parentForm && parentForm.$submitted));
27893 }, mdInputContainer.setInvalid);
27894 } else if (parentForm) {
27895 // If invalid, highlights the input when the parent form is submitted.
27896 var parentSubmittedWatcher = scope.$watch(function() {
27897 return parentForm.$submitted;
27898 }, function(isSubmitted) {
27899 if (isSubmitted) {
27900 mdDatePickerCtrl.updateErrorState();
27901 parentSubmittedWatcher();
27902 }
27903 });
27904 }
27905 }
27906 };
27907 }
27908
27909 /** Additional offset for the input's `size` attribute, which is updated based on its content. */
27910 var EXTRA_INPUT_SIZE = 3;
27911
27912 /** Class applied to the container if the date is invalid. */
27913 var INVALID_CLASS = 'md-datepicker-invalid';
27914
27915 /** Class applied to the datepicker when it's open. */
27916 var OPEN_CLASS = 'md-datepicker-open';
27917
27918 /** Class applied to the md-input-container, if a datepicker is placed inside it */
27919 var INPUT_CONTAINER_CLASS = '_md-datepicker-floating-label';
27920
27921 /** Class to be applied when the calendar icon is enabled. */
27922 var HAS_ICON_CLASS = '_md-datepicker-has-calendar-icon';
27923
27924 /** Default time in ms to debounce input event by. */
27925 var DEFAULT_DEBOUNCE_INTERVAL = 500;
27926
27927 /**
27928 * Height of the calendar pane used to check if the pane is going outside the boundary of
27929 * the viewport. See calendar.scss for how $md-calendar-height is computed; an extra 20px is
27930 * also added to space the pane away from the exact edge of the screen.
27931 *
27932 * This is computed statically now, but can be changed to be measured if the circumstances
27933 * of calendar sizing are changed.
27934 */
27935 var CALENDAR_PANE_HEIGHT = 368;
27936
27937 /**
27938 * Width of the calendar pane used to check if the pane is going outside the boundary of
27939 * the viewport. See calendar.scss for how $md-calendar-width is computed; an extra 20px is
27940 * also added to space the pane away from the exact edge of the screen.
27941 *
27942 * This is computed statically now, but can be changed to be measured if the circumstances
27943 * of calendar sizing are changed.
27944 */
27945 var CALENDAR_PANE_WIDTH = 360;
27946
27947 /**
27948 * Controller for md-datepicker.
27949 *
27950 * @ngInject @constructor
27951 */
27952 function DatePickerCtrl($scope, $element, $attrs, $window, $mdConstant,
27953 $mdTheming, $mdUtil, $mdDateLocale, $$mdDateUtil, $$rAF, $mdGesture, $filter) {
27954
27955 /** @final */
27956 this.$window = $window;
27957
27958 /** @final */
27959 this.dateLocale = $mdDateLocale;
27960
27961 /** @final */
27962 this.dateUtil = $$mdDateUtil;
27963
27964 /** @final */
27965 this.$mdConstant = $mdConstant;
27966
27967 /* @final */
27968 this.$mdUtil = $mdUtil;
27969
27970 /** @final */
27971 this.$$rAF = $$rAF;
27972
27973 /**
27974 * The root document element. This is used for attaching a top-level click handler to
27975 * close the calendar panel when a click outside said panel occurs. We use `documentElement`
27976 * instead of body because, when scrolling is disabled, some browsers consider the body element
27977 * to be completely off the screen and propagate events directly to the html element.
27978 * @type {!angular.JQLite}
27979 */
27980 this.documentElement = angular.element(document.documentElement);
27981
27982 /** @type {!angular.NgModelController} */
27983 this.ngModelCtrl = null;
27984
27985 /** @type {HTMLInputElement} */
27986 this.inputElement = $element[0].querySelector('input');
27987
27988 /** @final {!angular.JQLite} */
27989 this.ngInputElement = angular.element(this.inputElement);
27990
27991 /** @type {HTMLElement} */
27992 this.inputContainer = $element[0].querySelector('.md-datepicker-input-container');
27993
27994 /** @type {HTMLElement} Floating calendar pane. */
27995 this.calendarPane = $element[0].querySelector('.md-datepicker-calendar-pane');
27996
27997 /** @type {HTMLElement} Calendar icon button. */
27998 this.calendarButton = $element[0].querySelector('.md-datepicker-button');
27999
28000 /**
28001 * Element covering everything but the input in the top of the floating calendar pane.
28002 * @type {!angular.JQLite}
28003 */
28004 this.inputMask = angular.element($element[0].querySelector('.md-datepicker-input-mask-opaque'));
28005
28006 /** @final {!angular.JQLite} */
28007 this.$element = $element;
28008
28009 /** @final {!angular.Attributes} */
28010 this.$attrs = $attrs;
28011
28012 /** @final {!angular.Scope} */
28013 this.$scope = $scope;
28014
28015 /** @type {Date} */
28016 this.date = null;
28017
28018 /** @type {boolean} */
28019 this.isFocused = false;
28020
28021 /** @type {boolean} */
28022 this.isDisabled;
28023 this.setDisabled($element[0].disabled || angular.isString($attrs.disabled));
28024
28025 /** @type {boolean} Whether the date-picker's calendar pane is open. */
28026 this.isCalendarOpen = false;
28027
28028 /** @type {boolean} Whether the calendar should open when the input is focused. */
28029 this.openOnFocus = $attrs.hasOwnProperty('mdOpenOnFocus');
28030
28031 /** @final */
28032 this.mdInputContainer = null;
28033
28034 /**
28035 * Element from which the calendar pane was opened. Keep track of this so that we can return
28036 * focus to it when the pane is closed.
28037 * @type {HTMLElement}
28038 */
28039 this.calendarPaneOpenedFrom = null;
28040
28041 /** @type {String} Unique id for the calendar pane. */
28042 this.calendarPane.id = 'md-date-pane' + $mdUtil.nextUid();
28043
28044 /** Pre-bound click handler is saved so that the event listener can be removed. */
28045 this.bodyClickHandler = angular.bind(this, this.handleBodyClick);
28046
28047 /**
28048 * Name of the event that will trigger a close. Necessary to sniff the browser, because
28049 * the resize event doesn't make sense on mobile and can have a negative impact since it
28050 * triggers whenever the browser zooms in on a focused input.
28051 */
28052 this.windowEventName = ($mdGesture.isIos || $mdGesture.isAndroid) ? 'orientationchange' : 'resize';
28053
28054 /** Pre-bound close handler so that the event listener can be removed. */
28055 this.windowEventHandler = $mdUtil.debounce(angular.bind(this, this.closeCalendarPane), 100);
28056
28057 /** Pre-bound handler for the window blur event. Allows for it to be removed later. */
28058 this.windowBlurHandler = angular.bind(this, this.handleWindowBlur);
28059
28060 /** The built-in Angular date filter. */
28061 this.ngDateFilter = $filter('date');
28062
28063 /** @type {Number} Extra margin for the left side of the floating calendar pane. */
28064 this.leftMargin = 20;
28065
28066 /** @type {Number} Extra margin for the top of the floating calendar. Gets determined on the first open. */
28067 this.topMargin = null;
28068
28069 // Unless the user specifies so, the datepicker should not be a tab stop.
28070 // This is necessary because ngAria might add a tabindex to anything with an ng-model
28071 // (based on whether or not the user has turned that particular feature on/off).
28072 if ($attrs.tabindex) {
28073 this.ngInputElement.attr('tabindex', $attrs.tabindex);
28074 $attrs.$set('tabindex', null);
28075 } else {
28076 $attrs.$set('tabindex', '-1');
28077 }
28078
28079 $mdTheming($element);
28080 $mdTheming(angular.element(this.calendarPane));
28081
28082 this.installPropertyInterceptors();
28083 this.attachChangeListeners();
28084 this.attachInteractionListeners();
28085
28086 var self = this;
28087
28088 $scope.$on('$destroy', function() {
28089 self.detachCalendarPane();
28090 });
28091
28092 if ($attrs.mdIsOpen) {
28093 $scope.$watch('ctrl.isOpen', function(shouldBeOpen) {
28094 if (shouldBeOpen) {
28095 self.openCalendarPane({
28096 target: self.inputElement
28097 });
28098 } else {
28099 self.closeCalendarPane();
28100 }
28101 });
28102 }
28103 }
28104
28105 /**
28106 * Sets up the controller's reference to ngModelController and
28107 * applies Angular's `input[type="date"]` directive.
28108 * @param {!angular.NgModelController} ngModelCtrl Instance of the ngModel controller.
28109 * @param {Object} mdInputContainer Instance of the mdInputContainer controller.
28110 * @param {Object} inputDirective Config for Angular's `input` directive.
28111 */
28112 DatePickerCtrl.prototype.configureNgModel = function(ngModelCtrl, mdInputContainer, inputDirective) {
28113 this.ngModelCtrl = ngModelCtrl;
28114 this.mdInputContainer = mdInputContainer;
28115
28116 // The input needs to be [type="date"] in order to be picked up by Angular.
28117 this.$attrs.$set('type', 'date');
28118
28119 // Invoke the `input` directive link function, adding a stub for the element.
28120 // This allows us to re-use Angular's logic for setting the timezone via ng-model-options.
28121 // It works by calling the link function directly which then adds the proper `$parsers` and
28122 // `$formatters` to the ngModel controller.
28123 inputDirective[0].link.pre(this.$scope, {
28124 on: angular.noop,
28125 val: angular.noop,
28126 0: {}
28127 }, this.$attrs, [ngModelCtrl]);
28128
28129 var self = this;
28130
28131 // Responds to external changes to the model value.
28132 self.ngModelCtrl.$formatters.push(function(value) {
28133 if (value && !(value instanceof Date)) {
28134 throw Error('The ng-model for md-datepicker must be a Date instance. ' +
28135 'Currently the model is a: ' + (typeof value));
28136 }
28137
28138 self.date = value;
28139 self.inputElement.value = self.dateLocale.formatDate(value);
28140 self.mdInputContainer && self.mdInputContainer.setHasValue(!!value);
28141 self.resizeInputElement();
28142 self.updateErrorState();
28143
28144 return value;
28145 });
28146
28147 // Responds to external error state changes (e.g. ng-required based on another input).
28148 ngModelCtrl.$viewChangeListeners.unshift(angular.bind(this, this.updateErrorState));
28149 };
28150
28151 /**
28152 * Attach event listeners for both the text input and the md-calendar.
28153 * Events are used instead of ng-model so that updates don't infinitely update the other
28154 * on a change. This should also be more performant than using a $watch.
28155 */
28156 DatePickerCtrl.prototype.attachChangeListeners = function() {
28157 var self = this;
28158
28159 self.$scope.$on('md-calendar-change', function(event, date) {
28160 self.setModelValue(date);
28161 self.date = date;
28162 self.inputElement.value = self.dateLocale.formatDate(date);
28163 self.mdInputContainer && self.mdInputContainer.setHasValue(!!date);
28164 self.closeCalendarPane();
28165 self.resizeInputElement();
28166 self.updateErrorState();
28167 });
28168
28169 self.ngInputElement.on('input', angular.bind(self, self.resizeInputElement));
28170
28171 var debounceInterval = angular.isDefined(this.debounceInterval) ?
28172 this.debounceInterval : DEFAULT_DEBOUNCE_INTERVAL;
28173 self.ngInputElement.on('input', self.$mdUtil.debounce(self.handleInputEvent,
28174 debounceInterval, self));
28175 };
28176
28177 /** Attach event listeners for user interaction. */
28178 DatePickerCtrl.prototype.attachInteractionListeners = function() {
28179 var self = this;
28180 var $scope = this.$scope;
28181 var keyCodes = this.$mdConstant.KEY_CODE;
28182
28183 // Add event listener through angular so that we can triggerHandler in unit tests.
28184 self.ngInputElement.on('keydown', function(event) {
28185 if (event.altKey && event.keyCode == keyCodes.DOWN_ARROW) {
28186 self.openCalendarPane(event);
28187 $scope.$digest();
28188 }
28189 });
28190
28191 if (self.openOnFocus) {
28192 self.ngInputElement.on('focus', angular.bind(self, self.openCalendarPane));
28193 angular.element(self.$window).on('blur', self.windowBlurHandler);
28194
28195 $scope.$on('$destroy', function() {
28196 angular.element(self.$window).off('blur', self.windowBlurHandler);
28197 });
28198 }
28199
28200 $scope.$on('md-calendar-close', function() {
28201 self.closeCalendarPane();
28202 });
28203 };
28204
28205 /**
28206 * Capture properties set to the date-picker and imperitively handle internal changes.
28207 * This is done to avoid setting up additional $watches.
28208 */
28209 DatePickerCtrl.prototype.installPropertyInterceptors = function() {
28210 var self = this;
28211
28212 if (this.$attrs.ngDisabled) {
28213 // The expression is to be evaluated against the directive element's scope and not
28214 // the directive's isolate scope.
28215 var scope = this.$scope.$parent;
28216
28217 if (scope) {
28218 scope.$watch(this.$attrs.ngDisabled, function(isDisabled) {
28219 self.setDisabled(isDisabled);
28220 });
28221 }
28222 }
28223
28224 Object.defineProperty(this, 'placeholder', {
28225 get: function() { return self.inputElement.placeholder; },
28226 set: function(value) { self.inputElement.placeholder = value || ''; }
28227 });
28228 };
28229
28230 /**
28231 * Sets whether the date-picker is disabled.
28232 * @param {boolean} isDisabled
28233 */
28234 DatePickerCtrl.prototype.setDisabled = function(isDisabled) {
28235 this.isDisabled = isDisabled;
28236 this.inputElement.disabled = isDisabled;
28237
28238 if (this.calendarButton) {
28239 this.calendarButton.disabled = isDisabled;
28240 }
28241 };
28242
28243 /**
28244 * Sets the custom ngModel.$error flags to be consumed by ngMessages. Flags are:
28245 * - mindate: whether the selected date is before the minimum date.
28246 * - maxdate: whether the selected flag is after the maximum date.
28247 * - filtered: whether the selected date is allowed by the custom filtering function.
28248 * - valid: whether the entered text input is a valid date
28249 *
28250 * The 'required' flag is handled automatically by ngModel.
28251 *
28252 * @param {Date=} opt_date Date to check. If not given, defaults to the datepicker's model value.
28253 */
28254 DatePickerCtrl.prototype.updateErrorState = function(opt_date) {
28255 var date = opt_date || this.date;
28256
28257 // Clear any existing errors to get rid of anything that's no longer relevant.
28258 this.clearErrorState();
28259
28260 if (this.dateUtil.isValidDate(date)) {
28261 // Force all dates to midnight in order to ignore the time portion.
28262 date = this.dateUtil.createDateAtMidnight(date);
28263
28264 if (this.dateUtil.isValidDate(this.minDate)) {
28265 var minDate = this.dateUtil.createDateAtMidnight(this.minDate);
28266 this.ngModelCtrl.$setValidity('mindate', date >= minDate);
28267 }
28268
28269 if (this.dateUtil.isValidDate(this.maxDate)) {
28270 var maxDate = this.dateUtil.createDateAtMidnight(this.maxDate);
28271 this.ngModelCtrl.$setValidity('maxdate', date <= maxDate);
28272 }
28273
28274 if (angular.isFunction(this.dateFilter)) {
28275 this.ngModelCtrl.$setValidity('filtered', this.dateFilter(date));
28276 }
28277 } else {
28278 // The date is seen as "not a valid date" if there is *something* set
28279 // (i.e.., not null or undefined), but that something isn't a valid date.
28280 this.ngModelCtrl.$setValidity('valid', date == null);
28281 }
28282
28283 // TODO(jelbourn): Change this to classList.toggle when we stop using PhantomJS in unit tests
28284 // because it doesn't conform to the DOMTokenList spec.
28285 // See https://github.com/ariya/phantomjs/issues/12782.
28286 if (!this.ngModelCtrl.$valid) {
28287 this.inputContainer.classList.add(INVALID_CLASS);
28288 }
28289 };
28290
28291 /** Clears any error flags set by `updateErrorState`. */
28292 DatePickerCtrl.prototype.clearErrorState = function() {
28293 this.inputContainer.classList.remove(INVALID_CLASS);
28294 ['mindate', 'maxdate', 'filtered', 'valid'].forEach(function(field) {
28295 this.ngModelCtrl.$setValidity(field, true);
28296 }, this);
28297 };
28298
28299 /** Resizes the input element based on the size of its content. */
28300 DatePickerCtrl.prototype.resizeInputElement = function() {
28301 this.inputElement.size = this.inputElement.value.length + EXTRA_INPUT_SIZE;
28302 };
28303
28304 /**
28305 * Sets the model value if the user input is a valid date.
28306 * Adds an invalid class to the input element if not.
28307 */
28308 DatePickerCtrl.prototype.handleInputEvent = function() {
28309 var inputString = this.inputElement.value;
28310 var parsedDate = inputString ? this.dateLocale.parseDate(inputString) : null;
28311 this.dateUtil.setDateTimeToMidnight(parsedDate);
28312
28313 // An input string is valid if it is either empty (representing no date)
28314 // or if it parses to a valid date that the user is allowed to select.
28315 var isValidInput = inputString == '' || (
28316 this.dateUtil.isValidDate(parsedDate) &&
28317 this.dateLocale.isDateComplete(inputString) &&
28318 this.isDateEnabled(parsedDate)
28319 );
28320
28321 // The datepicker's model is only updated when there is a valid input.
28322 if (isValidInput) {
28323 this.setModelValue(parsedDate);
28324 this.date = parsedDate;
28325 }
28326
28327 this.updateErrorState(parsedDate);
28328 };
28329
28330 /**
28331 * Check whether date is in range and enabled
28332 * @param {Date=} opt_date
28333 * @return {boolean} Whether the date is enabled.
28334 */
28335 DatePickerCtrl.prototype.isDateEnabled = function(opt_date) {
28336 return this.dateUtil.isDateWithinRange(opt_date, this.minDate, this.maxDate) &&
28337 (!angular.isFunction(this.dateFilter) || this.dateFilter(opt_date));
28338 };
28339
28340 /** Position and attach the floating calendar to the document. */
28341 DatePickerCtrl.prototype.attachCalendarPane = function() {
28342 var calendarPane = this.calendarPane;
28343 var body = document.body;
28344
28345 calendarPane.style.transform = '';
28346 this.$element.addClass(OPEN_CLASS);
28347 this.mdInputContainer && this.mdInputContainer.element.addClass(OPEN_CLASS);
28348 angular.element(body).addClass('md-datepicker-is-showing');
28349
28350 var elementRect = this.inputContainer.getBoundingClientRect();
28351 var bodyRect = body.getBoundingClientRect();
28352
28353 if (!this.topMargin || this.topMargin < 0) {
28354 this.topMargin = (this.inputMask.parent().prop('clientHeight') - this.ngInputElement.prop('clientHeight')) / 2;
28355 }
28356
28357 // Check to see if the calendar pane would go off the screen. If so, adjust position
28358 // accordingly to keep it within the viewport.
28359 var paneTop = elementRect.top - bodyRect.top - this.topMargin;
28360 var paneLeft = elementRect.left - bodyRect.left - this.leftMargin;
28361
28362 // If ng-material has disabled body scrolling (for example, if a dialog is open),
28363 // then it's possible that the already-scrolled body has a negative top/left. In this case,
28364 // we want to treat the "real" top as (0 - bodyRect.top). In a normal scrolling situation,
28365 // though, the top of the viewport should just be the body's scroll position.
28366 var viewportTop = (bodyRect.top < 0 && document.body.scrollTop == 0) ?
28367 -bodyRect.top :
28368 document.body.scrollTop;
28369
28370 var viewportLeft = (bodyRect.left < 0 && document.body.scrollLeft == 0) ?
28371 -bodyRect.left :
28372 document.body.scrollLeft;
28373
28374 var viewportBottom = viewportTop + this.$window.innerHeight;
28375 var viewportRight = viewportLeft + this.$window.innerWidth;
28376
28377 // Creates an overlay with a hole the same size as element. We remove a pixel or two
28378 // on each end to make it overlap slightly. The overlay's background is added in
28379 // the theme in the form of a box-shadow with a huge spread.
28380 this.inputMask.css({
28381 position: 'absolute',
28382 left: this.leftMargin + 'px',
28383 top: this.topMargin + 'px',
28384 width: (elementRect.width - 1) + 'px',
28385 height: (elementRect.height - 2) + 'px'
28386 });
28387
28388 // If the right edge of the pane would be off the screen and shifting it left by the
28389 // difference would not go past the left edge of the screen. If the calendar pane is too
28390 // big to fit on the screen at all, move it to the left of the screen and scale the entire
28391 // element down to fit.
28392 if (paneLeft + CALENDAR_PANE_WIDTH > viewportRight) {
28393 if (viewportRight - CALENDAR_PANE_WIDTH > 0) {
28394 paneLeft = viewportRight - CALENDAR_PANE_WIDTH;
28395 } else {
28396 paneLeft = viewportLeft;
28397 var scale = this.$window.innerWidth / CALENDAR_PANE_WIDTH;
28398 calendarPane.style.transform = 'scale(' + scale + ')';
28399 }
28400
28401 calendarPane.classList.add('md-datepicker-pos-adjusted');
28402 }
28403
28404 // If the bottom edge of the pane would be off the screen and shifting it up by the
28405 // difference would not go past the top edge of the screen.
28406 if (paneTop + CALENDAR_PANE_HEIGHT > viewportBottom &&
28407 viewportBottom - CALENDAR_PANE_HEIGHT > viewportTop) {
28408 paneTop = viewportBottom - CALENDAR_PANE_HEIGHT;
28409 calendarPane.classList.add('md-datepicker-pos-adjusted');
28410 }
28411
28412 calendarPane.style.left = paneLeft + 'px';
28413 calendarPane.style.top = paneTop + 'px';
28414 document.body.appendChild(calendarPane);
28415
28416 // Add CSS class after one frame to trigger open animation.
28417 this.$$rAF(function() {
28418 calendarPane.classList.add('md-pane-open');
28419 });
28420 };
28421
28422 /** Detach the floating calendar pane from the document. */
28423 DatePickerCtrl.prototype.detachCalendarPane = function() {
28424 this.$element.removeClass(OPEN_CLASS);
28425 this.mdInputContainer && this.mdInputContainer.element.removeClass(OPEN_CLASS);
28426 angular.element(document.body).removeClass('md-datepicker-is-showing');
28427 this.calendarPane.classList.remove('md-pane-open');
28428 this.calendarPane.classList.remove('md-datepicker-pos-adjusted');
28429
28430 if (this.isCalendarOpen) {
28431 this.$mdUtil.enableScrolling();
28432 }
28433
28434 if (this.calendarPane.parentNode) {
28435 // Use native DOM removal because we do not want any of the
28436 // angular state of this element to be disposed.
28437 this.calendarPane.parentNode.removeChild(this.calendarPane);
28438 }
28439 };
28440
28441 /**
28442 * Open the floating calendar pane.
28443 * @param {Event} event
28444 */
28445 DatePickerCtrl.prototype.openCalendarPane = function(event) {
28446 if (!this.isCalendarOpen && !this.isDisabled && !this.inputFocusedOnWindowBlur) {
28447 this.isCalendarOpen = this.isOpen = true;
28448 this.calendarPaneOpenedFrom = event.target;
28449
28450 // Because the calendar pane is attached directly to the body, it is possible that the
28451 // rest of the component (input, etc) is in a different scrolling container, such as
28452 // an md-content. This means that, if the container is scrolled, the pane would remain
28453 // stationary. To remedy this, we disable scrolling while the calendar pane is open, which
28454 // also matches the native behavior for things like `<select>` on Mac and Windows.
28455 this.$mdUtil.disableScrollAround(this.calendarPane);
28456
28457 this.attachCalendarPane();
28458 this.focusCalendar();
28459 this.evalAttr('ngFocus');
28460
28461 // Attach click listener inside of a timeout because, if this open call was triggered by a
28462 // click, we don't want it to be immediately propogated up to the body and handled.
28463 var self = this;
28464 this.$mdUtil.nextTick(function() {
28465 // Use 'touchstart` in addition to click in order to work on iOS Safari, where click
28466 // events aren't propogated under most circumstances.
28467 // See http://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html
28468 self.documentElement.on('click touchstart', self.bodyClickHandler);
28469 }, false);
28470
28471 window.addEventListener(this.windowEventName, this.windowEventHandler);
28472 }
28473 };
28474
28475 /** Close the floating calendar pane. */
28476 DatePickerCtrl.prototype.closeCalendarPane = function() {
28477 if (this.isCalendarOpen) {
28478 var self = this;
28479
28480 self.detachCalendarPane();
28481 self.ngModelCtrl.$setTouched();
28482 self.evalAttr('ngBlur');
28483
28484 self.documentElement.off('click touchstart', self.bodyClickHandler);
28485 window.removeEventListener(self.windowEventName, self.windowEventHandler);
28486
28487 self.calendarPaneOpenedFrom.focus();
28488 self.calendarPaneOpenedFrom = null;
28489
28490 if (self.openOnFocus) {
28491 // Ensures that all focus events have fired before resetting
28492 // the calendar. Prevents the calendar from reopening immediately
28493 // in IE when md-open-on-focus is set. Also it needs to trigger
28494 // a digest, in order to prevent issues where the calendar wasn't
28495 // showing up on the next open.
28496 self.$mdUtil.nextTick(reset);
28497 } else {
28498 reset();
28499 }
28500 }
28501
28502 function reset(){
28503 self.isCalendarOpen = self.isOpen = false;
28504 }
28505 };
28506
28507 /** Gets the controller instance for the calendar in the floating pane. */
28508 DatePickerCtrl.prototype.getCalendarCtrl = function() {
28509 return angular.element(this.calendarPane.querySelector('md-calendar')).controller('mdCalendar');
28510 };
28511
28512 /** Focus the calendar in the floating pane. */
28513 DatePickerCtrl.prototype.focusCalendar = function() {
28514 // Use a timeout in order to allow the calendar to be rendered, as it is gated behind an ng-if.
28515 var self = this;
28516 this.$mdUtil.nextTick(function() {
28517 self.getCalendarCtrl().focus();
28518 }, false);
28519 };
28520
28521 /**
28522 * Sets whether the input is currently focused.
28523 * @param {boolean} isFocused
28524 */
28525 DatePickerCtrl.prototype.setFocused = function(isFocused) {
28526 if (!isFocused) {
28527 this.ngModelCtrl.$setTouched();
28528 }
28529
28530 // The ng* expressions shouldn't be evaluated when mdOpenOnFocus is on,
28531 // because they also get called when the calendar is opened/closed.
28532 if (!this.openOnFocus) {
28533 this.evalAttr(isFocused ? 'ngFocus' : 'ngBlur');
28534 }
28535
28536 this.isFocused = isFocused;
28537 };
28538
28539 /**
28540 * Handles a click on the document body when the floating calendar pane is open.
28541 * Closes the floating calendar pane if the click is not inside of it.
28542 * @param {MouseEvent} event
28543 */
28544 DatePickerCtrl.prototype.handleBodyClick = function(event) {
28545 if (this.isCalendarOpen) {
28546 var isInCalendar = this.$mdUtil.getClosest(event.target, 'md-calendar');
28547
28548 if (!isInCalendar) {
28549 this.closeCalendarPane();
28550 }
28551
28552 this.$scope.$digest();
28553 }
28554 };
28555
28556 /**
28557 * Handles the event when the user navigates away from the current tab. Keeps track of
28558 * whether the input was focused when the event happened, in order to prevent the calendar
28559 * from re-opening.
28560 */
28561 DatePickerCtrl.prototype.handleWindowBlur = function() {
28562 this.inputFocusedOnWindowBlur = document.activeElement === this.inputElement;
28563 };
28564
28565 /**
28566 * Evaluates an attribute expression against the parent scope.
28567 * @param {String} attr Name of the attribute to be evaluated.
28568 */
28569 DatePickerCtrl.prototype.evalAttr = function(attr) {
28570 if (this.$attrs[attr]) {
28571 this.$scope.$parent.$eval(this.$attrs[attr]);
28572 }
28573 };
28574
28575 /**
28576 * Sets the ng-model value by first converting the date object into a strng. Converting it
28577 * is necessary, in order to pass Angular's `input[type="date"]` validations. Angular turns
28578 * the value into a Date object afterwards, before setting it on the model.
28579 * @param {Date=} value Date to be set as the model value.
28580 */
28581 DatePickerCtrl.prototype.setModelValue = function(value) {
28582 this.ngModelCtrl.$setViewValue(this.ngDateFilter(value, 'yyyy-MM-dd'));
28583 };
28584})();
28585
28586})();
28587(function(){
28588"use strict";
28589
28590angular
28591 .module('material.components.icon')
28592 .directive('mdIcon', ['$mdIcon', '$mdTheming', '$mdAria', '$sce', mdIconDirective]);
28593
28594/**
28595 * @ngdoc directive
28596 * @name mdIcon
28597 * @module material.components.icon
28598 *
28599 * @restrict E
28600 *
28601 * @description
28602 * The `md-icon` directive makes it easier to use vector-based icons in your app (as opposed to
28603 * raster-based icons types like PNG). The directive supports both icon fonts and SVG icons.
28604 *
28605 * Icons should be considered view-only elements that should not be used directly as buttons; instead nest a `<md-icon>`
28606 * inside a `md-button` to add hover and click features.
28607 *
28608 * ### Icon fonts
28609 * Icon fonts are a technique in which you use a font where the glyphs in the font are
28610 * your icons instead of text. Benefits include a straightforward way to bundle everything into a
28611 * single HTTP request, simple scaling, easy color changing, and more.
28612 *
28613 * `md-icon` lets you consume an icon font by letting you reference specific icons in that font
28614 * by name rather than character code.
28615 *
28616 * ### SVG
28617 * For SVGs, the problem with using `<img>` or a CSS `background-image` is that you can't take
28618 * advantage of some SVG features, such as styling specific parts of the icon with CSS or SVG
28619 * animation.
28620 *
28621 * `md-icon` makes it easier to use SVG icons by *inlining* the SVG into an `<svg>` element in the
28622 * document. The most straightforward way of referencing an SVG icon is via URL, just like a
28623 * traditional `<img>`. `$mdIconProvider`, as a convenience, lets you _name_ an icon so you can
28624 * reference it by name instead of URL throughout your templates.
28625 *
28626 * Additionally, you may not want to make separate HTTP requests for every icon, so you can bundle
28627 * your SVG icons together and pre-load them with $mdIconProvider as an icon set. An icon set can
28628 * also be given a name, which acts as a namespace for individual icons, so you can reference them
28629 * like `"social:cake"`.
28630 *
28631 * When using SVGs, both external SVGs (via URLs) or sets of SVGs [from icon sets] can be
28632 * easily loaded and used.When use font-icons, developers must following three (3) simple steps:
28633 *
28634 * <ol>
28635 * <li>Load the font library. e.g.<br/>
28636 * `<link href="https://fonts.googleapis.com/icon?family=Material+Icons"
28637 * rel="stylesheet">`
28638 * </li>
28639 * <li>
28640 * Use either (a) font-icon class names or (b) font ligatures to render the font glyph by using
28641 * its textual name
28642 * </li>
28643 * <li>
28644 * Use `<md-icon md-font-icon="classname" />` or <br/>
28645 * use `<md-icon md-font-set="font library classname or alias"> textual_name </md-icon>` or <br/>
28646 * use `<md-icon md-font-set="font library classname or alias"> numerical_character_reference </md-icon>`
28647 * </li>
28648 * </ol>
28649 *
28650 * Full details for these steps can be found:
28651 *
28652 * <ul>
28653 * <li>http://google.github.io/material-design-icons/</li>
28654 * <li>http://google.github.io/material-design-icons/#icon-font-for-the-web</li>
28655 * </ul>
28656 *
28657 * The Material Design icon style <code>.material-icons</code> and the icon font references are published in
28658 * Material Design Icons:
28659 *
28660 * <ul>
28661 * <li>https://design.google.com/icons/</li>
28662 * <li>https://design.google.com/icons/#ic_accessibility</li>
28663 * </ul>
28664 *
28665 * <h2 id="material_design_icons">Material Design Icons</h2>
28666 * Using the Material Design Icon-Selector, developers can easily and quickly search for a Material Design font-icon and
28667 * determine its textual name and character reference code. Click on any icon to see the slide-up information
28668 * panel with details regarding a SVG download or information on the font-icon usage.
28669 *
28670 * <a href="https://www.google.com/design/icons/#ic_accessibility" target="_blank" style="border-bottom:none;">
28671 * <img src="https://cloud.githubusercontent.com/assets/210413/7902490/fe8dd14c-0780-11e5-98fb-c821cc6475e6.png"
28672 * aria-label="Material Design Icon-Selector" style="max-width:75%;padding-left:10%">
28673 * </a>
28674 *
28675 * <span class="image_caption">
28676 * Click on the image above to link to the
28677 * <a href="https://design.google.com/icons/#ic_accessibility" target="_blank">Material Design Icon-Selector</a>.
28678 * </span>
28679 *
28680 * @param {string} md-font-icon String name of CSS icon associated with the font-face will be used
28681 * to render the icon. Requires the fonts and the named CSS styles to be preloaded.
28682 * @param {string} md-font-set CSS style name associated with the font library; which will be assigned as
28683 * the class for the font-icon ligature. This value may also be an alias that is used to lookup the classname;
28684 * internally use `$mdIconProvider.fontSet(<alias>)` to determine the style name.
28685 * @param {string} md-svg-src String URL (or expression) used to load, cache, and display an
28686 * external SVG.
28687 * @param {string} md-svg-icon md-svg-icon String name used for lookup of the icon from the internal cache;
28688 * interpolated strings or expressions may also be used. Specific set names can be used with
28689 * the syntax `<set name>:<icon name>`.<br/><br/>
28690 * To use icon sets, developers are required to pre-register the sets using the `$mdIconProvider` service.
28691 * @param {string=} aria-label Labels icon for accessibility. If an empty string is provided, icon
28692 * will be hidden from accessibility layer with `aria-hidden="true"`. If there's no aria-label on the icon
28693 * nor a label on the parent element, a warning will be logged to the console.
28694 * @param {string=} alt Labels icon for accessibility. If an empty string is provided, icon
28695 * will be hidden from accessibility layer with `aria-hidden="true"`. If there's no alt on the icon
28696 * nor a label on the parent element, a warning will be logged to the console.
28697 *
28698 * @usage
28699 * When using SVGs:
28700 * <hljs lang="html">
28701 *
28702 * <!-- Icon ID; may contain optional icon set prefix; icons must registered using $mdIconProvider -->
28703 * <md-icon md-svg-icon="social:android" aria-label="android " ></md-icon>
28704 *
28705 * <!-- Icon urls; may be preloaded in templateCache -->
28706 * <md-icon md-svg-src="/android.svg" aria-label="android " ></md-icon>
28707 * <md-icon md-svg-src="{{ getAndroid() }}" aria-label="android " ></md-icon>
28708 *
28709 * </hljs>
28710 *
28711 * Use the <code>$mdIconProvider</code> to configure your application with
28712 * svg iconsets.
28713 *
28714 * <hljs lang="js">
28715 * angular.module('appSvgIconSets', ['ngMaterial'])
28716 * .controller('DemoCtrl', function($scope) {})
28717 * .config(function($mdIconProvider) {
28718 * $mdIconProvider
28719 * .iconSet('social', 'img/icons/sets/social-icons.svg', 24)
28720 * .defaultIconSet('img/icons/sets/core-icons.svg', 24);
28721 * });
28722 * </hljs>
28723 *
28724 *
28725 * When using Font Icons with classnames:
28726 * <hljs lang="html">
28727 *
28728 * <md-icon md-font-icon="android" aria-label="android" ></md-icon>
28729 * <md-icon class="icon_home" aria-label="Home" ></md-icon>
28730 *
28731 * </hljs>
28732 *
28733 * When using Material Font Icons with ligatures:
28734 * <hljs lang="html">
28735 * <!--
28736 * For Material Design Icons
28737 * The class '.material-icons' is auto-added if a style has NOT been specified
28738 * since `material-icons` is the default fontset. So your markup:
28739 * -->
28740 * <md-icon> face </md-icon>
28741 * <!-- becomes this at runtime: -->
28742 * <md-icon md-font-set="material-icons"> face </md-icon>
28743 * <!-- If the fontset does not support ligature names, then we need to use the ligature unicode.-->
28744 * <md-icon>  </md-icon>
28745 * <!-- The class '.material-icons' must be manually added if other styles are also specified-->
28746 * <md-icon class="material-icons md-light md-48"> face </md-icon>
28747 * </hljs>
28748 *
28749 * When using other Font-Icon libraries:
28750 *
28751 * <hljs lang="js">
28752 * // Specify a font-icon style alias
28753 * angular.config(function($mdIconProvider) {
28754 * $mdIconProvider.fontSet('md', 'material-icons');
28755 * });
28756 * </hljs>
28757 *
28758 * <hljs lang="html">
28759 * <md-icon md-font-set="md">favorite</md-icon>
28760 * </hljs>
28761 *
28762 */
28763function mdIconDirective($mdIcon, $mdTheming, $mdAria, $sce) {
28764
28765 return {
28766 restrict: 'E',
28767 link : postLink
28768 };
28769
28770
28771 /**
28772 * Directive postLink
28773 * Supports embedded SVGs, font-icons, & external SVGs
28774 */
28775 function postLink(scope, element, attr) {
28776 $mdTheming(element);
28777 var lastFontIcon = attr.mdFontIcon;
28778 var lastFontSet = $mdIcon.fontSet(attr.mdFontSet);
28779
28780 prepareForFontIcon();
28781
28782 attr.$observe('mdFontIcon', fontIconChanged);
28783 attr.$observe('mdFontSet', fontIconChanged);
28784
28785 // Keep track of the content of the svg src so we can compare against it later to see if the
28786 // attribute is static (and thus safe).
28787 var originalSvgSrc = element[0].getAttribute(attr.$attr.mdSvgSrc);
28788
28789 // If using a font-icon, then the textual name of the icon itself
28790 // provides the aria-label.
28791
28792 var label = attr.alt || attr.mdFontIcon || attr.mdSvgIcon || element.text();
28793 var attrName = attr.$normalize(attr.$attr.mdSvgIcon || attr.$attr.mdSvgSrc || '');
28794
28795 if ( !attr['aria-label'] ) {
28796
28797 if (label !== '' && !parentsHaveText() ) {
28798
28799 $mdAria.expect(element, 'aria-label', label);
28800 $mdAria.expect(element, 'role', 'img');
28801
28802 } else if ( !element.text() ) {
28803 // If not a font-icon with ligature, then
28804 // hide from the accessibility layer.
28805
28806 $mdAria.expect(element, 'aria-hidden', 'true');
28807 }
28808 }
28809
28810 if (attrName) {
28811 // Use either pre-configured SVG or URL source, respectively.
28812 attr.$observe(attrName, function(attrVal) {
28813 element.empty();
28814 if (attrVal) {
28815 $mdIcon(attrVal)
28816 .then(function(svg) {
28817 element.empty();
28818 element.append(svg);
28819 });
28820 }
28821
28822 });
28823 }
28824
28825 function parentsHaveText() {
28826 var parent = element.parent();
28827 if (parent.attr('aria-label') || parent.text()) {
28828 return true;
28829 }
28830 else if(parent.parent().attr('aria-label') || parent.parent().text()) {
28831 return true;
28832 }
28833 return false;
28834 }
28835
28836 function prepareForFontIcon() {
28837 if (!attr.mdSvgIcon && !attr.mdSvgSrc) {
28838 if (attr.mdFontIcon) {
28839 element.addClass('md-font ' + attr.mdFontIcon);
28840 }
28841
28842 element.addClass(lastFontSet);
28843 }
28844 }
28845
28846 function fontIconChanged() {
28847 if (!attr.mdSvgIcon && !attr.mdSvgSrc) {
28848 if (attr.mdFontIcon) {
28849 element.removeClass(lastFontIcon);
28850 element.addClass(attr.mdFontIcon);
28851
28852 lastFontIcon = attr.mdFontIcon;
28853 }
28854
28855 var fontSet = $mdIcon.fontSet(attr.mdFontSet);
28856
28857 if (lastFontSet !== fontSet) {
28858 element.removeClass(lastFontSet);
28859 element.addClass(fontSet);
28860
28861 lastFontSet = fontSet;
28862 }
28863 }
28864 }
28865 }
28866}
28867
28868})();
28869(function(){
28870"use strict";
28871
28872
28873MdIconService.$inject = ["config", "$templateRequest", "$q", "$log", "$mdUtil", "$sce"];angular
28874 .module('material.components.icon')
28875 .constant('$$mdSvgRegistry', {
28876 'mdTabsArrow': 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxnPjxwb2x5Z29uIHBvaW50cz0iMTUuNCw3LjQgMTQsNiA4LDEyIDE0LDE4IDE1LjQsMTYuNiAxMC44LDEyICIvPjwvZz48L3N2Zz4=',
28877 'mdClose': 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxnPjxwYXRoIGQ9Ik0xOSA2LjQxbC0xLjQxLTEuNDEtNS41OSA1LjU5LTUuNTktNS41OS0xLjQxIDEuNDEgNS41OSA1LjU5LTUuNTkgNS41OSAxLjQxIDEuNDEgNS41OS01LjU5IDUuNTkgNS41OSAxLjQxLTEuNDEtNS41OS01LjU5eiIvPjwvZz48L3N2Zz4=',
28878 'mdCancel': 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxnPjxwYXRoIGQ9Ik0xMiAyYy01LjUzIDAtMTAgNC40Ny0xMCAxMHM0LjQ3IDEwIDEwIDEwIDEwLTQuNDcgMTAtMTAtNC40Ny0xMC0xMC0xMHptNSAxMy41OWwtMS40MSAxLjQxLTMuNTktMy41OS0zLjU5IDMuNTktMS40MS0xLjQxIDMuNTktMy41OS0zLjU5LTMuNTkgMS40MS0xLjQxIDMuNTkgMy41OSAzLjU5LTMuNTkgMS40MSAxLjQxLTMuNTkgMy41OSAzLjU5IDMuNTl6Ii8+PC9nPjwvc3ZnPg==',
28879 'mdMenu': 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0zLDZIMjFWOEgzVjZNMywxMUgyMVYxM0gzVjExTTMsMTZIMjFWMThIM1YxNloiIC8+PC9zdmc+',
28880 'mdToggleArrow': 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgNDggNDgiPjxwYXRoIGQ9Ik0yNCAxNmwtMTIgMTIgMi44MyAyLjgzIDkuMTctOS4xNyA5LjE3IDkuMTcgMi44My0yLjgzeiIvPjxwYXRoIGQ9Ik0wIDBoNDh2NDhoLTQ4eiIgZmlsbD0ibm9uZSIvPjwvc3ZnPg==',
28881 'mdCalendar': 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTkgM2gtMVYxaC0ydjJIOFYxSDZ2Mkg1Yy0xLjExIDAtMS45OS45LTEuOTkgMkwzIDE5YzAgMS4xLjg5IDIgMiAyaDE0YzEuMSAwIDItLjkgMi0yVjVjMC0xLjEtLjktMi0yLTJ6bTAgMTZINVY4aDE0djExek03IDEwaDV2NUg3eiIvPjwvc3ZnPg==',
28882 'mdChecked': 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxnPjxwYXRoIGQ9Ik05IDE2LjE3TDQuODMgMTJsLTEuNDIgMS40MUw5IDE5IDIxIDdsLTEuNDEtMS40MXoiLz48L2c+PC9zdmc+'
28883 })
28884 .provider('$mdIcon', MdIconProvider);
28885
28886/**
28887 * @ngdoc service
28888 * @name $mdIconProvider
28889 * @module material.components.icon
28890 *
28891 * @description
28892 * `$mdIconProvider` is used only to register icon IDs with URLs. These configuration features allow
28893 * icons and icon sets to be pre-registered and associated with source URLs **before** the `<md-icon />`
28894 * directives are compiled.
28895 *
28896 * If using font-icons, the developer is responsible for loading the fonts.
28897 *
28898 * If using SVGs, loading of the actual svg files are deferred to on-demand requests and are loaded
28899 * internally by the `$mdIcon` service using the `$templateRequest` service. When an SVG is
28900 * requested by name/ID, the `$mdIcon` service searches its registry for the associated source URL;
28901 * that URL is used to on-demand load and parse the SVG dynamically.
28902 *
28903 * The `$templateRequest` service expects the icons source to be loaded over trusted URLs.<br/>
28904 * This means, when loading icons from an external URL, you have to trust the URL in the `$sceDelegateProvider`.
28905 *
28906 * <hljs lang="js">
28907 * app.config(function($sceDelegateProvider) {
28908 * $sceDelegateProvider.resourceUrlWhitelist([
28909 * // Adding 'self' to the whitelist, will allow requests from the current origin.
28910 * 'self',
28911 * // Using double asterisks here, will allow all URLs to load.
28912 * // We recommend to only specify the given domain you want to allow.
28913 * '**'
28914 * ]);
28915 * });
28916 * </hljs>
28917 *
28918 * Read more about the [$sceDelegateProvider](https://docs.angularjs.org/api/ng/provider/$sceDelegateProvider).
28919 *
28920 * **Notice:** Most font-icons libraries do not support ligatures (for example `fontawesome`).<br/>
28921 * In such cases you are not able to use the icon's ligature name - Like so:
28922 *
28923 * <hljs lang="html">
28924 * <md-icon md-font-set="fa">fa-bell</md-icon>
28925 * </hljs>
28926 *
28927 * You should instead use the given unicode, instead of the ligature name.
28928 *
28929 * <p ng-hide="true"> ##// Notice we can't use a hljs element here, because the characters will be escaped.</p>
28930 * ```html
28931 * <md-icon md-font-set="fa"></md-icon>
28932 * ```
28933 *
28934 * All unicode ligatures are prefixed with the `&#x` string.
28935 *
28936 * @usage
28937 * <hljs lang="js">
28938 * app.config(function($mdIconProvider) {
28939 *
28940 * // Configure URLs for icons specified by [set:]id.
28941 *
28942 * $mdIconProvider
28943 * .defaultFontSet( 'fa' ) // This sets our default fontset className.
28944 * .defaultIconSet('my/app/icons.svg') // Register a default set of SVG icons
28945 * .iconSet('social', 'my/app/social.svg') // Register a named icon set of SVGs
28946 * .icon('android', 'my/app/android.svg') // Register a specific icon (by name)
28947 * .icon('work:chair', 'my/app/chair.svg'); // Register icon in a specific set
28948 * });
28949 * </hljs>
28950 *
28951 * SVG icons and icon sets can be easily pre-loaded and cached using either (a) a build process or (b) a runtime
28952 * **startup** process (shown below):
28953 *
28954 * <hljs lang="js">
28955 * app.config(function($mdIconProvider) {
28956 *
28957 * // Register a default set of SVG icon definitions
28958 * $mdIconProvider.defaultIconSet('my/app/icons.svg')
28959 *
28960 * })
28961 * .run(function($templateRequest){
28962 *
28963 * // Pre-fetch icons sources by URL and cache in the $templateCache...
28964 * // subsequent $templateRequest calls will look there first.
28965 *
28966 * var urls = [ 'imy/app/icons.svg', 'img/icons/android.svg'];
28967 *
28968 * angular.forEach(urls, function(url) {
28969 * $templateRequest(url);
28970 * });
28971 *
28972 * });
28973 *
28974 * </hljs>
28975 *
28976 * > <b>Note:</b> The loaded SVG data is subsequently cached internally for future requests.
28977 *
28978 */
28979
28980/**
28981 * @ngdoc method
28982 * @name $mdIconProvider#icon
28983 *
28984 * @description
28985 * Register a source URL for a specific icon name; the name may include optional 'icon set' name prefix.
28986 * These icons will later be retrieved from the cache using `$mdIcon( <icon name> )`
28987 *
28988 * @param {string} id Icon name/id used to register the icon
28989 * @param {string} url specifies the external location for the data file. Used internally by
28990 * `$templateRequest` to load the data or as part of the lookup in `$templateCache` if pre-loading
28991 * was configured.
28992 * @param {number=} viewBoxSize Sets the width and height the icon's viewBox.
28993 * It is ignored for icons with an existing viewBox. Default size is 24.
28994 *
28995 * @returns {obj} an `$mdIconProvider` reference; used to support method call chains for the API
28996 *
28997 * @usage
28998 * <hljs lang="js">
28999 * app.config(function($mdIconProvider) {
29000 *
29001 * // Configure URLs for icons specified by [set:]id.
29002 *
29003 * $mdIconProvider
29004 * .icon('android', 'my/app/android.svg') // Register a specific icon (by name)
29005 * .icon('work:chair', 'my/app/chair.svg'); // Register icon in a specific set
29006 * });
29007 * </hljs>
29008 *
29009 */
29010/**
29011 * @ngdoc method
29012 * @name $mdIconProvider#iconSet
29013 *
29014 * @description
29015 * Register a source URL for a 'named' set of icons; group of SVG definitions where each definition
29016 * has an icon id. Individual icons can be subsequently retrieved from this cached set using
29017 * `$mdIcon(<icon set name>:<icon name>)`
29018 *
29019 * @param {string} id Icon name/id used to register the iconset
29020 * @param {string} url specifies the external location for the data file. Used internally by
29021 * `$templateRequest` to load the data or as part of the lookup in `$templateCache` if pre-loading
29022 * was configured.
29023 * @param {number=} viewBoxSize Sets the width and height of the viewBox of all icons in the set.
29024 * It is ignored for icons with an existing viewBox. All icons in the icon set should be the same size.
29025 * Default value is 24.
29026 *
29027 * @returns {obj} an `$mdIconProvider` reference; used to support method call chains for the API
29028 *
29029 *
29030 * @usage
29031 * <hljs lang="js">
29032 * app.config(function($mdIconProvider) {
29033 *
29034 * // Configure URLs for icons specified by [set:]id.
29035 *
29036 * $mdIconProvider
29037 * .iconSet('social', 'my/app/social.svg') // Register a named icon set
29038 * });
29039 * </hljs>
29040 *
29041 */
29042/**
29043 * @ngdoc method
29044 * @name $mdIconProvider#defaultIconSet
29045 *
29046 * @description
29047 * Register a source URL for the default 'named' set of icons. Unless explicitly registered,
29048 * subsequent lookups of icons will failover to search this 'default' icon set.
29049 * Icon can be retrieved from this cached, default set using `$mdIcon(<name>)`
29050 *
29051 * @param {string} url specifies the external location for the data file. Used internally by
29052 * `$templateRequest` to load the data or as part of the lookup in `$templateCache` if pre-loading
29053 * was configured.
29054 * @param {number=} viewBoxSize Sets the width and height of the viewBox of all icons in the set.
29055 * It is ignored for icons with an existing viewBox. All icons in the icon set should be the same size.
29056 * Default value is 24.
29057 *
29058 * @returns {obj} an `$mdIconProvider` reference; used to support method call chains for the API
29059 *
29060 * @usage
29061 * <hljs lang="js">
29062 * app.config(function($mdIconProvider) {
29063 *
29064 * // Configure URLs for icons specified by [set:]id.
29065 *
29066 * $mdIconProvider
29067 * .defaultIconSet( 'my/app/social.svg' ) // Register a default icon set
29068 * });
29069 * </hljs>
29070 *
29071 */
29072/**
29073 * @ngdoc method
29074 * @name $mdIconProvider#defaultFontSet
29075 *
29076 * @description
29077 * When using Font-Icons, Angular Material assumes the the Material Design icons will be used and automatically
29078 * configures the default font-set == 'material-icons'. Note that the font-set references the font-icon library
29079 * class style that should be applied to the `<md-icon>`.
29080 *
29081 * Configuring the default means that the attributes
29082 * `md-font-set="material-icons"` or `class="material-icons"` do not need to be explicitly declared on the
29083 * `<md-icon>` markup. For example:
29084 *
29085 * `<md-icon> face </md-icon>`
29086 * will render as
29087 * `<span class="material-icons"> face </span>`, and
29088 *
29089 * `<md-icon md-font-set="fa"> face </md-icon>`
29090 * will render as
29091 * `<span class="fa"> face </span>`
29092 *
29093 * @param {string} name of the font-library style that should be applied to the md-icon DOM element
29094 *
29095 * @usage
29096 * <hljs lang="js">
29097 * app.config(function($mdIconProvider) {
29098 * $mdIconProvider.defaultFontSet( 'fa' );
29099 * });
29100 * </hljs>
29101 *
29102 */
29103
29104/**
29105 * @ngdoc method
29106 * @name $mdIconProvider#fontSet
29107 *
29108 * @description
29109 * When using a font set for `<md-icon>` you must specify the correct font classname in the `md-font-set`
29110 * attribute. If the fonset className is really long, your markup may become cluttered... an easy
29111 * solution is to define an `alias` for your fontset:
29112 *
29113 * @param {string} alias of the specified fontset.
29114 * @param {string} className of the fontset.
29115 *
29116 * @usage
29117 * <hljs lang="js">
29118 * app.config(function($mdIconProvider) {
29119 * // In this case, we set an alias for the `material-icons` fontset.
29120 * $mdIconProvider.fontSet('md', 'material-icons');
29121 * });
29122 * </hljs>
29123 *
29124 */
29125
29126/**
29127 * @ngdoc method
29128 * @name $mdIconProvider#defaultViewBoxSize
29129 *
29130 * @description
29131 * While `<md-icon />` markup can also be style with sizing CSS, this method configures
29132 * the default width **and** height used for all icons; unless overridden by specific CSS.
29133 * The default sizing is (24px, 24px).
29134 * @param {number=} viewBoxSize Sets the width and height of the viewBox for an icon or an icon set.
29135 * All icons in a set should be the same size. The default value is 24.
29136 *
29137 * @returns {obj} an `$mdIconProvider` reference; used to support method call chains for the API
29138 *
29139 * @usage
29140 * <hljs lang="js">
29141 * app.config(function($mdIconProvider) {
29142 *
29143 * // Configure URLs for icons specified by [set:]id.
29144 *
29145 * $mdIconProvider
29146 * .defaultViewBoxSize(36) // Register a default icon size (width == height)
29147 * });
29148 * </hljs>
29149 *
29150 */
29151
29152var config = {
29153 defaultViewBoxSize: 24,
29154 defaultFontSet: 'material-icons',
29155 fontSets: []
29156};
29157
29158function MdIconProvider() {
29159}
29160
29161MdIconProvider.prototype = {
29162 icon: function(id, url, viewBoxSize) {
29163 if (id.indexOf(':') == -1) id = '$default:' + id;
29164
29165 config[id] = new ConfigurationItem(url, viewBoxSize);
29166 return this;
29167 },
29168
29169 iconSet: function(id, url, viewBoxSize) {
29170 config[id] = new ConfigurationItem(url, viewBoxSize);
29171 return this;
29172 },
29173
29174 defaultIconSet: function(url, viewBoxSize) {
29175 var setName = '$default';
29176
29177 if (!config[setName]) {
29178 config[setName] = new ConfigurationItem(url, viewBoxSize);
29179 }
29180
29181 config[setName].viewBoxSize = viewBoxSize || config.defaultViewBoxSize;
29182
29183 return this;
29184 },
29185
29186 defaultViewBoxSize: function(viewBoxSize) {
29187 config.defaultViewBoxSize = viewBoxSize;
29188 return this;
29189 },
29190
29191 /**
29192 * Register an alias name associated with a font-icon library style ;
29193 */
29194 fontSet: function fontSet(alias, className) {
29195 config.fontSets.push({
29196 alias: alias,
29197 fontSet: className || alias
29198 });
29199 return this;
29200 },
29201
29202 /**
29203 * Specify a default style name associated with a font-icon library
29204 * fallback to Material Icons.
29205 *
29206 */
29207 defaultFontSet: function defaultFontSet(className) {
29208 config.defaultFontSet = !className ? '' : className;
29209 return this;
29210 },
29211
29212 defaultIconSize: function defaultIconSize(iconSize) {
29213 config.defaultIconSize = iconSize;
29214 return this;
29215 },
29216
29217 $get: ['$templateRequest', '$q', '$log', '$mdUtil', '$sce', function($templateRequest, $q, $log, $mdUtil, $sce) {
29218 return MdIconService(config, $templateRequest, $q, $log, $mdUtil, $sce);
29219 }]
29220};
29221
29222/**
29223 * Configuration item stored in the Icon registry; used for lookups
29224 * to load if not already cached in the `loaded` cache
29225 */
29226function ConfigurationItem(url, viewBoxSize) {
29227 this.url = url;
29228 this.viewBoxSize = viewBoxSize || config.defaultViewBoxSize;
29229}
29230
29231/**
29232 * @ngdoc service
29233 * @name $mdIcon
29234 * @module material.components.icon
29235 *
29236 * @description
29237 * The `$mdIcon` service is a function used to lookup SVG icons.
29238 *
29239 * @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
29240 * 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
29241 * performed. The Id may be a unique icon ID or may include an iconSet ID prefix.
29242 *
29243 * For the **id** query to work properly, this means that all id-to-URL mappings must have been previously configured
29244 * using the `$mdIconProvider`.
29245 *
29246 * @returns {angular.$q.Promise} A promise that gets resolved to a clone of the initial SVG DOM element; which was
29247 * created from the SVG markup in the SVG data file. If an error occurs (e.g. the icon cannot be found) the promise
29248 * will get rejected.
29249 *
29250 * @usage
29251 * <hljs lang="js">
29252 * function SomeDirective($mdIcon) {
29253 *
29254 * // See if the icon has already been loaded, if not
29255 * // then lookup the icon from the registry cache, load and cache
29256 * // it for future requests.
29257 * // NOTE: ID queries require configuration with $mdIconProvider
29258 *
29259 * $mdIcon('android').then(function(iconEl) { element.append(iconEl); });
29260 * $mdIcon('work:chair').then(function(iconEl) { element.append(iconEl); });
29261 *
29262 * // Load and cache the external SVG using a URL
29263 *
29264 * $mdIcon('img/icons/android.svg').then(function(iconEl) {
29265 * element.append(iconEl);
29266 * });
29267 * };
29268 * </hljs>
29269 *
29270 * > <b>Note:</b> The `<md-icon>` directive internally uses the `$mdIcon` service to query, loaded,
29271 * and instantiate SVG DOM elements.
29272 */
29273
29274/* @ngInject */
29275function MdIconService(config, $templateRequest, $q, $log, $mdUtil, $sce) {
29276 var iconCache = {};
29277 var svgCache = {};
29278 var urlRegex = /[-\w@:%\+.~#?&//=]{2,}\.[a-z]{2,4}\b(\/[-\w@:%\+.~#?&//=]*)?/i;
29279 var dataUrlRegex = /^data:image\/svg\+xml[\s*;\w\-\=]*?(base64)?,(.*)$/i;
29280
29281 Icon.prototype = {clone: cloneSVG, prepare: prepareAndStyle};
29282 getIcon.fontSet = findRegisteredFontSet;
29283
29284 // Publish service...
29285 return getIcon;
29286
29287 /**
29288 * Actual $mdIcon service is essentially a lookup function
29289 */
29290 function getIcon(id) {
29291 id = id || '';
29292
29293 // If the "id" provided is not a string, the only other valid value is a $sce trust wrapper
29294 // over a URL string. If the value is not trusted, this will intentionally throw an error
29295 // because the user is attempted to use an unsafe URL, potentially opening themselves up
29296 // to an XSS attack.
29297 if (!angular.isString(id)) {
29298 id = $sce.getTrustedUrl(id);
29299 }
29300
29301 // If already loaded and cached, use a clone of the cached icon.
29302 // Otherwise either load by URL, or lookup in the registry and then load by URL, and cache.
29303
29304 if (iconCache[id]) {
29305 return $q.when(transformClone(iconCache[id]));
29306 }
29307
29308 if (urlRegex.test(id) || dataUrlRegex.test(id)) {
29309 return loadByURL(id).then(cacheIcon(id));
29310 }
29311
29312 if (id.indexOf(':') == -1) {
29313 id = '$default:' + id;
29314 }
29315
29316 var load = config[id] ? loadByID : loadFromIconSet;
29317 return load(id)
29318 .then(cacheIcon(id));
29319 }
29320
29321 /**
29322 * Lookup registered fontSet style using its alias...
29323 * If not found,
29324 */
29325 function findRegisteredFontSet(alias) {
29326 var useDefault = angular.isUndefined(alias) || !(alias && alias.length);
29327 if (useDefault) return config.defaultFontSet;
29328
29329 var result = alias;
29330 angular.forEach(config.fontSets, function(it) {
29331 if (it.alias == alias) result = it.fontSet || result;
29332 });
29333
29334 return result;
29335 }
29336
29337 function transformClone(cacheElement) {
29338 var clone = cacheElement.clone();
29339 var cacheSuffix = '_cache' + $mdUtil.nextUid();
29340
29341 // We need to modify for each cached icon the id attributes.
29342 // This is needed because SVG id's are treated as normal DOM ids
29343 // and should not have a duplicated id.
29344 if (clone.id) clone.id += cacheSuffix;
29345 angular.forEach(clone.querySelectorAll('[id]'), function(item) {
29346 item.id += cacheSuffix;
29347 });
29348
29349 return clone;
29350 }
29351
29352 /**
29353 * Prepare and cache the loaded icon for the specified `id`
29354 */
29355 function cacheIcon(id) {
29356
29357 return function updateCache(icon) {
29358 iconCache[id] = isIcon(icon) ? icon : new Icon(icon, config[id]);
29359
29360 return iconCache[id].clone();
29361 };
29362 }
29363
29364 /**
29365 * Lookup the configuration in the registry, if !registered throw an error
29366 * otherwise load the icon [on-demand] using the registered URL.
29367 *
29368 */
29369 function loadByID(id) {
29370 var iconConfig = config[id];
29371 return loadByURL(iconConfig.url).then(function(icon) {
29372 return new Icon(icon, iconConfig);
29373 });
29374 }
29375
29376 /**
29377 * Loads the file as XML and uses querySelector( <id> ) to find
29378 * the desired node...
29379 */
29380 function loadFromIconSet(id) {
29381 var setName = id.substring(0, id.lastIndexOf(':')) || '$default';
29382 var iconSetConfig = config[setName];
29383
29384 return !iconSetConfig ? announceIdNotFound(id) : loadByURL(iconSetConfig.url).then(extractFromSet);
29385
29386 function extractFromSet(set) {
29387 var iconName = id.slice(id.lastIndexOf(':') + 1);
29388 var icon = set.querySelector('#' + iconName);
29389 return icon ? new Icon(icon, iconSetConfig) : announceIdNotFound(id);
29390 }
29391
29392 function announceIdNotFound(id) {
29393 var msg = 'icon ' + id + ' not found';
29394 $log.warn(msg);
29395
29396 return $q.reject(msg || id);
29397 }
29398 }
29399
29400 /**
29401 * Load the icon by URL (may use the $templateCache).
29402 * Extract the data for later conversion to Icon
29403 */
29404 function loadByURL(url) {
29405 /* Load the icon from embedded data URL. */
29406 function loadByDataUrl(url) {
29407 var results = dataUrlRegex.exec(url);
29408 var isBase64 = /base64/i.test(url);
29409 var data = isBase64 ? window.atob(results[2]) : results[2];
29410
29411 return $q.when(angular.element(data)[0]);
29412 }
29413
29414 /* Load the icon by URL using HTTP. */
29415 function loadByHttpUrl(url) {
29416 return $q(function(resolve, reject) {
29417 // Catch HTTP or generic errors not related to incorrect icon IDs.
29418 var announceAndReject = function(err) {
29419 var msg = angular.isString(err) ? err : (err.message || err.data || err.statusText);
29420 $log.warn(msg);
29421 reject(err);
29422 },
29423 extractSvg = function(response) {
29424 if (!svgCache[url]) {
29425 svgCache[url] = angular.element('<div>').append(response)[0].querySelector('svg');
29426 }
29427 resolve(svgCache[url]);
29428 };
29429
29430 $templateRequest(url, true).then(extractSvg, announceAndReject);
29431 });
29432 }
29433
29434 return dataUrlRegex.test(url)
29435 ? loadByDataUrl(url)
29436 : loadByHttpUrl(url);
29437 }
29438
29439 /**
29440 * Check target signature to see if it is an Icon instance.
29441 */
29442 function isIcon(target) {
29443 return angular.isDefined(target.element) && angular.isDefined(target.config);
29444 }
29445
29446 /**
29447 * Define the Icon class
29448 */
29449 function Icon(el, config) {
29450 if (el && el.tagName != 'svg') {
29451 el = angular.element('<svg xmlns="http://www.w3.org/2000/svg">').append(el.cloneNode(true))[0];
29452 }
29453
29454 // Inject the namespace if not available...
29455 if (!el.getAttribute('xmlns')) {
29456 el.setAttribute('xmlns', "http://www.w3.org/2000/svg");
29457 }
29458
29459 this.element = el;
29460 this.config = config;
29461 this.prepare();
29462 }
29463
29464 /**
29465 * Prepare the DOM element that will be cached in the
29466 * loaded iconCache store.
29467 */
29468 function prepareAndStyle() {
29469 var viewBoxSize = this.config ? this.config.viewBoxSize : config.defaultViewBoxSize;
29470 angular.forEach({
29471 'fit': '',
29472 'height': '100%',
29473 'width': '100%',
29474 'preserveAspectRatio': 'xMidYMid meet',
29475 'viewBox': this.element.getAttribute('viewBox') || ('0 0 ' + viewBoxSize + ' ' + viewBoxSize),
29476 'focusable': false // Disable IE11s default behavior to make SVGs focusable
29477 }, function(val, attr) {
29478 this.element.setAttribute(attr, val);
29479 }, this);
29480 }
29481
29482 /**
29483 * Clone the Icon DOM element.
29484 */
29485 function cloneSVG() {
29486 // If the element or any of its children have a style attribute, then a CSP policy without
29487 // 'unsafe-inline' in the style-src directive, will result in a violation.
29488 return this.element.cloneNode(true);
29489 }
29490
29491}
29492
29493})();
29494(function(){
29495"use strict";
29496
29497
29498
29499MenuController.$inject = ["$mdMenu", "$attrs", "$element", "$scope", "$mdUtil", "$timeout", "$rootScope", "$q"];
29500angular
29501 .module('material.components.menu')
29502 .controller('mdMenuCtrl', MenuController);
29503
29504/**
29505 * @ngInject
29506 */
29507function MenuController($mdMenu, $attrs, $element, $scope, $mdUtil, $timeout, $rootScope, $q) {
29508
29509 var prefixer = $mdUtil.prefixer();
29510 var menuContainer;
29511 var self = this;
29512 var triggerElement;
29513
29514 this.nestLevel = parseInt($attrs.mdNestLevel, 10) || 0;
29515
29516 /**
29517 * Called by our linking fn to provide access to the menu-content
29518 * element removed during link
29519 */
29520 this.init = function init(setMenuContainer, opts) {
29521 opts = opts || {};
29522 menuContainer = setMenuContainer;
29523
29524 // Default element for ARIA attributes has the ngClick or ngMouseenter expression
29525 triggerElement = $element[0].querySelector(prefixer.buildSelector(['ng-click', 'ng-mouseenter']));
29526 triggerElement.setAttribute('aria-expanded', 'false');
29527
29528 this.isInMenuBar = opts.isInMenuBar;
29529 this.nestedMenus = $mdUtil.nodesToArray(menuContainer[0].querySelectorAll('.md-nested-menu'));
29530
29531 menuContainer.on('$mdInterimElementRemove', function() {
29532 self.isOpen = false;
29533 $mdUtil.nextTick(function(){ self.onIsOpenChanged(self.isOpen);});
29534 });
29535 $mdUtil.nextTick(function(){ self.onIsOpenChanged(self.isOpen);});
29536
29537 var menuContainerId = 'menu_container_' + $mdUtil.nextUid();
29538 menuContainer.attr('id', menuContainerId);
29539 angular.element(triggerElement).attr({
29540 'aria-owns': menuContainerId,
29541 'aria-haspopup': 'true'
29542 });
29543
29544 $scope.$on('$destroy', angular.bind(this, function() {
29545 this.disableHoverListener();
29546 $mdMenu.destroy();
29547 }));
29548
29549 menuContainer.on('$destroy', function() {
29550 $mdMenu.destroy();
29551 });
29552 };
29553
29554 var openMenuTimeout, menuItems, deregisterScopeListeners = [];
29555 this.enableHoverListener = function() {
29556 deregisterScopeListeners.push($rootScope.$on('$mdMenuOpen', function(event, el) {
29557 if (menuContainer[0].contains(el[0])) {
29558 self.currentlyOpenMenu = el.controller('mdMenu');
29559 self.isAlreadyOpening = false;
29560 self.currentlyOpenMenu.registerContainerProxy(self.triggerContainerProxy.bind(self));
29561 }
29562 }));
29563 deregisterScopeListeners.push($rootScope.$on('$mdMenuClose', function(event, el) {
29564 if (menuContainer[0].contains(el[0])) {
29565 self.currentlyOpenMenu = undefined;
29566 }
29567 }));
29568 menuItems = angular.element($mdUtil.nodesToArray(menuContainer[0].children[0].children));
29569 menuItems.on('mouseenter', self.handleMenuItemHover);
29570 menuItems.on('mouseleave', self.handleMenuItemMouseLeave);
29571 };
29572
29573 this.disableHoverListener = function() {
29574 while (deregisterScopeListeners.length) {
29575 deregisterScopeListeners.shift()();
29576 }
29577 menuItems && menuItems.off('mouseenter', self.handleMenuItemHover);
29578 menuItems && menuItems.off('mouseleave', self.handleMenuItemMouseLeave);
29579 };
29580
29581 this.handleMenuItemHover = function(event) {
29582 if (self.isAlreadyOpening) return;
29583 var nestedMenu = (
29584 event.target.querySelector('md-menu')
29585 || $mdUtil.getClosest(event.target, 'MD-MENU')
29586 );
29587 openMenuTimeout = $timeout(function() {
29588 if (nestedMenu) {
29589 nestedMenu = angular.element(nestedMenu).controller('mdMenu');
29590 }
29591
29592 if (self.currentlyOpenMenu && self.currentlyOpenMenu != nestedMenu) {
29593 var closeTo = self.nestLevel + 1;
29594 self.currentlyOpenMenu.close(true, { closeTo: closeTo });
29595 self.isAlreadyOpening = !!nestedMenu;
29596 nestedMenu && nestedMenu.open();
29597 } else if (nestedMenu && !nestedMenu.isOpen && nestedMenu.open) {
29598 self.isAlreadyOpening = !!nestedMenu;
29599 nestedMenu && nestedMenu.open();
29600 }
29601 }, nestedMenu ? 100 : 250);
29602 var focusableTarget = event.currentTarget.querySelector('.md-button:not([disabled])');
29603 focusableTarget && focusableTarget.focus();
29604 };
29605
29606 this.handleMenuItemMouseLeave = function() {
29607 if (openMenuTimeout) {
29608 $timeout.cancel(openMenuTimeout);
29609 openMenuTimeout = undefined;
29610 }
29611 };
29612
29613
29614 /**
29615 * Uses the $mdMenu interim element service to open the menu contents
29616 */
29617 this.open = function openMenu(ev) {
29618 ev && ev.stopPropagation();
29619 ev && ev.preventDefault();
29620 if (self.isOpen) return;
29621 self.enableHoverListener();
29622 self.isOpen = true;
29623 $mdUtil.nextTick(function(){ self.onIsOpenChanged(self.isOpen);});
29624 triggerElement = triggerElement || (ev ? ev.target : $element[0]);
29625 triggerElement.setAttribute('aria-expanded', 'true');
29626 $scope.$emit('$mdMenuOpen', $element);
29627 $mdMenu.show({
29628 scope: $scope,
29629 mdMenuCtrl: self,
29630 nestLevel: self.nestLevel,
29631 element: menuContainer,
29632 target: triggerElement,
29633 preserveElement: true,
29634 parent: 'body'
29635 }).finally(function() {
29636 triggerElement.setAttribute('aria-expanded', 'false');
29637 self.disableHoverListener();
29638 });
29639 };
29640
29641 // Expose a open function to the child scope for html to use
29642 $scope.$mdOpenMenu = this.open;
29643
29644 this.onIsOpenChanged = function(isOpen) {
29645 if (isOpen) {
29646 menuContainer.attr('aria-hidden', 'false');
29647 $element[0].classList.add('md-open');
29648 angular.forEach(self.nestedMenus, function(el) {
29649 el.classList.remove('md-open');
29650 });
29651 } else {
29652 menuContainer.attr('aria-hidden', 'true');
29653 $element[0].classList.remove('md-open');
29654 }
29655 $scope.$mdMenuIsOpen = self.isOpen;
29656 };
29657
29658 this.focusMenuContainer = function focusMenuContainer() {
29659 var focusTarget = menuContainer[0]
29660 .querySelector(prefixer.buildSelector(['md-menu-focus-target', 'md-autofocus']));
29661
29662 if (!focusTarget) focusTarget = menuContainer[0].querySelector('.md-button');
29663 focusTarget.focus();
29664 };
29665
29666 this.registerContainerProxy = function registerContainerProxy(handler) {
29667 this.containerProxy = handler;
29668 };
29669
29670 this.triggerContainerProxy = function triggerContainerProxy(ev) {
29671 this.containerProxy && this.containerProxy(ev);
29672 };
29673
29674 this.destroy = function() {
29675 return self.isOpen ? $mdMenu.destroy() : $q.when(false);
29676 };
29677
29678 // Use the $mdMenu interim element service to close the menu contents
29679 this.close = function closeMenu(skipFocus, closeOpts) {
29680 if ( !self.isOpen ) return;
29681 self.isOpen = false;
29682 $mdUtil.nextTick(function(){ self.onIsOpenChanged(self.isOpen);});
29683
29684 var eventDetails = angular.extend({}, closeOpts, { skipFocus: skipFocus });
29685 $scope.$emit('$mdMenuClose', $element, eventDetails);
29686 $mdMenu.hide(null, closeOpts);
29687
29688 if (!skipFocus) {
29689 var el = self.restoreFocusTo || $element.find('button')[0];
29690 if (el instanceof angular.element) el = el[0];
29691 if (el) el.focus();
29692 }
29693 };
29694
29695 /**
29696 * Build a nice object out of our string attribute which specifies the
29697 * target mode for left and top positioning
29698 */
29699 this.positionMode = function positionMode() {
29700 var attachment = ($attrs.mdPositionMode || 'target').split(' ');
29701
29702 // If attachment is a single item, duplicate it for our second value.
29703 // ie. 'target' -> 'target target'
29704 if (attachment.length == 1) {
29705 attachment.push(attachment[0]);
29706 }
29707
29708 return {
29709 left: attachment[0],
29710 top: attachment[1]
29711 };
29712 };
29713
29714 /**
29715 * Build a nice object out of our string attribute which specifies
29716 * the offset of top and left in pixels.
29717 */
29718 this.offsets = function offsets() {
29719 var position = ($attrs.mdOffset || '0 0').split(' ').map(parseFloat);
29720 if (position.length == 2) {
29721 return {
29722 left: position[0],
29723 top: position[1]
29724 };
29725 } else if (position.length == 1) {
29726 return {
29727 top: position[0],
29728 left: position[0]
29729 };
29730 } else {
29731 throw Error('Invalid offsets specified. Please follow format <x, y> or <n>');
29732 }
29733 };
29734}
29735
29736})();
29737(function(){
29738"use strict";
29739
29740/**
29741 * @ngdoc directive
29742 * @name mdMenu
29743 * @module material.components.menu
29744 * @restrict E
29745 * @description
29746 *
29747 * Menus are elements that open when clicked. They are useful for displaying
29748 * additional options within the context of an action.
29749 *
29750 * Every `md-menu` must specify exactly two child elements. The first element is what is
29751 * left in the DOM and is used to open the menu. This element is called the trigger element.
29752 * The trigger element's scope has access to `$mdOpenMenu($event)`
29753 * which it may call to open the menu. By passing $event as argument, the
29754 * corresponding event is stopped from propagating up the DOM-tree.
29755 *
29756 * The second element is the `md-menu-content` element which represents the
29757 * contents of the menu when it is open. Typically this will contain `md-menu-item`s,
29758 * but you can do custom content as well.
29759 *
29760 * <hljs lang="html">
29761 * <md-menu>
29762 * <!-- Trigger element is a md-button with an icon -->
29763 * <md-button ng-click="$mdOpenMenu($event)" class="md-icon-button" aria-label="Open sample menu">
29764 * <md-icon md-svg-icon="call:phone"></md-icon>
29765 * </md-button>
29766 * <md-menu-content>
29767 * <md-menu-item><md-button ng-click="doSomething()">Do Something</md-button></md-menu-item>
29768 * </md-menu-content>
29769 * </md-menu>
29770 * </hljs>
29771
29772 * ## Sizing Menus
29773 *
29774 * The width of the menu when it is open may be specified by specifying a `width`
29775 * attribute on the `md-menu-content` element.
29776 * See the [Material Design Spec](https://material.google.com/components/menus.html#menus-simple-menus)
29777 * for more information.
29778 *
29779 *
29780 * ## Aligning Menus
29781 *
29782 * When a menu opens, it is important that the content aligns with the trigger element.
29783 * Failure to align menus can result in jarring experiences for users as content
29784 * suddenly shifts. To help with this, `md-menu` provides serveral APIs to help
29785 * with alignment.
29786 *
29787 * ### Target Mode
29788 *
29789 * By default, `md-menu` will attempt to align the `md-menu-content` by aligning
29790 * designated child elements in both the trigger and the menu content.
29791 *
29792 * To specify the alignment element in the `trigger` you can use the `md-menu-origin`
29793 * attribute on a child element. If no `md-menu-origin` is specified, the `md-menu`
29794 * will be used as the origin element.
29795 *
29796 * Similarly, the `md-menu-content` may specify a `md-menu-align-target` for a
29797 * `md-menu-item` to specify the node that it should try and align with.
29798 *
29799 * In this example code, we specify an icon to be our origin element, and an
29800 * icon in our menu content to be our alignment target. This ensures that both
29801 * icons are aligned when the menu opens.
29802 *
29803 * <hljs lang="html">
29804 * <md-menu>
29805 * <md-button ng-click="$mdOpenMenu($event)" class="md-icon-button" aria-label="Open some menu">
29806 * <md-icon md-menu-origin md-svg-icon="call:phone"></md-icon>
29807 * </md-button>
29808 * <md-menu-content>
29809 * <md-menu-item>
29810 * <md-button ng-click="doSomething()" aria-label="Do something">
29811 * <md-icon md-menu-align-target md-svg-icon="call:phone"></md-icon>
29812 * Do Something
29813 * </md-button>
29814 * </md-menu-item>
29815 * </md-menu-content>
29816 * </md-menu>
29817 * </hljs>
29818 *
29819 * Sometimes we want to specify alignment on the right side of an element, for example
29820 * if we have a menu on the right side a toolbar, we want to right align our menu content.
29821 *
29822 * We can specify the origin by using the `md-position-mode` attribute on both
29823 * the `x` and `y` axis. Right now only the `x-axis` has more than one option.
29824 * You may specify the default mode of `target target` or
29825 * `target-right target` to specify a right-oriented alignment target. See the
29826 * position section of the demos for more examples.
29827 *
29828 * ### Menu Offsets
29829 *
29830 * It is sometimes unavoidable to need to have a deeper level of control for
29831 * the positioning of a menu to ensure perfect alignment. `md-menu` provides
29832 * the `md-offset` attribute to allow pixel level specificty of adjusting the
29833 * exact positioning.
29834 *
29835 * This offset is provided in the format of `x y` or `n` where `n` will be used
29836 * in both the `x` and `y` axis.
29837 *
29838 * For example, to move a menu by `2px` from the top, we can use:
29839 * <hljs lang="html">
29840 * <md-menu md-offset="2 0">
29841 * <!-- menu-content -->
29842 * </md-menu>
29843 * </hljs>
29844 *
29845 * ### Auto Focus
29846 * By default, when a menu opens, `md-menu` focuses the first button in the menu content.
29847 *
29848 * But sometimes you would like to focus another specific menu item instead of the first.<br/>
29849 * This can be done by applying the `md-autofocus` directive on the given element.
29850 *
29851 * <hljs lang="html">
29852 * <md-menu-item>
29853 * <md-button md-autofocus ng-click="doSomething()">
29854 * Auto Focus
29855 * </md-button>
29856 * </md-menu-item>
29857 * </hljs>
29858 *
29859 *
29860 * ### Preventing close
29861 *
29862 * Sometimes you would like to be able to click on a menu item without having the menu
29863 * close. To do this, ngMaterial exposes the `md-prevent-menu-close` attribute which
29864 * can be added to a button inside a menu to stop the menu from automatically closing.
29865 * You can then close the menu programatically by injecting `$mdMenu` and calling
29866 * `$mdMenu.hide()`.
29867 *
29868 * <hljs lang="html">
29869 * <md-menu-item>
29870 * <md-button ng-click="doSomething()" aria-label="Do something" md-prevent-menu-close="md-prevent-menu-close">
29871 * <md-icon md-menu-align-target md-svg-icon="call:phone"></md-icon>
29872 * Do Something
29873 * </md-button>
29874 * </md-menu-item>
29875 * </hljs>
29876 *
29877 * @usage
29878 * <hljs lang="html">
29879 * <md-menu>
29880 * <md-button ng-click="$mdOpenMenu($event)" class="md-icon-button">
29881 * <md-icon md-svg-icon="call:phone"></md-icon>
29882 * </md-button>
29883 * <md-menu-content>
29884 * <md-menu-item><md-button ng-click="doSomething()">Do Something</md-button></md-menu-item>
29885 * </md-menu-content>
29886 * </md-menu>
29887 * </hljs>
29888 *
29889 * @param {string} md-position-mode The position mode in the form of
29890 * `x`, `y`. Default value is `target`,`target`. Right now the `x` axis
29891 * also suppports `target-right`.
29892 * @param {string} md-offset An offset to apply to the dropdown after positioning
29893 * `x`, `y`. Default value is `0`,`0`.
29894 *
29895 */
29896
29897MenuDirective.$inject = ["$mdUtil"];
29898angular
29899 .module('material.components.menu')
29900 .directive('mdMenu', MenuDirective);
29901
29902/**
29903 * @ngInject
29904 */
29905function MenuDirective($mdUtil) {
29906 var INVALID_PREFIX = 'Invalid HTML for md-menu: ';
29907 return {
29908 restrict: 'E',
29909 require: ['mdMenu', '?^mdMenuBar'],
29910 controller: 'mdMenuCtrl', // empty function to be built by link
29911 scope: true,
29912 compile: compile
29913 };
29914
29915 function compile(templateElement) {
29916 templateElement.addClass('md-menu');
29917 var triggerElement = templateElement.children()[0];
29918 var prefixer = $mdUtil.prefixer();
29919
29920 if (!prefixer.hasAttribute(triggerElement, 'ng-click')) {
29921 triggerElement = triggerElement
29922 .querySelector(prefixer.buildSelector(['ng-click', 'ng-mouseenter'])) || triggerElement;
29923 }
29924 if (triggerElement && (
29925 triggerElement.nodeName == 'MD-BUTTON' ||
29926 triggerElement.nodeName == 'BUTTON'
29927 ) && !triggerElement.hasAttribute('type')) {
29928 triggerElement.setAttribute('type', 'button');
29929 }
29930
29931 if (templateElement.children().length != 2) {
29932 throw Error(INVALID_PREFIX + 'Expected two children elements.');
29933 }
29934
29935 // Default element for ARIA attributes has the ngClick or ngMouseenter expression
29936 triggerElement && triggerElement.setAttribute('aria-haspopup', 'true');
29937
29938 var nestedMenus = templateElement[0].querySelectorAll('md-menu');
29939 var nestingDepth = parseInt(templateElement[0].getAttribute('md-nest-level'), 10) || 0;
29940 if (nestedMenus) {
29941 angular.forEach($mdUtil.nodesToArray(nestedMenus), function(menuEl) {
29942 if (!menuEl.hasAttribute('md-position-mode')) {
29943 menuEl.setAttribute('md-position-mode', 'cascade');
29944 }
29945 menuEl.classList.add('_md-nested-menu');
29946 menuEl.setAttribute('md-nest-level', nestingDepth + 1);
29947 });
29948 }
29949 return link;
29950 }
29951
29952 function link(scope, element, attr, ctrls) {
29953 var mdMenuCtrl = ctrls[0];
29954 var isInMenuBar = ctrls[1] != undefined;
29955 // Move everything into a md-menu-container and pass it to the controller
29956 var menuContainer = angular.element( '<div class="_md md-open-menu-container md-whiteframe-z2"></div>');
29957 var menuContents = element.children()[1];
29958
29959 element.addClass('_md'); // private md component indicator for styling
29960
29961 if (!menuContents.hasAttribute('role')) {
29962 menuContents.setAttribute('role', 'menu');
29963 }
29964 menuContainer.append(menuContents);
29965
29966 element.on('$destroy', function() {
29967 menuContainer.remove();
29968 });
29969
29970 element.append(menuContainer);
29971 menuContainer[0].style.display = 'none';
29972 mdMenuCtrl.init(menuContainer, { isInMenuBar: isInMenuBar });
29973
29974 }
29975}
29976
29977})();
29978(function(){
29979"use strict";
29980
29981
29982MenuProvider.$inject = ["$$interimElementProvider"];angular
29983 .module('material.components.menu')
29984 .provider('$mdMenu', MenuProvider);
29985
29986/*
29987 * Interim element provider for the menu.
29988 * Handles behavior for a menu while it is open, including:
29989 * - handling animating the menu opening/closing
29990 * - handling key/mouse events on the menu element
29991 * - handling enabling/disabling scroll while the menu is open
29992 * - handling redrawing during resizes and orientation changes
29993 *
29994 */
29995
29996function MenuProvider($$interimElementProvider) {
29997 menuDefaultOptions.$inject = ["$mdUtil", "$mdTheming", "$mdConstant", "$document", "$window", "$q", "$$rAF", "$animateCss", "$animate"];
29998 var MENU_EDGE_MARGIN = 8;
29999
30000 return $$interimElementProvider('$mdMenu')
30001 .setDefaults({
30002 methods: ['target'],
30003 options: menuDefaultOptions
30004 });
30005
30006 /* @ngInject */
30007 function menuDefaultOptions($mdUtil, $mdTheming, $mdConstant, $document, $window, $q, $$rAF, $animateCss, $animate) {
30008 var prefixer = $mdUtil.prefixer();
30009 var animator = $mdUtil.dom.animator;
30010
30011 return {
30012 parent: 'body',
30013 onShow: onShow,
30014 onRemove: onRemove,
30015 hasBackdrop: true,
30016 disableParentScroll: true,
30017 skipCompile: true,
30018 preserveScope: true,
30019 skipHide: true,
30020 themable: true
30021 };
30022
30023 /**
30024 * Show modal backdrop element...
30025 * @returns {function(): void} A function that removes this backdrop
30026 */
30027 function showBackdrop(scope, element, options) {
30028 if (options.nestLevel) return angular.noop;
30029
30030 // If we are not within a dialog...
30031 if (options.disableParentScroll && !$mdUtil.getClosest(options.target, 'MD-DIALOG')) {
30032 // !! DO this before creating the backdrop; since disableScrollAround()
30033 // configures the scroll offset; which is used by mdBackDrop postLink()
30034 options.restoreScroll = $mdUtil.disableScrollAround(options.element, options.parent);
30035 } else {
30036 options.disableParentScroll = false;
30037 }
30038
30039 if (options.hasBackdrop) {
30040 options.backdrop = $mdUtil.createBackdrop(scope, "md-menu-backdrop md-click-catcher");
30041
30042 $animate.enter(options.backdrop, $document[0].body);
30043 }
30044
30045 /**
30046 * Hide and destroys the backdrop created by showBackdrop()
30047 */
30048 return function hideBackdrop() {
30049 if (options.backdrop) options.backdrop.remove();
30050 if (options.disableParentScroll) options.restoreScroll();
30051 };
30052 }
30053
30054 /**
30055 * Removing the menu element from the DOM and remove all associated event listeners
30056 * and backdrop
30057 */
30058 function onRemove(scope, element, opts) {
30059 opts.cleanupInteraction && opts.cleanupInteraction();
30060 opts.cleanupResizing();
30061 opts.hideBackdrop();
30062
30063 // For navigation $destroy events, do a quick, non-animated removal,
30064 // but for normal closes (from clicks, etc) animate the removal
30065
30066 return (opts.$destroy === true) ? detachAndClean() : animateRemoval().then( detachAndClean );
30067
30068 /**
30069 * For normal closes, animate the removal.
30070 * For forced closes (like $destroy events), skip the animations
30071 */
30072 function animateRemoval() {
30073 return $animateCss(element, {addClass: 'md-leave'}).start();
30074 }
30075
30076 /**
30077 * Detach the element
30078 */
30079 function detachAndClean() {
30080 element.removeClass('md-active');
30081 detachElement(element, opts);
30082 opts.alreadyOpen = false;
30083 }
30084
30085 }
30086
30087 /**
30088 * Inserts and configures the staged Menu element into the DOM, positioning it,
30089 * and wiring up various interaction events
30090 */
30091 function onShow(scope, element, opts) {
30092 sanitizeAndConfigure(opts);
30093
30094 // Wire up theming on our menu element
30095 $mdTheming.inherit(opts.menuContentEl, opts.target);
30096
30097 // Register various listeners to move menu on resize/orientation change
30098 opts.cleanupResizing = startRepositioningOnResize();
30099 opts.hideBackdrop = showBackdrop(scope, element, opts);
30100
30101 // Return the promise for when our menu is done animating in
30102 return showMenu()
30103 .then(function(response) {
30104 opts.alreadyOpen = true;
30105 opts.cleanupInteraction = activateInteraction();
30106 return response;
30107 });
30108
30109 /**
30110 * Place the menu into the DOM and call positioning related functions
30111 */
30112 function showMenu() {
30113 opts.parent.append(element);
30114 element[0].style.display = '';
30115
30116 return $q(function(resolve) {
30117 var position = calculateMenuPosition(element, opts);
30118
30119 element.removeClass('md-leave');
30120
30121 // Animate the menu scaling, and opacity [from its position origin (default == top-left)]
30122 // to normal scale.
30123 $animateCss(element, {
30124 addClass: 'md-active',
30125 from: animator.toCss(position),
30126 to: animator.toCss({transform: ''})
30127 })
30128 .start()
30129 .then(resolve);
30130
30131 });
30132 }
30133
30134 /**
30135 * Check for valid opts and set some sane defaults
30136 */
30137 function sanitizeAndConfigure() {
30138 if (!opts.target) {
30139 throw Error(
30140 '$mdMenu.show() expected a target to animate from in options.target'
30141 );
30142 }
30143 angular.extend(opts, {
30144 alreadyOpen: false,
30145 isRemoved: false,
30146 target: angular.element(opts.target), //make sure it's not a naked dom node
30147 parent: angular.element(opts.parent),
30148 menuContentEl: angular.element(element[0].querySelector('md-menu-content'))
30149 });
30150 }
30151
30152 /**
30153 * Configure various resize listeners for screen changes
30154 */
30155 function startRepositioningOnResize() {
30156
30157 var repositionMenu = (function(target, options) {
30158 return $$rAF.throttle(function() {
30159 if (opts.isRemoved) return;
30160 var position = calculateMenuPosition(target, options);
30161
30162 target.css(animator.toCss(position));
30163 });
30164 })(element, opts);
30165
30166 $window.addEventListener('resize', repositionMenu);
30167 $window.addEventListener('orientationchange', repositionMenu);
30168
30169 return function stopRepositioningOnResize() {
30170
30171 // Disable resizing handlers
30172 $window.removeEventListener('resize', repositionMenu);
30173 $window.removeEventListener('orientationchange', repositionMenu);
30174
30175 }
30176 }
30177
30178 /**
30179 * Activate interaction on the menu. Wire up keyboard listerns for
30180 * clicks, keypresses, backdrop closing, etc.
30181 */
30182 function activateInteraction() {
30183 element.addClass('md-clickable');
30184
30185 // close on backdrop click
30186 if (opts.backdrop) opts.backdrop.on('click', onBackdropClick);
30187
30188 // Wire up keyboard listeners.
30189 // - Close on escape,
30190 // - focus next item on down arrow,
30191 // - focus prev item on up
30192 opts.menuContentEl.on('keydown', onMenuKeyDown);
30193 opts.menuContentEl[0].addEventListener('click', captureClickListener, true);
30194
30195 // kick off initial focus in the menu on the first element
30196 var focusTarget = opts.menuContentEl[0]
30197 .querySelector(prefixer.buildSelector(['md-menu-focus-target', 'md-autofocus']));
30198
30199 if ( !focusTarget ) {
30200 var firstChild = opts.menuContentEl[0].firstElementChild;
30201
30202 focusTarget = firstChild && (firstChild.querySelector('.md-button:not([disabled])') || firstChild.firstElementChild);
30203 }
30204
30205 focusTarget && focusTarget.focus();
30206
30207 return function cleanupInteraction() {
30208 element.removeClass('md-clickable');
30209 if (opts.backdrop) opts.backdrop.off('click', onBackdropClick);
30210 opts.menuContentEl.off('keydown', onMenuKeyDown);
30211 opts.menuContentEl[0].removeEventListener('click', captureClickListener, true);
30212 };
30213
30214 // ************************************
30215 // internal functions
30216 // ************************************
30217
30218 function onMenuKeyDown(ev) {
30219 var handled;
30220 switch (ev.keyCode) {
30221 case $mdConstant.KEY_CODE.ESCAPE:
30222 opts.mdMenuCtrl.close(false, { closeAll: true });
30223 handled = true;
30224 break;
30225 case $mdConstant.KEY_CODE.UP_ARROW:
30226 if (!focusMenuItem(ev, opts.menuContentEl, opts, -1) && !opts.nestLevel) {
30227 opts.mdMenuCtrl.triggerContainerProxy(ev);
30228 }
30229 handled = true;
30230 break;
30231 case $mdConstant.KEY_CODE.DOWN_ARROW:
30232 if (!focusMenuItem(ev, opts.menuContentEl, opts, 1) && !opts.nestLevel) {
30233 opts.mdMenuCtrl.triggerContainerProxy(ev);
30234 }
30235 handled = true;
30236 break;
30237 case $mdConstant.KEY_CODE.LEFT_ARROW:
30238 if (opts.nestLevel) {
30239 opts.mdMenuCtrl.close();
30240 } else {
30241 opts.mdMenuCtrl.triggerContainerProxy(ev);
30242 }
30243 handled = true;
30244 break;
30245 case $mdConstant.KEY_CODE.RIGHT_ARROW:
30246 var parentMenu = $mdUtil.getClosest(ev.target, 'MD-MENU');
30247 if (parentMenu && parentMenu != opts.parent[0]) {
30248 ev.target.click();
30249 } else {
30250 opts.mdMenuCtrl.triggerContainerProxy(ev);
30251 }
30252 handled = true;
30253 break;
30254 }
30255 if (handled) {
30256 ev.preventDefault();
30257 ev.stopImmediatePropagation();
30258 }
30259 }
30260
30261 function onBackdropClick(e) {
30262 e.preventDefault();
30263 e.stopPropagation();
30264 scope.$apply(function() {
30265 opts.mdMenuCtrl.close(true, { closeAll: true });
30266 });
30267 }
30268
30269 // Close menu on menu item click, if said menu-item is not disabled
30270 function captureClickListener(e) {
30271 var target = e.target;
30272 // Traverse up the event until we get to the menuContentEl to see if
30273 // there is an ng-click and that the ng-click is not disabled
30274 do {
30275 if (target == opts.menuContentEl[0]) return;
30276 if ((hasAnyAttribute(target, ['ng-click', 'ng-href', 'ui-sref']) ||
30277 target.nodeName == 'BUTTON' || target.nodeName == 'MD-BUTTON') && !hasAnyAttribute(target, ['md-prevent-menu-close'])) {
30278 var closestMenu = $mdUtil.getClosest(target, 'MD-MENU');
30279 if (!target.hasAttribute('disabled') && (!closestMenu || closestMenu == opts.parent[0])) {
30280 close();
30281 }
30282 break;
30283 }
30284 } while (target = target.parentNode)
30285
30286 function close() {
30287 scope.$apply(function() {
30288 opts.mdMenuCtrl.close(true, { closeAll: true });
30289 });
30290 }
30291
30292 function hasAnyAttribute(target, attrs) {
30293 if (!target) return false;
30294
30295 for (var i = 0, attr; attr = attrs[i]; ++i) {
30296 if (prefixer.hasAttribute(target, attr)) {
30297 return true;
30298 }
30299 }
30300
30301 return false;
30302 }
30303 }
30304
30305 }
30306 }
30307
30308 /**
30309 * Takes a keypress event and focuses the next/previous menu
30310 * item from the emitting element
30311 * @param {event} e - The origin keypress event
30312 * @param {angular.element} menuEl - The menu element
30313 * @param {object} opts - The interim element options for the mdMenu
30314 * @param {number} direction - The direction to move in (+1 = next, -1 = prev)
30315 */
30316 function focusMenuItem(e, menuEl, opts, direction) {
30317 var currentItem = $mdUtil.getClosest(e.target, 'MD-MENU-ITEM');
30318
30319 var items = $mdUtil.nodesToArray(menuEl[0].children);
30320 var currentIndex = items.indexOf(currentItem);
30321
30322 // Traverse through our elements in the specified direction (+/-1) and try to
30323 // focus them until we find one that accepts focus
30324 var didFocus;
30325 for (var i = currentIndex + direction; i >= 0 && i < items.length; i = i + direction) {
30326 var focusTarget = items[i].querySelector('.md-button');
30327 didFocus = attemptFocus(focusTarget);
30328 if (didFocus) {
30329 break;
30330 }
30331 }
30332 return didFocus;
30333 }
30334
30335 /**
30336 * Attempts to focus an element. Checks whether that element is the currently
30337 * focused element after attempting.
30338 * @param {HTMLElement} el - the element to attempt focus on
30339 * @returns {bool} - whether the element was successfully focused
30340 */
30341 function attemptFocus(el) {
30342 if (el && el.getAttribute('tabindex') != -1) {
30343 el.focus();
30344 return ($document[0].activeElement == el);
30345 }
30346 }
30347
30348 /**
30349 * Use browser to remove this element without triggering a $destroy event
30350 */
30351 function detachElement(element, opts) {
30352 if (!opts.preserveElement) {
30353 if (toNode(element).parentNode === toNode(opts.parent)) {
30354 toNode(opts.parent).removeChild(toNode(element));
30355 }
30356 } else {
30357 toNode(element).style.display = 'none';
30358 }
30359 }
30360
30361 /**
30362 * Computes menu position and sets the style on the menu container
30363 * @param {HTMLElement} el - the menu container element
30364 * @param {object} opts - the interim element options object
30365 */
30366 function calculateMenuPosition(el, opts) {
30367
30368 var containerNode = el[0],
30369 openMenuNode = el[0].firstElementChild,
30370 openMenuNodeRect = openMenuNode.getBoundingClientRect(),
30371 boundryNode = $document[0].body,
30372 boundryNodeRect = boundryNode.getBoundingClientRect();
30373
30374 var menuStyle = $window.getComputedStyle(openMenuNode);
30375
30376 var originNode = opts.target[0].querySelector(prefixer.buildSelector('md-menu-origin')) || opts.target[0],
30377 originNodeRect = originNode.getBoundingClientRect();
30378
30379 var bounds = {
30380 left: boundryNodeRect.left + MENU_EDGE_MARGIN,
30381 top: Math.max(boundryNodeRect.top, 0) + MENU_EDGE_MARGIN,
30382 bottom: Math.max(boundryNodeRect.bottom, Math.max(boundryNodeRect.top, 0) + boundryNodeRect.height) - MENU_EDGE_MARGIN,
30383 right: boundryNodeRect.right - MENU_EDGE_MARGIN
30384 };
30385
30386 var alignTarget, alignTargetRect = { top:0, left : 0, right:0, bottom:0 }, existingOffsets = { top:0, left : 0, right:0, bottom:0 };
30387 var positionMode = opts.mdMenuCtrl.positionMode();
30388
30389 if (positionMode.top == 'target' || positionMode.left == 'target' || positionMode.left == 'target-right') {
30390 alignTarget = firstVisibleChild();
30391 if ( alignTarget ) {
30392 // TODO: Allow centering on an arbitrary node, for now center on first menu-item's child
30393 alignTarget = alignTarget.firstElementChild || alignTarget;
30394 alignTarget = alignTarget.querySelector(prefixer.buildSelector('md-menu-align-target')) || alignTarget;
30395 alignTargetRect = alignTarget.getBoundingClientRect();
30396
30397 existingOffsets = {
30398 top: parseFloat(containerNode.style.top || 0),
30399 left: parseFloat(containerNode.style.left || 0)
30400 };
30401 }
30402 }
30403
30404 var position = {};
30405 var transformOrigin = 'top ';
30406
30407 switch (positionMode.top) {
30408 case 'target':
30409 position.top = existingOffsets.top + originNodeRect.top - alignTargetRect.top;
30410 break;
30411 case 'cascade':
30412 position.top = originNodeRect.top - parseFloat(menuStyle.paddingTop) - originNode.style.top;
30413 break;
30414 case 'bottom':
30415 position.top = originNodeRect.top + originNodeRect.height;
30416 break;
30417 default:
30418 throw new Error('Invalid target mode "' + positionMode.top + '" specified for md-menu on Y axis.');
30419 }
30420
30421 var rtl = ($mdUtil.bidi() == 'rtl');
30422
30423 switch (positionMode.left) {
30424 case 'target':
30425 position.left = existingOffsets.left + originNodeRect.left - alignTargetRect.left;
30426 transformOrigin += rtl ? 'right' : 'left';
30427 break;
30428 case 'target-left':
30429 position.left = originNodeRect.left;
30430 transformOrigin += 'left';
30431 break;
30432 case 'target-right':
30433 position.left = originNodeRect.right - openMenuNodeRect.width + (openMenuNodeRect.right - alignTargetRect.right);
30434 transformOrigin += 'right';
30435 break;
30436 case 'cascade':
30437 var willFitRight = rtl ? (originNodeRect.left - openMenuNodeRect.width) < bounds.left : (originNodeRect.right + openMenuNodeRect.width) < bounds.right;
30438 position.left = willFitRight ? originNodeRect.right - originNode.style.left : originNodeRect.left - originNode.style.left - openMenuNodeRect.width;
30439 transformOrigin += willFitRight ? 'left' : 'right';
30440 break;
30441 case 'right':
30442 if (rtl) {
30443 position.left = originNodeRect.right - originNodeRect.width;
30444 transformOrigin += 'left';
30445 } else {
30446 position.left = originNodeRect.right - openMenuNodeRect.width;
30447 transformOrigin += 'right';
30448 }
30449 break;
30450 case 'left':
30451 if (rtl) {
30452 position.left = originNodeRect.right - openMenuNodeRect.width;
30453 transformOrigin += 'right';
30454 } else {
30455 position.left = originNodeRect.left;
30456 transformOrigin += 'left';
30457 }
30458 break;
30459 default:
30460 throw new Error('Invalid target mode "' + positionMode.left + '" specified for md-menu on X axis.');
30461 }
30462
30463 var offsets = opts.mdMenuCtrl.offsets();
30464 position.top += offsets.top;
30465 position.left += offsets.left;
30466
30467 clamp(position);
30468
30469 var scaleX = Math.round(100 * Math.min(originNodeRect.width / containerNode.offsetWidth, 1.0)) / 100;
30470 var scaleY = Math.round(100 * Math.min(originNodeRect.height / containerNode.offsetHeight, 1.0)) / 100;
30471
30472 return {
30473 top: Math.round(position.top),
30474 left: Math.round(position.left),
30475 // Animate a scale out if we aren't just repositioning
30476 transform: !opts.alreadyOpen ? $mdUtil.supplant('scale({0},{1})', [scaleX, scaleY]) : undefined,
30477 transformOrigin: transformOrigin
30478 };
30479
30480 /**
30481 * Clamps the repositioning of the menu within the confines of
30482 * bounding element (often the screen/body)
30483 */
30484 function clamp(pos) {
30485 pos.top = Math.max(Math.min(pos.top, bounds.bottom - containerNode.offsetHeight), bounds.top);
30486 pos.left = Math.max(Math.min(pos.left, bounds.right - containerNode.offsetWidth), bounds.left);
30487 }
30488
30489 /**
30490 * Gets the first visible child in the openMenuNode
30491 * Necessary incase menu nodes are being dynamically hidden
30492 */
30493 function firstVisibleChild() {
30494 for (var i = 0; i < openMenuNode.children.length; ++i) {
30495 if ($window.getComputedStyle(openMenuNode.children[i]).display != 'none') {
30496 return openMenuNode.children[i];
30497 }
30498 }
30499 }
30500 }
30501 }
30502 function toNode(el) {
30503 if (el instanceof angular.element) {
30504 el = el[0];
30505 }
30506 return el;
30507 }
30508}
30509
30510})();
30511(function(){
30512"use strict";
30513
30514
30515MenuBarController.$inject = ["$scope", "$rootScope", "$element", "$attrs", "$mdConstant", "$document", "$mdUtil", "$timeout"];
30516angular
30517 .module('material.components.menuBar')
30518 .controller('MenuBarController', MenuBarController);
30519
30520var BOUND_MENU_METHODS = ['handleKeyDown', 'handleMenuHover', 'scheduleOpenHoveredMenu', 'cancelScheduledOpen'];
30521
30522/**
30523 * @ngInject
30524 */
30525function MenuBarController($scope, $rootScope, $element, $attrs, $mdConstant, $document, $mdUtil, $timeout) {
30526 this.$element = $element;
30527 this.$attrs = $attrs;
30528 this.$mdConstant = $mdConstant;
30529 this.$mdUtil = $mdUtil;
30530 this.$document = $document;
30531 this.$scope = $scope;
30532 this.$rootScope = $rootScope;
30533 this.$timeout = $timeout;
30534
30535 var self = this;
30536 angular.forEach(BOUND_MENU_METHODS, function(methodName) {
30537 self[methodName] = angular.bind(self, self[methodName]);
30538 });
30539}
30540
30541MenuBarController.prototype.init = function() {
30542 var $element = this.$element;
30543 var $mdUtil = this.$mdUtil;
30544 var $scope = this.$scope;
30545
30546 var self = this;
30547 var deregisterFns = [];
30548 $element.on('keydown', this.handleKeyDown);
30549 this.parentToolbar = $mdUtil.getClosest($element, 'MD-TOOLBAR');
30550
30551 deregisterFns.push(this.$rootScope.$on('$mdMenuOpen', function(event, el) {
30552 if (self.getMenus().indexOf(el[0]) != -1) {
30553 $element[0].classList.add('md-open');
30554 el[0].classList.add('md-open');
30555 self.currentlyOpenMenu = el.controller('mdMenu');
30556 self.currentlyOpenMenu.registerContainerProxy(self.handleKeyDown);
30557 self.enableOpenOnHover();
30558 }
30559 }));
30560
30561 deregisterFns.push(this.$rootScope.$on('$mdMenuClose', function(event, el, opts) {
30562 var rootMenus = self.getMenus();
30563 if (rootMenus.indexOf(el[0]) != -1) {
30564 $element[0].classList.remove('md-open');
30565 el[0].classList.remove('md-open');
30566 }
30567
30568 if ($element[0].contains(el[0])) {
30569 var parentMenu = el[0];
30570 while (parentMenu && rootMenus.indexOf(parentMenu) == -1) {
30571 parentMenu = $mdUtil.getClosest(parentMenu, 'MD-MENU', true);
30572 }
30573 if (parentMenu) {
30574 if (!opts.skipFocus) parentMenu.querySelector('button:not([disabled])').focus();
30575 self.currentlyOpenMenu = undefined;
30576 self.disableOpenOnHover();
30577 self.setKeyboardMode(true);
30578 }
30579 }
30580 }));
30581
30582 $scope.$on('$destroy', function() {
30583 self.disableOpenOnHover();
30584 while (deregisterFns.length) {
30585 deregisterFns.shift()();
30586 }
30587 });
30588
30589
30590 this.setKeyboardMode(true);
30591};
30592
30593MenuBarController.prototype.setKeyboardMode = function(enabled) {
30594 if (enabled) this.$element[0].classList.add('md-keyboard-mode');
30595 else this.$element[0].classList.remove('md-keyboard-mode');
30596};
30597
30598MenuBarController.prototype.enableOpenOnHover = function() {
30599 if (this.openOnHoverEnabled) return;
30600
30601 var self = this;
30602
30603 self.openOnHoverEnabled = true;
30604
30605 if (self.parentToolbar) {
30606 self.parentToolbar.classList.add('md-has-open-menu');
30607
30608 // Needs to be on the next tick so it doesn't close immediately.
30609 self.$mdUtil.nextTick(function() {
30610 angular.element(self.parentToolbar).on('click', self.handleParentClick);
30611 }, false);
30612 }
30613
30614 angular
30615 .element(self.getMenus())
30616 .on('mouseenter', self.handleMenuHover);
30617};
30618
30619MenuBarController.prototype.handleMenuHover = function(e) {
30620 this.setKeyboardMode(false);
30621 if (this.openOnHoverEnabled) {
30622 this.scheduleOpenHoveredMenu(e);
30623 }
30624};
30625
30626MenuBarController.prototype.disableOpenOnHover = function() {
30627 if (!this.openOnHoverEnabled) return;
30628
30629 this.openOnHoverEnabled = false;
30630
30631 if (this.parentToolbar) {
30632 this.parentToolbar.classList.remove('md-has-open-menu');
30633 angular.element(this.parentToolbar).off('click', this.handleParentClick);
30634 }
30635
30636 angular
30637 .element(this.getMenus())
30638 .off('mouseenter', this.handleMenuHover);
30639};
30640
30641MenuBarController.prototype.scheduleOpenHoveredMenu = function(e) {
30642 var menuEl = angular.element(e.currentTarget);
30643 var menuCtrl = menuEl.controller('mdMenu');
30644 this.setKeyboardMode(false);
30645 this.scheduleOpenMenu(menuCtrl);
30646};
30647
30648MenuBarController.prototype.scheduleOpenMenu = function(menuCtrl) {
30649 var self = this;
30650 var $timeout = this.$timeout;
30651 if (menuCtrl != self.currentlyOpenMenu) {
30652 $timeout.cancel(self.pendingMenuOpen);
30653 self.pendingMenuOpen = $timeout(function() {
30654 self.pendingMenuOpen = undefined;
30655 if (self.currentlyOpenMenu) {
30656 self.currentlyOpenMenu.close(true, { closeAll: true });
30657 }
30658 menuCtrl.open();
30659 }, 200, false);
30660 }
30661};
30662
30663MenuBarController.prototype.handleKeyDown = function(e) {
30664 var keyCodes = this.$mdConstant.KEY_CODE;
30665 var currentMenu = this.currentlyOpenMenu;
30666 var wasOpen = currentMenu && currentMenu.isOpen;
30667 this.setKeyboardMode(true);
30668 var handled, newMenu, newMenuCtrl;
30669 switch (e.keyCode) {
30670 case keyCodes.DOWN_ARROW:
30671 if (currentMenu) {
30672 currentMenu.focusMenuContainer();
30673 } else {
30674 this.openFocusedMenu();
30675 }
30676 handled = true;
30677 break;
30678 case keyCodes.UP_ARROW:
30679 currentMenu && currentMenu.close();
30680 handled = true;
30681 break;
30682 case keyCodes.LEFT_ARROW:
30683 newMenu = this.focusMenu(-1);
30684 if (wasOpen) {
30685 newMenuCtrl = angular.element(newMenu).controller('mdMenu');
30686 this.scheduleOpenMenu(newMenuCtrl);
30687 }
30688 handled = true;
30689 break;
30690 case keyCodes.RIGHT_ARROW:
30691 newMenu = this.focusMenu(+1);
30692 if (wasOpen) {
30693 newMenuCtrl = angular.element(newMenu).controller('mdMenu');
30694 this.scheduleOpenMenu(newMenuCtrl);
30695 }
30696 handled = true;
30697 break;
30698 }
30699 if (handled) {
30700 e && e.preventDefault && e.preventDefault();
30701 e && e.stopImmediatePropagation && e.stopImmediatePropagation();
30702 }
30703};
30704
30705MenuBarController.prototype.focusMenu = function(direction) {
30706 var menus = this.getMenus();
30707 var focusedIndex = this.getFocusedMenuIndex();
30708
30709 if (focusedIndex == -1) { focusedIndex = this.getOpenMenuIndex(); }
30710
30711 var changed = false;
30712
30713 if (focusedIndex == -1) { focusedIndex = 0; changed = true; }
30714 else if (
30715 direction < 0 && focusedIndex > 0 ||
30716 direction > 0 && focusedIndex < menus.length - direction
30717 ) {
30718 focusedIndex += direction;
30719 changed = true;
30720 }
30721 if (changed) {
30722 menus[focusedIndex].querySelector('button').focus();
30723 return menus[focusedIndex];
30724 }
30725};
30726
30727MenuBarController.prototype.openFocusedMenu = function() {
30728 var menu = this.getFocusedMenu();
30729 menu && angular.element(menu).controller('mdMenu').open();
30730};
30731
30732MenuBarController.prototype.getMenus = function() {
30733 var $element = this.$element;
30734 return this.$mdUtil.nodesToArray($element[0].children)
30735 .filter(function(el) { return el.nodeName == 'MD-MENU'; });
30736};
30737
30738MenuBarController.prototype.getFocusedMenu = function() {
30739 return this.getMenus()[this.getFocusedMenuIndex()];
30740};
30741
30742MenuBarController.prototype.getFocusedMenuIndex = function() {
30743 var $mdUtil = this.$mdUtil;
30744 var focusedEl = $mdUtil.getClosest(
30745 this.$document[0].activeElement,
30746 'MD-MENU'
30747 );
30748 if (!focusedEl) return -1;
30749
30750 var focusedIndex = this.getMenus().indexOf(focusedEl);
30751 return focusedIndex;
30752};
30753
30754MenuBarController.prototype.getOpenMenuIndex = function() {
30755 var menus = this.getMenus();
30756 for (var i = 0; i < menus.length; ++i) {
30757 if (menus[i].classList.contains('md-open')) return i;
30758 }
30759 return -1;
30760};
30761
30762MenuBarController.prototype.handleParentClick = function(event) {
30763 var openMenu = this.querySelector('md-menu.md-open');
30764
30765 if (openMenu && !openMenu.contains(event.target)) {
30766 angular.element(openMenu).controller('mdMenu').close();
30767 }
30768};
30769
30770
30771
30772
30773
30774
30775
30776})();
30777(function(){
30778"use strict";
30779
30780/**
30781 * @ngdoc directive
30782 * @name mdMenuBar
30783 * @module material.components.menu-bar
30784 * @restrict E
30785 * @description
30786 *
30787 * Menu bars are containers that hold multiple menus. They change the behavior and appearence
30788 * of the `md-menu` directive to behave similar to an operating system provided menu.
30789 *
30790 * @usage
30791 * <hljs lang="html">
30792 * <md-menu-bar>
30793 * <md-menu>
30794 * <button ng-click="$mdOpenMenu()">
30795 * File
30796 * </button>
30797 * <md-menu-content>
30798 * <md-menu-item>
30799 * <md-button ng-click="ctrl.sampleAction('share', $event)">
30800 * Share...
30801 * </md-button>
30802 * </md-menu-item>
30803 * <md-menu-divider></md-menu-divider>
30804 * <md-menu-item>
30805 * <md-menu-item>
30806 * <md-menu>
30807 * <md-button ng-click="$mdOpenMenu()">New</md-button>
30808 * <md-menu-content>
30809 * <md-menu-item><md-button ng-click="ctrl.sampleAction('New Document', $event)">Document</md-button></md-menu-item>
30810 * <md-menu-item><md-button ng-click="ctrl.sampleAction('New Spreadsheet', $event)">Spreadsheet</md-button></md-menu-item>
30811 * <md-menu-item><md-button ng-click="ctrl.sampleAction('New Presentation', $event)">Presentation</md-button></md-menu-item>
30812 * <md-menu-item><md-button ng-click="ctrl.sampleAction('New Form', $event)">Form</md-button></md-menu-item>
30813 * <md-menu-item><md-button ng-click="ctrl.sampleAction('New Drawing', $event)">Drawing</md-button></md-menu-item>
30814 * </md-menu-content>
30815 * </md-menu>
30816 * </md-menu-item>
30817 * </md-menu-content>
30818 * </md-menu>
30819 * </md-menu-bar>
30820 * </hljs>
30821 *
30822 * ## Menu Bar Controls
30823 *
30824 * You may place `md-menu-items` that function as controls within menu bars.
30825 * There are two modes that are exposed via the `type` attribute of the `md-menu-item`.
30826 * `type="checkbox"` will function as a boolean control for the `ng-model` attribute of the
30827 * `md-menu-item`. `type="radio"` will function like a radio button, setting the `ngModel`
30828 * to the `string` value of the `value` attribute. If you need non-string values, you can use
30829 * `ng-value` to provide an expression (this is similar to how angular's native `input[type=radio]` works.
30830 *
30831 * <hljs lang="html">
30832 * <md-menu-bar>
30833 * <md-menu>
30834 * <button ng-click="$mdOpenMenu()">
30835 * Sample Menu
30836 * </button>
30837 * <md-menu-content>
30838 * <md-menu-item type="checkbox" ng-model="settings.allowChanges">Allow changes</md-menu-item>
30839 * <md-menu-divider></md-menu-divider>
30840 * <md-menu-item type="radio" ng-model="settings.mode" ng-value="1">Mode 1</md-menu-item>
30841 * <md-menu-item type="radio" ng-model="settings.mode" ng-value="1">Mode 2</md-menu-item>
30842 * <md-menu-item type="radio" ng-model="settings.mode" ng-value="1">Mode 3</md-menu-item>
30843 * </md-menu-content>
30844 * </md-menu>
30845 * </md-menu-bar>
30846 * </hljs>
30847 *
30848 *
30849 * ### Nesting Menus
30850 *
30851 * Menus may be nested within menu bars. This is commonly called cascading menus.
30852 * To nest a menu place the nested menu inside the content of the `md-menu-item`.
30853 * <hljs lang="html">
30854 * <md-menu-item>
30855 * <md-menu>
30856 * <button ng-click="$mdOpenMenu()">New</md-button>
30857 * <md-menu-content>
30858 * <md-menu-item><md-button ng-click="ctrl.sampleAction('New Document', $event)">Document</md-button></md-menu-item>
30859 * <md-menu-item><md-button ng-click="ctrl.sampleAction('New Spreadsheet', $event)">Spreadsheet</md-button></md-menu-item>
30860 * <md-menu-item><md-button ng-click="ctrl.sampleAction('New Presentation', $event)">Presentation</md-button></md-menu-item>
30861 * <md-menu-item><md-button ng-click="ctrl.sampleAction('New Form', $event)">Form</md-button></md-menu-item>
30862 * <md-menu-item><md-button ng-click="ctrl.sampleAction('New Drawing', $event)">Drawing</md-button></md-menu-item>
30863 * </md-menu-content>
30864 * </md-menu>
30865 * </md-menu-item>
30866 * </hljs>
30867 *
30868 */
30869
30870MenuBarDirective.$inject = ["$mdUtil", "$mdTheming"];
30871angular
30872 .module('material.components.menuBar')
30873 .directive('mdMenuBar', MenuBarDirective);
30874
30875/* @ngInject */
30876function MenuBarDirective($mdUtil, $mdTheming) {
30877 return {
30878 restrict: 'E',
30879 require: 'mdMenuBar',
30880 controller: 'MenuBarController',
30881
30882 compile: function compile(templateEl, templateAttrs) {
30883 if (!templateAttrs.ariaRole) {
30884 templateEl[0].setAttribute('role', 'menubar');
30885 }
30886 angular.forEach(templateEl[0].children, function(menuEl) {
30887 if (menuEl.nodeName == 'MD-MENU') {
30888 if (!menuEl.hasAttribute('md-position-mode')) {
30889 menuEl.setAttribute('md-position-mode', 'left bottom');
30890
30891 // Since we're in the compile function and actual `md-buttons` are not compiled yet,
30892 // we need to query for possible `md-buttons` as well.
30893 menuEl.querySelector('button, a, md-button').setAttribute('role', 'menuitem');
30894 }
30895 var contentEls = $mdUtil.nodesToArray(menuEl.querySelectorAll('md-menu-content'));
30896 angular.forEach(contentEls, function(contentEl) {
30897 contentEl.classList.add('md-menu-bar-menu');
30898 contentEl.classList.add('md-dense');
30899 if (!contentEl.hasAttribute('width')) {
30900 contentEl.setAttribute('width', 5);
30901 }
30902 });
30903 }
30904 });
30905
30906 // Mark the child menu items that they're inside a menu bar. This is necessary,
30907 // because mnMenuItem has special behaviour during compilation, depending on
30908 // whether it is inside a mdMenuBar. We can usually figure this out via the DOM,
30909 // however if a directive that uses documentFragment is applied to the child (e.g. ngRepeat),
30910 // the element won't have a parent and won't compile properly.
30911 templateEl.find('md-menu-item').addClass('md-in-menu-bar');
30912
30913 return function postLink(scope, el, attr, ctrl) {
30914 el.addClass('_md'); // private md component indicator for styling
30915 $mdTheming(scope, el);
30916 ctrl.init();
30917 };
30918 }
30919 };
30920
30921}
30922
30923})();
30924(function(){
30925"use strict";
30926
30927
30928angular
30929 .module('material.components.menuBar')
30930 .directive('mdMenuDivider', MenuDividerDirective);
30931
30932
30933function MenuDividerDirective() {
30934 return {
30935 restrict: 'E',
30936 compile: function(templateEl, templateAttrs) {
30937 if (!templateAttrs.role) {
30938 templateEl[0].setAttribute('role', 'separator');
30939 }
30940 }
30941 };
30942}
30943
30944})();
30945(function(){
30946"use strict";
30947
30948
30949MenuItemController.$inject = ["$scope", "$element", "$attrs"];
30950angular
30951 .module('material.components.menuBar')
30952 .controller('MenuItemController', MenuItemController);
30953
30954
30955/**
30956 * @ngInject
30957 */
30958function MenuItemController($scope, $element, $attrs) {
30959 this.$element = $element;
30960 this.$attrs = $attrs;
30961 this.$scope = $scope;
30962}
30963
30964MenuItemController.prototype.init = function(ngModel) {
30965 var $element = this.$element;
30966 var $attrs = this.$attrs;
30967
30968 this.ngModel = ngModel;
30969 if ($attrs.type == 'checkbox' || $attrs.type == 'radio') {
30970 this.mode = $attrs.type;
30971 this.iconEl = $element[0].children[0];
30972 this.buttonEl = $element[0].children[1];
30973 if (ngModel) {
30974 // Clear ngAria set attributes
30975 this.initClickListeners();
30976 }
30977 }
30978};
30979
30980// ngAria auto sets attributes on a menu-item with a ngModel.
30981// We don't want this because our content (buttons) get the focus
30982// and set their own aria attributes appropritately. Having both
30983// breaks NVDA / JAWS. This undeoes ngAria's attrs.
30984MenuItemController.prototype.clearNgAria = function() {
30985 var el = this.$element[0];
30986 var clearAttrs = ['role', 'tabindex', 'aria-invalid', 'aria-checked'];
30987 angular.forEach(clearAttrs, function(attr) {
30988 el.removeAttribute(attr);
30989 });
30990};
30991
30992MenuItemController.prototype.initClickListeners = function() {
30993 var self = this;
30994 var ngModel = this.ngModel;
30995 var $scope = this.$scope;
30996 var $attrs = this.$attrs;
30997 var $element = this.$element;
30998 var mode = this.mode;
30999
31000 this.handleClick = angular.bind(this, this.handleClick);
31001
31002 var icon = this.iconEl;
31003 var button = angular.element(this.buttonEl);
31004 var handleClick = this.handleClick;
31005
31006 $attrs.$observe('disabled', setDisabled);
31007 setDisabled($attrs.disabled);
31008
31009 ngModel.$render = function render() {
31010 self.clearNgAria();
31011 if (isSelected()) {
31012 icon.style.display = '';
31013 button.attr('aria-checked', 'true');
31014 } else {
31015 icon.style.display = 'none';
31016 button.attr('aria-checked', 'false');
31017 }
31018 };
31019
31020 $scope.$$postDigest(ngModel.$render);
31021
31022 function isSelected() {
31023 if (mode == 'radio') {
31024 var val = $attrs.ngValue ? $scope.$eval($attrs.ngValue) : $attrs.value;
31025 return ngModel.$modelValue == val;
31026 } else {
31027 return ngModel.$modelValue;
31028 }
31029 }
31030
31031 function setDisabled(disabled) {
31032 if (disabled) {
31033 button.off('click', handleClick);
31034 } else {
31035 button.on('click', handleClick);
31036 }
31037 }
31038};
31039
31040MenuItemController.prototype.handleClick = function(e) {
31041 var mode = this.mode;
31042 var ngModel = this.ngModel;
31043 var $attrs = this.$attrs;
31044 var newVal;
31045 if (mode == 'checkbox') {
31046 newVal = !ngModel.$modelValue;
31047 } else if (mode == 'radio') {
31048 newVal = $attrs.ngValue ? this.$scope.$eval($attrs.ngValue) : $attrs.value;
31049 }
31050 ngModel.$setViewValue(newVal);
31051 ngModel.$render();
31052};
31053
31054})();
31055(function(){
31056"use strict";
31057
31058
31059MenuItemDirective.$inject = ["$mdUtil", "$$mdSvgRegistry"];
31060angular
31061 .module('material.components.menuBar')
31062 .directive('mdMenuItem', MenuItemDirective);
31063
31064 /* @ngInject */
31065function MenuItemDirective($mdUtil, $$mdSvgRegistry) {
31066 return {
31067 controller: 'MenuItemController',
31068 require: ['mdMenuItem', '?ngModel'],
31069 priority: 210, // ensure that our post link runs after ngAria
31070 compile: function(templateEl, templateAttrs) {
31071 var type = templateAttrs.type;
31072 var inMenuBarClass = 'md-in-menu-bar';
31073
31074 // Note: This allows us to show the `check` icon for the md-menu-bar items.
31075 // The `md-in-menu-bar` class is set by the mdMenuBar directive.
31076 if ((type == 'checkbox' || type == 'radio') && templateEl.hasClass(inMenuBarClass)) {
31077 var text = templateEl[0].textContent;
31078 var buttonEl = angular.element('<md-button type="button"></md-button>');
31079 var iconTemplate = '<md-icon md-svg-src="' + $$mdSvgRegistry.mdChecked + '"></md-icon>';
31080
31081 buttonEl.html(text);
31082 buttonEl.attr('tabindex', '0');
31083
31084 templateEl.html('');
31085 templateEl.append(angular.element(iconTemplate));
31086 templateEl.append(buttonEl);
31087 templateEl.addClass('md-indent').removeClass(inMenuBarClass);
31088
31089 setDefault('role', type == 'checkbox' ? 'menuitemcheckbox' : 'menuitemradio', buttonEl);
31090 moveAttrToButton('ng-disabled');
31091
31092 } else {
31093 setDefault('role', 'menuitem', templateEl[0].querySelector('md-button, button, a'));
31094 }
31095
31096
31097 return function(scope, el, attrs, ctrls) {
31098 var ctrl = ctrls[0];
31099 var ngModel = ctrls[1];
31100 ctrl.init(ngModel);
31101 };
31102
31103 function setDefault(attr, val, el) {
31104 el = el || templateEl;
31105 if (el instanceof angular.element) {
31106 el = el[0];
31107 }
31108 if (!el.hasAttribute(attr)) {
31109 el.setAttribute(attr, val);
31110 }
31111 }
31112
31113 function moveAttrToButton(attribute) {
31114 var attributes = $mdUtil.prefixer(attribute);
31115
31116 angular.forEach(attributes, function(attr) {
31117 if (templateEl[0].hasAttribute(attr)) {
31118 var val = templateEl[0].getAttribute(attr);
31119 buttonEl[0].setAttribute(attr, val);
31120 templateEl[0].removeAttribute(attr);
31121 }
31122 });
31123 }
31124 }
31125 };
31126}
31127
31128})();
31129(function(){
31130"use strict";
31131
31132/**
31133 * @ngdoc directive
31134 * @name mdProgressCircular
31135 * @module material.components.progressCircular
31136 * @restrict E
31137 *
31138 * @description
31139 * The circular progress directive is used to make loading content in your app as delightful and
31140 * painless as possible by minimizing the amount of visual change a user sees before they can view
31141 * and interact with content.
31142 *
31143 * For operations where the percentage of the operation completed can be determined, use a
31144 * determinate indicator. They give users a quick sense of how long an operation will take.
31145 *
31146 * For operations where the user is asked to wait a moment while something finishes up, and it’s
31147 * not necessary to expose what's happening behind the scenes and how long it will take, use an
31148 * indeterminate indicator.
31149 *
31150 * @param {string} md-mode Select from one of two modes: **'determinate'** and **'indeterminate'**.
31151 *
31152 * Note: if the `md-mode` value is set as undefined or specified as not 1 of the two (2) valid modes, then **'indeterminate'**
31153 * will be auto-applied as the mode.
31154 *
31155 * Note: if not configured, the `md-mode="indeterminate"` will be auto injected as an attribute.
31156 * If `value=""` is also specified, however, then `md-mode="determinate"` would be auto-injected instead.
31157 * @param {number=} value In determinate mode, this number represents the percentage of the
31158 * circular progress. Default: 0
31159 * @param {number=} md-diameter This specifies the diameter of the circular progress. The value
31160 * should be a pixel-size value (eg '100'). If this attribute is
31161 * not present then a default value of '50px' is assumed.
31162 *
31163 * @param {boolean=} ng-disabled Determines whether to disable the progress element.
31164 *
31165 * @usage
31166 * <hljs lang="html">
31167 * <md-progress-circular md-mode="determinate" value="..."></md-progress-circular>
31168 *
31169 * <md-progress-circular md-mode="determinate" ng-value="..."></md-progress-circular>
31170 *
31171 * <md-progress-circular md-mode="determinate" value="..." md-diameter="100"></md-progress-circular>
31172 *
31173 * <md-progress-circular md-mode="indeterminate"></md-progress-circular>
31174 * </hljs>
31175 */
31176
31177MdProgressCircularDirective.$inject = ["$window", "$mdProgressCircular", "$mdTheming", "$mdUtil", "$interval", "$log"];
31178angular
31179 .module('material.components.progressCircular')
31180 .directive('mdProgressCircular', MdProgressCircularDirective);
31181
31182/* @ngInject */
31183function MdProgressCircularDirective($window, $mdProgressCircular, $mdTheming,
31184 $mdUtil, $interval, $log) {
31185
31186 // Note that this shouldn't use use $$rAF, because it can cause an infinite loop
31187 // in any tests that call $animate.flush.
31188 var rAF = $window.requestAnimationFrame ||
31189 $window.webkitRequestAnimationFrame ||
31190 angular.noop;
31191
31192 var cAF = $window.cancelAnimationFrame ||
31193 $window.webkitCancelAnimationFrame ||
31194 $window.webkitCancelRequestAnimationFrame ||
31195 angular.noop;
31196
31197 var DEGREE_IN_RADIANS = $window.Math.PI / 180;
31198 var MODE_DETERMINATE = 'determinate';
31199 var MODE_INDETERMINATE = 'indeterminate';
31200 var DISABLED_CLASS = '_md-progress-circular-disabled';
31201 var INDETERMINATE_CLASS = 'md-mode-indeterminate';
31202
31203 return {
31204 restrict: 'E',
31205 scope: {
31206 value: '@',
31207 mdDiameter: '@',
31208 mdMode: '@'
31209 },
31210 template:
31211 '<svg xmlns="http://www.w3.org/2000/svg">' +
31212 '<path fill="none"/>' +
31213 '</svg>',
31214 compile: function(element, attrs) {
31215 element.attr({
31216 'aria-valuemin': 0,
31217 'aria-valuemax': 100,
31218 'role': 'progressbar'
31219 });
31220
31221 if (angular.isUndefined(attrs.mdMode)) {
31222 var hasValue = angular.isDefined(attrs.value);
31223 var mode = hasValue ? MODE_DETERMINATE : MODE_INDETERMINATE;
31224 var info = "Auto-adding the missing md-mode='{0}' to the ProgressCircular element";
31225
31226 // $log.debug( $mdUtil.supplant(info, [mode]) );
31227 attrs.$set('mdMode', mode);
31228 } else {
31229 attrs.$set('mdMode', attrs.mdMode.trim());
31230 }
31231
31232 return MdProgressCircularLink;
31233 }
31234 };
31235
31236 function MdProgressCircularLink(scope, element, attrs) {
31237 var node = element[0];
31238 var svg = angular.element(node.querySelector('svg'));
31239 var path = angular.element(node.querySelector('path'));
31240 var startIndeterminate = $mdProgressCircular.startIndeterminate;
31241 var endIndeterminate = $mdProgressCircular.endIndeterminate;
31242 var rotationIndeterminate = 0;
31243 var lastAnimationId = 0;
31244 var lastDrawFrame;
31245 var interval;
31246
31247 $mdTheming(element);
31248 element.toggleClass(DISABLED_CLASS, attrs.hasOwnProperty('disabled'));
31249
31250 // If the mode is indeterminate, it doesn't need to
31251 // wait for the next digest. It can start right away.
31252 if(scope.mdMode === MODE_INDETERMINATE){
31253 startIndeterminateAnimation();
31254 }
31255
31256 scope.$on('$destroy', function(){
31257 cleanupIndeterminateAnimation();
31258
31259 if (lastDrawFrame) {
31260 cAF(lastDrawFrame);
31261 }
31262 });
31263
31264 scope.$watchGroup(['value', 'mdMode', function() {
31265 var isDisabled = node.disabled;
31266
31267 // Sometimes the browser doesn't return a boolean, in
31268 // which case we should check whether the attribute is
31269 // present.
31270 if (isDisabled === true || isDisabled === false){
31271 return isDisabled;
31272 }
31273 return angular.isDefined(element.attr('disabled'));
31274
31275 }], function(newValues, oldValues) {
31276 var mode = newValues[1];
31277 var isDisabled = newValues[2];
31278 var wasDisabled = oldValues[2];
31279
31280 if (isDisabled !== wasDisabled) {
31281 element.toggleClass(DISABLED_CLASS, !!isDisabled);
31282 }
31283
31284 if (isDisabled) {
31285 cleanupIndeterminateAnimation();
31286 } else {
31287 if (mode !== MODE_DETERMINATE && mode !== MODE_INDETERMINATE) {
31288 mode = MODE_INDETERMINATE;
31289 attrs.$set('mdMode', mode);
31290 }
31291
31292 if (mode === MODE_INDETERMINATE) {
31293 startIndeterminateAnimation();
31294 } else {
31295 var newValue = clamp(newValues[0]);
31296
31297 cleanupIndeterminateAnimation();
31298
31299 element.attr('aria-valuenow', newValue);
31300 renderCircle(clamp(oldValues[0]), newValue);
31301 }
31302 }
31303
31304 });
31305
31306 // This is in a separate watch in order to avoid layout, unless
31307 // the value has actually changed.
31308 scope.$watch('mdDiameter', function(newValue) {
31309 var diameter = getSize(newValue);
31310 var strokeWidth = getStroke(diameter);
31311 var transformOrigin = (diameter / 2) + 'px';
31312 var dimensions = {
31313 width: diameter + 'px',
31314 height: diameter + 'px'
31315 };
31316
31317 // The viewBox has to be applied via setAttribute, because it is
31318 // case-sensitive. If jQuery is included in the page, `.attr` lowercases
31319 // all attribute names.
31320 svg[0].setAttribute('viewBox', '0 0 ' + diameter + ' ' + diameter);
31321
31322 // Usually viewBox sets the dimensions for the SVG, however that doesn't
31323 // seem to be the case on IE10.
31324 // Important! The transform origin has to be set from here and it has to
31325 // be in the format of "Ypx Ypx Ypx", otherwise the rotation wobbles in
31326 // IE and Edge, because they don't account for the stroke width when
31327 // rotating. Also "center" doesn't help in this case, it has to be a
31328 // precise value.
31329 svg
31330 .css(dimensions)
31331 .css('transform-origin', transformOrigin + ' ' + transformOrigin + ' ' + transformOrigin);
31332
31333 element.css(dimensions);
31334 path.css('stroke-width', strokeWidth + 'px');
31335 });
31336
31337 function renderCircle(animateFrom, animateTo, easing, duration, rotation) {
31338 var id = ++lastAnimationId;
31339 var startTime = $mdUtil.now();
31340 var changeInValue = animateTo - animateFrom;
31341 var diameter = getSize(scope.mdDiameter);
31342 var pathDiameter = diameter - getStroke(diameter);
31343 var ease = easing || $mdProgressCircular.easeFn;
31344 var animationDuration = duration || $mdProgressCircular.duration;
31345
31346 // No need to animate it if the values are the same
31347 if (animateTo === animateFrom) {
31348 path.attr('d', getSvgArc(animateTo, diameter, pathDiameter, rotation));
31349 } else {
31350 lastDrawFrame = rAF(function animation() {
31351 var currentTime = $window.Math.max(0, $window.Math.min($mdUtil.now() - startTime, animationDuration));
31352
31353 path.attr('d', getSvgArc(
31354 ease(currentTime, animateFrom, changeInValue, animationDuration),
31355 diameter,
31356 pathDiameter,
31357 rotation
31358 ));
31359
31360 // Do not allow overlapping animations
31361 if (id === lastAnimationId && currentTime < animationDuration) {
31362 lastDrawFrame = rAF(animation);
31363 }
31364 });
31365 }
31366 }
31367
31368 function animateIndeterminate() {
31369 renderCircle(
31370 startIndeterminate,
31371 endIndeterminate,
31372 $mdProgressCircular.easeFnIndeterminate,
31373 $mdProgressCircular.durationIndeterminate,
31374 rotationIndeterminate
31375 );
31376
31377 // The % 100 technically isn't necessary, but it keeps the rotation
31378 // under 100, instead of becoming a crazy large number.
31379 rotationIndeterminate = (rotationIndeterminate + endIndeterminate) % 100;
31380
31381 var temp = startIndeterminate;
31382 startIndeterminate = -endIndeterminate;
31383 endIndeterminate = -temp;
31384 }
31385
31386 function startIndeterminateAnimation() {
31387 if (!interval) {
31388 // Note that this interval isn't supposed to trigger a digest.
31389 interval = $interval(
31390 animateIndeterminate,
31391 $mdProgressCircular.durationIndeterminate + 50,
31392 0,
31393 false
31394 );
31395
31396 animateIndeterminate();
31397
31398 element
31399 .addClass(INDETERMINATE_CLASS)
31400 .removeAttr('aria-valuenow');
31401 }
31402 }
31403
31404 function cleanupIndeterminateAnimation() {
31405 if (interval) {
31406 $interval.cancel(interval);
31407 interval = null;
31408 element.removeClass(INDETERMINATE_CLASS);
31409 }
31410 }
31411 }
31412
31413 /**
31414 * Generates an arc following the SVG arc syntax.
31415 * Syntax spec: https://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands
31416 *
31417 * @param {number} current Current value between 0 and 100.
31418 * @param {number} diameter Diameter of the container.
31419 * @param {number} pathDiameter Diameter of the path element.
31420 * @param {number=0} rotation The point at which the semicircle should start rendering.
31421 * Used for doing the indeterminate animation.
31422 *
31423 * @returns {string} String representation of an SVG arc.
31424 */
31425 function getSvgArc(current, diameter, pathDiameter, rotation) {
31426 // The angle can't be exactly 360, because the arc becomes hidden.
31427 var maximumAngle = 359.99 / 100;
31428 var startPoint = rotation || 0;
31429 var radius = diameter / 2;
31430 var pathRadius = pathDiameter / 2;
31431
31432 var startAngle = startPoint * maximumAngle;
31433 var endAngle = current * maximumAngle;
31434 var start = polarToCartesian(radius, pathRadius, startAngle);
31435 var end = polarToCartesian(radius, pathRadius, endAngle + startAngle);
31436 var arcSweep = endAngle < 0 ? 0 : 1;
31437 var largeArcFlag;
31438
31439 if (endAngle < 0) {
31440 largeArcFlag = endAngle >= -180 ? 0 : 1;
31441 } else {
31442 largeArcFlag = endAngle <= 180 ? 0 : 1;
31443 }
31444
31445 return 'M' + start + 'A' + pathRadius + ',' + pathRadius +
31446 ' 0 ' + largeArcFlag + ',' + arcSweep + ' ' + end;
31447 }
31448
31449 /**
31450 * Converts Polar coordinates to Cartesian.
31451 *
31452 * @param {number} radius Radius of the container.
31453 * @param {number} pathRadius Radius of the path element
31454 * @param {number} angleInDegress Angle at which to place the point.
31455 *
31456 * @returns {string} Cartesian coordinates in the format of `x,y`.
31457 */
31458 function polarToCartesian(radius, pathRadius, angleInDegrees) {
31459 var angleInRadians = (angleInDegrees - 90) * DEGREE_IN_RADIANS;
31460
31461 return (radius + (pathRadius * $window.Math.cos(angleInRadians))) +
31462 ',' + (radius + (pathRadius * $window.Math.sin(angleInRadians)));
31463 }
31464
31465 /**
31466 * Limits a value between 0 and 100.
31467 */
31468 function clamp(value) {
31469 return $window.Math.max(0, $window.Math.min(value || 0, 100));
31470 }
31471
31472 /**
31473 * Determines the size of a progress circle, based on the provided
31474 * value in the following formats: `X`, `Ypx`, `Z%`.
31475 */
31476 function getSize(value) {
31477 var defaultValue = $mdProgressCircular.progressSize;
31478
31479 if (value) {
31480 var parsed = parseFloat(value);
31481
31482 if (value.lastIndexOf('%') === value.length - 1) {
31483 parsed = (parsed / 100) * defaultValue;
31484 }
31485
31486 return parsed;
31487 }
31488
31489 return defaultValue;
31490 }
31491
31492 /**
31493 * Determines the circle's stroke width, based on
31494 * the provided diameter.
31495 */
31496 function getStroke(diameter) {
31497 return $mdProgressCircular.strokeWidth / 100 * diameter;
31498 }
31499}
31500
31501})();
31502(function(){
31503"use strict";
31504
31505/**
31506 * @ngdoc service
31507 * @name $mdProgressCircular
31508 * @module material.components.progressCircular
31509 *
31510 * @description
31511 * Allows the user to specify the default options for the `progressCircular` directive.
31512 *
31513 * @property {number} progressSize Diameter of the progress circle in pixels.
31514 * @property {number} strokeWidth Width of the circle's stroke as a percentage of the circle's size.
31515 * @property {number} duration Length of the circle animation in milliseconds.
31516 * @property {function} easeFn Default easing animation function.
31517 * @property {object} easingPresets Collection of pre-defined easing functions.
31518 *
31519 * @property {number} durationIndeterminate Duration of the indeterminate animation.
31520 * @property {number} startIndeterminate Indeterminate animation start point.
31521 * @property {number} endIndeterminate Indeterminate animation end point.
31522 * @property {function} easeFnIndeterminate Easing function to be used when animating
31523 * between the indeterminate values.
31524 *
31525 * @property {(function(object): object)} configure Used to modify the default options.
31526 *
31527 * @usage
31528 * <hljs lang="js">
31529 * myAppModule.config(function($mdProgressCircularProvider) {
31530 *
31531 * // Example of changing the default progress options.
31532 * $mdProgressCircularProvider.configure({
31533 * progressSize: 100,
31534 * strokeWidth: 20,
31535 * duration: 800
31536 * });
31537 * });
31538 * </hljs>
31539 *
31540 */
31541
31542angular
31543 .module('material.components.progressCircular')
31544 .provider("$mdProgressCircular", MdProgressCircularProvider);
31545
31546function MdProgressCircularProvider() {
31547 var progressConfig = {
31548 progressSize: 50,
31549 strokeWidth: 10,
31550 duration: 100,
31551 easeFn: linearEase,
31552
31553 durationIndeterminate: 500,
31554 startIndeterminate: 3,
31555 endIndeterminate: 80,
31556 easeFnIndeterminate: materialEase,
31557
31558 easingPresets: {
31559 linearEase: linearEase,
31560 materialEase: materialEase
31561 }
31562 };
31563
31564 return {
31565 configure: function(options) {
31566 progressConfig = angular.extend(progressConfig, options || {});
31567 return progressConfig;
31568 },
31569 $get: function() { return progressConfig; }
31570 };
31571
31572 function linearEase(t, b, c, d) {
31573 return c * t / d + b;
31574 }
31575
31576 function materialEase(t, b, c, d) {
31577 // via http://www.timotheegroleau.com/Flash/experiments/easing_function_generator.htm
31578 // with settings of [0, 0, 1, 1]
31579 var ts = (t /= d) * t;
31580 var tc = ts * t;
31581 return b + c * (6 * tc * ts + -15 * ts * ts + 10 * tc);
31582 }
31583}
31584
31585})();
31586(function(){
31587"use strict";
31588
31589/**
31590 * @ngdoc directive
31591 * @name mdTab
31592 * @module material.components.tabs
31593 *
31594 * @restrict E
31595 *
31596 * @description
31597 * Use the `<md-tab>` a nested directive used within `<md-tabs>` to specify a tab with a **label** and optional *view content*.
31598 *
31599 * If the `label` attribute is not specified, then an optional `<md-tab-label>` tag can be used to specify more
31600 * complex tab header markup. If neither the **label** nor the **md-tab-label** are specified, then the nested
31601 * markup of the `<md-tab>` is used as the tab header markup.
31602 *
31603 * Please note that if you use `<md-tab-label>`, your content **MUST** be wrapped in the `<md-tab-body>` tag. This
31604 * is to define a clear separation between the tab content and the tab label.
31605 *
31606 * This container is used by the TabsController to show/hide the active tab's content view. This synchronization is
31607 * automatically managed by the internal TabsController whenever the tab selection changes. Selection changes can
31608 * be initiated via data binding changes, programmatic invocation, or user gestures.
31609 *
31610 * @param {string=} label Optional attribute to specify a simple string as the tab label
31611 * @param {boolean=} ng-disabled If present and expression evaluates to truthy, disabled tab selection.
31612 * @param {expression=} md-on-deselect Expression to be evaluated after the tab has been de-selected.
31613 * @param {expression=} md-on-select Expression to be evaluated after the tab has been selected.
31614 * @param {boolean=} md-active When true, sets the active tab. Note: There can only be one active tab at a time.
31615 *
31616 *
31617 * @usage
31618 *
31619 * <hljs lang="html">
31620 * <md-tab label="" ng-disabled md-on-select="" md-on-deselect="" >
31621 * <h3>My Tab content</h3>
31622 * </md-tab>
31623 *
31624 * <md-tab >
31625 * <md-tab-label>
31626 * <h3>My Tab content</h3>
31627 * </md-tab-label>
31628 * <md-tab-body>
31629 * <p>
31630 * Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium,
31631 * totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae
31632 * dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit,
31633 * sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.
31634 * </p>
31635 * </md-tab-body>
31636 * </md-tab>
31637 * </hljs>
31638 *
31639 */
31640angular
31641 .module('material.components.tabs')
31642 .directive('mdTab', MdTab);
31643
31644function MdTab () {
31645 return {
31646 require: '^?mdTabs',
31647 terminal: true,
31648 compile: function (element, attr) {
31649 var label = firstChild(element, 'md-tab-label'),
31650 body = firstChild(element, 'md-tab-body');
31651
31652 if (label.length == 0) {
31653 label = angular.element('<md-tab-label></md-tab-label>');
31654 if (attr.label) label.text(attr.label);
31655 else label.append(element.contents());
31656
31657 if (body.length == 0) {
31658 var contents = element.contents().detach();
31659 body = angular.element('<md-tab-body></md-tab-body>');
31660 body.append(contents);
31661 }
31662 }
31663
31664 element.append(label);
31665 if (body.html()) element.append(body);
31666
31667 return postLink;
31668 },
31669 scope: {
31670 active: '=?mdActive',
31671 disabled: '=?ngDisabled',
31672 select: '&?mdOnSelect',
31673 deselect: '&?mdOnDeselect'
31674 }
31675 };
31676
31677 function postLink (scope, element, attr, ctrl) {
31678 if (!ctrl) return;
31679 var index = ctrl.getTabElementIndex(element),
31680 body = firstChild(element, 'md-tab-body').remove(),
31681 label = firstChild(element, 'md-tab-label').remove(),
31682 data = ctrl.insertTab({
31683 scope: scope,
31684 parent: scope.$parent,
31685 index: index,
31686 element: element,
31687 template: body.html(),
31688 label: label.html()
31689 }, index);
31690
31691 scope.select = scope.select || angular.noop;
31692 scope.deselect = scope.deselect || angular.noop;
31693
31694 scope.$watch('active', function (active) { if (active) ctrl.select(data.getIndex(), true); });
31695 scope.$watch('disabled', function () { ctrl.refreshIndex(); });
31696 scope.$watch(
31697 function () {
31698 return ctrl.getTabElementIndex(element);
31699 },
31700 function (newIndex) {
31701 data.index = newIndex;
31702 ctrl.updateTabOrder();
31703 }
31704 );
31705 scope.$on('$destroy', function () { ctrl.removeTab(data); });
31706 }
31707
31708 function firstChild (element, tagName) {
31709 var children = element[0].children;
31710 for (var i = 0, len = children.length; i < len; i++) {
31711 var child = children[i];
31712 if (child.tagName === tagName.toUpperCase()) return angular.element(child);
31713 }
31714 return angular.element();
31715 }
31716}
31717
31718})();
31719(function(){
31720"use strict";
31721
31722angular
31723 .module('material.components.tabs')
31724 .directive('mdTabItem', MdTabItem);
31725
31726function MdTabItem () {
31727 return {
31728 require: '^?mdTabs',
31729 link: function link (scope, element, attr, ctrl) {
31730 if (!ctrl) return;
31731 ctrl.attachRipple(scope, element);
31732 }
31733 };
31734}
31735
31736})();
31737(function(){
31738"use strict";
31739
31740angular
31741 .module('material.components.tabs')
31742 .directive('mdTabLabel', MdTabLabel);
31743
31744function MdTabLabel () {
31745 return { terminal: true };
31746}
31747
31748
31749})();
31750(function(){
31751"use strict";
31752
31753
31754MdTabScroll.$inject = ["$parse"];angular.module('material.components.tabs')
31755 .directive('mdTabScroll', MdTabScroll);
31756
31757function MdTabScroll ($parse) {
31758 return {
31759 restrict: 'A',
31760 compile: function ($element, attr) {
31761 var fn = $parse(attr.mdTabScroll, null, true);
31762 return function ngEventHandler (scope, element) {
31763 element.on('mousewheel', function (event) {
31764 scope.$apply(function () { fn(scope, { $event: event }); });
31765 });
31766 };
31767 }
31768 }
31769}
31770
31771})();
31772(function(){
31773"use strict";
31774
31775
31776MdTabsController.$inject = ["$scope", "$element", "$window", "$mdConstant", "$mdTabInkRipple", "$mdUtil", "$animateCss", "$attrs", "$compile", "$mdTheming"];angular
31777 .module('material.components.tabs')
31778 .controller('MdTabsController', MdTabsController);
31779
31780/**
31781 * @ngInject
31782 */
31783function MdTabsController ($scope, $element, $window, $mdConstant, $mdTabInkRipple,
31784 $mdUtil, $animateCss, $attrs, $compile, $mdTheming) {
31785 // define private properties
31786 var ctrl = this,
31787 locked = false,
31788 elements = getElements(),
31789 queue = [],
31790 destroyed = false,
31791 loaded = false;
31792
31793 // define one-way bindings
31794 defineOneWayBinding('stretchTabs', handleStretchTabs);
31795
31796 // define public properties with change handlers
31797 defineProperty('focusIndex', handleFocusIndexChange, ctrl.selectedIndex || 0);
31798 defineProperty('offsetLeft', handleOffsetChange, 0);
31799 defineProperty('hasContent', handleHasContent, false);
31800 defineProperty('maxTabWidth', handleMaxTabWidth, getMaxTabWidth());
31801 defineProperty('shouldPaginate', handleShouldPaginate, false);
31802
31803 // define boolean attributes
31804 defineBooleanAttribute('noInkBar', handleInkBar);
31805 defineBooleanAttribute('dynamicHeight', handleDynamicHeight);
31806 defineBooleanAttribute('noPagination');
31807 defineBooleanAttribute('swipeContent');
31808 defineBooleanAttribute('noDisconnect');
31809 defineBooleanAttribute('autoselect');
31810 defineBooleanAttribute('noSelectClick');
31811 defineBooleanAttribute('centerTabs', handleCenterTabs, false);
31812 defineBooleanAttribute('enableDisconnect');
31813
31814 // define public properties
31815 ctrl.scope = $scope;
31816 ctrl.parent = $scope.$parent;
31817 ctrl.tabs = [];
31818 ctrl.lastSelectedIndex = null;
31819 ctrl.hasFocus = false;
31820 ctrl.lastClick = true;
31821 ctrl.shouldCenterTabs = shouldCenterTabs();
31822
31823 // define public methods
31824 ctrl.updatePagination = $mdUtil.debounce(updatePagination, 100);
31825 ctrl.redirectFocus = redirectFocus;
31826 ctrl.attachRipple = attachRipple;
31827 ctrl.insertTab = insertTab;
31828 ctrl.removeTab = removeTab;
31829 ctrl.select = select;
31830 ctrl.scroll = scroll;
31831 ctrl.nextPage = nextPage;
31832 ctrl.previousPage = previousPage;
31833 ctrl.keydown = keydown;
31834 ctrl.canPageForward = canPageForward;
31835 ctrl.canPageBack = canPageBack;
31836 ctrl.refreshIndex = refreshIndex;
31837 ctrl.incrementIndex = incrementIndex;
31838 ctrl.getTabElementIndex = getTabElementIndex;
31839 ctrl.updateInkBarStyles = $mdUtil.debounce(updateInkBarStyles, 100);
31840 ctrl.updateTabOrder = $mdUtil.debounce(updateTabOrder, 100);
31841
31842 init();
31843
31844 /**
31845 * Perform initialization for the controller, setup events and watcher(s)
31846 */
31847 function init () {
31848 ctrl.selectedIndex = ctrl.selectedIndex || 0;
31849 compileTemplate();
31850 configureWatchers();
31851 bindEvents();
31852 $mdTheming($element);
31853 $mdUtil.nextTick(function () {
31854 // Note that the element references need to be updated, because certain "browsers"
31855 // (IE/Edge) lose them and start throwing "Invalid calling object" errors, when we
31856 // compile the element contents down in `compileElement`.
31857 elements = getElements();
31858 updateHeightFromContent();
31859 adjustOffset();
31860 updateInkBarStyles();
31861 ctrl.tabs[ ctrl.selectedIndex ] && ctrl.tabs[ ctrl.selectedIndex ].scope.select();
31862 loaded = true;
31863 updatePagination();
31864 });
31865 }
31866
31867 /**
31868 * Compiles the template provided by the user. This is passed as an attribute from the tabs
31869 * directive's template function.
31870 */
31871 function compileTemplate () {
31872 var template = $attrs.$mdTabsTemplate,
31873 element = angular.element($element[0].querySelector('md-tab-data'));
31874
31875 element.html(template);
31876 $compile(element.contents())(ctrl.parent);
31877 delete $attrs.$mdTabsTemplate;
31878 }
31879
31880 /**
31881 * Binds events used by the tabs component.
31882 */
31883 function bindEvents () {
31884 angular.element($window).on('resize', handleWindowResize);
31885 $scope.$on('$destroy', cleanup);
31886 }
31887
31888 /**
31889 * Configure watcher(s) used by Tabs
31890 */
31891 function configureWatchers () {
31892 $scope.$watch('$mdTabsCtrl.selectedIndex', handleSelectedIndexChange);
31893 }
31894
31895 /**
31896 * Creates a one-way binding manually rather than relying on Angular's isolated scope
31897 * @param key
31898 * @param handler
31899 */
31900 function defineOneWayBinding (key, handler) {
31901 var attr = $attrs.$normalize('md-' + key);
31902 if (handler) defineProperty(key, handler);
31903 $attrs.$observe(attr, function (newValue) { ctrl[ key ] = newValue; });
31904 }
31905
31906 /**
31907 * Defines boolean attributes with default value set to true. (ie. md-stretch-tabs with no value
31908 * will be treated as being truthy)
31909 * @param key
31910 * @param handler
31911 */
31912 function defineBooleanAttribute (key, handler) {
31913 var attr = $attrs.$normalize('md-' + key);
31914 if (handler) defineProperty(key, handler);
31915 if ($attrs.hasOwnProperty(attr)) updateValue($attrs[attr]);
31916 $attrs.$observe(attr, updateValue);
31917 function updateValue (newValue) {
31918 ctrl[ key ] = newValue !== 'false';
31919 }
31920 }
31921
31922 /**
31923 * Remove any events defined by this controller
31924 */
31925 function cleanup () {
31926 destroyed = true;
31927 angular.element($window).off('resize', handleWindowResize);
31928 }
31929
31930 // Change handlers
31931
31932 /**
31933 * Toggles stretch tabs class and updates inkbar when tab stretching changes
31934 * @param stretchTabs
31935 */
31936 function handleStretchTabs (stretchTabs) {
31937 var elements = getElements();
31938 angular.element(elements.wrapper).toggleClass('md-stretch-tabs', shouldStretchTabs());
31939 updateInkBarStyles();
31940 }
31941
31942 function handleCenterTabs (newValue) {
31943 ctrl.shouldCenterTabs = shouldCenterTabs();
31944 }
31945
31946 function handleMaxTabWidth (newWidth, oldWidth) {
31947 if (newWidth !== oldWidth) {
31948 var elements = getElements();
31949
31950 angular.forEach(elements.tabs, function(tab) {
31951 tab.style.maxWidth = newWidth + 'px';
31952 });
31953 $mdUtil.nextTick(ctrl.updateInkBarStyles);
31954 }
31955 }
31956
31957 function handleShouldPaginate (newValue, oldValue) {
31958 if (newValue !== oldValue) {
31959 ctrl.maxTabWidth = getMaxTabWidth();
31960 ctrl.shouldCenterTabs = shouldCenterTabs();
31961 $mdUtil.nextTick(function () {
31962 ctrl.maxTabWidth = getMaxTabWidth();
31963 adjustOffset(ctrl.selectedIndex);
31964 });
31965 }
31966 }
31967
31968 /**
31969 * Add/remove the `md-no-tab-content` class depending on `ctrl.hasContent`
31970 * @param hasContent
31971 */
31972 function handleHasContent (hasContent) {
31973 $element[ hasContent ? 'removeClass' : 'addClass' ]('md-no-tab-content');
31974 }
31975
31976 /**
31977 * Apply ctrl.offsetLeft to the paging element when it changes
31978 * @param left
31979 */
31980 function handleOffsetChange (left) {
31981 var elements = getElements();
31982 var newValue = ctrl.shouldCenterTabs ? '' : '-' + left + 'px';
31983
31984 angular.element(elements.paging).css($mdConstant.CSS.TRANSFORM, 'translate3d(' + newValue + ', 0, 0)');
31985 $scope.$broadcast('$mdTabsPaginationChanged');
31986 }
31987
31988 /**
31989 * Update the UI whenever `ctrl.focusIndex` is updated
31990 * @param newIndex
31991 * @param oldIndex
31992 */
31993 function handleFocusIndexChange (newIndex, oldIndex) {
31994 if (newIndex === oldIndex) return;
31995 if (!getElements().tabs[ newIndex ]) return;
31996 adjustOffset();
31997 redirectFocus();
31998 }
31999
32000 /**
32001 * Update the UI whenever the selected index changes. Calls user-defined select/deselect methods.
32002 * @param newValue
32003 * @param oldValue
32004 */
32005 function handleSelectedIndexChange (newValue, oldValue) {
32006 if (newValue === oldValue) return;
32007
32008 ctrl.selectedIndex = getNearestSafeIndex(newValue);
32009 ctrl.lastSelectedIndex = oldValue;
32010 ctrl.updateInkBarStyles();
32011 updateHeightFromContent();
32012 adjustOffset(newValue);
32013 $scope.$broadcast('$mdTabsChanged');
32014 ctrl.tabs[ oldValue ] && ctrl.tabs[ oldValue ].scope.deselect();
32015 ctrl.tabs[ newValue ] && ctrl.tabs[ newValue ].scope.select();
32016 }
32017
32018 function getTabElementIndex(tabEl){
32019 var tabs = $element[0].getElementsByTagName('md-tab');
32020 return Array.prototype.indexOf.call(tabs, tabEl[0]);
32021 }
32022
32023 /**
32024 * Queues up a call to `handleWindowResize` when a resize occurs while the tabs component is
32025 * hidden.
32026 */
32027 function handleResizeWhenVisible () {
32028 // if there is already a watcher waiting for resize, do nothing
32029 if (handleResizeWhenVisible.watcher) return;
32030 // otherwise, we will abuse the $watch function to check for visible
32031 handleResizeWhenVisible.watcher = $scope.$watch(function () {
32032 // since we are checking for DOM size, we use $mdUtil.nextTick() to wait for after the DOM updates
32033 $mdUtil.nextTick(function () {
32034 // if the watcher has already run (ie. multiple digests in one cycle), do nothing
32035 if (!handleResizeWhenVisible.watcher) return;
32036
32037 if ($element.prop('offsetParent')) {
32038 handleResizeWhenVisible.watcher();
32039 handleResizeWhenVisible.watcher = null;
32040
32041 handleWindowResize();
32042 }
32043 }, false);
32044 });
32045 }
32046
32047 // Event handlers / actions
32048
32049 /**
32050 * Handle user keyboard interactions
32051 * @param event
32052 */
32053 function keydown (event) {
32054 switch (event.keyCode) {
32055 case $mdConstant.KEY_CODE.LEFT_ARROW:
32056 event.preventDefault();
32057 incrementIndex(-1, true);
32058 break;
32059 case $mdConstant.KEY_CODE.RIGHT_ARROW:
32060 event.preventDefault();
32061 incrementIndex(1, true);
32062 break;
32063 case $mdConstant.KEY_CODE.SPACE:
32064 case $mdConstant.KEY_CODE.ENTER:
32065 event.preventDefault();
32066 if (!locked) select(ctrl.focusIndex);
32067 break;
32068 }
32069 ctrl.lastClick = false;
32070 }
32071
32072 /**
32073 * Update the selected index. Triggers a click event on the original `md-tab` element in order
32074 * to fire user-added click events if canSkipClick or `md-no-select-click` are false.
32075 * @param index
32076 * @param canSkipClick Optionally allow not firing the click event if `md-no-select-click` is also true.
32077 */
32078 function select (index, canSkipClick) {
32079 if (!locked) ctrl.focusIndex = ctrl.selectedIndex = index;
32080 ctrl.lastClick = true;
32081 // skip the click event if noSelectClick is enabled
32082 if (canSkipClick && ctrl.noSelectClick) return;
32083 // nextTick is required to prevent errors in user-defined click events
32084 $mdUtil.nextTick(function () {
32085 ctrl.tabs[ index ].element.triggerHandler('click');
32086 }, false);
32087 }
32088
32089 /**
32090 * When pagination is on, this makes sure the selected index is in view.
32091 * @param event
32092 */
32093 function scroll (event) {
32094 if (!ctrl.shouldPaginate) return;
32095 event.preventDefault();
32096 ctrl.offsetLeft = fixOffset(ctrl.offsetLeft - event.wheelDelta);
32097 }
32098
32099 /**
32100 * Slides the tabs over approximately one page forward.
32101 */
32102 function nextPage () {
32103 var elements = getElements();
32104 var viewportWidth = elements.canvas.clientWidth,
32105 totalWidth = viewportWidth + ctrl.offsetLeft,
32106 i, tab;
32107 for (i = 0; i < elements.tabs.length; i++) {
32108 tab = elements.tabs[ i ];
32109 if (tab.offsetLeft + tab.offsetWidth > totalWidth) break;
32110 }
32111
32112 if (viewportWidth > tab.offsetWidth) {
32113 //Canvas width *greater* than tab width: usual positioning
32114 ctrl.offsetLeft = fixOffset(tab.offsetLeft);
32115 } else {
32116 /**
32117 * Canvas width *smaller* than tab width: positioning at the *end* of current tab to let
32118 * pagination "for loop" to proceed correctly on next tab when nextPage() is called again
32119 */
32120 ctrl.offsetLeft = fixOffset(tab.offsetLeft + (tab.offsetWidth - viewportWidth + 1));
32121 }
32122 }
32123
32124 /**
32125 * Slides the tabs over approximately one page backward.
32126 */
32127 function previousPage () {
32128 var i, tab, elements = getElements();
32129
32130 for (i = 0; i < elements.tabs.length; i++) {
32131 tab = elements.tabs[ i ];
32132 if (tab.offsetLeft + tab.offsetWidth >= ctrl.offsetLeft) break;
32133 }
32134
32135 if (elements.canvas.clientWidth > tab.offsetWidth) {
32136 //Canvas width *greater* than tab width: usual positioning
32137 ctrl.offsetLeft = fixOffset(tab.offsetLeft + tab.offsetWidth - elements.canvas.clientWidth);
32138 } else {
32139 /**
32140 * Canvas width *smaller* than tab width: positioning at the *beginning* of current tab to let
32141 * pagination "for loop" to break correctly on previous tab when previousPage() is called again
32142 */
32143 ctrl.offsetLeft = fixOffset(tab.offsetLeft);
32144 }
32145 }
32146
32147 /**
32148 * Update size calculations when the window is resized.
32149 */
32150 function handleWindowResize () {
32151 ctrl.lastSelectedIndex = ctrl.selectedIndex;
32152 ctrl.offsetLeft = fixOffset(ctrl.offsetLeft);
32153 $mdUtil.nextTick(function () {
32154 ctrl.updateInkBarStyles();
32155 updatePagination();
32156 });
32157 }
32158
32159 function handleInkBar (hide) {
32160 angular.element(getElements().inkBar).toggleClass('ng-hide', hide);
32161 }
32162
32163 /**
32164 * Toggle dynamic height class when value changes
32165 * @param value
32166 */
32167 function handleDynamicHeight (value) {
32168 $element.toggleClass('md-dynamic-height', value);
32169 }
32170
32171 /**
32172 * Remove a tab from the data and select the nearest valid tab.
32173 * @param tabData
32174 */
32175 function removeTab (tabData) {
32176 if (destroyed) return;
32177 var selectedIndex = ctrl.selectedIndex,
32178 tab = ctrl.tabs.splice(tabData.getIndex(), 1)[ 0 ];
32179 refreshIndex();
32180 // when removing a tab, if the selected index did not change, we have to manually trigger the
32181 // tab select/deselect events
32182 if (ctrl.selectedIndex === selectedIndex) {
32183 tab.scope.deselect();
32184 ctrl.tabs[ ctrl.selectedIndex ] && ctrl.tabs[ ctrl.selectedIndex ].scope.select();
32185 }
32186 $mdUtil.nextTick(function () {
32187 updatePagination();
32188 ctrl.offsetLeft = fixOffset(ctrl.offsetLeft);
32189 });
32190 }
32191
32192 /**
32193 * Create an entry in the tabs array for a new tab at the specified index.
32194 * @param tabData
32195 * @param index
32196 * @returns {*}
32197 */
32198 function insertTab (tabData, index) {
32199 var hasLoaded = loaded;
32200 var proto = {
32201 getIndex: function () { return ctrl.tabs.indexOf(tab); },
32202 isActive: function () { return this.getIndex() === ctrl.selectedIndex; },
32203 isLeft: function () { return this.getIndex() < ctrl.selectedIndex; },
32204 isRight: function () { return this.getIndex() > ctrl.selectedIndex; },
32205 shouldRender: function () { return !ctrl.noDisconnect || this.isActive(); },
32206 hasFocus: function () {
32207 return !ctrl.lastClick
32208 && ctrl.hasFocus && this.getIndex() === ctrl.focusIndex;
32209 },
32210 id: $mdUtil.nextUid()
32211 },
32212 tab = angular.extend(proto, tabData);
32213 if (angular.isDefined(index)) {
32214 ctrl.tabs.splice(index, 0, tab);
32215 } else {
32216 ctrl.tabs.push(tab);
32217 }
32218 processQueue();
32219 updateHasContent();
32220 $mdUtil.nextTick(function () {
32221 updatePagination();
32222 // if autoselect is enabled, select the newly added tab
32223 if (hasLoaded && ctrl.autoselect) $mdUtil.nextTick(function () {
32224 $mdUtil.nextTick(function () { select(ctrl.tabs.indexOf(tab)); });
32225 });
32226 });
32227 return tab;
32228 }
32229
32230 // Getter methods
32231
32232 /**
32233 * Gathers references to all of the DOM elements used by this controller.
32234 * @returns {{}}
32235 */
32236 function getElements () {
32237 var elements = {};
32238 var node = $element[0];
32239
32240 // gather tab bar elements
32241 elements.wrapper = node.querySelector('md-tabs-wrapper');
32242 elements.canvas = elements.wrapper.querySelector('md-tabs-canvas');
32243 elements.paging = elements.canvas.querySelector('md-pagination-wrapper');
32244 elements.inkBar = elements.paging.querySelector('md-ink-bar');
32245
32246 elements.contents = node.querySelectorAll('md-tabs-content-wrapper > md-tab-content');
32247 elements.tabs = elements.paging.querySelectorAll('md-tab-item');
32248 elements.dummies = elements.canvas.querySelectorAll('md-dummy-tab');
32249
32250 return elements;
32251 }
32252
32253 /**
32254 * Determines whether or not the left pagination arrow should be enabled.
32255 * @returns {boolean}
32256 */
32257 function canPageBack () {
32258 return ctrl.offsetLeft > 0;
32259 }
32260
32261 /**
32262 * Determines whether or not the right pagination arrow should be enabled.
32263 * @returns {*|boolean}
32264 */
32265 function canPageForward () {
32266 var elements = getElements();
32267 var lastTab = elements.tabs[ elements.tabs.length - 1 ];
32268 return lastTab && lastTab.offsetLeft + lastTab.offsetWidth > elements.canvas.clientWidth +
32269 ctrl.offsetLeft;
32270 }
32271
32272 /**
32273 * Determines if the UI should stretch the tabs to fill the available space.
32274 * @returns {*}
32275 */
32276 function shouldStretchTabs () {
32277 switch (ctrl.stretchTabs) {
32278 case 'always':
32279 return true;
32280 case 'never':
32281 return false;
32282 default:
32283 return !ctrl.shouldPaginate
32284 && $window.matchMedia('(max-width: 600px)').matches;
32285 }
32286 }
32287
32288 /**
32289 * Determines if the tabs should appear centered.
32290 * @returns {string|boolean}
32291 */
32292 function shouldCenterTabs () {
32293 return ctrl.centerTabs && !ctrl.shouldPaginate;
32294 }
32295
32296 /**
32297 * Determines if pagination is necessary to display the tabs within the available space.
32298 * @returns {boolean}
32299 */
32300 function shouldPaginate () {
32301 if (ctrl.noPagination || !loaded) return false;
32302 var canvasWidth = $element.prop('clientWidth');
32303
32304 angular.forEach(getElements().dummies, function (tab) {
32305 canvasWidth -= tab.offsetWidth;
32306 });
32307
32308 return canvasWidth < 0;
32309 }
32310
32311 /**
32312 * Finds the nearest tab index that is available. This is primarily used for when the active
32313 * tab is removed.
32314 * @param newIndex
32315 * @returns {*}
32316 */
32317 function getNearestSafeIndex (newIndex) {
32318 if (newIndex === -1) return -1;
32319 var maxOffset = Math.max(ctrl.tabs.length - newIndex, newIndex),
32320 i, tab;
32321 for (i = 0; i <= maxOffset; i++) {
32322 tab = ctrl.tabs[ newIndex + i ];
32323 if (tab && (tab.scope.disabled !== true)) return tab.getIndex();
32324 tab = ctrl.tabs[ newIndex - i ];
32325 if (tab && (tab.scope.disabled !== true)) return tab.getIndex();
32326 }
32327 return newIndex;
32328 }
32329
32330 // Utility methods
32331
32332 /**
32333 * Defines a property using a getter and setter in order to trigger a change handler without
32334 * using `$watch` to observe changes.
32335 * @param key
32336 * @param handler
32337 * @param value
32338 */
32339 function defineProperty (key, handler, value) {
32340 Object.defineProperty(ctrl, key, {
32341 get: function () { return value; },
32342 set: function (newValue) {
32343 var oldValue = value;
32344 value = newValue;
32345 handler && handler(newValue, oldValue);
32346 }
32347 });
32348 }
32349
32350 /**
32351 * Updates whether or not pagination should be displayed.
32352 */
32353 function updatePagination () {
32354 updatePagingWidth();
32355 ctrl.maxTabWidth = getMaxTabWidth();
32356 ctrl.shouldPaginate = shouldPaginate();
32357 }
32358
32359 /**
32360 * Sets or clears fixed width for md-pagination-wrapper depending on if tabs should stretch.
32361 */
32362 function updatePagingWidth() {
32363 var elements = getElements();
32364 if (shouldStretchTabs()) {
32365 angular.element(elements.paging).css('width', '');
32366 } else {
32367 angular.element(elements.paging).css('width', calcPagingWidth() + 'px');
32368 }
32369 }
32370
32371 /**
32372 * Calculates the width of the pagination wrapper by summing the widths of the dummy tabs.
32373 * @returns {number}
32374 */
32375 function calcPagingWidth () {
32376 return calcTabsWidth(getElements().dummies);
32377 }
32378
32379 function calcTabsWidth(tabs) {
32380 var width = 0;
32381
32382 angular.forEach(tabs, function (tab) {
32383 //-- Uses the larger value between `getBoundingClientRect().width` and `offsetWidth`. This
32384 // prevents `offsetWidth` value from being rounded down and causing wrapping issues, but
32385 // also handles scenarios where `getBoundingClientRect()` is inaccurate (ie. tabs inside
32386 // of a dialog)
32387 width += Math.max(tab.offsetWidth, tab.getBoundingClientRect().width);
32388 });
32389
32390 return Math.ceil(width);
32391 }
32392
32393 function getMaxTabWidth () {
32394 return $element.prop('clientWidth');
32395 }
32396
32397 /**
32398 * Re-orders the tabs and updates the selected and focus indexes to their new positions.
32399 * This is triggered by `tabDirective.js` when the user's tabs have been re-ordered.
32400 */
32401 function updateTabOrder () {
32402 var selectedItem = ctrl.tabs[ ctrl.selectedIndex ],
32403 focusItem = ctrl.tabs[ ctrl.focusIndex ];
32404 ctrl.tabs = ctrl.tabs.sort(function (a, b) {
32405 return a.index - b.index;
32406 });
32407 ctrl.selectedIndex = ctrl.tabs.indexOf(selectedItem);
32408 ctrl.focusIndex = ctrl.tabs.indexOf(focusItem);
32409 }
32410
32411 /**
32412 * This moves the selected or focus index left or right. This is used by the keydown handler.
32413 * @param inc
32414 */
32415 function incrementIndex (inc, focus) {
32416 var newIndex,
32417 key = focus ? 'focusIndex' : 'selectedIndex',
32418 index = ctrl[ key ];
32419 for (newIndex = index + inc;
32420 ctrl.tabs[ newIndex ] && ctrl.tabs[ newIndex ].scope.disabled;
32421 newIndex += inc) {}
32422 if (ctrl.tabs[ newIndex ]) {
32423 ctrl[ key ] = newIndex;
32424 }
32425 }
32426
32427 /**
32428 * This is used to forward focus to dummy elements. This method is necessary to avoid animation
32429 * issues when attempting to focus an item that is out of view.
32430 */
32431 function redirectFocus () {
32432 getElements().dummies[ ctrl.focusIndex ].focus();
32433 }
32434
32435 /**
32436 * Forces the pagination to move the focused tab into view.
32437 */
32438 function adjustOffset (index) {
32439 var elements = getElements();
32440
32441 if (index == null) index = ctrl.focusIndex;
32442 if (!elements.tabs[ index ]) return;
32443 if (ctrl.shouldCenterTabs) return;
32444 var tab = elements.tabs[ index ],
32445 left = tab.offsetLeft,
32446 right = tab.offsetWidth + left;
32447 ctrl.offsetLeft = Math.max(ctrl.offsetLeft, fixOffset(right - elements.canvas.clientWidth + 32 * 2));
32448 ctrl.offsetLeft = Math.min(ctrl.offsetLeft, fixOffset(left));
32449 }
32450
32451 /**
32452 * Iterates through all queued functions and clears the queue. This is used for functions that
32453 * are called before the UI is ready, such as size calculations.
32454 */
32455 function processQueue () {
32456 queue.forEach(function (func) { $mdUtil.nextTick(func); });
32457 queue = [];
32458 }
32459
32460 /**
32461 * Determines if the tab content area is needed.
32462 */
32463 function updateHasContent () {
32464 var hasContent = false;
32465 angular.forEach(ctrl.tabs, function (tab) {
32466 if (tab.template) hasContent = true;
32467 });
32468 ctrl.hasContent = hasContent;
32469 }
32470
32471 /**
32472 * Moves the indexes to their nearest valid values.
32473 */
32474 function refreshIndex () {
32475 ctrl.selectedIndex = getNearestSafeIndex(ctrl.selectedIndex);
32476 ctrl.focusIndex = getNearestSafeIndex(ctrl.focusIndex);
32477 }
32478
32479 /**
32480 * Calculates the content height of the current tab.
32481 * @returns {*}
32482 */
32483 function updateHeightFromContent () {
32484 if (!ctrl.dynamicHeight) return $element.css('height', '');
32485 if (!ctrl.tabs.length) return queue.push(updateHeightFromContent);
32486
32487 var elements = getElements();
32488
32489 var tabContent = elements.contents[ ctrl.selectedIndex ],
32490 contentHeight = tabContent ? tabContent.offsetHeight : 0,
32491 tabsHeight = elements.wrapper.offsetHeight,
32492 newHeight = contentHeight + tabsHeight,
32493 currentHeight = $element.prop('clientHeight');
32494
32495 if (currentHeight === newHeight) return;
32496
32497 // Adjusts calculations for when the buttons are bottom-aligned since this relies on absolute
32498 // positioning. This should probably be cleaned up if a cleaner solution is possible.
32499 if ($element.attr('md-align-tabs') === 'bottom') {
32500 currentHeight -= tabsHeight;
32501 newHeight -= tabsHeight;
32502 // Need to include bottom border in these calculations
32503 if ($element.attr('md-border-bottom') !== undefined) ++currentHeight;
32504 }
32505
32506 // Lock during animation so the user can't change tabs
32507 locked = true;
32508
32509 var fromHeight = { height: currentHeight + 'px' },
32510 toHeight = { height: newHeight + 'px' };
32511
32512 // Set the height to the current, specific pixel height to fix a bug on iOS where the height
32513 // first animates to 0, then back to the proper height causing a visual glitch
32514 $element.css(fromHeight);
32515
32516 // Animate the height from the old to the new
32517 $animateCss($element, {
32518 from: fromHeight,
32519 to: toHeight,
32520 easing: 'cubic-bezier(0.35, 0, 0.25, 1)',
32521 duration: 0.5
32522 }).start().done(function () {
32523 // Then (to fix the same iOS issue as above), disable transitions and remove the specific
32524 // pixel height so the height can size with browser width/content changes, etc.
32525 $element.css({
32526 transition: 'none',
32527 height: ''
32528 });
32529
32530 // In the next tick, re-allow transitions (if we do it all at once, $element.css is "smart"
32531 // enough to batch it for us instead of doing it immediately, which undoes the original
32532 // transition: none)
32533 $mdUtil.nextTick(function() {
32534 $element.css('transition', '');
32535 });
32536
32537 // And unlock so tab changes can occur
32538 locked = false;
32539 });
32540 }
32541
32542 /**
32543 * Repositions the ink bar to the selected tab.
32544 * @returns {*}
32545 */
32546 function updateInkBarStyles () {
32547 var elements = getElements();
32548
32549 if (!elements.tabs[ ctrl.selectedIndex ]) {
32550 angular.element(elements.inkBar).css({ left: 'auto', right: 'auto' });
32551 return;
32552 }
32553
32554 if (!ctrl.tabs.length) return queue.push(ctrl.updateInkBarStyles);
32555 // if the element is not visible, we will not be able to calculate sizes until it is
32556 // we should treat that as a resize event rather than just updating the ink bar
32557 if (!$element.prop('offsetParent')) return handleResizeWhenVisible();
32558
32559 var index = ctrl.selectedIndex,
32560 totalWidth = elements.paging.offsetWidth,
32561 tab = elements.tabs[ index ],
32562 left = tab.offsetLeft,
32563 right = totalWidth - left - tab.offsetWidth;
32564
32565 if (ctrl.shouldCenterTabs) {
32566 // We need to use the same calculate process as in the pagination wrapper, to avoid rounding deviations.
32567 var tabWidth = calcTabsWidth(elements.tabs);
32568
32569 if (totalWidth > tabWidth) {
32570 $mdUtil.nextTick(updateInkBarStyles, false);
32571 }
32572 }
32573 updateInkBarClassName();
32574 angular.element(elements.inkBar).css({ left: left + 'px', right: right + 'px' });
32575 }
32576
32577 /**
32578 * Adds left/right classes so that the ink bar will animate properly.
32579 */
32580 function updateInkBarClassName () {
32581 var elements = getElements();
32582 var newIndex = ctrl.selectedIndex,
32583 oldIndex = ctrl.lastSelectedIndex,
32584 ink = angular.element(elements.inkBar);
32585 if (!angular.isNumber(oldIndex)) return;
32586 ink
32587 .toggleClass('md-left', newIndex < oldIndex)
32588 .toggleClass('md-right', newIndex > oldIndex);
32589 }
32590
32591 /**
32592 * Takes an offset value and makes sure that it is within the min/max allowed values.
32593 * @param value
32594 * @returns {*}
32595 */
32596 function fixOffset (value) {
32597 var elements = getElements();
32598
32599 if (!elements.tabs.length || !ctrl.shouldPaginate) return 0;
32600 var lastTab = elements.tabs[ elements.tabs.length - 1 ],
32601 totalWidth = lastTab.offsetLeft + lastTab.offsetWidth;
32602 value = Math.max(0, value);
32603 value = Math.min(totalWidth - elements.canvas.clientWidth, value);
32604 return value;
32605 }
32606
32607 /**
32608 * Attaches a ripple to the tab item element.
32609 * @param scope
32610 * @param element
32611 */
32612 function attachRipple (scope, element) {
32613 var elements = getElements();
32614 var options = { colorElement: angular.element(elements.inkBar) };
32615 $mdTabInkRipple.attach(scope, element, options);
32616 }
32617}
32618
32619})();
32620(function(){
32621"use strict";
32622
32623/**
32624 * @ngdoc directive
32625 * @name mdTabs
32626 * @module material.components.tabs
32627 *
32628 * @restrict E
32629 *
32630 * @description
32631 * The `<md-tabs>` directive serves as the container for 1..n `<md-tab>` child directives to
32632 * produces a Tabs components. In turn, the nested `<md-tab>` directive is used to specify a tab
32633 * label for the **header button** and a [optional] tab view content that will be associated with
32634 * each tab button.
32635 *
32636 * Below is the markup for its simplest usage:
32637 *
32638 * <hljs lang="html">
32639 * <md-tabs>
32640 * <md-tab label="Tab #1"></md-tab>
32641 * <md-tab label="Tab #2"></md-tab>
32642 * <md-tab label="Tab #3"></md-tab>
32643 * </md-tabs>
32644 * </hljs>
32645 *
32646 * Tabs supports three (3) usage scenarios:
32647 *
32648 * 1. Tabs (buttons only)
32649 * 2. Tabs with internal view content
32650 * 3. Tabs with external view content
32651 *
32652 * **Tab-only** support is useful when tab buttons are used for custom navigation regardless of any
32653 * other components, content, or views.
32654 *
32655 * <i><b>Note:</b> If you are using the Tabs component for page-level navigation, please take a look
32656 * at the <a ng-href="./api/directive/mdNavBar">NavBar component</a> instead as it can handle this
32657 * case a bit more natively.</i>
32658 *
32659 * **Tabs with internal views** are the traditional usages where each tab has associated view
32660 * content and the view switching is managed internally by the Tabs component.
32661 *
32662 * **Tabs with external view content** is often useful when content associated with each tab is
32663 * independently managed and data-binding notifications announce tab selection changes.
32664 *
32665 * Additional features also include:
32666 *
32667 * * Content can include any markup.
32668 * * If a tab is disabled while active/selected, then the next tab will be auto-selected.
32669 *
32670 * ### Explanation of tab stretching
32671 *
32672 * 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.
32673 *
32674 * On mobile devices, tabs will be expanded to fill the available horizontal space. When this happens, all tabs will become the same size.
32675 *
32676 * On desktops, by default, stretching will never occur.
32677 *
32678 * This default behavior can be overridden through the `md-stretch-tabs` attribute. Here is a table showing when stretching will occur:
32679 *
32680 * `md-stretch-tabs` | mobile | desktop
32681 * ------------------|-----------|--------
32682 * `auto` | stretched | ---
32683 * `always` | stretched | stretched
32684 * `never` | --- | ---
32685 *
32686 * @param {integer=} md-selected Index of the active/selected tab
32687 * @param {boolean=} md-no-ink If present, disables ink ripple effects.
32688 * @param {boolean=} md-no-ink-bar If present, disables the selection ink bar.
32689 * @param {string=} md-align-tabs Attribute to indicate position of tab buttons: `bottom` or `top`; default is `top`
32690 * @param {string=} md-stretch-tabs Attribute to indicate whether or not to stretch tabs: `auto`, `always`, or `never`; default is `auto`
32691 * @param {boolean=} md-dynamic-height When enabled, the tab wrapper will resize based on the contents of the selected tab
32692 * @param {boolean=} md-border-bottom If present, shows a solid `1px` border between the tabs and their content
32693 * @param {boolean=} md-center-tabs When enabled, tabs will be centered provided there is no need for pagination
32694 * @param {boolean=} md-no-pagination When enabled, pagination will remain off
32695 * @param {boolean=} md-swipe-content When enabled, swipe gestures will be enabled for the content area to jump between tabs
32696 * @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.
32697 * @param {boolean=} md-autoselect When present, any tabs added after the initial load will be automatically selected
32698 * @param {boolean=} md-no-select-click When enabled, click events will not be fired when selecting tabs
32699 *
32700 * @usage
32701 * <hljs lang="html">
32702 * <md-tabs md-selected="selectedIndex" >
32703 * <img ng-src="img/angular.png" class="centered">
32704 * <md-tab
32705 * ng-repeat="tab in tabs | orderBy:predicate:reversed"
32706 * md-on-select="onTabSelected(tab)"
32707 * md-on-deselect="announceDeselected(tab)"
32708 * ng-disabled="tab.disabled">
32709 * <md-tab-label>
32710 * {{tab.title}}
32711 * <img src="img/removeTab.png" ng-click="removeTab(tab)" class="delete">
32712 * </md-tab-label>
32713 * <md-tab-body>
32714 * {{tab.content}}
32715 * </md-tab-body>
32716 * </md-tab>
32717 * </md-tabs>
32718 * </hljs>
32719 *
32720 */
32721MdTabs.$inject = ["$$mdSvgRegistry"];
32722angular
32723 .module('material.components.tabs')
32724 .directive('mdTabs', MdTabs);
32725
32726function MdTabs ($$mdSvgRegistry) {
32727 return {
32728 scope: {
32729 selectedIndex: '=?mdSelected'
32730 },
32731 template: function (element, attr) {
32732 attr[ "$mdTabsTemplate" ] = element.html();
32733 return '' +
32734 '<md-tabs-wrapper> ' +
32735 '<md-tab-data></md-tab-data> ' +
32736 '<md-prev-button ' +
32737 'tabindex="-1" ' +
32738 'role="button" ' +
32739 'aria-label="Previous Page" ' +
32740 'aria-disabled="{{!$mdTabsCtrl.canPageBack()}}" ' +
32741 'ng-class="{ \'md-disabled\': !$mdTabsCtrl.canPageBack() }" ' +
32742 'ng-if="$mdTabsCtrl.shouldPaginate" ' +
32743 'ng-click="$mdTabsCtrl.previousPage()"> ' +
32744 '<md-icon md-svg-src="'+ $$mdSvgRegistry.mdTabsArrow +'"></md-icon> ' +
32745 '</md-prev-button> ' +
32746 '<md-next-button ' +
32747 'tabindex="-1" ' +
32748 'role="button" ' +
32749 'aria-label="Next Page" ' +
32750 'aria-disabled="{{!$mdTabsCtrl.canPageForward()}}" ' +
32751 'ng-class="{ \'md-disabled\': !$mdTabsCtrl.canPageForward() }" ' +
32752 'ng-if="$mdTabsCtrl.shouldPaginate" ' +
32753 'ng-click="$mdTabsCtrl.nextPage()"> ' +
32754 '<md-icon md-svg-src="'+ $$mdSvgRegistry.mdTabsArrow +'"></md-icon> ' +
32755 '</md-next-button> ' +
32756 '<md-tabs-canvas ' +
32757 'tabindex="{{ $mdTabsCtrl.hasFocus ? -1 : 0 }}" ' +
32758 'aria-activedescendant="tab-item-{{$mdTabsCtrl.tabs[$mdTabsCtrl.focusIndex].id}}" ' +
32759 'ng-focus="$mdTabsCtrl.redirectFocus()" ' +
32760 'ng-class="{ ' +
32761 '\'md-paginated\': $mdTabsCtrl.shouldPaginate, ' +
32762 '\'md-center-tabs\': $mdTabsCtrl.shouldCenterTabs ' +
32763 '}" ' +
32764 'ng-keydown="$mdTabsCtrl.keydown($event)" ' +
32765 'role="tablist"> ' +
32766 '<md-pagination-wrapper ' +
32767 'ng-class="{ \'md-center-tabs\': $mdTabsCtrl.shouldCenterTabs }" ' +
32768 'md-tab-scroll="$mdTabsCtrl.scroll($event)"> ' +
32769 '<md-tab-item ' +
32770 'tabindex="-1" ' +
32771 'class="md-tab" ' +
32772 'ng-repeat="tab in $mdTabsCtrl.tabs" ' +
32773 'role="tab" ' +
32774 'aria-controls="tab-content-{{::tab.id}}" ' +
32775 'aria-selected="{{tab.isActive()}}" ' +
32776 'aria-disabled="{{tab.scope.disabled || \'false\'}}" ' +
32777 'ng-click="$mdTabsCtrl.select(tab.getIndex())" ' +
32778 'ng-class="{ ' +
32779 '\'md-active\': tab.isActive(), ' +
32780 '\'md-focused\': tab.hasFocus(), ' +
32781 '\'md-disabled\': tab.scope.disabled ' +
32782 '}" ' +
32783 'ng-disabled="tab.scope.disabled" ' +
32784 'md-swipe-left="$mdTabsCtrl.nextPage()" ' +
32785 'md-swipe-right="$mdTabsCtrl.previousPage()" ' +
32786 'md-tabs-template="::tab.label" ' +
32787 'md-scope="::tab.parent"></md-tab-item> ' +
32788 '<md-ink-bar></md-ink-bar> ' +
32789 '</md-pagination-wrapper> ' +
32790 '<md-tabs-dummy-wrapper class="md-visually-hidden md-dummy-wrapper"> ' +
32791 '<md-dummy-tab ' +
32792 'class="md-tab" ' +
32793 'tabindex="-1" ' +
32794 'id="tab-item-{{::tab.id}}" ' +
32795 'role="tab" ' +
32796 'aria-controls="tab-content-{{::tab.id}}" ' +
32797 'aria-selected="{{tab.isActive()}}" ' +
32798 'aria-disabled="{{tab.scope.disabled || \'false\'}}" ' +
32799 'ng-focus="$mdTabsCtrl.hasFocus = true" ' +
32800 'ng-blur="$mdTabsCtrl.hasFocus = false" ' +
32801 'ng-repeat="tab in $mdTabsCtrl.tabs" ' +
32802 'md-tabs-template="::tab.label" ' +
32803 'md-scope="::tab.parent"></md-dummy-tab> ' +
32804 '</md-tabs-dummy-wrapper> ' +
32805 '</md-tabs-canvas> ' +
32806 '</md-tabs-wrapper> ' +
32807 '<md-tabs-content-wrapper ng-show="$mdTabsCtrl.hasContent && $mdTabsCtrl.selectedIndex >= 0" class="_md"> ' +
32808 '<md-tab-content ' +
32809 'id="tab-content-{{::tab.id}}" ' +
32810 'class="_md" ' +
32811 'role="tabpanel" ' +
32812 'aria-labelledby="tab-item-{{::tab.id}}" ' +
32813 'md-swipe-left="$mdTabsCtrl.swipeContent && $mdTabsCtrl.incrementIndex(1)" ' +
32814 'md-swipe-right="$mdTabsCtrl.swipeContent && $mdTabsCtrl.incrementIndex(-1)" ' +
32815 'ng-if="$mdTabsCtrl.hasContent" ' +
32816 'ng-repeat="(index, tab) in $mdTabsCtrl.tabs" ' +
32817 'ng-class="{ ' +
32818 '\'md-no-transition\': $mdTabsCtrl.lastSelectedIndex == null, ' +
32819 '\'md-active\': tab.isActive(), ' +
32820 '\'md-left\': tab.isLeft(), ' +
32821 '\'md-right\': tab.isRight(), ' +
32822 '\'md-no-scroll\': $mdTabsCtrl.dynamicHeight ' +
32823 '}"> ' +
32824 '<div ' +
32825 'md-tabs-template="::tab.template" ' +
32826 'md-connected-if="tab.isActive()" ' +
32827 'md-scope="::tab.parent" ' +
32828 'ng-if="$mdTabsCtrl.enableDisconnect || tab.shouldRender()"></div> ' +
32829 '</md-tab-content> ' +
32830 '</md-tabs-content-wrapper>';
32831 },
32832 controller: 'MdTabsController',
32833 controllerAs: '$mdTabsCtrl',
32834 bindToController: true
32835 };
32836}
32837
32838})();
32839(function(){
32840"use strict";
32841
32842
32843MdTabsDummyWrapper.$inject = ["$mdUtil", "$window"];angular
32844 .module('material.components.tabs')
32845 .directive('mdTabsDummyWrapper', MdTabsDummyWrapper);
32846
32847/**
32848 * @private
32849 *
32850 * @param $mdUtil
32851 * @param $window
32852 * @returns {{require: string, link: link}}
32853 * @constructor
32854 *
32855 * @ngInject
32856 */
32857function MdTabsDummyWrapper ($mdUtil, $window) {
32858 return {
32859 require: '^?mdTabs',
32860 link: function link (scope, element, attr, ctrl) {
32861 if (!ctrl) return;
32862
32863 var observer;
32864 var disconnect;
32865
32866 var mutationCallback = function() {
32867 ctrl.updatePagination();
32868 ctrl.updateInkBarStyles();
32869 };
32870
32871 if('MutationObserver' in $window) {
32872 var config = {
32873 childList: true,
32874 subtree: true,
32875 // Per https://bugzilla.mozilla.org/show_bug.cgi?id=1138368, browsers will not fire
32876 // the childList mutation, once a <span> element's innerText changes.
32877 // The characterData of the <span> element will change.
32878 characterData: true
32879 };
32880
32881 observer = new MutationObserver(mutationCallback);
32882 observer.observe(element[0], config);
32883 disconnect = observer.disconnect.bind(observer);
32884 } else {
32885 var debounced = $mdUtil.debounce(mutationCallback, 15, null, false);
32886
32887 element.on('DOMSubtreeModified', debounced);
32888 disconnect = element.off.bind(element, 'DOMSubtreeModified', debounced);
32889 }
32890
32891 // Disconnect the observer
32892 scope.$on('$destroy', function() {
32893 disconnect();
32894 });
32895 }
32896 };
32897}
32898
32899})();
32900(function(){
32901"use strict";
32902
32903
32904MdTabsTemplate.$inject = ["$compile", "$mdUtil"];angular
32905 .module('material.components.tabs')
32906 .directive('mdTabsTemplate', MdTabsTemplate);
32907
32908function MdTabsTemplate ($compile, $mdUtil) {
32909 return {
32910 restrict: 'A',
32911 link: link,
32912 scope: {
32913 template: '=mdTabsTemplate',
32914 connected: '=?mdConnectedIf',
32915 compileScope: '=mdScope'
32916 },
32917 require: '^?mdTabs'
32918 };
32919 function link (scope, element, attr, ctrl) {
32920 if (!ctrl) return;
32921
32922 var compileScope = ctrl.enableDisconnect ? scope.compileScope.$new() : scope.compileScope;
32923
32924 element.html(scope.template);
32925 $compile(element.contents())(compileScope);
32926
32927 return $mdUtil.nextTick(handleScope);
32928
32929 function handleScope () {
32930 scope.$watch('connected', function (value) { value === false ? disconnect() : reconnect(); });
32931 scope.$on('$destroy', reconnect);
32932 }
32933
32934 function disconnect () {
32935 if (ctrl.enableDisconnect) $mdUtil.disconnectScope(compileScope);
32936 }
32937
32938 function reconnect () {
32939 if (ctrl.enableDisconnect) $mdUtil.reconnectScope(compileScope);
32940 }
32941 }
32942}
32943
32944})();
32945(function(){
32946angular.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:hover, .md-autocomplete-suggestions-container.md-THEME_NAME-theme li.selected { background: '{{background-200}}'; }md-backdrop { background-color: '{{background-900-0.0}}'; } md-backdrop.md-opaque.md-THEME_NAME-theme { background-color: '{{background-900-1.0}}'; }md-bottom-sheet.md-THEME_NAME-theme { background-color: '{{background-50}}'; border-top-color: '{{background-300}}'; } md-bottom-sheet.md-THEME_NAME-theme.md-list md-list-item { color: '{{foreground-1}}'; } md-bottom-sheet.md-THEME_NAME-theme .md-subheader { background-color: '{{background-50}}'; } md-bottom-sheet.md-THEME_NAME-theme .md-subheader { color: '{{foreground-1}}'; }.md-button.md-THEME_NAME-theme:not([disabled]):hover { background-color: '{{background-500-0.2}}'; }.md-button.md-THEME_NAME-theme:not([disabled]).md-focused { 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 { background-color: '{{accent-color}}'; color: '{{accent-contrast}}'; } .md-button.md-THEME_NAME-theme.md-fab md-icon { color: '{{accent-contrast}}'; } .md-button.md-THEME_NAME-theme.md-fab:not([disabled]):hover { background-color: '{{accent-A700}}'; } .md-button.md-THEME_NAME-theme.md-fab:not([disabled]).md-focused { background-color: '{{accent-A700}}'; }.md-button.md-THEME_NAME-theme.md-primary { color: '{{primary-color}}'; } .md-button.md-THEME_NAME-theme.md-primary.md-raised, .md-button.md-THEME_NAME-theme.md-primary.md-fab { color: '{{primary-contrast}}'; background-color: '{{primary-color}}'; } .md-button.md-THEME_NAME-theme.md-primary.md-raised:not([disabled]) md-icon, .md-button.md-THEME_NAME-theme.md-primary.md-fab:not([disabled]) md-icon { color: '{{primary-contrast}}'; } .md-button.md-THEME_NAME-theme.md-primary.md-raised:not([disabled]):hover, .md-button.md-THEME_NAME-theme.md-primary.md-fab:not([disabled]):hover { background-color: '{{primary-600}}'; } .md-button.md-THEME_NAME-theme.md-primary.md-raised:not([disabled]).md-focused, .md-button.md-THEME_NAME-theme.md-primary.md-fab:not([disabled]).md-focused { background-color: '{{primary-600}}'; } .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]):hover { background-color: '{{accent-A700}}'; } .md-button.md-THEME_NAME-theme.md-fab:not([disabled]).md-focused { 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-raised, .md-button.md-THEME_NAME-theme.md-warn.md-fab { color: '{{warn-contrast}}'; background-color: '{{warn-color}}'; } .md-button.md-THEME_NAME-theme.md-warn.md-raised:not([disabled]) md-icon, .md-button.md-THEME_NAME-theme.md-warn.md-fab:not([disabled]) md-icon { color: '{{warn-contrast}}'; } .md-button.md-THEME_NAME-theme.md-warn.md-raised:not([disabled]):hover, .md-button.md-THEME_NAME-theme.md-warn.md-fab:not([disabled]):hover { background-color: '{{warn-600}}'; } .md-button.md-THEME_NAME-theme.md-warn.md-raised:not([disabled]).md-focused, .md-button.md-THEME_NAME-theme.md-warn.md-fab:not([disabled]).md-focused { background-color: '{{warn-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-raised, .md-button.md-THEME_NAME-theme.md-accent.md-fab { color: '{{accent-contrast}}'; background-color: '{{accent-color}}'; } .md-button.md-THEME_NAME-theme.md-accent.md-raised:not([disabled]) md-icon, .md-button.md-THEME_NAME-theme.md-accent.md-fab:not([disabled]) md-icon { color: '{{accent-contrast}}'; } .md-button.md-THEME_NAME-theme.md-accent.md-raised:not([disabled]):hover, .md-button.md-THEME_NAME-theme.md-accent.md-fab:not([disabled]):hover { background-color: '{{accent-A700}}'; } .md-button.md-THEME_NAME-theme.md-accent.md-raised:not([disabled]).md-focused, .md-button.md-THEME_NAME-theme.md-accent.md-fab:not([disabled]).md-focused { background-color: '{{accent-A700}}'; } .md-button.md-THEME_NAME-theme.md-accent:not([disabled]) md-icon { color: '{{accent-color}}'; }.md-button.md-THEME_NAME-theme[disabled], .md-button.md-THEME_NAME-theme.md-raised[disabled], .md-button.md-THEME_NAME-theme.md-fab[disabled], .md-button.md-THEME_NAME-theme.md-accent[disabled], .md-button.md-THEME_NAME-theme.md-warn[disabled] { color: '{{foreground-3}}'; cursor: default; } .md-button.md-THEME_NAME-theme[disabled] md-icon, .md-button.md-THEME_NAME-theme.md-raised[disabled] md-icon, .md-button.md-THEME_NAME-theme.md-fab[disabled] md-icon, .md-button.md-THEME_NAME-theme.md-accent[disabled] md-icon, .md-button.md-THEME_NAME-theme.md-warn[disabled] md-icon { color: '{{foreground-3}}'; }.md-button.md-THEME_NAME-theme.md-raised[disabled], .md-button.md-THEME_NAME-theme.md-fab[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-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 { color: '{{foreground-2}}'; } md-card.md-THEME_NAME-theme md-card-title md-card-title-text:not(:only-child) .md-subhead { color: '{{foreground-2}}'; }md-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::-webkit-input-placeholder { color: '{{foreground-3}}'; } 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::-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-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 { color: '{{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-content.md-THEME_NAME-theme { color: '{{foreground-1}}'; background-color: '{{background-default}}'; }/** Theme styles for mdCalendar. */.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}}'; }/** Theme styles for mdDatepicker. */.md-THEME_NAME-theme .md-datepicker-input { color: '{{foreground-1}}'; } .md-THEME_NAME-theme .md-datepicker-input::-webkit-input-placeholder { color: '{{foreground-3}}'; } .md-THEME_NAME-theme .md-datepicker-input:-moz-placeholder { color: '{{foreground-3}}'; } .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-warn .md-THEME_NAME-theme .md-datepicker-input-container.md-datepicker-focused { border-bottom-color: '{{warn-A700}}'; } .md-THEME_NAME-theme .md-datepicker-input-container.md-datepicker-invalid { border-bottom-color: '{{warn-A700}}'; }.md-THEME_NAME-theme .md-datepicker-calendar-pane { border-color: '{{background-hue-1}}'; }.md-THEME_NAME-theme .md-datepicker-triangle-button .md-datepicker-expand-triangle { border-top-color: '{{foreground-3}}'; }.md-THEME_NAME-theme .md-datepicker-triangle-button:hover .md-datepicker-expand-triangle { border-top-color: '{{foreground-2}}'; }.md-THEME_NAME-theme .md-datepicker-open .md-datepicker-calendar-icon { color: '{{primary-color}}'; }.md-THEME_NAME-theme .md-datepicker-open.md-accent .md-datepicker-calendar-icon, .md-accent .md-THEME_NAME-theme .md-datepicker-open .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 { border-top-color: '{{foreground-4}}'; }md-divider.md-THEME_NAME-theme { border-top-color: '{{foreground-4}}'; }.layout-row > md-divider.md-THEME_NAME-theme,.layout-xs-row > md-divider.md-THEME_NAME-theme, .layout-gt-xs-row > md-divider.md-THEME_NAME-theme,.layout-sm-row > md-divider.md-THEME_NAME-theme, .layout-gt-sm-row > md-divider.md-THEME_NAME-theme,.layout-md-row > md-divider.md-THEME_NAME-theme, .layout-gt-md-row > md-divider.md-THEME_NAME-theme,.layout-lg-row > md-divider.md-THEME_NAME-theme, .layout-gt-lg-row > md-divider.md-THEME_NAME-theme,.layout-xl-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::-webkit-input-placeholder { color: '{{foreground-3}}'; } md-input-container.md-THEME_NAME-theme .md-input:-moz-placeholder { color: '{{foreground-3}}'; } 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 label,md-input-container.md-THEME_NAME-theme .md-placeholder { 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-messages-animation, md-input-container.md-THEME_NAME-theme .md-input-message-animation { color: '{{warn-A700}}'; } md-input-container.md-THEME_NAME-theme .md-input-messages-animation .md-char-counter, md-input-container.md-THEME_NAME-theme .md-input-message-animation .md-char-counter { color: '{{foreground-1}}'; }md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-has-value label { color: '{{foreground-2}}'; }md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused .md-input, 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 label,md-input-container.md-THEME_NAME-theme.md-input-invalid .md-input-message-animation,md-input-container.md-THEME_NAME-theme.md-input-invalid .md-char-counter { color: '{{warn-A700}}'; }md-input-container.md-THEME_NAME-theme .md-input[disabled],[disabled] md-input-container.md-THEME_NAME-theme .md-input { border-bottom-color: transparent; color: '{{foreground-3}}'; background-image: linear-gradient(to right, \"{{foreground-3}}\" 0%, \"{{foreground-3}}\" 33%, transparent 0%); background-image: -ms-linear-gradient(left, transparent 0%, \"{{foreground-3}}\" 100%); }md-list.md-THEME_NAME-theme md-list-item.md-2-line .md-list-item-text h3, md-list.md-THEME_NAME-theme md-list-item.md-2-line .md-list-item-text h4,md-list.md-THEME_NAME-theme md-list-item.md-3-line .md-list-item-text h3,md-list.md-THEME_NAME-theme md-list-item.md-3-line .md-list-item-text h4 { color: '{{foreground-1}}'; }md-list.md-THEME_NAME-theme md-list-item.md-2-line .md-list-item-text p,md-list.md-THEME_NAME-theme md-list-item.md-3-line .md-list-item-text p { color: '{{foreground-2}}'; }md-list.md-THEME_NAME-theme .md-proxy-focus.md-focused div.md-no-style { background-color: '{{background-100}}'; }md-list.md-THEME_NAME-theme md-list-item .md-avatar-icon { background-color: '{{foreground-3}}'; color: '{{background-color}}'; }md-list.md-THEME_NAME-theme md-list-item > md-icon { color: '{{foreground-2}}'; } md-list.md-THEME_NAME-theme md-list-item > md-icon.md-highlight { color: '{{primary-color}}'; } md-list.md-THEME_NAME-theme md-list-item > md-icon.md-highlight.md-accent { color: '{{accent-color}}'; }md-menu-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] { color: '{{background-A200-0.25}}'; } 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:hover,md-menu-bar.md-THEME_NAME-theme:not(.md-keyboard-mode):not(.md-open) md-menu button:focus { background: transparent; }md-menu-content.md-THEME_NAME-theme .md-menu > .md-button:after { color: '{{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-panel { background-color: '{{background-900-0.0}}'; } .md-panel._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-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-group.md-THEME_NAME-theme:not([disabled]) .md-primary .md-on, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-primary .md-on,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-primary .md-on,md-radio-button.md-THEME_NAME-theme:not([disabled]).md-primary .md-on { background-color: '{{primary-color-0.87}}'; }md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-primary .md-checked .md-off, md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-primary.md-checked .md-off, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-primary .md-checked .md-off, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-primary.md-checked .md-off,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-primary .md-checked .md-off,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-primary.md-checked .md-off,md-radio-button.md-THEME_NAME-theme:not([disabled]).md-primary .md-checked .md-off,md-radio-button.md-THEME_NAME-theme:not([disabled]).md-primary.md-checked .md-off { border-color: '{{primary-color-0.87}}'; }md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-primary .md-checked .md-ink-ripple, md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-primary.md-checked .md-ink-ripple, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-primary .md-checked .md-ink-ripple, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-primary.md-checked .md-ink-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-primary .md-checked .md-ink-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-primary.md-checked .md-ink-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]).md-primary .md-checked .md-ink-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]).md-primary.md-checked .md-ink-ripple { color: '{{primary-color-0.87}}'; }md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-primary .md-container .md-ripple, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-primary .md-container .md-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-primary .md-container .md-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]).md-primary .md-container .md-ripple { color: '{{primary-600}}'; }md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-warn .md-on, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-warn .md-on,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-warn .md-on,md-radio-button.md-THEME_NAME-theme:not([disabled]).md-warn .md-on { background-color: '{{warn-color-0.87}}'; }md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-warn .md-checked .md-off, md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-warn.md-checked .md-off, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-warn .md-checked .md-off, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-warn.md-checked .md-off,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-warn .md-checked .md-off,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-warn.md-checked .md-off,md-radio-button.md-THEME_NAME-theme:not([disabled]).md-warn .md-checked .md-off,md-radio-button.md-THEME_NAME-theme:not([disabled]).md-warn.md-checked .md-off { border-color: '{{warn-color-0.87}}'; }md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-warn .md-checked .md-ink-ripple, md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-warn.md-checked .md-ink-ripple, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-warn .md-checked .md-ink-ripple, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-warn.md-checked .md-ink-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-warn .md-checked .md-ink-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-warn.md-checked .md-ink-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]).md-warn .md-checked .md-ink-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]).md-warn.md-checked .md-ink-ripple { color: '{{warn-color-0.87}}'; }md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-warn .md-container .md-ripple, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-warn .md-container .md-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-warn .md-container .md-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]).md-warn .md-container .md-ripple { color: '{{warn-600}}'; }md-radio-group.md-THEME_NAME-theme[disabled],md-radio-button.md-THEME_NAME-theme[disabled] { color: '{{foreground-3}}'; } md-radio-group.md-THEME_NAME-theme[disabled] .md-container .md-off, md-radio-button.md-THEME_NAME-theme[disabled] .md-container .md-off { border-color: '{{foreground-3}}'; } md-radio-group.md-THEME_NAME-theme[disabled] .md-container .md-on, md-radio-button.md-THEME_NAME-theme[disabled] .md-container .md-on { border-color: '{{foreground-3}}'; }md-radio-group.md-THEME_NAME-theme .md-checked .md-ink-ripple { color: '{{accent-color-0.26}}'; }md-radio-group.md-THEME_NAME-theme.md-primary .md-checked:not([disabled]) .md-ink-ripple, md-radio-group.md-THEME_NAME-theme .md-checked:not([disabled]).md-primary .md-ink-ripple { color: '{{primary-color-0.26}}'; }md-radio-group.md-THEME_NAME-theme .md-checked.md-primary .md-ink-ripple { color: '{{warn-color-0.26}}'; }md-radio-group.md-THEME_NAME-theme.md-focused:not(:empty) .md-checked .md-container:before { background-color: '{{accent-color-0.26}}'; }md-radio-group.md-THEME_NAME-theme.md-focused:not(:empty).md-primary .md-checked .md-container:before,md-radio-group.md-THEME_NAME-theme.md-focused:not(:empty) .md-checked.md-primary .md-container:before { background-color: '{{primary-color-0.26}}'; }md-radio-group.md-THEME_NAME-theme.md-focused:not(:empty).md-warn .md-checked .md-container:before,md-radio-group.md-THEME_NAME-theme.md-focused:not(:empty) .md-checked.md-warn .md-container:before { background-color: '{{warn-color-0.26}}'; }md-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 { color: '{{primary-color}}'; } 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(to right, \"{{foreground-3}}\" 0%, \"{{foreground-3}}\" 33%, transparent 0%); background-image: -ms-linear-gradient(left, transparent 0%, \"{{foreground-3}}\" 100%); }md-select.md-THEME_NAME-theme .md-select-value { border-bottom-color: '{{foreground-4}}'; } md-select.md-THEME_NAME-theme .md-select-value.md-select-placeholder { color: '{{foreground-3}}'; } md-select.md-THEME_NAME-theme .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-value { color: '{{foreground-3}}'; } md-select.md-THEME_NAME-theme[disabled] .md-select-value.md-select-placeholder { color: '{{foreground-3}}'; }md-select-menu.md-THEME_NAME-theme md-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 { color: '{{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}}'; }md-tabs.md-THEME_NAME-theme .md-paginator md-icon { color: '{{primary-color}}'; }md-tabs.md-THEME_NAME-theme md-ink-bar { color: '{{accent-color}}'; background: '{{accent-color}}'; }md-tabs.md-THEME_NAME-theme .md-tab { color: '{{foreground-2}}'; } md-tabs.md-THEME_NAME-theme .md-tab[disabled], md-tabs.md-THEME_NAME-theme .md-tab[disabled] md-icon { color: '{{foreground-3}}'; } md-tabs.md-THEME_NAME-theme .md-tab.md-active, md-tabs.md-THEME_NAME-theme .md-tab.md-active md-icon, md-tabs.md-THEME_NAME-theme .md-tab.md-focused, md-tabs.md-THEME_NAME-theme .md-tab.md-focused md-icon { color: '{{primary-color}}'; } md-tabs.md-THEME_NAME-theme .md-tab.md-focused { background: '{{primary-color-0.1}}'; } md-tabs.md-THEME_NAME-theme .md-tab .md-ripple-container { color: '{{accent-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]) { 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]) { color: '{{primary-100}}'; } md-tabs.md-THEME_NAME-theme.md-primary > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-active, md-tabs.md-THEME_NAME-theme.md-primary > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-active md-icon, md-tabs.md-THEME_NAME-theme.md-primary > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-focused, md-tabs.md-THEME_NAME-theme.md-primary > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-focused md-icon { color: '{{primary-contrast}}'; } md-tabs.md-THEME_NAME-theme.md-primary > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-focused { background: '{{primary-contrast-0.1}}'; }md-tabs.md-THEME_NAME-theme.md-warn > md-tabs-wrapper { background-color: '{{warn-color}}'; } md-tabs.md-THEME_NAME-theme.md-warn > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]) { color: '{{warn-100}}'; } md-tabs.md-THEME_NAME-theme.md-warn > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-active, md-tabs.md-THEME_NAME-theme.md-warn > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-active md-icon, md-tabs.md-THEME_NAME-theme.md-warn > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-focused, md-tabs.md-THEME_NAME-theme.md-warn > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-focused md-icon { color: '{{warn-contrast}}'; } md-tabs.md-THEME_NAME-theme.md-warn > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-focused { background: '{{warn-contrast-0.1}}'; }md-toolbar > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper { background-color: '{{primary-color}}'; } md-toolbar > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]) { color: '{{primary-100}}'; } md-toolbar > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-active, md-toolbar > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-active md-icon, md-toolbar > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-focused, md-toolbar > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-focused md-icon { color: '{{primary-contrast}}'; } md-toolbar > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-focused { background: '{{primary-contrast-0.1}}'; }md-toolbar.md-accent > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper { background-color: '{{accent-color}}'; } md-toolbar.md-accent > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]) { color: '{{accent-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]) { 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-tooltip.md-THEME_NAME-theme { color: '{{background-700-contrast}}'; } md-tooltip.md-THEME_NAME-theme .md-content { background-color: '{{background-700}}'; }/* Only used with Theme processes */html.md-THEME_NAME-theme, body.md-THEME_NAME-theme { color: '{{foreground-1}}'; background-color: '{{background-color}}'; }");
32947})();
32948
32949
32950})(window, window.angular);;window.ngMaterial={version:{full: "1.1.1"}};