socketioMEANseed/www/bower_components/angular-material/angular-material.js
www/bower_components/angular-material/angular-material.jsRaw
1/*!
2 * Angular Material Design
3 * https://github.com/angular/material
4 * @license MIT
5 * v1.1.3
6 */
7(function( window, angular, undefined ){
8"use strict";
9
10(function(){
11"use strict";
12
13angular.module('ngMaterial', ["ng","ngAnimate","ngAria","material.core","material.core.gestures","material.core.interaction","material.core.layout","material.core.meta","material.core.theming.palette","material.core.theming","material.core.animate","material.components.autocomplete","material.components.backdrop","material.components.bottomSheet","material.components.button","material.components.card","material.components.checkbox","material.components.chips","material.components.colors","material.components.content","material.components.datepicker","material.components.dialog","material.components.divider","material.components.fabActions","material.components.fabShared","material.components.fabSpeedDial","material.components.fabToolbar","material.components.gridList","material.components.icon","material.components.input","material.components.list","material.components.menu","material.components.menuBar","material.components.panel","material.components.navBar","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.toast","material.components.tabs","material.components.toolbar","material.components.tooltip","material.components.truncate","material.components.virtualRepeat","material.components.whiteframe"]);
14})();
15(function(){
16"use strict";
17
18/**
19 * Initialization function that validates environment
20 * requirements.
21 */
22DetectNgTouch.$inject = ["$log", "$injector"];
23MdCoreConfigure.$inject = ["$provide", "$mdThemingProvider"];
24rAFDecorator.$inject = ["$delegate"];
25qDecorator.$inject = ["$delegate"];
26angular
27 .module('material.core', [
28 'ngAnimate',
29 'material.core.animate',
30 'material.core.layout',
31 'material.core.interaction',
32 'material.core.gestures',
33 'material.core.theming'
34 ])
35 .config(MdCoreConfigure)
36 .run(DetectNgTouch);
37
38
39/**
40 * Detect if the ng-Touch module is also being used.
41 * Warn if detected.
42 * @ngInject
43 */
44function DetectNgTouch($log, $injector) {
45 if ( $injector.has('$swipe') ) {
46 var msg = "" +
47 "You are using the ngTouch module. \n" +
48 "Angular Material already has mobile click, tap, and swipe support... \n" +
49 "ngTouch is not supported with Angular Material!";
50 $log.warn(msg);
51 }
52}
53
54/**
55 * @ngInject
56 */
57function MdCoreConfigure($provide, $mdThemingProvider) {
58
59 $provide.decorator('$$rAF', ['$delegate', rAFDecorator]);
60 $provide.decorator('$q', ['$delegate', qDecorator]);
61
62 $mdThemingProvider.theme('default')
63 .primaryPalette('indigo')
64 .accentPalette('pink')
65 .warnPalette('deep-orange')
66 .backgroundPalette('grey');
67}
68
69/**
70 * @ngInject
71 */
72function rAFDecorator($delegate) {
73 /**
74 * Use this to throttle events that come in often.
75 * The throttled function will always use the *last* invocation before the
76 * coming frame.
77 *
78 * For example, window resize events that fire many times a second:
79 * If we set to use an raf-throttled callback on window resize, then
80 * our callback will only be fired once per frame, with the last resize
81 * event that happened before that frame.
82 *
83 * @param {function} callback function to debounce
84 */
85 $delegate.throttle = function(cb) {
86 var queuedArgs, alreadyQueued, queueCb, context;
87 return function debounced() {
88 queuedArgs = arguments;
89 context = this;
90 queueCb = cb;
91 if (!alreadyQueued) {
92 alreadyQueued = true;
93 $delegate(function() {
94 queueCb.apply(context, Array.prototype.slice.call(queuedArgs));
95 alreadyQueued = false;
96 });
97 }
98 };
99 };
100 return $delegate;
101}
102
103/**
104 * @ngInject
105 */
106function qDecorator($delegate) {
107 /**
108 * Adds a shim for $q.resolve for Angular version that don't have it,
109 * so we don't have to think about it.
110 *
111 * via https://github.com/angular/angular.js/pull/11987
112 */
113
114 // TODO(crisbeto): this won't be necessary once we drop Angular 1.3
115 if (!$delegate.resolve) {
116 $delegate.resolve = $delegate.when;
117 }
118 return $delegate;
119}
120
121})();
122(function(){
123"use strict";
124
125
126MdAutofocusDirective.$inject = ["$parse"];angular.module('material.core')
127 .directive('mdAutofocus', MdAutofocusDirective)
128
129 // Support the deprecated md-auto-focus and md-sidenav-focus as well
130 .directive('mdAutoFocus', MdAutofocusDirective)
131 .directive('mdSidenavFocus', MdAutofocusDirective);
132
133/**
134 * @ngdoc directive
135 * @name mdAutofocus
136 * @module material.core.util
137 *
138 * @description
139 *
140 * `[md-autofocus]` provides an optional way to identify the focused element when a `$mdDialog`,
141 * `$mdBottomSheet`, `$mdMenu` or `$mdSidenav` opens or upon page load for input-like elements.
142 *
143 * When one of these opens, it will find the first nested element with the `[md-autofocus]`
144 * attribute directive and optional expression. An expression may be specified as the directive
145 * value to enable conditional activation of the autofocus.
146 *
147 * @usage
148 *
149 * ### Dialog
150 * <hljs lang="html">
151 * <md-dialog>
152 * <form>
153 * <md-input-container>
154 * <label for="testInput">Label</label>
155 * <input id="testInput" type="text" md-autofocus>
156 * </md-input-container>
157 * </form>
158 * </md-dialog>
159 * </hljs>
160 *
161 * ### Bottomsheet
162 * <hljs lang="html">
163 * <md-bottom-sheet class="md-list md-has-header">
164 * <md-subheader>Comment Actions</md-subheader>
165 * <md-list>
166 * <md-list-item ng-repeat="item in items">
167 *
168 * <md-button md-autofocus="$index == 2">
169 * <md-icon md-svg-src="{{item.icon}}"></md-icon>
170 * <span class="md-inline-list-icon-label">{{ item.name }}</span>
171 * </md-button>
172 *
173 * </md-list-item>
174 * </md-list>
175 * </md-bottom-sheet>
176 * </hljs>
177 *
178 * ### Autocomplete
179 * <hljs lang="html">
180 * <md-autocomplete
181 * md-autofocus
182 * md-selected-item="selectedItem"
183 * md-search-text="searchText"
184 * md-items="item in getMatches(searchText)"
185 * md-item-text="item.display">
186 * <span md-highlight-text="searchText">{{item.display}}</span>
187 * </md-autocomplete>
188 * </hljs>
189 *
190 * ### Sidenav
191 * <hljs lang="html">
192 * <div layout="row" ng-controller="MyController">
193 * <md-sidenav md-component-id="left" class="md-sidenav-left">
194 * Left Nav!
195 * </md-sidenav>
196 *
197 * <md-content>
198 * Center Content
199 * <md-button ng-click="openLeftMenu()">
200 * Open Left Menu
201 * </md-button>
202 * </md-content>
203 *
204 * <md-sidenav md-component-id="right"
205 * md-is-locked-open="$mdMedia('min-width: 333px')"
206 * class="md-sidenav-right">
207 * <form>
208 * <md-input-container>
209 * <label for="testInput">Test input</label>
210 * <input id="testInput" type="text"
211 * ng-model="data" md-autofocus>
212 * </md-input-container>
213 * </form>
214 * </md-sidenav>
215 * </div>
216 * </hljs>
217 **/
218function MdAutofocusDirective($parse) {
219 return {
220 restrict: 'A',
221 link: {
222 pre: preLink
223 }
224 };
225
226 function preLink(scope, element, attr) {
227 var attrExp = attr.mdAutoFocus || attr.mdAutofocus || attr.mdSidenavFocus;
228
229 // Initially update the expression by manually parsing the expression as per $watch source.
230 updateExpression($parse(attrExp)(scope));
231
232 // Only watch the expression if it is not empty.
233 if (attrExp) {
234 scope.$watch(attrExp, updateExpression);
235 }
236
237 /**
238 * Updates the autofocus class which is used to determine whether the attribute
239 * expression evaluates to true or false.
240 * @param {string|boolean} value Attribute Value
241 */
242 function updateExpression(value) {
243
244 // Rather than passing undefined to the jqLite toggle class function we explicitly set the
245 // value to true. Otherwise the class will be just toggled instead of being forced.
246 if (angular.isUndefined(value)) {
247 value = true;
248 }
249
250 element.toggleClass('md-autofocus', !!value);
251 }
252 }
253
254}
255
256})();
257(function(){
258"use strict";
259
260/**
261 * @ngdoc module
262 * @name material.core.colorUtil
263 * @description
264 * Color Util
265 */
266angular
267 .module('material.core')
268 .factory('$mdColorUtil', ColorUtilFactory);
269
270function ColorUtilFactory() {
271 /**
272 * Converts hex value to RGBA string
273 * @param color {string}
274 * @returns {string}
275 */
276 function hexToRgba (color) {
277 var hex = color[ 0 ] === '#' ? color.substr(1) : color,
278 dig = hex.length / 3,
279 red = hex.substr(0, dig),
280 green = hex.substr(dig, dig),
281 blue = hex.substr(dig * 2);
282 if (dig === 1) {
283 red += red;
284 green += green;
285 blue += blue;
286 }
287 return 'rgba(' + parseInt(red, 16) + ',' + parseInt(green, 16) + ',' + parseInt(blue, 16) + ',0.1)';
288 }
289
290 /**
291 * Converts rgba value to hex string
292 * @param color {string}
293 * @returns {string}
294 */
295 function rgbaToHex(color) {
296 color = color.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);
297
298 var hex = (color && color.length === 4) ? "#" +
299 ("0" + parseInt(color[1],10).toString(16)).slice(-2) +
300 ("0" + parseInt(color[2],10).toString(16)).slice(-2) +
301 ("0" + parseInt(color[3],10).toString(16)).slice(-2) : '';
302
303 return hex.toUpperCase();
304 }
305
306 /**
307 * Converts an RGB color to RGBA
308 * @param color {string}
309 * @returns {string}
310 */
311 function rgbToRgba (color) {
312 return color.replace(')', ', 0.1)').replace('(', 'a(');
313 }
314
315 /**
316 * Converts an RGBA color to RGB
317 * @param color {string}
318 * @returns {string}
319 */
320 function rgbaToRgb (color) {
321 return color
322 ? color.replace('rgba', 'rgb').replace(/,[^\),]+\)/, ')')
323 : 'rgb(0,0,0)';
324 }
325
326 return {
327 rgbaToHex: rgbaToHex,
328 hexToRgba: hexToRgba,
329 rgbToRgba: rgbToRgba,
330 rgbaToRgb: rgbaToRgb
331 };
332}
333
334})();
335(function(){
336"use strict";
337
338angular.module('material.core')
339.factory('$mdConstant', MdConstantFactory);
340
341/**
342 * Factory function that creates the grab-bag $mdConstant service.
343 * @ngInject
344 */
345function MdConstantFactory() {
346
347 var prefixTestEl = document.createElement('div');
348 var vendorPrefix = getVendorPrefix(prefixTestEl);
349 var isWebkit = /webkit/i.test(vendorPrefix);
350 var SPECIAL_CHARS_REGEXP = /([:\-_]+(.))/g;
351
352 function vendorProperty(name) {
353 // Add a dash between the prefix and name, to be able to transform the string into camelcase.
354 var prefixedName = vendorPrefix + '-' + name;
355 var ucPrefix = camelCase(prefixedName);
356 var lcPrefix = ucPrefix.charAt(0).toLowerCase() + ucPrefix.substring(1);
357
358 return hasStyleProperty(prefixTestEl, name) ? name : // The current browser supports the un-prefixed property
359 hasStyleProperty(prefixTestEl, ucPrefix) ? ucPrefix : // The current browser only supports the prefixed property.
360 hasStyleProperty(prefixTestEl, lcPrefix) ? lcPrefix : name; // Some browsers are only supporting the prefix in lowercase.
361 }
362
363 function hasStyleProperty(testElement, property) {
364 return angular.isDefined(testElement.style[property]);
365 }
366
367 function camelCase(input) {
368 return input.replace(SPECIAL_CHARS_REGEXP, function(matches, separator, letter, offset) {
369 return offset ? letter.toUpperCase() : letter;
370 });
371 }
372
373 function getVendorPrefix(testElement) {
374 var prop, match;
375 var vendorRegex = /^(Moz|webkit|ms)(?=[A-Z])/;
376
377 for (prop in testElement.style) {
378 if (match = vendorRegex.exec(prop)) {
379 return match[0];
380 }
381 }
382 }
383
384 var self = {
385 isInputKey : function(e) { return (e.keyCode >= 31 && e.keyCode <= 90); },
386 isNumPadKey : function (e){ return (3 === e.location && e.keyCode >= 97 && e.keyCode <= 105); },
387 isNavigationKey : function(e) {
388 var kc = self.KEY_CODE, NAVIGATION_KEYS = [kc.SPACE, kc.ENTER, kc.UP_ARROW, kc.DOWN_ARROW];
389 return (NAVIGATION_KEYS.indexOf(e.keyCode) != -1);
390 },
391
392 /**
393 * Maximum size, in pixels, that can be explicitly set to an element. The actual value varies
394 * between browsers, but IE11 has the very lowest size at a mere 1,533,917px. Ideally we could
395 * compute this value, but Firefox always reports an element to have a size of zero if it
396 * goes over the max, meaning that we'd have to binary search for the value.
397 */
398 ELEMENT_MAX_PIXELS: 1533917,
399
400 /**
401 * Priority for a directive that should run before the directives from ngAria.
402 */
403 BEFORE_NG_ARIA: 210,
404
405 /**
406 * Common Keyboard actions and their associated keycode.
407 */
408 KEY_CODE: {
409 COMMA: 188,
410 SEMICOLON : 186,
411 ENTER: 13,
412 ESCAPE: 27,
413 SPACE: 32,
414 PAGE_UP: 33,
415 PAGE_DOWN: 34,
416 END: 35,
417 HOME: 36,
418 LEFT_ARROW : 37,
419 UP_ARROW : 38,
420 RIGHT_ARROW : 39,
421 DOWN_ARROW : 40,
422 TAB : 9,
423 BACKSPACE: 8,
424 DELETE: 46
425 },
426
427 /**
428 * Vendor prefixed CSS properties to be used to support the given functionality in older browsers
429 * as well.
430 */
431 CSS: {
432 /* Constants */
433 TRANSITIONEND: 'transitionend' + (isWebkit ? ' webkitTransitionEnd' : ''),
434 ANIMATIONEND: 'animationend' + (isWebkit ? ' webkitAnimationEnd' : ''),
435
436 TRANSFORM: vendorProperty('transform'),
437 TRANSFORM_ORIGIN: vendorProperty('transformOrigin'),
438 TRANSITION: vendorProperty('transition'),
439 TRANSITION_DURATION: vendorProperty('transitionDuration'),
440 ANIMATION_PLAY_STATE: vendorProperty('animationPlayState'),
441 ANIMATION_DURATION: vendorProperty('animationDuration'),
442 ANIMATION_NAME: vendorProperty('animationName'),
443 ANIMATION_TIMING: vendorProperty('animationTimingFunction'),
444 ANIMATION_DIRECTION: vendorProperty('animationDirection')
445 },
446
447 /**
448 * As defined in core/style/variables.scss
449 *
450 * $layout-breakpoint-xs: 600px !default;
451 * $layout-breakpoint-sm: 960px !default;
452 * $layout-breakpoint-md: 1280px !default;
453 * $layout-breakpoint-lg: 1920px !default;
454 *
455 */
456 MEDIA: {
457 'xs' : '(max-width: 599px)' ,
458 'gt-xs' : '(min-width: 600px)' ,
459 'sm' : '(min-width: 600px) and (max-width: 959px)' ,
460 'gt-sm' : '(min-width: 960px)' ,
461 'md' : '(min-width: 960px) and (max-width: 1279px)' ,
462 'gt-md' : '(min-width: 1280px)' ,
463 'lg' : '(min-width: 1280px) and (max-width: 1919px)',
464 'gt-lg' : '(min-width: 1920px)' ,
465 'xl' : '(min-width: 1920px)' ,
466 'landscape' : '(orientation: landscape)' ,
467 'portrait' : '(orientation: portrait)' ,
468 'print' : 'print'
469 },
470
471 MEDIA_PRIORITY: [
472 'xl',
473 'gt-lg',
474 'lg',
475 'gt-md',
476 'md',
477 'gt-sm',
478 'sm',
479 'gt-xs',
480 'xs',
481 'landscape',
482 'portrait',
483 'print'
484 ]
485 };
486
487 return self;
488}
489
490})();
491(function(){
492"use strict";
493
494 angular
495 .module('material.core')
496 .config( ["$provide", function($provide){
497 $provide.decorator('$mdUtil', ['$delegate', function ($delegate){
498 /**
499 * Inject the iterator facade to easily support iteration and accessors
500 * @see iterator below
501 */
502 $delegate.iterator = MdIterator;
503
504 return $delegate;
505 }
506 ]);
507 }]);
508
509 /**
510 * iterator is a list facade to easily support iteration and accessors
511 *
512 * @param items Array list which this iterator will enumerate
513 * @param reloop Boolean enables iterator to consider the list as an endless reloop
514 */
515 function MdIterator(items, reloop) {
516 var trueFn = function() { return true; };
517
518 if (items && !angular.isArray(items)) {
519 items = Array.prototype.slice.call(items);
520 }
521
522 reloop = !!reloop;
523 var _items = items || [ ];
524
525 // Published API
526 return {
527 items: getItems,
528 count: count,
529
530 inRange: inRange,
531 contains: contains,
532 indexOf: indexOf,
533 itemAt: itemAt,
534
535 findBy: findBy,
536
537 add: add,
538 remove: remove,
539
540 first: first,
541 last: last,
542 next: angular.bind(null, findSubsequentItem, false),
543 previous: angular.bind(null, findSubsequentItem, true),
544
545 hasPrevious: hasPrevious,
546 hasNext: hasNext
547
548 };
549
550 /**
551 * Publish copy of the enumerable set
552 * @returns {Array|*}
553 */
554 function getItems() {
555 return [].concat(_items);
556 }
557
558 /**
559 * Determine length of the list
560 * @returns {Array.length|*|number}
561 */
562 function count() {
563 return _items.length;
564 }
565
566 /**
567 * Is the index specified valid
568 * @param index
569 * @returns {Array.length|*|number|boolean}
570 */
571 function inRange(index) {
572 return _items.length && ( index > -1 ) && (index < _items.length );
573 }
574
575 /**
576 * Can the iterator proceed to the next item in the list; relative to
577 * the specified item.
578 *
579 * @param item
580 * @returns {Array.length|*|number|boolean}
581 */
582 function hasNext(item) {
583 return item ? inRange(indexOf(item) + 1) : false;
584 }
585
586 /**
587 * Can the iterator proceed to the previous item in the list; relative to
588 * the specified item.
589 *
590 * @param item
591 * @returns {Array.length|*|number|boolean}
592 */
593 function hasPrevious(item) {
594 return item ? inRange(indexOf(item) - 1) : false;
595 }
596
597 /**
598 * Get item at specified index/position
599 * @param index
600 * @returns {*}
601 */
602 function itemAt(index) {
603 return inRange(index) ? _items[index] : null;
604 }
605
606 /**
607 * Find all elements matching the key/value pair
608 * otherwise return null
609 *
610 * @param val
611 * @param key
612 *
613 * @return array
614 */
615 function findBy(key, val) {
616 return _items.filter(function(item) {
617 return item[key] === val;
618 });
619 }
620
621 /**
622 * Add item to list
623 * @param item
624 * @param index
625 * @returns {*}
626 */
627 function add(item, index) {
628 if ( !item ) return -1;
629
630 if (!angular.isNumber(index)) {
631 index = _items.length;
632 }
633
634 _items.splice(index, 0, item);
635
636 return indexOf(item);
637 }
638
639 /**
640 * Remove item from list...
641 * @param item
642 */
643 function remove(item) {
644 if ( contains(item) ){
645 _items.splice(indexOf(item), 1);
646 }
647 }
648
649 /**
650 * Get the zero-based index of the target item
651 * @param item
652 * @returns {*}
653 */
654 function indexOf(item) {
655 return _items.indexOf(item);
656 }
657
658 /**
659 * Boolean existence check
660 * @param item
661 * @returns {boolean}
662 */
663 function contains(item) {
664 return item && (indexOf(item) > -1);
665 }
666
667 /**
668 * Return first item in the list
669 * @returns {*}
670 */
671 function first() {
672 return _items.length ? _items[0] : null;
673 }
674
675 /**
676 * Return last item in the list...
677 * @returns {*}
678 */
679 function last() {
680 return _items.length ? _items[_items.length - 1] : null;
681 }
682
683 /**
684 * Find the next item. If reloop is true and at the end of the list, it will go back to the
685 * first item. If given, the `validate` callback will be used to determine whether the next item
686 * is valid. If not valid, it will try to find the next item again.
687 *
688 * @param {boolean} backwards Specifies the direction of searching (forwards/backwards)
689 * @param {*} item The item whose subsequent item we are looking for
690 * @param {Function=} validate The `validate` function
691 * @param {integer=} limit The recursion limit
692 *
693 * @returns {*} The subsequent item or null
694 */
695 function findSubsequentItem(backwards, item, validate, limit) {
696 validate = validate || trueFn;
697
698 var curIndex = indexOf(item);
699 while (true) {
700 if (!inRange(curIndex)) return null;
701
702 var nextIndex = curIndex + (backwards ? -1 : 1);
703 var foundItem = null;
704 if (inRange(nextIndex)) {
705 foundItem = _items[nextIndex];
706 } else if (reloop) {
707 foundItem = backwards ? last() : first();
708 nextIndex = indexOf(foundItem);
709 }
710
711 if ((foundItem === null) || (nextIndex === limit)) return null;
712 if (validate(foundItem)) return foundItem;
713
714 if (angular.isUndefined(limit)) limit = nextIndex;
715
716 curIndex = nextIndex;
717 }
718 }
719 }
720
721
722})();
723(function(){
724"use strict";
725
726
727mdMediaFactory.$inject = ["$mdConstant", "$rootScope", "$window"];angular.module('material.core')
728.factory('$mdMedia', mdMediaFactory);
729
730/**
731 * @ngdoc service
732 * @name $mdMedia
733 * @module material.core
734 *
735 * @description
736 * `$mdMedia` is used to evaluate whether a given media query is true or false given the
737 * current device's screen / window size. The media query will be re-evaluated on resize, allowing
738 * you to register a watch.
739 *
740 * `$mdMedia` also has pre-programmed support for media queries that match the layout breakpoints:
741 *
742 * <table class="md-api-table">
743 * <thead>
744 * <tr>
745 * <th>Breakpoint</th>
746 * <th>mediaQuery</th>
747 * </tr>
748 * </thead>
749 * <tbody>
750 * <tr>
751 * <td>xs</td>
752 * <td>(max-width: 599px)</td>
753 * </tr>
754 * <tr>
755 * <td>gt-xs</td>
756 * <td>(min-width: 600px)</td>
757 * </tr>
758 * <tr>
759 * <td>sm</td>
760 * <td>(min-width: 600px) and (max-width: 959px)</td>
761 * </tr>
762 * <tr>
763 * <td>gt-sm</td>
764 * <td>(min-width: 960px)</td>
765 * </tr>
766 * <tr>
767 * <td>md</td>
768 * <td>(min-width: 960px) and (max-width: 1279px)</td>
769 * </tr>
770 * <tr>
771 * <td>gt-md</td>
772 * <td>(min-width: 1280px)</td>
773 * </tr>
774 * <tr>
775 * <td>lg</td>
776 * <td>(min-width: 1280px) and (max-width: 1919px)</td>
777 * </tr>
778 * <tr>
779 * <td>gt-lg</td>
780 * <td>(min-width: 1920px)</td>
781 * </tr>
782 * <tr>
783 * <td>xl</td>
784 * <td>(min-width: 1920px)</td>
785 * </tr>
786 * <tr>
787 * <td>landscape</td>
788 * <td>landscape</td>
789 * </tr>
790 * <tr>
791 * <td>portrait</td>
792 * <td>portrait</td>
793 * </tr>
794 * <tr>
795 * <td>print</td>
796 * <td>print</td>
797 * </tr>
798 * </tbody>
799 * </table>
800 *
801 * See Material Design's <a href="https://material.google.com/layout/responsive-ui.html">Layout - Adaptive UI</a> for more details.
802 *
803 * <a href="https://www.google.com/design/spec/layout/adaptive-ui.html">
804 * <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>
805 * </a>
806 *
807 * @returns {boolean} a boolean representing whether or not the given media query is true or false.
808 *
809 * @usage
810 * <hljs lang="js">
811 * app.controller('MyController', function($mdMedia, $scope) {
812 * $scope.$watch(function() { return $mdMedia('lg'); }, function(big) {
813 * $scope.bigScreen = big;
814 * });
815 *
816 * $scope.screenIsSmall = $mdMedia('sm');
817 * $scope.customQuery = $mdMedia('(min-width: 1234px)');
818 * $scope.anotherCustom = $mdMedia('max-width: 300px');
819 * });
820 * </hljs>
821 */
822
823/* @ngInject */
824function mdMediaFactory($mdConstant, $rootScope, $window) {
825 var queries = {};
826 var mqls = {};
827 var results = {};
828 var normalizeCache = {};
829
830 $mdMedia.getResponsiveAttribute = getResponsiveAttribute;
831 $mdMedia.getQuery = getQuery;
832 $mdMedia.watchResponsiveAttributes = watchResponsiveAttributes;
833
834 return $mdMedia;
835
836 function $mdMedia(query) {
837 var validated = queries[query];
838 if (angular.isUndefined(validated)) {
839 validated = queries[query] = validate(query);
840 }
841
842 var result = results[validated];
843 if (angular.isUndefined(result)) {
844 result = add(validated);
845 }
846
847 return result;
848 }
849
850 function validate(query) {
851 return $mdConstant.MEDIA[query] ||
852 ((query.charAt(0) !== '(') ? ('(' + query + ')') : query);
853 }
854
855 function add(query) {
856 var result = mqls[query];
857 if ( !result ) {
858 result = mqls[query] = $window.matchMedia(query);
859 }
860
861 result.addListener(onQueryChange);
862 return (results[result.media] = !!result.matches);
863 }
864
865 function onQueryChange(query) {
866 $rootScope.$evalAsync(function() {
867 results[query.media] = !!query.matches;
868 });
869 }
870
871 function getQuery(name) {
872 return mqls[name];
873 }
874
875 function getResponsiveAttribute(attrs, attrName) {
876 for (var i = 0; i < $mdConstant.MEDIA_PRIORITY.length; i++) {
877 var mediaName = $mdConstant.MEDIA_PRIORITY[i];
878 if (!mqls[queries[mediaName]].matches) {
879 continue;
880 }
881
882 var normalizedName = getNormalizedName(attrs, attrName + '-' + mediaName);
883 if (attrs[normalizedName]) {
884 return attrs[normalizedName];
885 }
886 }
887
888 // fallback on unprefixed
889 return attrs[getNormalizedName(attrs, attrName)];
890 }
891
892 function watchResponsiveAttributes(attrNames, attrs, watchFn) {
893 var unwatchFns = [];
894 attrNames.forEach(function(attrName) {
895 var normalizedName = getNormalizedName(attrs, attrName);
896 if (angular.isDefined(attrs[normalizedName])) {
897 unwatchFns.push(
898 attrs.$observe(normalizedName, angular.bind(void 0, watchFn, null)));
899 }
900
901 for (var mediaName in $mdConstant.MEDIA) {
902 normalizedName = getNormalizedName(attrs, attrName + '-' + mediaName);
903 if (angular.isDefined(attrs[normalizedName])) {
904 unwatchFns.push(
905 attrs.$observe(normalizedName, angular.bind(void 0, watchFn, mediaName)));
906 }
907 }
908 });
909
910 return function unwatch() {
911 unwatchFns.forEach(function(fn) { fn(); });
912 };
913 }
914
915 // Improves performance dramatically
916 function getNormalizedName(attrs, attrName) {
917 return normalizeCache[attrName] ||
918 (normalizeCache[attrName] = attrs.$normalize(attrName));
919 }
920}
921
922})();
923(function(){
924"use strict";
925
926angular
927 .module('material.core')
928 .config( ["$provide", function($provide) {
929 $provide.decorator('$mdUtil', ['$delegate', function ($delegate) {
930
931 // Inject the prefixer into our original $mdUtil service.
932 $delegate.prefixer = MdPrefixer;
933
934 return $delegate;
935 }]);
936 }]);
937
938function MdPrefixer(initialAttributes, buildSelector) {
939 var PREFIXES = ['data', 'x'];
940
941 if (initialAttributes) {
942 // The prefixer also accepts attributes as a parameter, and immediately builds a list or selector for
943 // the specified attributes.
944 return buildSelector ? _buildSelector(initialAttributes) : _buildList(initialAttributes);
945 }
946
947 return {
948 buildList: _buildList,
949 buildSelector: _buildSelector,
950 hasAttribute: _hasAttribute,
951 removeAttribute: _removeAttribute
952 };
953
954 function _buildList(attributes) {
955 attributes = angular.isArray(attributes) ? attributes : [attributes];
956
957 attributes.forEach(function(item) {
958 PREFIXES.forEach(function(prefix) {
959 attributes.push(prefix + '-' + item);
960 });
961 });
962
963 return attributes;
964 }
965
966 function _buildSelector(attributes) {
967 attributes = angular.isArray(attributes) ? attributes : [attributes];
968
969 return _buildList(attributes)
970 .map(function(item) {
971 return '[' + item + ']';
972 })
973 .join(',');
974 }
975
976 function _hasAttribute(element, attribute) {
977 element = _getNativeElement(element);
978
979 if (!element) {
980 return false;
981 }
982
983 var prefixedAttrs = _buildList(attribute);
984
985 for (var i = 0; i < prefixedAttrs.length; i++) {
986 if (element.hasAttribute(prefixedAttrs[i])) {
987 return true;
988 }
989 }
990
991 return false;
992 }
993
994 function _removeAttribute(element, attribute) {
995 element = _getNativeElement(element);
996
997 if (!element) {
998 return;
999 }
1000
1001 _buildList(attribute).forEach(function(prefixedAttribute) {
1002 element.removeAttribute(prefixedAttribute);
1003 });
1004 }
1005
1006 /**
1007 * Transforms a jqLite or DOM element into a HTML element.
1008 * This is useful when supporting jqLite elements and DOM elements at
1009 * same time.
1010 * @param element {JQLite|Element} Element to be parsed
1011 * @returns {HTMLElement} Parsed HTMLElement
1012 */
1013 function _getNativeElement(element) {
1014 element = element[0] || element;
1015
1016 if (element.nodeType) {
1017 return element;
1018 }
1019 }
1020
1021}
1022
1023})();
1024(function(){
1025"use strict";
1026
1027/*
1028 * This var has to be outside the angular factory, otherwise when
1029 * there are multiple material apps on the same page, each app
1030 * will create its own instance of this array and the app's IDs
1031 * will not be unique.
1032 */
1033UtilFactory.$inject = ["$document", "$timeout", "$compile", "$rootScope", "$$mdAnimate", "$interpolate", "$log", "$rootElement", "$window", "$$rAF"];
1034var nextUniqueId = 0;
1035
1036/**
1037 * @ngdoc module
1038 * @name material.core.util
1039 * @description
1040 * Util
1041 */
1042angular
1043 .module('material.core')
1044 .factory('$mdUtil', UtilFactory);
1045
1046/**
1047 * @ngInject
1048 */
1049function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $interpolate, $log, $rootElement, $window, $$rAF) {
1050 // Setup some core variables for the processTemplate method
1051 var startSymbol = $interpolate.startSymbol(),
1052 endSymbol = $interpolate.endSymbol(),
1053 usesStandardSymbols = ((startSymbol === '{{') && (endSymbol === '}}'));
1054
1055 /**
1056 * Checks if the target element has the requested style by key
1057 * @param {DOMElement|JQLite} target Target element
1058 * @param {string} key Style key
1059 * @param {string=} expectedVal Optional expected value
1060 * @returns {boolean} Whether the target element has the style or not
1061 */
1062 var hasComputedStyle = function (target, key, expectedVal) {
1063 var hasValue = false;
1064
1065 if ( target && target.length ) {
1066 var computedStyles = $window.getComputedStyle(target[0]);
1067 hasValue = angular.isDefined(computedStyles[key]) && (expectedVal ? computedStyles[key] == expectedVal : true);
1068 }
1069
1070 return hasValue;
1071 };
1072
1073 function validateCssValue(value) {
1074 return !value ? '0' :
1075 hasPx(value) || hasPercent(value) ? value : value + 'px';
1076 }
1077
1078 function hasPx(value) {
1079 return String(value).indexOf('px') > -1;
1080 }
1081
1082 function hasPercent(value) {
1083 return String(value).indexOf('%') > -1;
1084
1085 }
1086
1087 var $mdUtil = {
1088 dom: {},
1089 now: window.performance && window.performance.now ?
1090 angular.bind(window.performance, window.performance.now) : Date.now || function() {
1091 return new Date().getTime();
1092 },
1093
1094 /**
1095 * Cross-version compatibility method to retrieve an option of a ngModel controller,
1096 * which supports the breaking changes in the AngularJS snapshot (SHA 87a2ff76af5d0a9268d8eb84db5755077d27c84c).
1097 * @param {!angular.ngModelCtrl} ngModelCtrl
1098 * @param {!string} optionName
1099 * @returns {Object|undefined}
1100 */
1101 getModelOption: function (ngModelCtrl, optionName) {
1102 if (!ngModelCtrl.$options) {
1103 return;
1104 }
1105
1106 var $options = ngModelCtrl.$options;
1107
1108 // The newer versions of Angular introduced a `getOption function and made the option values no longer
1109 // visible on the $options object.
1110 return $options.getOption ? $options.getOption(optionName) : $options[optionName]
1111 },
1112
1113 /**
1114 * Bi-directional accessor/mutator used to easily update an element's
1115 * property based on the current 'dir'ectional value.
1116 */
1117 bidi : function(element, property, lValue, rValue) {
1118 var ltr = !($document[0].dir == 'rtl' || $document[0].body.dir == 'rtl');
1119
1120 // If accessor
1121 if ( arguments.length == 0 ) return ltr ? 'ltr' : 'rtl';
1122
1123 // If mutator
1124 var elem = angular.element(element);
1125
1126 if ( ltr && angular.isDefined(lValue)) {
1127 elem.css(property, validateCssValue(lValue));
1128 }
1129 else if ( !ltr && angular.isDefined(rValue)) {
1130 elem.css(property, validateCssValue(rValue) );
1131 }
1132 },
1133
1134 bidiProperty: function (element, lProperty, rProperty, value) {
1135 var ltr = !($document[0].dir == 'rtl' || $document[0].body.dir == 'rtl');
1136
1137 var elem = angular.element(element);
1138
1139 if ( ltr && angular.isDefined(lProperty)) {
1140 elem.css(lProperty, validateCssValue(value));
1141 elem.css(rProperty, '');
1142 }
1143 else if ( !ltr && angular.isDefined(rProperty)) {
1144 elem.css(rProperty, validateCssValue(value) );
1145 elem.css(lProperty, '');
1146 }
1147 },
1148
1149 clientRect: function(element, offsetParent, isOffsetRect) {
1150 var node = getNode(element);
1151 offsetParent = getNode(offsetParent || node.offsetParent || document.body);
1152 var nodeRect = node.getBoundingClientRect();
1153
1154 // The user can ask for an offsetRect: a rect relative to the offsetParent,
1155 // or a clientRect: a rect relative to the page
1156 var offsetRect = isOffsetRect ?
1157 offsetParent.getBoundingClientRect() :
1158 {left: 0, top: 0, width: 0, height: 0};
1159 return {
1160 left: nodeRect.left - offsetRect.left,
1161 top: nodeRect.top - offsetRect.top,
1162 width: nodeRect.width,
1163 height: nodeRect.height
1164 };
1165 },
1166 offsetRect: function(element, offsetParent) {
1167 return $mdUtil.clientRect(element, offsetParent, true);
1168 },
1169
1170 // Annoying method to copy nodes to an array, thanks to IE
1171 nodesToArray: function(nodes) {
1172 nodes = nodes || [];
1173
1174 var results = [];
1175 for (var i = 0; i < nodes.length; ++i) {
1176 results.push(nodes.item(i));
1177 }
1178 return results;
1179 },
1180
1181 /**
1182 * Determines the absolute position of the viewport.
1183 * Useful when making client rectangles absolute.
1184 * @returns {number}
1185 */
1186 getViewportTop: function() {
1187 return window.scrollY || window.pageYOffset || 0;
1188 },
1189
1190 /**
1191 * Finds the proper focus target by searching the DOM.
1192 *
1193 * @param containerEl
1194 * @param attributeVal
1195 * @returns {*}
1196 */
1197 findFocusTarget: function(containerEl, attributeVal) {
1198 var AUTO_FOCUS = this.prefixer('md-autofocus', true);
1199 var elToFocus;
1200
1201 elToFocus = scanForFocusable(containerEl, attributeVal || AUTO_FOCUS);
1202
1203 if ( !elToFocus && attributeVal != AUTO_FOCUS) {
1204 // Scan for deprecated attribute
1205 elToFocus = scanForFocusable(containerEl, this.prefixer('md-auto-focus', true));
1206
1207 if ( !elToFocus ) {
1208 // Scan for fallback to 'universal' API
1209 elToFocus = scanForFocusable(containerEl, AUTO_FOCUS);
1210 }
1211 }
1212
1213 return elToFocus;
1214
1215 /**
1216 * Can target and nested children for specified Selector (attribute)
1217 * whose value may be an expression that evaluates to True/False.
1218 */
1219 function scanForFocusable(target, selector) {
1220 var elFound, items = target[0].querySelectorAll(selector);
1221
1222 // Find the last child element with the focus attribute
1223 if ( items && items.length ){
1224 items.length && angular.forEach(items, function(it) {
1225 it = angular.element(it);
1226
1227 // Check the element for the md-autofocus class to ensure any associated expression
1228 // evaluated to true.
1229 var isFocusable = it.hasClass('md-autofocus');
1230 if (isFocusable) elFound = it;
1231 });
1232 }
1233 return elFound;
1234 }
1235 },
1236
1237 /**
1238 * Disables scroll around the passed parent element.
1239 * @param element Unused
1240 * @param {!Element|!angular.JQLite} parent Element to disable scrolling within.
1241 * Defaults to body if none supplied.
1242 * @param options Object of options to modify functionality
1243 * - disableScrollMask Boolean of whether or not to create a scroll mask element or
1244 * use the passed parent element.
1245 */
1246 disableScrollAround: function(element, parent, options) {
1247 options = options || {};
1248
1249 $mdUtil.disableScrollAround._count = Math.max(0, $mdUtil.disableScrollAround._count || 0);
1250 $mdUtil.disableScrollAround._count++;
1251
1252 if ($mdUtil.disableScrollAround._restoreScroll) {
1253 return $mdUtil.disableScrollAround._restoreScroll;
1254 }
1255
1256 var body = $document[0].body;
1257 var restoreBody = disableBodyScroll();
1258 var restoreElement = disableElementScroll(parent);
1259
1260 return $mdUtil.disableScrollAround._restoreScroll = function() {
1261 if (--$mdUtil.disableScrollAround._count <= 0) {
1262 restoreBody();
1263 restoreElement();
1264 delete $mdUtil.disableScrollAround._restoreScroll;
1265 }
1266 };
1267
1268 /**
1269 * Creates a virtual scrolling mask to prevent touchmove, keyboard, scrollbar clicking,
1270 * and wheel events
1271 */
1272 function disableElementScroll(element) {
1273 element = angular.element(element || body);
1274
1275 var scrollMask;
1276
1277 if (options.disableScrollMask) {
1278 scrollMask = element;
1279 } else {
1280 scrollMask = angular.element(
1281 '<div class="md-scroll-mask">' +
1282 ' <div class="md-scroll-mask-bar"></div>' +
1283 '</div>');
1284 element.append(scrollMask);
1285 }
1286
1287 scrollMask.on('wheel', preventDefault);
1288 scrollMask.on('touchmove', preventDefault);
1289
1290 return function restoreElementScroll() {
1291 scrollMask.off('wheel');
1292 scrollMask.off('touchmove');
1293
1294 if (!options.disableScrollMask) {
1295 scrollMask[0].parentNode.removeChild(scrollMask[0]);
1296 }
1297 };
1298
1299 function preventDefault(e) {
1300 e.preventDefault();
1301 }
1302 }
1303
1304 // Converts the body to a position fixed block and translate it to the proper scroll position
1305 function disableBodyScroll() {
1306 var documentElement = $document[0].documentElement;
1307
1308 var prevDocumentStyle = documentElement.style.cssText || '';
1309 var prevBodyStyle = body.style.cssText || '';
1310
1311 var viewportTop = $mdUtil.getViewportTop();
1312 var clientWidth = body.clientWidth;
1313 var hasVerticalScrollbar = body.scrollHeight > body.clientHeight + 1;
1314
1315 if (hasVerticalScrollbar) {
1316 angular.element(body).css({
1317 position: 'fixed',
1318 width: '100%',
1319 top: -viewportTop + 'px'
1320 });
1321 }
1322
1323 if (body.clientWidth < clientWidth) {
1324 body.style.overflow = 'hidden';
1325 }
1326
1327 // This should be applied after the manipulation to the body, because
1328 // adding a scrollbar can potentially resize it, causing the measurement
1329 // to change.
1330 if (hasVerticalScrollbar) {
1331 documentElement.style.overflowY = 'scroll';
1332 }
1333
1334 return function restoreScroll() {
1335 // Reset the inline style CSS to the previous.
1336 body.style.cssText = prevBodyStyle;
1337 documentElement.style.cssText = prevDocumentStyle;
1338
1339 // The body loses its scroll position while being fixed.
1340 body.scrollTop = viewportTop;
1341 };
1342 }
1343
1344 },
1345
1346 enableScrolling: function() {
1347 var restoreFn = this.disableScrollAround._restoreScroll;
1348 restoreFn && restoreFn();
1349 },
1350
1351 floatingScrollbars: function() {
1352 if (this.floatingScrollbars.cached === undefined) {
1353 var tempNode = angular.element('<div><div></div></div>').css({
1354 width: '100%',
1355 'z-index': -1,
1356 position: 'absolute',
1357 height: '35px',
1358 'overflow-y': 'scroll'
1359 });
1360 tempNode.children().css('height', '60px');
1361
1362 $document[0].body.appendChild(tempNode[0]);
1363 this.floatingScrollbars.cached = (tempNode[0].offsetWidth == tempNode[0].childNodes[0].offsetWidth);
1364 tempNode.remove();
1365 }
1366 return this.floatingScrollbars.cached;
1367 },
1368
1369 // Mobile safari only allows you to set focus in click event listeners...
1370 forceFocus: function(element) {
1371 var node = element[0] || element;
1372
1373 document.addEventListener('click', function focusOnClick(ev) {
1374 if (ev.target === node && ev.$focus) {
1375 node.focus();
1376 ev.stopImmediatePropagation();
1377 ev.preventDefault();
1378 node.removeEventListener('click', focusOnClick);
1379 }
1380 }, true);
1381
1382 var newEvent = document.createEvent('MouseEvents');
1383 newEvent.initMouseEvent('click', false, true, window, {}, 0, 0, 0, 0,
1384 false, false, false, false, 0, null);
1385 newEvent.$material = true;
1386 newEvent.$focus = true;
1387 node.dispatchEvent(newEvent);
1388 },
1389
1390 /**
1391 * facade to build md-backdrop element with desired styles
1392 * NOTE: Use $compile to trigger backdrop postLink function
1393 */
1394 createBackdrop: function(scope, addClass) {
1395 return $compile($mdUtil.supplant('<md-backdrop class="{0}">', [addClass]))(scope);
1396 },
1397
1398 /**
1399 * supplant() method from Crockford's `Remedial Javascript`
1400 * Equivalent to use of $interpolate; without dependency on
1401 * interpolation symbols and scope. Note: the '{<token>}' can
1402 * be property names, property chains, or array indices.
1403 */
1404 supplant: function(template, values, pattern) {
1405 pattern = pattern || /\{([^\{\}]*)\}/g;
1406 return template.replace(pattern, function(a, b) {
1407 var p = b.split('.'),
1408 r = values;
1409 try {
1410 for (var s in p) {
1411 if (p.hasOwnProperty(s) ) {
1412 r = r[p[s]];
1413 }
1414 }
1415 } catch (e) {
1416 r = a;
1417 }
1418 return (typeof r === 'string' || typeof r === 'number') ? r : a;
1419 });
1420 },
1421
1422 fakeNgModel: function() {
1423 return {
1424 $fake: true,
1425 $setTouched: angular.noop,
1426 $setViewValue: function(value) {
1427 this.$viewValue = value;
1428 this.$render(value);
1429 this.$viewChangeListeners.forEach(function(cb) {
1430 cb();
1431 });
1432 },
1433 $isEmpty: function(value) {
1434 return ('' + value).length === 0;
1435 },
1436 $parsers: [],
1437 $formatters: [],
1438 $viewChangeListeners: [],
1439 $render: angular.noop
1440 };
1441 },
1442
1443 // Returns a function, that, as long as it continues to be invoked, will not
1444 // be triggered. The function will be called after it stops being called for
1445 // N milliseconds.
1446 // @param wait Integer value of msecs to delay (since last debounce reset); default value 10 msecs
1447 // @param invokeApply should the $timeout trigger $digest() dirty checking
1448 debounce: function(func, wait, scope, invokeApply) {
1449 var timer;
1450
1451 return function debounced() {
1452 var context = scope,
1453 args = Array.prototype.slice.call(arguments);
1454
1455 $timeout.cancel(timer);
1456 timer = $timeout(function() {
1457
1458 timer = undefined;
1459 func.apply(context, args);
1460
1461 }, wait || 10, invokeApply);
1462 };
1463 },
1464
1465 // Returns a function that can only be triggered every `delay` milliseconds.
1466 // In other words, the function will not be called unless it has been more
1467 // than `delay` milliseconds since the last call.
1468 throttle: function throttle(func, delay) {
1469 var recent;
1470 return function throttled() {
1471 var context = this;
1472 var args = arguments;
1473 var now = $mdUtil.now();
1474
1475 if (!recent || (now - recent > delay)) {
1476 func.apply(context, args);
1477 recent = now;
1478 }
1479 };
1480 },
1481
1482 /**
1483 * Measures the number of milliseconds taken to run the provided callback
1484 * function. Uses a high-precision timer if available.
1485 */
1486 time: function time(cb) {
1487 var start = $mdUtil.now();
1488 cb();
1489 return $mdUtil.now() - start;
1490 },
1491
1492 /**
1493 * Create an implicit getter that caches its `getter()`
1494 * lookup value
1495 */
1496 valueOnUse : function (scope, key, getter) {
1497 var value = null, args = Array.prototype.slice.call(arguments);
1498 var params = (args.length > 3) ? args.slice(3) : [ ];
1499
1500 Object.defineProperty(scope, key, {
1501 get: function () {
1502 if (value === null) value = getter.apply(scope, params);
1503 return value;
1504 }
1505 });
1506 },
1507
1508 /**
1509 * Get a unique ID.
1510 *
1511 * @returns {string} an unique numeric string
1512 */
1513 nextUid: function() {
1514 return '' + nextUniqueId++;
1515 },
1516
1517 // Stop watchers and events from firing on a scope without destroying it,
1518 // by disconnecting it from its parent and its siblings' linked lists.
1519 disconnectScope: function disconnectScope(scope) {
1520 if (!scope) return;
1521
1522 // we can't destroy the root scope or a scope that has been already destroyed
1523 if (scope.$root === scope) return;
1524 if (scope.$$destroyed) return;
1525
1526 var parent = scope.$parent;
1527 scope.$$disconnected = true;
1528
1529 // See Scope.$destroy
1530 if (parent.$$childHead === scope) parent.$$childHead = scope.$$nextSibling;
1531 if (parent.$$childTail === scope) parent.$$childTail = scope.$$prevSibling;
1532 if (scope.$$prevSibling) scope.$$prevSibling.$$nextSibling = scope.$$nextSibling;
1533 if (scope.$$nextSibling) scope.$$nextSibling.$$prevSibling = scope.$$prevSibling;
1534
1535 scope.$$nextSibling = scope.$$prevSibling = null;
1536
1537 },
1538
1539 // Undo the effects of disconnectScope above.
1540 reconnectScope: function reconnectScope(scope) {
1541 if (!scope) return;
1542
1543 // we can't disconnect the root node or scope already disconnected
1544 if (scope.$root === scope) return;
1545 if (!scope.$$disconnected) return;
1546
1547 var child = scope;
1548
1549 var parent = child.$parent;
1550 child.$$disconnected = false;
1551 // See Scope.$new for this logic...
1552 child.$$prevSibling = parent.$$childTail;
1553 if (parent.$$childHead) {
1554 parent.$$childTail.$$nextSibling = child;
1555 parent.$$childTail = child;
1556 } else {
1557 parent.$$childHead = parent.$$childTail = child;
1558 }
1559 },
1560
1561 /*
1562 * getClosest replicates jQuery.closest() to walk up the DOM tree until it finds a matching nodeName
1563 *
1564 * @param el Element to start walking the DOM from
1565 * @param check Either a string or a function. If a string is passed, it will be evaluated against
1566 * each of the parent nodes' tag name. If a function is passed, the loop will call it with each of
1567 * the parents and will use the return value to determine whether the node is a match.
1568 * @param onlyParent Only start checking from the parent element, not `el`.
1569 */
1570 getClosest: function getClosest(el, validateWith, onlyParent) {
1571 if ( angular.isString(validateWith) ) {
1572 var tagName = validateWith.toUpperCase();
1573 validateWith = function(el) {
1574 return el.nodeName.toUpperCase() === tagName;
1575 };
1576 }
1577
1578 if (el instanceof angular.element) el = el[0];
1579 if (onlyParent) el = el.parentNode;
1580 if (!el) return null;
1581
1582 do {
1583 if (validateWith(el)) {
1584 return el;
1585 }
1586 } while (el = el.parentNode);
1587
1588 return null;
1589 },
1590
1591 /**
1592 * Build polyfill for the Node.contains feature (if needed)
1593 */
1594 elementContains: function(node, child) {
1595 var hasContains = (window.Node && window.Node.prototype && Node.prototype.contains);
1596 var findFn = hasContains ? angular.bind(node, node.contains) : angular.bind(node, function(arg) {
1597 // compares the positions of two nodes and returns a bitmask
1598 return (node === child) || !!(this.compareDocumentPosition(arg) & 16)
1599 });
1600
1601 return findFn(child);
1602 },
1603
1604 /**
1605 * Functional equivalent for $element.filter(‘md-bottom-sheet’)
1606 * useful with interimElements where the element and its container are important...
1607 *
1608 * @param {[]} elements to scan
1609 * @param {string} name of node to find (e.g. 'md-dialog')
1610 * @param {boolean=} optional flag to allow deep scans; defaults to 'false'.
1611 * @param {boolean=} optional flag to enable log warnings; defaults to false
1612 */
1613 extractElementByName: function(element, nodeName, scanDeep, warnNotFound) {
1614 var found = scanTree(element);
1615 if (!found && !!warnNotFound) {
1616 $log.warn( $mdUtil.supplant("Unable to find node '{0}' in element '{1}'.",[nodeName, element[0].outerHTML]) );
1617 }
1618
1619 return angular.element(found || element);
1620
1621 /**
1622 * Breadth-First tree scan for element with matching `nodeName`
1623 */
1624 function scanTree(element) {
1625 return scanLevel(element) || (!!scanDeep ? scanChildren(element) : null);
1626 }
1627
1628 /**
1629 * Case-insensitive scan of current elements only (do not descend).
1630 */
1631 function scanLevel(element) {
1632 if ( element ) {
1633 for (var i = 0, len = element.length; i < len; i++) {
1634 if (element[i].nodeName.toLowerCase() === nodeName) {
1635 return element[i];
1636 }
1637 }
1638 }
1639 return null;
1640 }
1641
1642 /**
1643 * Scan children of specified node
1644 */
1645 function scanChildren(element) {
1646 var found;
1647 if ( element ) {
1648 for (var i = 0, len = element.length; i < len; i++) {
1649 var target = element[i];
1650 if ( !found ) {
1651 for (var j = 0, numChild = target.childNodes.length; j < numChild; j++) {
1652 found = found || scanTree([target.childNodes[j]]);
1653 }
1654 }
1655 }
1656 }
1657 return found;
1658 }
1659
1660 },
1661
1662 /**
1663 * Give optional properties with no value a boolean true if attr provided or false otherwise
1664 */
1665 initOptionalProperties: function(scope, attr, defaults) {
1666 defaults = defaults || {};
1667 angular.forEach(scope.$$isolateBindings, function(binding, key) {
1668 if (binding.optional && angular.isUndefined(scope[key])) {
1669 var attrIsDefined = angular.isDefined(attr[binding.attrName]);
1670 scope[key] = angular.isDefined(defaults[key]) ? defaults[key] : attrIsDefined;
1671 }
1672 });
1673 },
1674
1675 /**
1676 * Alternative to $timeout calls with 0 delay.
1677 * nextTick() coalesces all calls within a single frame
1678 * to minimize $digest thrashing
1679 *
1680 * @param callback
1681 * @param digest
1682 * @returns {*}
1683 */
1684 nextTick: function(callback, digest, scope) {
1685 //-- grab function reference for storing state details
1686 var nextTick = $mdUtil.nextTick;
1687 var timeout = nextTick.timeout;
1688 var queue = nextTick.queue || [];
1689
1690 //-- add callback to the queue
1691 queue.push({scope: scope, callback: callback});
1692
1693 //-- set default value for digest
1694 if (digest == null) digest = true;
1695
1696 //-- store updated digest/queue values
1697 nextTick.digest = nextTick.digest || digest;
1698 nextTick.queue = queue;
1699
1700 //-- either return existing timeout or create a new one
1701 return timeout || (nextTick.timeout = $timeout(processQueue, 0, false));
1702
1703 /**
1704 * Grab a copy of the current queue
1705 * Clear the queue for future use
1706 * Process the existing queue
1707 * Trigger digest if necessary
1708 */
1709 function processQueue() {
1710 var queue = nextTick.queue;
1711 var digest = nextTick.digest;
1712
1713 nextTick.queue = [];
1714 nextTick.timeout = null;
1715 nextTick.digest = false;
1716
1717 queue.forEach(function(queueItem) {
1718 var skip = queueItem.scope && queueItem.scope.$$destroyed;
1719 if (!skip) {
1720 queueItem.callback();
1721 }
1722 });
1723
1724 if (digest) $rootScope.$digest();
1725 }
1726 },
1727
1728 /**
1729 * Processes a template and replaces the start/end symbols if the application has
1730 * overriden them.
1731 *
1732 * @param template The template to process whose start/end tags may be replaced.
1733 * @returns {*}
1734 */
1735 processTemplate: function(template) {
1736 if (usesStandardSymbols) {
1737 return template;
1738 } else {
1739 if (!template || !angular.isString(template)) return template;
1740 return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol);
1741 }
1742 },
1743
1744 /**
1745 * Scan up dom hierarchy for enabled parent;
1746 */
1747 getParentWithPointerEvents: function (element) {
1748 var parent = element.parent();
1749
1750 // jqLite might return a non-null, but still empty, parent; so check for parent and length
1751 while (hasComputedStyle(parent, 'pointer-events', 'none')) {
1752 parent = parent.parent();
1753 }
1754
1755 return parent;
1756 },
1757
1758 getNearestContentElement: function (element) {
1759 var current = element.parent()[0];
1760 // Look for the nearest parent md-content, stopping at the rootElement.
1761 while (current && current !== $rootElement[0] && current !== document.body && current.nodeName.toUpperCase() !== 'MD-CONTENT') {
1762 current = current.parentNode;
1763 }
1764 return current;
1765 },
1766
1767 /**
1768 * Checks if the current browser is natively supporting the `sticky` position.
1769 * @returns {string} supported sticky property name
1770 */
1771 checkStickySupport: function() {
1772 var stickyProp;
1773 var testEl = angular.element('<div>');
1774 $document[0].body.appendChild(testEl[0]);
1775
1776 var stickyProps = ['sticky', '-webkit-sticky'];
1777 for (var i = 0; i < stickyProps.length; ++i) {
1778 testEl.css({
1779 position: stickyProps[i],
1780 top: 0,
1781 'z-index': 2
1782 });
1783
1784 if (testEl.css('position') == stickyProps[i]) {
1785 stickyProp = stickyProps[i];
1786 break;
1787 }
1788 }
1789
1790 testEl.remove();
1791
1792 return stickyProp;
1793 },
1794
1795 /**
1796 * Parses an attribute value, mostly a string.
1797 * By default checks for negated values and returns `false´ if present.
1798 * Negated values are: (native falsy) and negative strings like:
1799 * `false` or `0`.
1800 * @param value Attribute value which should be parsed.
1801 * @param negatedCheck When set to false, won't check for negated values.
1802 * @returns {boolean}
1803 */
1804 parseAttributeBoolean: function(value, negatedCheck) {
1805 return value === '' || !!value && (negatedCheck === false || value !== 'false' && value !== '0');
1806 },
1807
1808 hasComputedStyle: hasComputedStyle,
1809
1810 /**
1811 * Returns true if the parent form of the element has been submitted.
1812 *
1813 * @param element An Angular or HTML5 element.
1814 *
1815 * @returns {boolean}
1816 */
1817 isParentFormSubmitted: function(element) {
1818 var parent = $mdUtil.getClosest(element, 'form');
1819 var form = parent ? angular.element(parent).controller('form') : null;
1820
1821 return form ? form.$submitted : false;
1822 },
1823
1824 /**
1825 * Animate the requested element's scrollTop to the requested scrollPosition with basic easing.
1826 *
1827 * @param {!HTMLElement} element The element to scroll.
1828 * @param {number} scrollEnd The new/final scroll position.
1829 * @param {number=} duration Duration of the scroll. Default is 1000ms.
1830 */
1831 animateScrollTo: function(element, scrollEnd, duration) {
1832 var scrollStart = element.scrollTop;
1833 var scrollChange = scrollEnd - scrollStart;
1834 var scrollingDown = scrollStart < scrollEnd;
1835 var startTime = $mdUtil.now();
1836
1837 $$rAF(scrollChunk);
1838
1839 function scrollChunk() {
1840 var newPosition = calculateNewPosition();
1841
1842 element.scrollTop = newPosition;
1843
1844 if (scrollingDown ? newPosition < scrollEnd : newPosition > scrollEnd) {
1845 $$rAF(scrollChunk);
1846 }
1847 }
1848
1849 function calculateNewPosition() {
1850 var easeDuration = duration || 1000;
1851 var currentTime = $mdUtil.now() - startTime;
1852
1853 return ease(currentTime, scrollStart, scrollChange, easeDuration);
1854 }
1855
1856 function ease(currentTime, start, change, duration) {
1857 // If the duration has passed (which can occur if our app loses focus due to $$rAF), jump
1858 // straight to the proper position
1859 if (currentTime > duration) {
1860 return start + change;
1861 }
1862
1863 var ts = (currentTime /= duration) * currentTime;
1864 var tc = ts * currentTime;
1865
1866 return start + change * (-2 * tc + 3 * ts);
1867 }
1868 },
1869
1870 /**
1871 * Provides an easy mechanism for removing duplicates from an array.
1872 *
1873 * var myArray = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4];
1874 *
1875 * $mdUtil.uniq(myArray) => [1, 2, 3, 4]
1876 *
1877 * @param {array} array The array whose unique values should be returned.
1878 *
1879 * @returns {array} A copy of the array containing only unique values.
1880 */
1881 uniq: function(array) {
1882 if (!array) { return; }
1883
1884 return array.filter(function(value, index, self) {
1885 return self.indexOf(value) === index;
1886 });
1887 }
1888 };
1889
1890
1891// Instantiate other namespace utility methods
1892
1893 $mdUtil.dom.animator = $$mdAnimate($mdUtil);
1894
1895 return $mdUtil;
1896
1897 function getNode(el) {
1898 return el[0] || el;
1899 }
1900
1901}
1902
1903/*
1904 * Since removing jQuery from the demos, some code that uses `element.focus()` is broken.
1905 * We need to add `element.focus()`, because it's testable unlike `element[0].focus`.
1906 */
1907
1908angular.element.prototype.focus = angular.element.prototype.focus || function() {
1909 if (this.length) {
1910 this[0].focus();
1911 }
1912 return this;
1913 };
1914angular.element.prototype.blur = angular.element.prototype.blur || function() {
1915 if (this.length) {
1916 this[0].blur();
1917 }
1918 return this;
1919 };
1920
1921})();
1922(function(){
1923"use strict";
1924
1925/**
1926 * @ngdoc module
1927 * @name material.core.compiler
1928 * @description
1929 * Angular Material template and element compiler.
1930 */
1931MdCompilerService.$inject = ["$q", "$templateRequest", "$injector", "$compile", "$controller"];
1932angular
1933 .module('material.core')
1934 .service('$mdCompiler', MdCompilerService);
1935
1936/**
1937 * @ngdoc service
1938 * @name $mdCompiler
1939 * @module material.core.compiler
1940 * @description
1941 * The $mdCompiler service is an abstraction of Angular's compiler, that allows developers
1942 * to easily compile an element with options like in a Directive Definition Object.
1943 *
1944 * > The compiler powers a lot of components inside of Angular Material.
1945 * > Like the `$mdPanel` or `$mdDialog`.
1946 *
1947 * @usage
1948 *
1949 * Basic Usage with a template
1950 *
1951 * <hljs lang="js">
1952 * $mdCompiler.compile({
1953 * templateUrl: 'modal.html',
1954 * controller: 'ModalCtrl',
1955 * locals: {
1956 * modal: myModalInstance;
1957 * }
1958 * }).then(function (compileData) {
1959 * compileData.element; // Compiled DOM element
1960 * compileData.link(myScope); // Instantiate controller and link element to scope.
1961 * });
1962 * </hljs>
1963 *
1964 * Example with a content element
1965 *
1966 * <hljs lang="js">
1967 *
1968 * // Create a virtual element and link it manually.
1969 * // The compiler doesn't need to recompile the element each time.
1970 * var myElement = $compile('<span>Test</span>')(myScope);
1971 *
1972 * $mdCompiler.compile({
1973 * contentElement: myElement
1974 * }).then(function (compileData) {
1975 * compileData.element // Content Element (same as above)
1976 * compileData.link // This does nothing when using a contentElement.
1977 * });
1978 * </hljs>
1979 *
1980 * > Content Element is a significant performance improvement when the developer already knows that the
1981 * > compiled element will be always the same and the scope will not change either.
1982 *
1983 * The `contentElement` option also supports DOM elements which will be temporary removed and restored
1984 * at its old position.
1985 *
1986 * <hljs lang="js">
1987 * var domElement = document.querySelector('#myElement');
1988 *
1989 * $mdCompiler.compile({
1990 * contentElement: myElement
1991 * }).then(function (compileData) {
1992 * compileData.element // Content Element (same as above)
1993 * compileData.link // This does nothing when using a contentElement.
1994 * });
1995 * </hljs>
1996 *
1997 * The `$mdCompiler` can also query for the element in the DOM itself.
1998 *
1999 * <hljs lang="js">
2000 * $mdCompiler.compile({
2001 * contentElement: '#myElement'
2002 * }).then(function (compileData) {
2003 * compileData.element // Content Element (same as above)
2004 * compileData.link // This does nothing when using a contentElement.
2005 * });
2006 * </hljs>
2007 *
2008 */
2009function MdCompilerService($q, $templateRequest, $injector, $compile, $controller) {
2010 /** @private @const {!angular.$q} */
2011 this.$q = $q;
2012
2013 /** @private @const {!angular.$templateRequest} */
2014 this.$templateRequest = $templateRequest;
2015
2016 /** @private @const {!angular.$injector} */
2017 this.$injector = $injector;
2018
2019 /** @private @const {!angular.$compile} */
2020 this.$compile = $compile;
2021
2022 /** @private @const {!angular.$controller} */
2023 this.$controller = $controller;
2024}
2025
2026/**
2027 * @ngdoc method
2028 * @name $mdCompiler#compile
2029 * @description
2030 *
2031 * A method to compile a HTML template with the Angular compiler.
2032 * The `$mdCompiler` is wrapper around the Angular compiler and provides extra functionality
2033 * like controller instantiation or async resolves.
2034 *
2035 * @param {!Object} options An options object, with the following properties:
2036 *
2037 * - `controller` - `{string|Function}` Controller fn that should be associated with
2038 * newly created scope or the name of a registered controller if passed as a string.
2039 * - `controllerAs` - `{string=}` A controller alias name. If present the controller will be
2040 * published to scope under the `controllerAs` name.
2041 * - `contentElement` - `{string|Element}`: Instead of using a template, which will be
2042 * compiled each time, you can also use a DOM element.<br/>
2043 * - `template` - `{string=}` An html template as a string.
2044 * - `templateUrl` - `{string=}` A path to an html template.
2045 * - `transformTemplate` - `{function(template)=}` A function which transforms the template after
2046 * it is loaded. It will be given the template string as a parameter, and should
2047 * return a a new string representing the transformed template.
2048 * - `resolve` - `{Object.<string, function>=}` - An optional map of dependencies which should
2049 * be injected into the controller. If any of these dependencies are promises, the compiler
2050 * will wait for them all to be resolved, or if one is rejected before the controller is
2051 * instantiated `compile()` will fail..
2052 * * `key` - `{string}`: a name of a dependency to be injected into the controller.
2053 * * `factory` - `{string|function}`: If `string` then it is an alias for a service.
2054 * Otherwise if function, then it is injected and the return value is treated as the
2055 * dependency. If the result is a promise, it is resolved before its value is
2056 * injected into the controller.
2057 *
2058 * @returns {Object} promise A promise, which will be resolved with a `compileData` object.
2059 * `compileData` has the following properties:
2060 *
2061 * - `element` - `{element}`: an uncompiled element matching the provided template.
2062 * - `link` - `{function(scope)}`: A link function, which, when called, will compile
2063 * the element and instantiate the provided controller (if given).
2064 * - `locals` - `{object}`: The locals which will be passed into the controller once `link` is
2065 * called. If `bindToController` is true, they will be coppied to the ctrl instead
2066 *
2067 */
2068MdCompilerService.prototype.compile = function(options) {
2069
2070 if (options.contentElement) {
2071 return this._prepareContentElement(options);
2072 } else {
2073 return this._compileTemplate(options);
2074 }
2075
2076};
2077
2078/**
2079 * Instead of compiling any template, the compiler just fetches an existing HTML element from the DOM and
2080 * provides a restore function to put the element back it old DOM position.
2081 * @param {!Object} options Options to be used for the compiler.
2082 * @private
2083 */
2084MdCompilerService.prototype._prepareContentElement = function(options) {
2085
2086 var contentElement = this._fetchContentElement(options);
2087
2088 return this.$q.resolve({
2089 element: contentElement.element,
2090 cleanup: contentElement.restore,
2091 locals: {},
2092 link: function() {
2093 return contentElement.element;
2094 }
2095 });
2096
2097};
2098
2099/**
2100 * Compiles a template by considering all options and waiting for all resolves to be ready.
2101 * @param {!Object} options Compile options
2102 * @returns {!Object} Compile data with link function.
2103 * @private
2104 */
2105MdCompilerService.prototype._compileTemplate = function(options) {
2106
2107 var self = this;
2108 var templateUrl = options.templateUrl;
2109 var template = options.template || '';
2110 var resolve = angular.extend({}, options.resolve);
2111 var locals = angular.extend({}, options.locals);
2112 var transformTemplate = options.transformTemplate || angular.identity;
2113
2114 // Take resolve values and invoke them.
2115 // Resolves can either be a string (value: 'MyRegisteredAngularConst'),
2116 // or an invokable 'factory' of sorts: (value: function ValueGetter($dependency) {})
2117 angular.forEach(resolve, function(value, key) {
2118 if (angular.isString(value)) {
2119 resolve[key] = self.$injector.get(value);
2120 } else {
2121 resolve[key] = self.$injector.invoke(value);
2122 }
2123 });
2124
2125 // Add the locals, which are just straight values to inject
2126 // eg locals: { three: 3 }, will inject three into the controller
2127 angular.extend(resolve, locals);
2128
2129 if (templateUrl) {
2130 resolve.$$ngTemplate = this.$templateRequest(templateUrl);
2131 } else {
2132 resolve.$$ngTemplate = this.$q.when(template);
2133 }
2134
2135
2136 // Wait for all the resolves to finish if they are promises
2137 return this.$q.all(resolve).then(function(locals) {
2138
2139 var template = transformTemplate(locals.$$ngTemplate, options);
2140 var element = options.element || angular.element('<div>').html(template.trim()).contents();
2141
2142 return self._compileElement(locals, element, options);
2143 });
2144
2145};
2146
2147/**
2148 * Method to compile an element with the given options.
2149 * @param {!Object} locals Locals to be injected to the controller if present
2150 * @param {!JQLite} element Element to be compiled and linked
2151 * @param {!Object} options Options to be used for linking.
2152 * @returns {!Object} Compile data with link function.
2153 * @private
2154 */
2155MdCompilerService.prototype._compileElement = function(locals, element, options) {
2156 var self = this;
2157 var ngLinkFn = this.$compile(element);
2158
2159 var compileData = {
2160 element: element,
2161 cleanup: element.remove.bind(element),
2162 locals: locals,
2163 link: linkFn
2164 };
2165
2166 function linkFn(scope) {
2167 locals.$scope = scope;
2168
2169 // Instantiate controller if the developer provided one.
2170 if (options.controller) {
2171
2172 var injectLocals = angular.extend(locals, {
2173 $element: element
2174 });
2175
2176 var invokeCtrl = self.$controller(options.controller, injectLocals, true, options.controllerAs);
2177
2178 if (options.bindToController) {
2179 angular.extend(invokeCtrl.instance, locals);
2180 }
2181
2182 var ctrl = invokeCtrl();
2183
2184 // Unique identifier for Angular Route ngView controllers.
2185 element.data('$ngControllerController', ctrl);
2186 element.children().data('$ngControllerController', ctrl);
2187
2188 // Expose the instantiated controller to the compile data
2189 compileData.controller = ctrl;
2190 }
2191
2192 // Invoke the Angular $compile link function.
2193 return ngLinkFn(scope);
2194 }
2195
2196 return compileData;
2197
2198};
2199
2200/**
2201 * Fetches an element removing it from the DOM and using it temporary for the compiler.
2202 * Elements which were fetched will be restored after use.
2203 * @param {!Object} options Options to be used for the compilation.
2204 * @returns {{element: !JQLite, restore: !Function}}
2205 * @private
2206 */
2207MdCompilerService.prototype._fetchContentElement = function(options) {
2208
2209 var contentEl = options.contentElement;
2210 var restoreFn = null;
2211
2212 if (angular.isString(contentEl)) {
2213 contentEl = document.querySelector(contentEl);
2214 restoreFn = createRestoreFn(contentEl);
2215 } else {
2216 contentEl = contentEl[0] || contentEl;
2217
2218 // When the element is visible in the DOM, then we restore it at close of the dialog.
2219 // Otherwise it will be removed from the DOM after close.
2220 if (document.contains(contentEl)) {
2221 restoreFn = createRestoreFn(contentEl);
2222 } else {
2223 restoreFn = function() {
2224 if (contentEl.parentNode) {
2225 contentEl.parentNode.removeChild(contentEl);
2226 }
2227 }
2228 }
2229 }
2230
2231 return {
2232 element: angular.element(contentEl),
2233 restore: restoreFn
2234 };
2235
2236 function createRestoreFn(element) {
2237 var parent = element.parentNode;
2238 var nextSibling = element.nextElementSibling;
2239
2240 return function() {
2241 if (!nextSibling) {
2242 // When the element didn't had any sibling, then it can be simply appended to the
2243 // parent, because it plays no role, which index it had before.
2244 parent.appendChild(element);
2245 } else {
2246 // When the element had a sibling, which marks the previous position of the element
2247 // in the DOM, we insert it correctly before the sibling, to have the same index as
2248 // before.
2249 parent.insertBefore(element, nextSibling);
2250 }
2251 }
2252 }
2253};
2254
2255
2256})();
2257(function(){
2258"use strict";
2259
2260/**
2261 * @ngdoc module
2262 * @name material.core.aria
2263 * @description
2264 * Aria Expectations for ngMaterial components.
2265 */
2266MdAriaService.$inject = ["$$rAF", "$log", "$window", "$interpolate"];
2267angular
2268 .module('material.core')
2269 .provider('$mdAria', MdAriaProvider);
2270
2271/**
2272 * @ngdoc service
2273 * @name $mdAriaProvider
2274 * @module material.core.aria
2275 *
2276 * @description
2277 *
2278 * Modify options of the `$mdAria` service, which will be used by most of the Angular Material
2279 * components.
2280 *
2281 * You are able to disable `$mdAria` warnings, by using the following markup.
2282 *
2283 * <hljs lang="js">
2284 * app.config(function($mdAriaProvider) {
2285 * // Globally disables all ARIA warnings.
2286 * $mdAriaProvider.disableWarnings();
2287 * });
2288 * </hljs>
2289 *
2290 */
2291function MdAriaProvider() {
2292
2293 var config = {
2294 /** Whether we should show ARIA warnings in the console if labels are missing on the element */
2295 showWarnings: true
2296 };
2297
2298 return {
2299 disableWarnings: disableWarnings,
2300 $get: ["$$rAF", "$log", "$window", "$interpolate", function($$rAF, $log, $window, $interpolate) {
2301 return MdAriaService.apply(config, arguments);
2302 }]
2303 };
2304
2305 /**
2306 * @ngdoc method
2307 * @name $mdAriaProvider#disableWarnings
2308 * @description Disables all ARIA warnings generated by Angular Material.
2309 */
2310 function disableWarnings() {
2311 config.showWarnings = false;
2312 }
2313}
2314
2315/*
2316 * @ngInject
2317 */
2318function MdAriaService($$rAF, $log, $window, $interpolate) {
2319
2320 // Load the showWarnings option from the current context and store it inside of a scope variable,
2321 // because the context will be probably lost in some function calls.
2322 var showWarnings = this.showWarnings;
2323
2324 return {
2325 expect: expect,
2326 expectAsync: expectAsync,
2327 expectWithText: expectWithText,
2328 expectWithoutText: expectWithoutText,
2329 getText: getText
2330 };
2331
2332 /**
2333 * Check if expected attribute has been specified on the target element or child
2334 * @param element
2335 * @param attrName
2336 * @param {optional} defaultValue What to set the attr to if no value is found
2337 */
2338 function expect(element, attrName, defaultValue) {
2339
2340 var node = angular.element(element)[0] || element;
2341
2342 // if node exists and neither it nor its children have the attribute
2343 if (node &&
2344 ((!node.hasAttribute(attrName) ||
2345 node.getAttribute(attrName).length === 0) &&
2346 !childHasAttribute(node, attrName))) {
2347
2348 defaultValue = angular.isString(defaultValue) ? defaultValue.trim() : '';
2349 if (defaultValue.length) {
2350 element.attr(attrName, defaultValue);
2351 } else if (showWarnings) {
2352 $log.warn('ARIA: Attribute "', attrName, '", required for accessibility, is missing on node:', node);
2353 }
2354
2355 }
2356 }
2357
2358 function expectAsync(element, attrName, defaultValueGetter) {
2359 // Problem: when retrieving the element's contents synchronously to find the label,
2360 // the text may not be defined yet in the case of a binding.
2361 // There is a higher chance that a binding will be defined if we wait one frame.
2362 $$rAF(function() {
2363 expect(element, attrName, defaultValueGetter());
2364 });
2365 }
2366
2367 function expectWithText(element, attrName) {
2368 var content = getText(element) || "";
2369 var hasBinding = content.indexOf($interpolate.startSymbol()) > -1;
2370
2371 if (hasBinding) {
2372 expectAsync(element, attrName, function() {
2373 return getText(element);
2374 });
2375 } else {
2376 expect(element, attrName, content);
2377 }
2378 }
2379
2380 function expectWithoutText(element, attrName) {
2381 var content = getText(element);
2382 var hasBinding = content.indexOf($interpolate.startSymbol()) > -1;
2383
2384 if ( !hasBinding && !content) {
2385 expect(element, attrName, content);
2386 }
2387 }
2388
2389 function getText(element) {
2390 element = element[0] || element;
2391 var walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false);
2392 var text = '';
2393
2394 var node;
2395 while (node = walker.nextNode()) {
2396 if (!isAriaHiddenNode(node)) {
2397 text += node.textContent;
2398 }
2399 }
2400
2401 return text.trim() || '';
2402
2403 function isAriaHiddenNode(node) {
2404 while (node.parentNode && (node = node.parentNode) !== element) {
2405 if (node.getAttribute && node.getAttribute('aria-hidden') === 'true') {
2406 return true;
2407 }
2408 }
2409 }
2410 }
2411
2412 function childHasAttribute(node, attrName) {
2413 var hasChildren = node.hasChildNodes(),
2414 hasAttr = false;
2415
2416 function isHidden(el) {
2417 var style = el.currentStyle ? el.currentStyle : $window.getComputedStyle(el);
2418 return (style.display === 'none');
2419 }
2420
2421 if (hasChildren) {
2422 var children = node.childNodes;
2423 for (var i=0; i < children.length; i++) {
2424 var child = children[i];
2425 if (child.nodeType === 1 && child.hasAttribute(attrName)) {
2426 if (!isHidden(child)) {
2427 hasAttr = true;
2428 }
2429 }
2430 }
2431 }
2432
2433 return hasAttr;
2434 }
2435}
2436
2437})();
2438(function(){
2439"use strict";
2440
2441
2442MdGesture.$inject = ["$$MdGestureHandler", "$$rAF", "$timeout"];
2443attachToDocument.$inject = ["$mdGesture", "$$MdGestureHandler"];var HANDLERS = {};
2444
2445/* The state of the current 'pointer'
2446 * The pointer represents the state of the current touch.
2447 * It contains normalized x and y coordinates from DOM events,
2448 * as well as other information abstracted from the DOM.
2449 */
2450
2451var pointer, lastPointer, forceSkipClickHijack = false;
2452
2453/**
2454 * The position of the most recent click if that click was on a label element.
2455 * @type {{x: number, y: number}?}
2456 */
2457var lastLabelClickPos = null;
2458
2459// Used to attach event listeners once when multiple ng-apps are running.
2460var isInitialized = false;
2461
2462angular
2463 .module('material.core.gestures', [ ])
2464 .provider('$mdGesture', MdGestureProvider)
2465 .factory('$$MdGestureHandler', MdGestureHandler)
2466 .run( attachToDocument );
2467
2468/**
2469 * @ngdoc service
2470 * @name $mdGestureProvider
2471 * @module material.core.gestures
2472 *
2473 * @description
2474 * In some scenarios on Mobile devices (without jQuery), the click events should NOT be hijacked.
2475 * `$mdGestureProvider` is used to configure the Gesture module to ignore or skip click hijacking on mobile
2476 * devices.
2477 *
2478 * <hljs lang="js">
2479 * app.config(function($mdGestureProvider) {
2480 *
2481 * // For mobile devices without jQuery loaded, do not
2482 * // intercept click events during the capture phase.
2483 * $mdGestureProvider.skipClickHijack();
2484 *
2485 * });
2486 * </hljs>
2487 *
2488 */
2489function MdGestureProvider() { }
2490
2491MdGestureProvider.prototype = {
2492
2493 // Publish access to setter to configure a variable BEFORE the
2494 // $mdGesture service is instantiated...
2495 skipClickHijack: function() {
2496 return forceSkipClickHijack = true;
2497 },
2498
2499 /**
2500 * $get is used to build an instance of $mdGesture
2501 * @ngInject
2502 */
2503 $get : ["$$MdGestureHandler", "$$rAF", "$timeout", function($$MdGestureHandler, $$rAF, $timeout) {
2504 return new MdGesture($$MdGestureHandler, $$rAF, $timeout);
2505 }]
2506};
2507
2508
2509
2510/**
2511 * MdGesture factory construction function
2512 * @ngInject
2513 */
2514function MdGesture($$MdGestureHandler, $$rAF, $timeout) {
2515 var userAgent = navigator.userAgent || navigator.vendor || window.opera;
2516 var isIos = userAgent.match(/ipad|iphone|ipod/i);
2517 var isAndroid = userAgent.match(/android/i);
2518 var touchActionProperty = getTouchAction();
2519 var hasJQuery = (typeof window.jQuery !== 'undefined') && (angular.element === window.jQuery);
2520
2521 var self = {
2522 handler: addHandler,
2523 register: register,
2524 // On mobile w/out jQuery, we normally intercept clicks. Should we skip that?
2525 isHijackingClicks: (isIos || isAndroid) && !hasJQuery && !forceSkipClickHijack
2526 };
2527
2528 if (self.isHijackingClicks) {
2529 var maxClickDistance = 6;
2530 self.handler('click', {
2531 options: {
2532 maxDistance: maxClickDistance
2533 },
2534 onEnd: checkDistanceAndEmit('click')
2535 });
2536
2537 self.handler('focus', {
2538 options: {
2539 maxDistance: maxClickDistance
2540 },
2541 onEnd: function(ev, pointer) {
2542 if (pointer.distance < this.state.options.maxDistance) {
2543 if (canFocus(ev.target)) {
2544 this.dispatchEvent(ev, 'focus', pointer);
2545 ev.target.focus();
2546 }
2547 }
2548
2549 function canFocus(element) {
2550 var focusableElements = ['INPUT', 'SELECT', 'BUTTON', 'TEXTAREA', 'VIDEO', 'AUDIO'];
2551
2552 return (element.getAttribute('tabindex') != '-1') &&
2553 !element.hasAttribute('DISABLED') &&
2554 (element.hasAttribute('tabindex') || element.hasAttribute('href') || element.isContentEditable ||
2555 (focusableElements.indexOf(element.nodeName) != -1));
2556 }
2557 }
2558 });
2559
2560 self.handler('mouseup', {
2561 options: {
2562 maxDistance: maxClickDistance
2563 },
2564 onEnd: checkDistanceAndEmit('mouseup')
2565 });
2566
2567 self.handler('mousedown', {
2568 onStart: function(ev) {
2569 this.dispatchEvent(ev, 'mousedown');
2570 }
2571 });
2572 }
2573
2574 function checkDistanceAndEmit(eventName) {
2575 return function(ev, pointer) {
2576 if (pointer.distance < this.state.options.maxDistance) {
2577 this.dispatchEvent(ev, eventName, pointer);
2578 }
2579 };
2580 }
2581
2582 /*
2583 * Register an element to listen for a handler.
2584 * This allows an element to override the default options for a handler.
2585 * Additionally, some handlers like drag and hold only dispatch events if
2586 * the domEvent happens inside an element that's registered to listen for these events.
2587 *
2588 * @see GestureHandler for how overriding of default options works.
2589 * @example $mdGesture.register(myElement, 'drag', { minDistance: 20, horziontal: false })
2590 */
2591 function register(element, handlerName, options) {
2592 var handler = HANDLERS[handlerName.replace(/^\$md./, '')];
2593 if (!handler) {
2594 throw new Error('Failed to register element with handler ' + handlerName + '. ' +
2595 'Available handlers: ' + Object.keys(HANDLERS).join(', '));
2596 }
2597 return handler.registerElement(element, options);
2598 }
2599
2600 /*
2601 * add a handler to $mdGesture. see below.
2602 */
2603 function addHandler(name, definition) {
2604 var handler = new $$MdGestureHandler(name);
2605 angular.extend(handler, definition);
2606 HANDLERS[name] = handler;
2607
2608 return self;
2609 }
2610
2611 /*
2612 * Register handlers. These listen to touch/start/move events, interpret them,
2613 * and dispatch gesture events depending on options & conditions. These are all
2614 * instances of GestureHandler.
2615 * @see GestureHandler
2616 */
2617 return self
2618 /*
2619 * The press handler dispatches an event on touchdown/touchend.
2620 * It's a simple abstraction of touch/mouse/pointer start and end.
2621 */
2622 .handler('press', {
2623 onStart: function (ev, pointer) {
2624 this.dispatchEvent(ev, '$md.pressdown');
2625 },
2626 onEnd: function (ev, pointer) {
2627 this.dispatchEvent(ev, '$md.pressup');
2628 }
2629 })
2630
2631 /*
2632 * The hold handler dispatches an event if the user keeps their finger within
2633 * the same <maxDistance> area for <delay> ms.
2634 * The hold handler will only run if a parent of the touch target is registered
2635 * to listen for hold events through $mdGesture.register()
2636 */
2637 .handler('hold', {
2638 options: {
2639 maxDistance: 6,
2640 delay: 500
2641 },
2642 onCancel: function () {
2643 $timeout.cancel(this.state.timeout);
2644 },
2645 onStart: function (ev, pointer) {
2646 // For hold, require a parent to be registered with $mdGesture.register()
2647 // Because we prevent scroll events, this is necessary.
2648 if (!this.state.registeredParent) return this.cancel();
2649
2650 this.state.pos = {x: pointer.x, y: pointer.y};
2651 this.state.timeout = $timeout(angular.bind(this, function holdDelayFn() {
2652 this.dispatchEvent(ev, '$md.hold');
2653 this.cancel(); //we're done!
2654 }), this.state.options.delay, false);
2655 },
2656 onMove: function (ev, pointer) {
2657 // Don't scroll while waiting for hold.
2658 // If we don't preventDefault touchmove events here, Android will assume we don't
2659 // want to listen to anymore touch events. It will start scrolling and stop sending
2660 // touchmove events.
2661 if (!touchActionProperty && ev.type === 'touchmove') ev.preventDefault();
2662
2663 // If the user moves greater than <maxDistance> pixels, stop the hold timer
2664 // set in onStart
2665 var dx = this.state.pos.x - pointer.x;
2666 var dy = this.state.pos.y - pointer.y;
2667 if (Math.sqrt(dx * dx + dy * dy) > this.options.maxDistance) {
2668 this.cancel();
2669 }
2670 },
2671 onEnd: function () {
2672 this.onCancel();
2673 }
2674 })
2675
2676 /*
2677 * The drag handler dispatches a drag event if the user holds and moves his finger greater than
2678 * <minDistance> px in the x or y direction, depending on options.horizontal.
2679 * The drag will be cancelled if the user moves his finger greater than <minDistance>*<cancelMultiplier> in
2680 * the perpendicular direction. Eg if the drag is horizontal and the user moves his finger <minDistance>*<cancelMultiplier>
2681 * pixels vertically, this handler won't consider the move part of a drag.
2682 */
2683 .handler('drag', {
2684 options: {
2685 minDistance: 6,
2686 horizontal: true,
2687 cancelMultiplier: 1.5
2688 },
2689 onSetup: function(element, options) {
2690 if (touchActionProperty) {
2691 // We check for horizontal to be false, because otherwise we would overwrite the default opts.
2692 this.oldTouchAction = element[0].style[touchActionProperty];
2693 element[0].style[touchActionProperty] = options.horizontal ? 'pan-y' : 'pan-x';
2694 }
2695 },
2696 onCleanup: function(element) {
2697 if (this.oldTouchAction) {
2698 element[0].style[touchActionProperty] = this.oldTouchAction;
2699 }
2700 },
2701 onStart: function (ev) {
2702 // For drag, require a parent to be registered with $mdGesture.register()
2703 if (!this.state.registeredParent) this.cancel();
2704 },
2705 onMove: function (ev, pointer) {
2706 var shouldStartDrag, shouldCancel;
2707 // Don't scroll while deciding if this touchmove qualifies as a drag event.
2708 // If we don't preventDefault touchmove events here, Android will assume we don't
2709 // want to listen to anymore touch events. It will start scrolling and stop sending
2710 // touchmove events.
2711 if (!touchActionProperty && ev.type === 'touchmove') ev.preventDefault();
2712
2713 if (!this.state.dragPointer) {
2714 if (this.state.options.horizontal) {
2715 shouldStartDrag = Math.abs(pointer.distanceX) > this.state.options.minDistance;
2716 shouldCancel = Math.abs(pointer.distanceY) > this.state.options.minDistance * this.state.options.cancelMultiplier;
2717 } else {
2718 shouldStartDrag = Math.abs(pointer.distanceY) > this.state.options.minDistance;
2719 shouldCancel = Math.abs(pointer.distanceX) > this.state.options.minDistance * this.state.options.cancelMultiplier;
2720 }
2721
2722 if (shouldStartDrag) {
2723 // Create a new pointer representing this drag, starting at this point where the drag started.
2724 this.state.dragPointer = makeStartPointer(ev);
2725 updatePointerState(ev, this.state.dragPointer);
2726 this.dispatchEvent(ev, '$md.dragstart', this.state.dragPointer);
2727
2728 } else if (shouldCancel) {
2729 this.cancel();
2730 }
2731 } else {
2732 this.dispatchDragMove(ev);
2733 }
2734 },
2735 // Only dispatch dragmove events every frame; any more is unnecessary
2736 dispatchDragMove: $$rAF.throttle(function (ev) {
2737 // Make sure the drag didn't stop while waiting for the next frame
2738 if (this.state.isRunning) {
2739 updatePointerState(ev, this.state.dragPointer);
2740 this.dispatchEvent(ev, '$md.drag', this.state.dragPointer);
2741 }
2742 }),
2743 onEnd: function (ev, pointer) {
2744 if (this.state.dragPointer) {
2745 updatePointerState(ev, this.state.dragPointer);
2746 this.dispatchEvent(ev, '$md.dragend', this.state.dragPointer);
2747 }
2748 }
2749 })
2750
2751 /*
2752 * The swipe handler will dispatch a swipe event if, on the end of a touch,
2753 * the velocity and distance were high enough.
2754 */
2755 .handler('swipe', {
2756 options: {
2757 minVelocity: 0.65,
2758 minDistance: 10
2759 },
2760 onEnd: function (ev, pointer) {
2761 var eventType;
2762
2763 if (Math.abs(pointer.velocityX) > this.state.options.minVelocity &&
2764 Math.abs(pointer.distanceX) > this.state.options.minDistance) {
2765 eventType = pointer.directionX == 'left' ? '$md.swipeleft' : '$md.swiperight';
2766 this.dispatchEvent(ev, eventType);
2767 }
2768 else if (Math.abs(pointer.velocityY) > this.state.options.minVelocity &&
2769 Math.abs(pointer.distanceY) > this.state.options.minDistance) {
2770 eventType = pointer.directionY == 'up' ? '$md.swipeup' : '$md.swipedown';
2771 this.dispatchEvent(ev, eventType);
2772 }
2773 }
2774 });
2775
2776 function getTouchAction() {
2777 var testEl = document.createElement('div');
2778 var vendorPrefixes = ['', 'webkit', 'Moz', 'MS', 'ms', 'o'];
2779
2780 for (var i = 0; i < vendorPrefixes.length; i++) {
2781 var prefix = vendorPrefixes[i];
2782 var property = prefix ? prefix + 'TouchAction' : 'touchAction';
2783 if (angular.isDefined(testEl.style[property])) {
2784 return property;
2785 }
2786 }
2787 }
2788
2789}
2790
2791/**
2792 * MdGestureHandler
2793 * A GestureHandler is an object which is able to dispatch custom dom events
2794 * based on native dom {touch,pointer,mouse}{start,move,end} events.
2795 *
2796 * A gesture will manage its lifecycle through the start,move,end, and cancel
2797 * functions, which are called by native dom events.
2798 *
2799 * A gesture has the concept of 'options' (eg a swipe's required velocity), which can be
2800 * overridden by elements registering through $mdGesture.register()
2801 */
2802function GestureHandler (name) {
2803 this.name = name;
2804 this.state = {};
2805}
2806
2807function MdGestureHandler() {
2808 var hasJQuery = (typeof window.jQuery !== 'undefined') && (angular.element === window.jQuery);
2809
2810 GestureHandler.prototype = {
2811 options: {},
2812 // jQuery listeners don't work with custom DOMEvents, so we have to dispatch events
2813 // differently when jQuery is loaded
2814 dispatchEvent: hasJQuery ? jQueryDispatchEvent : nativeDispatchEvent,
2815
2816 // These are overridden by the registered handler
2817 onSetup: angular.noop,
2818 onCleanup: angular.noop,
2819 onStart: angular.noop,
2820 onMove: angular.noop,
2821 onEnd: angular.noop,
2822 onCancel: angular.noop,
2823
2824 // onStart sets up a new state for the handler, which includes options from the
2825 // nearest registered parent element of ev.target.
2826 start: function (ev, pointer) {
2827 if (this.state.isRunning) return;
2828 var parentTarget = this.getNearestParent(ev.target);
2829 // Get the options from the nearest registered parent
2830 var parentTargetOptions = parentTarget && parentTarget.$mdGesture[this.name] || {};
2831
2832 this.state = {
2833 isRunning: true,
2834 // Override the default options with the nearest registered parent's options
2835 options: angular.extend({}, this.options, parentTargetOptions),
2836 // Pass in the registered parent node to the state so the onStart listener can use
2837 registeredParent: parentTarget
2838 };
2839 this.onStart(ev, pointer);
2840 },
2841 move: function (ev, pointer) {
2842 if (!this.state.isRunning) return;
2843 this.onMove(ev, pointer);
2844 },
2845 end: function (ev, pointer) {
2846 if (!this.state.isRunning) return;
2847 this.onEnd(ev, pointer);
2848 this.state.isRunning = false;
2849 },
2850 cancel: function (ev, pointer) {
2851 this.onCancel(ev, pointer);
2852 this.state = {};
2853 },
2854
2855 // Find and return the nearest parent element that has been registered to
2856 // listen for this handler via $mdGesture.register(element, 'handlerName').
2857 getNearestParent: function (node) {
2858 var current = node;
2859 while (current) {
2860 if ((current.$mdGesture || {})[this.name]) {
2861 return current;
2862 }
2863 current = current.parentNode;
2864 }
2865 return null;
2866 },
2867
2868 // Called from $mdGesture.register when an element registers itself with a handler.
2869 // Store the options the user gave on the DOMElement itself. These options will
2870 // be retrieved with getNearestParent when the handler starts.
2871 registerElement: function (element, options) {
2872 var self = this;
2873 element[0].$mdGesture = element[0].$mdGesture || {};
2874 element[0].$mdGesture[this.name] = options || {};
2875 element.on('$destroy', onDestroy);
2876
2877 self.onSetup(element, options || {});
2878
2879 return onDestroy;
2880
2881 function onDestroy() {
2882 delete element[0].$mdGesture[self.name];
2883 element.off('$destroy', onDestroy);
2884
2885 self.onCleanup(element, options || {});
2886 }
2887 }
2888 };
2889
2890 return GestureHandler;
2891
2892 /*
2893 * Dispatch an event with jQuery
2894 * TODO: Make sure this sends bubbling events
2895 *
2896 * @param srcEvent the original DOM touch event that started this.
2897 * @param eventType the name of the custom event to send (eg 'click' or '$md.drag')
2898 * @param eventPointer the pointer object that matches this event.
2899 */
2900 function jQueryDispatchEvent(srcEvent, eventType, eventPointer) {
2901 eventPointer = eventPointer || pointer;
2902 var eventObj = new angular.element.Event(eventType);
2903
2904 eventObj.$material = true;
2905 eventObj.pointer = eventPointer;
2906 eventObj.srcEvent = srcEvent;
2907
2908 angular.extend(eventObj, {
2909 clientX: eventPointer.x,
2910 clientY: eventPointer.y,
2911 screenX: eventPointer.x,
2912 screenY: eventPointer.y,
2913 pageX: eventPointer.x,
2914 pageY: eventPointer.y,
2915 ctrlKey: srcEvent.ctrlKey,
2916 altKey: srcEvent.altKey,
2917 shiftKey: srcEvent.shiftKey,
2918 metaKey: srcEvent.metaKey
2919 });
2920 angular.element(eventPointer.target).trigger(eventObj);
2921 }
2922
2923 /*
2924 * NOTE: nativeDispatchEvent is very performance sensitive.
2925 * @param srcEvent the original DOM touch event that started this.
2926 * @param eventType the name of the custom event to send (eg 'click' or '$md.drag')
2927 * @param eventPointer the pointer object that matches this event.
2928 */
2929 function nativeDispatchEvent(srcEvent, eventType, eventPointer) {
2930 eventPointer = eventPointer || pointer;
2931 var eventObj;
2932
2933 if (eventType === 'click' || eventType == 'mouseup' || eventType == 'mousedown' ) {
2934 eventObj = document.createEvent('MouseEvents');
2935 eventObj.initMouseEvent(
2936 eventType, true, true, window, srcEvent.detail,
2937 eventPointer.x, eventPointer.y, eventPointer.x, eventPointer.y,
2938 srcEvent.ctrlKey, srcEvent.altKey, srcEvent.shiftKey, srcEvent.metaKey,
2939 srcEvent.button, srcEvent.relatedTarget || null
2940 );
2941
2942 } else {
2943 eventObj = document.createEvent('CustomEvent');
2944 eventObj.initCustomEvent(eventType, true, true, {});
2945 }
2946 eventObj.$material = true;
2947 eventObj.pointer = eventPointer;
2948 eventObj.srcEvent = srcEvent;
2949 eventPointer.target.dispatchEvent(eventObj);
2950 }
2951
2952}
2953
2954/**
2955 * Attach Gestures: hook document and check shouldHijack clicks
2956 * @ngInject
2957 */
2958function attachToDocument( $mdGesture, $$MdGestureHandler ) {
2959
2960 // Polyfill document.contains for IE11.
2961 // TODO: move to util
2962 document.contains || (document.contains = function (node) {
2963 return document.body.contains(node);
2964 });
2965
2966 if (!isInitialized && $mdGesture.isHijackingClicks ) {
2967 /*
2968 * If hijack clicks is true, we preventDefault any click that wasn't
2969 * sent by ngMaterial. This is because on older Android & iOS, a false, or 'ghost',
2970 * click event will be sent ~400ms after a touchend event happens.
2971 * The only way to know if this click is real is to prevent any normal
2972 * click events, and add a flag to events sent by material so we know not to prevent those.
2973 *
2974 * Two exceptions to click events that should be prevented are:
2975 * - click events sent by the keyboard (eg form submit)
2976 * - events that originate from an Ionic app
2977 */
2978 document.addEventListener('click' , clickHijacker , true);
2979 document.addEventListener('mouseup' , mouseInputHijacker, true);
2980 document.addEventListener('mousedown', mouseInputHijacker, true);
2981 document.addEventListener('focus' , mouseInputHijacker, true);
2982
2983 isInitialized = true;
2984 }
2985
2986 function mouseInputHijacker(ev) {
2987 var isKeyClick = !ev.clientX && !ev.clientY;
2988 if (!isKeyClick && !ev.$material && !ev.isIonicTap
2989 && !isInputEventFromLabelClick(ev)) {
2990 ev.preventDefault();
2991 ev.stopPropagation();
2992 }
2993 }
2994
2995 function clickHijacker(ev) {
2996 var isKeyClick = ev.clientX === 0 && ev.clientY === 0;
2997 if (!isKeyClick && !ev.$material && !ev.isIonicTap
2998 && !isInputEventFromLabelClick(ev)) {
2999 ev.preventDefault();
3000 ev.stopPropagation();
3001 lastLabelClickPos = null;
3002 } else {
3003 lastLabelClickPos = null;
3004 if (ev.target.tagName.toLowerCase() == 'label') {
3005 lastLabelClickPos = {x: ev.x, y: ev.y};
3006 }
3007 }
3008 }
3009
3010
3011 // Listen to all events to cover all platforms.
3012 var START_EVENTS = 'mousedown touchstart pointerdown';
3013 var MOVE_EVENTS = 'mousemove touchmove pointermove';
3014 var END_EVENTS = 'mouseup mouseleave touchend touchcancel pointerup pointercancel';
3015
3016 angular.element(document)
3017 .on(START_EVENTS, gestureStart)
3018 .on(MOVE_EVENTS, gestureMove)
3019 .on(END_EVENTS, gestureEnd)
3020 // For testing
3021 .on('$$mdGestureReset', function gestureClearCache () {
3022 lastPointer = pointer = null;
3023 });
3024
3025 /*
3026 * When a DOM event happens, run all registered gesture handlers' lifecycle
3027 * methods which match the DOM event.
3028 * Eg when a 'touchstart' event happens, runHandlers('start') will call and
3029 * run `handler.cancel()` and `handler.start()` on all registered handlers.
3030 */
3031 function runHandlers(handlerEvent, event) {
3032 var handler;
3033 for (var name in HANDLERS) {
3034 handler = HANDLERS[name];
3035 if( handler instanceof $$MdGestureHandler ) {
3036
3037 if (handlerEvent === 'start') {
3038 // Run cancel to reset any handlers' state
3039 handler.cancel();
3040 }
3041 handler[handlerEvent](event, pointer);
3042
3043 }
3044 }
3045 }
3046
3047 /*
3048 * gestureStart vets if a start event is legitimate (and not part of a 'ghost click' from iOS/Android)
3049 * If it is legitimate, we initiate the pointer state and mark the current pointer's type
3050 * For example, for a touchstart event, mark the current pointer as a 'touch' pointer, so mouse events
3051 * won't effect it.
3052 */
3053 function gestureStart(ev) {
3054 // If we're already touched down, abort
3055 if (pointer) return;
3056
3057 var now = +Date.now();
3058
3059 // iOS & old android bug: after a touch event, a click event is sent 350 ms later.
3060 // If <400ms have passed, don't allow an event of a different type than the previous event
3061 if (lastPointer && !typesMatch(ev, lastPointer) && (now - lastPointer.endTime < 1500)) {
3062 return;
3063 }
3064
3065 pointer = makeStartPointer(ev);
3066
3067 runHandlers('start', ev);
3068 }
3069 /*
3070 * If a move event happens of the right type, update the pointer and run all the move handlers.
3071 * "of the right type": if a mousemove happens but our pointer started with a touch event, do nothing.
3072 */
3073 function gestureMove(ev) {
3074 if (!pointer || !typesMatch(ev, pointer)) return;
3075
3076 updatePointerState(ev, pointer);
3077 runHandlers('move', ev);
3078 }
3079 /*
3080 * If an end event happens of the right type, update the pointer, run endHandlers, and save the pointer as 'lastPointer'
3081 */
3082 function gestureEnd(ev) {
3083 if (!pointer || !typesMatch(ev, pointer)) return;
3084
3085 updatePointerState(ev, pointer);
3086 pointer.endTime = +Date.now();
3087
3088 runHandlers('end', ev);
3089
3090 lastPointer = pointer;
3091 pointer = null;
3092 }
3093
3094}
3095
3096// ********************
3097// Module Functions
3098// ********************
3099
3100/*
3101 * Initiate the pointer. x, y, and the pointer's type.
3102 */
3103function makeStartPointer(ev) {
3104 var point = getEventPoint(ev);
3105 var startPointer = {
3106 startTime: +Date.now(),
3107 target: ev.target,
3108 // 'p' for pointer events, 'm' for mouse, 't' for touch
3109 type: ev.type.charAt(0)
3110 };
3111 startPointer.startX = startPointer.x = point.pageX;
3112 startPointer.startY = startPointer.y = point.pageY;
3113 return startPointer;
3114}
3115
3116/*
3117 * return whether the pointer's type matches the event's type.
3118 * Eg if a touch event happens but the pointer has a mouse type, return false.
3119 */
3120function typesMatch(ev, pointer) {
3121 return ev && pointer && ev.type.charAt(0) === pointer.type;
3122}
3123
3124/**
3125 * Gets whether the given event is an input event that was caused by clicking on an
3126 * associated label element.
3127 *
3128 * This is necessary because the browser will, upon clicking on a label element, fire an
3129 * *extra* click event on its associated input (if any). mdGesture is able to flag the label
3130 * click as with `$material` correctly, but not the second input click.
3131 *
3132 * In order to determine whether an input event is from a label click, we compare the (x, y) for
3133 * the event to the (x, y) for the most recent label click (which is cleared whenever a non-label
3134 * click occurs). Unfortunately, there are no event properties that tie the input and the label
3135 * together (such as relatedTarget).
3136 *
3137 * @param {MouseEvent} event
3138 * @returns {boolean}
3139 */
3140function isInputEventFromLabelClick(event) {
3141 return lastLabelClickPos
3142 && lastLabelClickPos.x == event.x
3143 && lastLabelClickPos.y == event.y;
3144}
3145
3146/*
3147 * Update the given pointer based upon the given DOMEvent.
3148 * Distance, velocity, direction, duration, etc
3149 */
3150function updatePointerState(ev, pointer) {
3151 var point = getEventPoint(ev);
3152 var x = pointer.x = point.pageX;
3153 var y = pointer.y = point.pageY;
3154
3155 pointer.distanceX = x - pointer.startX;
3156 pointer.distanceY = y - pointer.startY;
3157 pointer.distance = Math.sqrt(
3158 pointer.distanceX * pointer.distanceX + pointer.distanceY * pointer.distanceY
3159 );
3160
3161 pointer.directionX = pointer.distanceX > 0 ? 'right' : pointer.distanceX < 0 ? 'left' : '';
3162 pointer.directionY = pointer.distanceY > 0 ? 'down' : pointer.distanceY < 0 ? 'up' : '';
3163
3164 pointer.duration = +Date.now() - pointer.startTime;
3165 pointer.velocityX = pointer.distanceX / pointer.duration;
3166 pointer.velocityY = pointer.distanceY / pointer.duration;
3167}
3168
3169/*
3170 * Normalize the point where the DOM event happened whether it's touch or mouse.
3171 * @returns point event obj with pageX and pageY on it.
3172 */
3173function getEventPoint(ev) {
3174 ev = ev.originalEvent || ev; // support jQuery events
3175 return (ev.touches && ev.touches[0]) ||
3176 (ev.changedTouches && ev.changedTouches[0]) ||
3177 ev;
3178}
3179
3180})();
3181(function(){
3182"use strict";
3183
3184angular.module('material.core')
3185 .provider('$$interimElement', InterimElementProvider);
3186
3187/*
3188 * @ngdoc service
3189 * @name $$interimElement
3190 * @module material.core
3191 *
3192 * @description
3193 *
3194 * Factory that contructs `$$interimElement.$service` services.
3195 * Used internally in material design for elements that appear on screen temporarily.
3196 * The service provides a promise-like API for interacting with the temporary
3197 * elements.
3198 *
3199 * ```js
3200 * app.service('$mdToast', function($$interimElement) {
3201 * var $mdToast = $$interimElement(toastDefaultOptions);
3202 * return $mdToast;
3203 * });
3204 * ```
3205 * @param {object=} defaultOptions Options used by default for the `show` method on the service.
3206 *
3207 * @returns {$$interimElement.$service}
3208 *
3209 */
3210
3211function InterimElementProvider() {
3212 InterimElementFactory.$inject = ["$document", "$q", "$rootScope", "$timeout", "$rootElement", "$animate", "$mdUtil", "$mdCompiler", "$mdTheming", "$injector", "$exceptionHandler"];
3213 createInterimElementProvider.$get = InterimElementFactory;
3214 return createInterimElementProvider;
3215
3216 /**
3217 * Returns a new provider which allows configuration of a new interimElement
3218 * service. Allows configuration of default options & methods for options,
3219 * as well as configuration of 'preset' methods (eg dialog.basic(): basic is a preset method)
3220 */
3221 function createInterimElementProvider(interimFactoryName) {
3222 factory.$inject = ["$$interimElement", "$injector"];
3223 var EXPOSED_METHODS = ['onHide', 'onShow', 'onRemove'];
3224
3225 var customMethods = {};
3226 var providerConfig = {
3227 presets: {}
3228 };
3229
3230 var provider = {
3231 setDefaults: setDefaults,
3232 addPreset: addPreset,
3233 addMethod: addMethod,
3234 $get: factory
3235 };
3236
3237 /**
3238 * all interim elements will come with the 'build' preset
3239 */
3240 provider.addPreset('build', {
3241 methods: ['controller', 'controllerAs', 'resolve', 'multiple',
3242 'template', 'templateUrl', 'themable', 'transformTemplate', 'parent', 'contentElement']
3243 });
3244
3245 return provider;
3246
3247 /**
3248 * Save the configured defaults to be used when the factory is instantiated
3249 */
3250 function setDefaults(definition) {
3251 providerConfig.optionsFactory = definition.options;
3252 providerConfig.methods = (definition.methods || []).concat(EXPOSED_METHODS);
3253 return provider;
3254 }
3255
3256 /**
3257 * Add a method to the factory that isn't specific to any interim element operations
3258 */
3259
3260 function addMethod(name, fn) {
3261 customMethods[name] = fn;
3262 return provider;
3263 }
3264
3265 /**
3266 * Save the configured preset to be used when the factory is instantiated
3267 */
3268 function addPreset(name, definition) {
3269 definition = definition || {};
3270 definition.methods = definition.methods || [];
3271 definition.options = definition.options || function() { return {}; };
3272
3273 if (/^cancel|hide|show$/.test(name)) {
3274 throw new Error("Preset '" + name + "' in " + interimFactoryName + " is reserved!");
3275 }
3276 if (definition.methods.indexOf('_options') > -1) {
3277 throw new Error("Method '_options' in " + interimFactoryName + " is reserved!");
3278 }
3279 providerConfig.presets[name] = {
3280 methods: definition.methods.concat(EXPOSED_METHODS),
3281 optionsFactory: definition.options,
3282 argOption: definition.argOption
3283 };
3284 return provider;
3285 }
3286
3287 function addPresetMethod(presetName, methodName, method) {
3288 providerConfig.presets[presetName][methodName] = method;
3289 }
3290
3291 /**
3292 * Create a factory that has the given methods & defaults implementing interimElement
3293 */
3294 /* @ngInject */
3295 function factory($$interimElement, $injector) {
3296 var defaultMethods;
3297 var defaultOptions;
3298 var interimElementService = $$interimElement();
3299
3300 /*
3301 * publicService is what the developer will be using.
3302 * It has methods hide(), cancel(), show(), build(), and any other
3303 * presets which were set during the config phase.
3304 */
3305 var publicService = {
3306 hide: interimElementService.hide,
3307 cancel: interimElementService.cancel,
3308 show: showInterimElement,
3309
3310 // Special internal method to destroy an interim element without animations
3311 // used when navigation changes causes a $scope.$destroy() action
3312 destroy : destroyInterimElement
3313 };
3314
3315
3316 defaultMethods = providerConfig.methods || [];
3317 // This must be invoked after the publicService is initialized
3318 defaultOptions = invokeFactory(providerConfig.optionsFactory, {});
3319
3320 // Copy over the simple custom methods
3321 angular.forEach(customMethods, function(fn, name) {
3322 publicService[name] = fn;
3323 });
3324
3325 angular.forEach(providerConfig.presets, function(definition, name) {
3326 var presetDefaults = invokeFactory(definition.optionsFactory, {});
3327 var presetMethods = (definition.methods || []).concat(defaultMethods);
3328
3329 // Every interimElement built with a preset has a field called `$type`,
3330 // which matches the name of the preset.
3331 // Eg in preset 'confirm', options.$type === 'confirm'
3332 angular.extend(presetDefaults, { $type: name });
3333
3334 // This creates a preset class which has setter methods for every
3335 // method given in the `.addPreset()` function, as well as every
3336 // method given in the `.setDefaults()` function.
3337 //
3338 // @example
3339 // .setDefaults({
3340 // methods: ['hasBackdrop', 'clickOutsideToClose', 'escapeToClose', 'targetEvent'],
3341 // options: dialogDefaultOptions
3342 // })
3343 // .addPreset('alert', {
3344 // methods: ['title', 'ok'],
3345 // options: alertDialogOptions
3346 // })
3347 //
3348 // Set values will be passed to the options when interimElement.show() is called.
3349 function Preset(opts) {
3350 this._options = angular.extend({}, presetDefaults, opts);
3351 }
3352 angular.forEach(presetMethods, function(name) {
3353 Preset.prototype[name] = function(value) {
3354 this._options[name] = value;
3355 return this;
3356 };
3357 });
3358
3359 // Create shortcut method for one-linear methods
3360 if (definition.argOption) {
3361 var methodName = 'show' + name.charAt(0).toUpperCase() + name.slice(1);
3362 publicService[methodName] = function(arg) {
3363 var config = publicService[name](arg);
3364 return publicService.show(config);
3365 };
3366 }
3367
3368 // eg $mdDialog.alert() will return a new alert preset
3369 publicService[name] = function(arg) {
3370 // If argOption is supplied, eg `argOption: 'content'`, then we assume
3371 // if the argument is not an options object then it is the `argOption` option.
3372 //
3373 // @example `$mdToast.simple('hello')` // sets options.content to hello
3374 // // because argOption === 'content'
3375 if (arguments.length && definition.argOption &&
3376 !angular.isObject(arg) && !angular.isArray(arg)) {
3377
3378 return (new Preset())[definition.argOption](arg);
3379
3380 } else {
3381 return new Preset(arg);
3382 }
3383
3384 };
3385 });
3386
3387 return publicService;
3388
3389 /**
3390 *
3391 */
3392 function showInterimElement(opts) {
3393 // opts is either a preset which stores its options on an _options field,
3394 // or just an object made up of options
3395 opts = opts || { };
3396 if (opts._options) opts = opts._options;
3397
3398 return interimElementService.show(
3399 angular.extend({}, defaultOptions, opts)
3400 );
3401 }
3402
3403 /**
3404 * Special method to hide and destroy an interimElement WITHOUT
3405 * any 'leave` or hide animations ( an immediate force hide/remove )
3406 *
3407 * NOTE: This calls the onRemove() subclass method for each component...
3408 * which must have code to respond to `options.$destroy == true`
3409 */
3410 function destroyInterimElement(opts) {
3411 return interimElementService.destroy(opts);
3412 }
3413
3414 /**
3415 * Helper to call $injector.invoke with a local of the factory name for
3416 * this provider.
3417 * If an $mdDialog is providing options for a dialog and tries to inject
3418 * $mdDialog, a circular dependency error will happen.
3419 * We get around that by manually injecting $mdDialog as a local.
3420 */
3421 function invokeFactory(factory, defaultVal) {
3422 var locals = {};
3423 locals[interimFactoryName] = publicService;
3424 return $injector.invoke(factory || function() { return defaultVal; }, {}, locals);
3425 }
3426
3427 }
3428
3429 }
3430
3431 /* @ngInject */
3432 function InterimElementFactory($document, $q, $rootScope, $timeout, $rootElement, $animate,
3433 $mdUtil, $mdCompiler, $mdTheming, $injector, $exceptionHandler) {
3434 return function createInterimElementService() {
3435 var SHOW_CANCELLED = false;
3436
3437 /*
3438 * @ngdoc service
3439 * @name $$interimElement.$service
3440 *
3441 * @description
3442 * A service used to control inserting and removing an element into the DOM.
3443 *
3444 */
3445
3446 var service;
3447
3448 var showPromises = []; // Promises for the interim's which are currently opening.
3449 var hidePromises = []; // Promises for the interim's which are currently hiding.
3450 var showingInterims = []; // Interim elements which are currently showing up.
3451
3452 // Publish instance $$interimElement service;
3453 // ... used as $mdDialog, $mdToast, $mdMenu, and $mdSelect
3454
3455 return service = {
3456 show: show,
3457 hide: waitForInterim(hide),
3458 cancel: waitForInterim(cancel),
3459 destroy : destroy,
3460 $injector_: $injector
3461 };
3462
3463 /*
3464 * @ngdoc method
3465 * @name $$interimElement.$service#show
3466 * @kind function
3467 *
3468 * @description
3469 * Adds the `$interimElement` to the DOM and returns a special promise that will be resolved or rejected
3470 * with hide or cancel, respectively. To external cancel/hide, developers should use the
3471 *
3472 * @param {*} options is hashMap of settings
3473 * @returns a Promise
3474 *
3475 */
3476 function show(options) {
3477 options = options || {};
3478 var interimElement = new InterimElement(options || {});
3479
3480 // When an interim element is currently showing, we have to cancel it.
3481 // Just hiding it, will resolve the InterimElement's promise, the promise should be
3482 // rejected instead.
3483 var hideAction = options.multiple ? $q.resolve() : $q.all(showPromises);
3484
3485 if (!options.multiple) {
3486 // Wait for all opening interim's to finish their transition.
3487 hideAction = hideAction.then(function() {
3488 // Wait for all closing and showing interim's to be completely closed.
3489 var promiseArray = hidePromises.concat(showingInterims.map(service.cancel));
3490 return $q.all(promiseArray);
3491 });
3492 }
3493
3494 var showAction = hideAction.then(function() {
3495
3496 return interimElement
3497 .show()
3498 .catch(function(reason) { return reason; })
3499 .finally(function() {
3500 showPromises.splice(showPromises.indexOf(showAction), 1);
3501 showingInterims.push(interimElement);
3502 });
3503
3504 });
3505
3506 showPromises.push(showAction);
3507
3508 // In Angular 1.6+, exceptions inside promises will cause a rejection. We need to handle
3509 // the rejection and only log it if it's an error.
3510 interimElement.deferred.promise.catch(function(fault) {
3511 if (fault instanceof Error) {
3512 $exceptionHandler(fault);
3513 }
3514
3515 return fault;
3516 });
3517
3518 // Return a promise that will be resolved when the interim
3519 // element is hidden or cancelled...
3520 return interimElement.deferred.promise;
3521 }
3522
3523 /*
3524 * @ngdoc method
3525 * @name $$interimElement.$service#hide
3526 * @kind function
3527 *
3528 * @description
3529 * Removes the `$interimElement` from the DOM and resolves the promise returned from `show`
3530 *
3531 * @param {*} resolveParam Data to resolve the promise with
3532 * @returns a Promise that will be resolved after the element has been removed.
3533 *
3534 */
3535 function hide(reason, options) {
3536 options = options || {};
3537
3538 if (options.closeAll) {
3539 // We have to make a shallow copy of the array, because otherwise the map will break.
3540 return $q.all(showingInterims.slice().reverse().map(closeElement));
3541 } else if (options.closeTo !== undefined) {
3542 return $q.all(showingInterims.slice(options.closeTo).map(closeElement));
3543 }
3544
3545 // Hide the latest showing interim element.
3546 return closeElement(showingInterims[showingInterims.length - 1]);
3547
3548 function closeElement(interim) {
3549
3550 var hideAction = interim
3551 .remove(reason, false, options || { })
3552 .catch(function(reason) { return reason; })
3553 .finally(function() {
3554 hidePromises.splice(hidePromises.indexOf(hideAction), 1);
3555 });
3556
3557 showingInterims.splice(showingInterims.indexOf(interim), 1);
3558 hidePromises.push(hideAction);
3559
3560 return interim.deferred.promise;
3561 }
3562 }
3563
3564 /*
3565 * @ngdoc method
3566 * @name $$interimElement.$service#cancel
3567 * @kind function
3568 *
3569 * @description
3570 * Removes the `$interimElement` from the DOM and rejects the promise returned from `show`
3571 *
3572 * @param {*} reason Data to reject the promise with
3573 * @returns Promise that will be resolved after the element has been removed.
3574 *
3575 */
3576 function cancel(reason, options) {
3577 var interim = showingInterims.pop();
3578 if (!interim) {
3579 return $q.when(reason);
3580 }
3581
3582 var cancelAction = interim
3583 .remove(reason, true, options || {})
3584 .catch(function(reason) { return reason; })
3585 .finally(function() {
3586 hidePromises.splice(hidePromises.indexOf(cancelAction), 1);
3587 });
3588
3589 hidePromises.push(cancelAction);
3590
3591 // Since Angular 1.6.7, promises will be logged to $exceptionHandler when the promise
3592 // is not handling the rejection. We create a pseudo catch handler, which will prevent the
3593 // promise from being logged to the $exceptionHandler.
3594 return interim.deferred.promise.catch(angular.noop);
3595 }
3596
3597 /**
3598 * Creates a function to wait for at least one interim element to be available.
3599 * @param callbackFn Function to be used as callback
3600 * @returns {Function}
3601 */
3602 function waitForInterim(callbackFn) {
3603 return function() {
3604 var fnArguments = arguments;
3605
3606 if (!showingInterims.length) {
3607 // When there are still interim's opening, then wait for the first interim element to
3608 // finish its open animation.
3609 if (showPromises.length) {
3610 return showPromises[0].finally(function () {
3611 return callbackFn.apply(service, fnArguments);
3612 });
3613 }
3614
3615 return $q.when("No interim elements currently showing up.");
3616 }
3617
3618 return callbackFn.apply(service, fnArguments);
3619 };
3620 }
3621
3622 /*
3623 * Special method to quick-remove the interim element without animations
3624 * Note: interim elements are in "interim containers"
3625 */
3626 function destroy(targetEl) {
3627 var interim = !targetEl ? showingInterims.shift() : null;
3628
3629 var parentEl = angular.element(targetEl).length && angular.element(targetEl)[0].parentNode;
3630
3631 if (parentEl) {
3632 // Try to find the interim in the stack which corresponds to the supplied DOM element.
3633 var filtered = showingInterims.filter(function(entry) {
3634 return entry.options.element[0] === parentEl;
3635 });
3636
3637 // Note: This function might be called when the element already has been removed,
3638 // in which case we won't find any matches.
3639 if (filtered.length) {
3640 interim = filtered[0];
3641 showingInterims.splice(showingInterims.indexOf(interim), 1);
3642 }
3643 }
3644
3645 return interim ? interim.remove(SHOW_CANCELLED, false, { '$destroy': true }) :
3646 $q.when(SHOW_CANCELLED);
3647 }
3648
3649 /*
3650 * Internal Interim Element Object
3651 * Used internally to manage the DOM element and related data
3652 */
3653 function InterimElement(options) {
3654 var self, element, showAction = $q.when(true);
3655
3656 options = configureScopeAndTransitions(options);
3657
3658 return self = {
3659 options : options,
3660 deferred: $q.defer(),
3661 show : createAndTransitionIn,
3662 remove : transitionOutAndRemove
3663 };
3664
3665 /**
3666 * Compile, link, and show this interim element
3667 * Use optional autoHided and transition-in effects
3668 */
3669 function createAndTransitionIn() {
3670 return $q(function(resolve, reject) {
3671
3672 // Trigger onCompiling callback before the compilation starts.
3673 // This is useful, when modifying options, which can be influenced by developers.
3674 options.onCompiling && options.onCompiling(options);
3675
3676 compileElement(options)
3677 .then(function( compiledData ) {
3678 element = linkElement( compiledData, options );
3679
3680 // Expose the cleanup function from the compiler.
3681 options.cleanupElement = compiledData.cleanup;
3682
3683 showAction = showElement(element, options, compiledData.controller)
3684 .then(resolve, rejectAll);
3685 }).catch(rejectAll);
3686
3687 function rejectAll(fault) {
3688 // Force the '$md<xxx>.show()' promise to reject
3689 self.deferred.reject(fault);
3690
3691 // Continue rejection propagation
3692 reject(fault);
3693 }
3694 });
3695 }
3696
3697 /**
3698 * After the show process has finished/rejected:
3699 * - announce 'removing',
3700 * - perform the transition-out, and
3701 * - perform optional clean up scope.
3702 */
3703 function transitionOutAndRemove(response, isCancelled, opts) {
3704
3705 // abort if the show() and compile failed
3706 if ( !element ) return $q.when(false);
3707
3708 options = angular.extend(options || {}, opts || {});
3709 options.cancelAutoHide && options.cancelAutoHide();
3710 options.element.triggerHandler('$mdInterimElementRemove');
3711
3712 if ( options.$destroy === true ) {
3713
3714 return hideElement(options.element, options).then(function(){
3715 (isCancelled && rejectAll(response)) || resolveAll(response);
3716 });
3717
3718 } else {
3719 $q.when(showAction).finally(function() {
3720 hideElement(options.element, options).then(function() {
3721 isCancelled ? rejectAll(response) : resolveAll(response);
3722 }, rejectAll);
3723 });
3724
3725 return self.deferred.promise;
3726 }
3727
3728
3729 /**
3730 * The `show()` returns a promise that will be resolved when the interim
3731 * element is hidden or cancelled...
3732 */
3733 function resolveAll(response) {
3734 self.deferred.resolve(response);
3735 }
3736
3737 /**
3738 * Force the '$md<xxx>.show()' promise to reject
3739 */
3740 function rejectAll(fault) {
3741 self.deferred.reject(fault);
3742 }
3743 }
3744
3745 /**
3746 * Prepare optional isolated scope and prepare $animate with default enter and leave
3747 * transitions for the new element instance.
3748 */
3749 function configureScopeAndTransitions(options) {
3750 options = options || { };
3751 if ( options.template ) {
3752 options.template = $mdUtil.processTemplate(options.template);
3753 }
3754
3755 return angular.extend({
3756 preserveScope: false,
3757 cancelAutoHide : angular.noop,
3758 scope: options.scope || $rootScope.$new(options.isolateScope),
3759
3760 /**
3761 * Default usage to enable $animate to transition-in; can be easily overridden via 'options'
3762 */
3763 onShow: function transitionIn(scope, element, options) {
3764 return $animate.enter(element, options.parent);
3765 },
3766
3767 /**
3768 * Default usage to enable $animate to transition-out; can be easily overridden via 'options'
3769 */
3770 onRemove: function transitionOut(scope, element) {
3771 // Element could be undefined if a new element is shown before
3772 // the old one finishes compiling.
3773 return element && $animate.leave(element) || $q.when();
3774 }
3775 }, options );
3776
3777 }
3778
3779 /**
3780 * Compile an element with a templateUrl, controller, and locals
3781 */
3782 function compileElement(options) {
3783
3784 var compiled = !options.skipCompile ? $mdCompiler.compile(options) : null;
3785
3786 return compiled || $q(function (resolve) {
3787 resolve({
3788 locals: {},
3789 link: function () {
3790 return options.element;
3791 }
3792 });
3793 });
3794 }
3795
3796 /**
3797 * Link an element with compiled configuration
3798 */
3799 function linkElement(compileData, options){
3800 angular.extend(compileData.locals, options);
3801
3802 var element = compileData.link(options.scope);
3803
3804 // Search for parent at insertion time, if not specified
3805 options.element = element;
3806 options.parent = findParent(element, options);
3807 if (options.themable) $mdTheming(element);
3808
3809 return element;
3810 }
3811
3812 /**
3813 * Search for parent at insertion time, if not specified
3814 */
3815 function findParent(element, options) {
3816 var parent = options.parent;
3817
3818 // Search for parent at insertion time, if not specified
3819 if (angular.isFunction(parent)) {
3820 parent = parent(options.scope, element, options);
3821 } else if (angular.isString(parent)) {
3822 parent = angular.element($document[0].querySelector(parent));
3823 } else {
3824 parent = angular.element(parent);
3825 }
3826
3827 // If parent querySelector/getter function fails, or it's just null,
3828 // find a default.
3829 if (!(parent || {}).length) {
3830 var el;
3831 if ($rootElement[0] && $rootElement[0].querySelector) {
3832 el = $rootElement[0].querySelector(':not(svg) > body');
3833 }
3834 if (!el) el = $rootElement[0];
3835 if (el.nodeName == '#comment') {
3836 el = $document[0].body;
3837 }
3838 return angular.element(el);
3839 }
3840
3841 return parent;
3842 }
3843
3844 /**
3845 * If auto-hide is enabled, start timer and prepare cancel function
3846 */
3847 function startAutoHide() {
3848 var autoHideTimer, cancelAutoHide = angular.noop;
3849
3850 if (options.hideDelay) {
3851 autoHideTimer = $timeout(service.hide, options.hideDelay) ;
3852 cancelAutoHide = function() {
3853 $timeout.cancel(autoHideTimer);
3854 };
3855 }
3856
3857 // Cache for subsequent use
3858 options.cancelAutoHide = function() {
3859 cancelAutoHide();
3860 options.cancelAutoHide = undefined;
3861 };
3862 }
3863
3864 /**
3865 * Show the element ( with transitions), notify complete and start
3866 * optional auto-Hide
3867 */
3868 function showElement(element, options, controller) {
3869 // Trigger onShowing callback before the `show()` starts
3870 var notifyShowing = options.onShowing || angular.noop;
3871 // Trigger onComplete callback when the `show()` finishes
3872 var notifyComplete = options.onComplete || angular.noop;
3873
3874 // Necessary for consistency between Angular 1.5 and 1.6.
3875 try {
3876 notifyShowing(options.scope, element, options, controller);
3877 } catch (e) {
3878 return $q.reject(e);
3879 }
3880
3881 return $q(function (resolve, reject) {
3882 try {
3883 // Start transitionIn
3884 $q.when(options.onShow(options.scope, element, options, controller))
3885 .then(function () {
3886 notifyComplete(options.scope, element, options);
3887 startAutoHide();
3888
3889 resolve(element);
3890 }, reject);
3891
3892 } catch (e) {
3893 reject(e.message);
3894 }
3895 });
3896 }
3897
3898 function hideElement(element, options) {
3899 var announceRemoving = options.onRemoving || angular.noop;
3900
3901 return $q(function (resolve, reject) {
3902 try {
3903 // Start transitionIn
3904 var action = $q.when( options.onRemove(options.scope, element, options) || true );
3905
3906 // Trigger callback *before* the remove operation starts
3907 announceRemoving(element, action);
3908
3909 if (options.$destroy) {
3910 // For $destroy, onRemove should be synchronous
3911 resolve(element);
3912
3913 if (!options.preserveScope && options.scope ) {
3914 // scope destroy should still be be done after the current digest is done
3915 action.then( function() { options.scope.$destroy(); });
3916 }
3917 } else {
3918 // Wait until transition-out is done
3919 action.then(function () {
3920 if (!options.preserveScope && options.scope ) {
3921 options.scope.$destroy();
3922 }
3923
3924 resolve(element);
3925 }, reject);
3926 }
3927 } catch (e) {
3928 reject(e.message);
3929 }
3930 });
3931 }
3932
3933 }
3934 };
3935
3936 }
3937
3938}
3939
3940})();
3941(function(){
3942"use strict";
3943
3944/**
3945 * @ngdoc module
3946 * @name material.core.interaction
3947 * @description
3948 * User interaction detection to provide proper accessibility.
3949 */
3950MdInteractionService.$inject = ["$timeout", "$mdUtil"];
3951angular
3952 .module('material.core.interaction', [])
3953 .service('$mdInteraction', MdInteractionService);
3954
3955
3956/**
3957 * @ngdoc service
3958 * @name $mdInteraction
3959 * @module material.core.interaction
3960 *
3961 * @description
3962 *
3963 * Service which keeps track of the last interaction type and validates them for several browsers.
3964 * The service hooks into the document's body and listens for touch, mouse and keyboard events.
3965 *
3966 * The most recent interaction type can be retrieved by calling the `getLastInteractionType` method.
3967 *
3968 * Here is an example markup for using the interaction service.
3969 *
3970 * <hljs lang="js">
3971 * var lastType = $mdInteraction.getLastInteractionType();
3972 *
3973 * if (lastType === 'keyboard') {
3974 * // We only restore the focus for keyboard users.
3975 * restoreFocus();
3976 * }
3977 * </hljs>
3978 *
3979 */
3980function MdInteractionService($timeout, $mdUtil) {
3981 this.$timeout = $timeout;
3982 this.$mdUtil = $mdUtil;
3983
3984 this.bodyElement = angular.element(document.body);
3985 this.isBuffering = false;
3986 this.bufferTimeout = null;
3987 this.lastInteractionType = null;
3988 this.lastInteractionTime = null;
3989
3990 // Type Mappings for the different events
3991 // There will be three three interaction types
3992 // `keyboard`, `mouse` and `touch`
3993 // type `pointer` will be evaluated in `pointerMap` for IE Browser events
3994 this.inputEventMap = {
3995 'keydown': 'keyboard',
3996 'mousedown': 'mouse',
3997 'mouseenter': 'mouse',
3998 'touchstart': 'touch',
3999 'pointerdown': 'pointer',
4000 'MSPointerDown': 'pointer'
4001 };
4002
4003 // IE PointerDown events will be validated in `touch` or `mouse`
4004 // Index numbers referenced here: https://msdn.microsoft.com/library/windows/apps/hh466130.aspx
4005 this.iePointerMap = {
4006 2: 'touch',
4007 3: 'touch',
4008 4: 'mouse'
4009 };
4010
4011 this.initializeEvents();
4012}
4013
4014/**
4015 * Initializes the interaction service, by registering all interaction events to the
4016 * body element.
4017 */
4018MdInteractionService.prototype.initializeEvents = function() {
4019 // IE browsers can also trigger pointer events, which also leads to an interaction.
4020 var pointerEvent = 'MSPointerEvent' in window ? 'MSPointerDown' : 'PointerEvent' in window ? 'pointerdown' : null;
4021
4022 this.bodyElement.on('keydown mousedown', this.onInputEvent.bind(this));
4023
4024 if ('ontouchstart' in document.documentElement) {
4025 this.bodyElement.on('touchstart', this.onBufferInputEvent.bind(this));
4026 }
4027
4028 if (pointerEvent) {
4029 this.bodyElement.on(pointerEvent, this.onInputEvent.bind(this));
4030 }
4031
4032};
4033
4034/**
4035 * Event listener for normal interaction events, which should be tracked.
4036 * @param event {MouseEvent|KeyboardEvent|PointerEvent|TouchEvent}
4037 */
4038MdInteractionService.prototype.onInputEvent = function(event) {
4039 if (this.isBuffering) {
4040 return;
4041 }
4042
4043 var type = this.inputEventMap[event.type];
4044
4045 if (type === 'pointer') {
4046 type = this.iePointerMap[event.pointerType] || event.pointerType;
4047 }
4048
4049 this.lastInteractionType = type;
4050 this.lastInteractionTime = this.$mdUtil.now();
4051};
4052
4053/**
4054 * Event listener for interaction events which should be buffered (touch events).
4055 * @param event {TouchEvent}
4056 */
4057MdInteractionService.prototype.onBufferInputEvent = function(event) {
4058 this.$timeout.cancel(this.bufferTimeout);
4059
4060 this.onInputEvent(event);
4061 this.isBuffering = true;
4062
4063 // The timeout of 650ms is needed to delay the touchstart, because otherwise the touch will call
4064 // the `onInput` function multiple times.
4065 this.bufferTimeout = this.$timeout(function() {
4066 this.isBuffering = false;
4067 }.bind(this), 650, false);
4068
4069};
4070
4071/**
4072 * @ngdoc method
4073 * @name $mdInteraction#getLastInteractionType
4074 * @description Retrieves the last interaction type triggered in body.
4075 * @returns {string|null} Last interaction type.
4076 */
4077MdInteractionService.prototype.getLastInteractionType = function() {
4078 return this.lastInteractionType;
4079};
4080
4081/**
4082 * @ngdoc method
4083 * @name $mdInteraction#isUserInvoked
4084 * @description Method to detect whether any interaction happened recently or not.
4085 * @param {number=} checkDelay Time to check for any interaction to have been triggered.
4086 * @returns {boolean} Whether there was any interaction or not.
4087 */
4088MdInteractionService.prototype.isUserInvoked = function(checkDelay) {
4089 var delay = angular.isNumber(checkDelay) ? checkDelay : 15;
4090
4091 // Check for any interaction to be within the specified check time.
4092 return this.lastInteractionTime >= this.$mdUtil.now() - delay;
4093};
4094
4095})();
4096(function(){
4097"use strict";
4098
4099(function() {
4100 'use strict';
4101
4102 var $mdUtil, $interpolate, $log;
4103
4104 var SUFFIXES = /(-gt)?-(sm|md|lg|print)/g;
4105 var WHITESPACE = /\s+/g;
4106
4107 var FLEX_OPTIONS = ['grow', 'initial', 'auto', 'none', 'noshrink', 'nogrow' ];
4108 var LAYOUT_OPTIONS = ['row', 'column'];
4109 var ALIGNMENT_MAIN_AXIS= [ "", "start", "center", "end", "stretch", "space-around", "space-between" ];
4110 var ALIGNMENT_CROSS_AXIS= [ "", "start", "center", "end", "stretch" ];
4111
4112 var config = {
4113 /**
4114 * Enable directive attribute-to-class conversions
4115 * Developers can use `<body md-layout-css />` to quickly
4116 * disable the Layout directives and prohibit the injection of Layout classNames
4117 */
4118 enabled: true,
4119
4120 /**
4121 * List of mediaQuery breakpoints and associated suffixes
4122 *
4123 * [
4124 * { suffix: "sm", mediaQuery: "screen and (max-width: 599px)" },
4125 * { suffix: "md", mediaQuery: "screen and (min-width: 600px) and (max-width: 959px)" }
4126 * ]
4127 */
4128 breakpoints: []
4129 };
4130
4131 registerLayoutAPI( angular.module('material.core.layout', ['ng']) );
4132
4133 /**
4134 * registerLayoutAPI()
4135 *
4136 * The original ngMaterial Layout solution used attribute selectors and CSS.
4137 *
4138 * ```html
4139 * <div layout="column"> My Content </div>
4140 * ```
4141 *
4142 * ```css
4143 * [layout] {
4144 * box-sizing: border-box;
4145 * display:flex;
4146 * }
4147 * [layout=column] {
4148 * flex-direction : column
4149 * }
4150 * ```
4151 *
4152 * Use of attribute selectors creates significant performance impacts in some
4153 * browsers... mainly IE.
4154 *
4155 * This module registers directives that allow the same layout attributes to be
4156 * interpreted and converted to class selectors. The directive will add equivalent classes to each element that
4157 * contains a Layout directive.
4158 *
4159 * ```html
4160 * <div layout="column" class="layout layout-column"> My Content </div>
4161 *```
4162 *
4163 * ```css
4164 * .layout {
4165 * box-sizing: border-box;
4166 * display:flex;
4167 * }
4168 * .layout-column {
4169 * flex-direction : column
4170 * }
4171 * ```
4172 */
4173 function registerLayoutAPI(module){
4174 var PREFIX_REGEXP = /^((?:x|data)[\:\-_])/i;
4175 var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g;
4176
4177 // NOTE: these are also defined in constants::MEDIA_PRIORITY and constants::MEDIA
4178 var BREAKPOINTS = [ "", "xs", "gt-xs", "sm", "gt-sm", "md", "gt-md", "lg", "gt-lg", "xl", "print" ];
4179 var API_WITH_VALUES = [ "layout", "flex", "flex-order", "flex-offset", "layout-align" ];
4180 var API_NO_VALUES = [ "show", "hide", "layout-padding", "layout-margin" ];
4181
4182
4183 // Build directive registration functions for the standard Layout API... for all breakpoints.
4184 angular.forEach(BREAKPOINTS, function(mqb) {
4185
4186 // Attribute directives with expected, observable value(s)
4187 angular.forEach( API_WITH_VALUES, function(name){
4188 var fullName = mqb ? name + "-" + mqb : name;
4189 module.directive( directiveNormalize(fullName), attributeWithObserve(fullName));
4190 });
4191
4192 // Attribute directives with no expected value(s)
4193 angular.forEach( API_NO_VALUES, function(name){
4194 var fullName = mqb ? name + "-" + mqb : name;
4195 module.directive( directiveNormalize(fullName), attributeWithoutValue(fullName));
4196 });
4197
4198 });
4199
4200 // Register other, special directive functions for the Layout features:
4201 module
4202
4203 .provider('$$mdLayout' , function() {
4204 // Publish internal service for Layouts
4205 return {
4206 $get : angular.noop,
4207 validateAttributeValue : validateAttributeValue,
4208 validateAttributeUsage : validateAttributeUsage,
4209 /**
4210 * Easy way to disable/enable the Layout API.
4211 * When disabled, this stops all attribute-to-classname generations
4212 */
4213 disableLayouts : function(isDisabled) {
4214 config.enabled = (isDisabled !== true);
4215 }
4216 };
4217 })
4218
4219 .directive('mdLayoutCss' , disableLayoutDirective )
4220 .directive('ngCloak' , buildCloakInterceptor('ng-cloak'))
4221
4222 .directive('layoutWrap' , attributeWithoutValue('layout-wrap'))
4223 .directive('layoutNowrap' , attributeWithoutValue('layout-nowrap'))
4224 .directive('layoutNoWrap' , attributeWithoutValue('layout-no-wrap'))
4225 .directive('layoutFill' , attributeWithoutValue('layout-fill'))
4226
4227 // !! Deprecated attributes: use the `-lt` (aka less-than) notations
4228
4229 .directive('layoutLtMd' , warnAttrNotSupported('layout-lt-md', true))
4230 .directive('layoutLtLg' , warnAttrNotSupported('layout-lt-lg', true))
4231 .directive('flexLtMd' , warnAttrNotSupported('flex-lt-md', true))
4232 .directive('flexLtLg' , warnAttrNotSupported('flex-lt-lg', true))
4233
4234 .directive('layoutAlignLtMd', warnAttrNotSupported('layout-align-lt-md'))
4235 .directive('layoutAlignLtLg', warnAttrNotSupported('layout-align-lt-lg'))
4236 .directive('flexOrderLtMd' , warnAttrNotSupported('flex-order-lt-md'))
4237 .directive('flexOrderLtLg' , warnAttrNotSupported('flex-order-lt-lg'))
4238 .directive('offsetLtMd' , warnAttrNotSupported('flex-offset-lt-md'))
4239 .directive('offsetLtLg' , warnAttrNotSupported('flex-offset-lt-lg'))
4240
4241 .directive('hideLtMd' , warnAttrNotSupported('hide-lt-md'))
4242 .directive('hideLtLg' , warnAttrNotSupported('hide-lt-lg'))
4243 .directive('showLtMd' , warnAttrNotSupported('show-lt-md'))
4244 .directive('showLtLg' , warnAttrNotSupported('show-lt-lg'))
4245
4246 // Determine if
4247 .config( detectDisabledLayouts );
4248
4249 /**
4250 * Converts snake_case to camelCase.
4251 * Also there is special case for Moz prefix starting with upper case letter.
4252 * @param name Name to normalize
4253 */
4254 function directiveNormalize(name) {
4255 return name
4256 .replace(PREFIX_REGEXP, '')
4257 .replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) {
4258 return offset ? letter.toUpperCase() : letter;
4259 });
4260 }
4261
4262 }
4263
4264
4265 /**
4266 * Detect if any of the HTML tags has a [md-layouts-disabled] attribute;
4267 * If yes, then immediately disable all layout API features
4268 *
4269 * Note: this attribute should be specified on either the HTML or BODY tags
4270 */
4271 /**
4272 * @ngInject
4273 */
4274 function detectDisabledLayouts() {
4275 var isDisabled = !!document.querySelector('[md-layouts-disabled]');
4276 config.enabled = !isDisabled;
4277 }
4278
4279 /**
4280 * Special directive that will disable ALL Layout conversions of layout
4281 * attribute(s) to classname(s).
4282 *
4283 * <link rel="stylesheet" href="angular-material.min.css">
4284 * <link rel="stylesheet" href="angular-material.layout.css">
4285 *
4286 * <body md-layout-css>
4287 * ...
4288 * </body>
4289 *
4290 * Note: Using md-layout-css directive requires the developer to load the Material
4291 * Layout Attribute stylesheet (which only uses attribute selectors):
4292 *
4293 * `angular-material.layout.css`
4294 *
4295 * Another option is to use the LayoutProvider to configure and disable the attribute
4296 * conversions; this would obviate the use of the `md-layout-css` directive
4297 *
4298 */
4299 function disableLayoutDirective() {
4300 // Return a 1x-only, first-match attribute directive
4301 config.enabled = false;
4302
4303 return {
4304 restrict : 'A',
4305 priority : '900'
4306 };
4307 }
4308
4309 /**
4310 * Tail-hook ngCloak to delay the uncloaking while Layout transformers
4311 * finish processing. Eliminates flicker with Material.Layouts
4312 */
4313 function buildCloakInterceptor(className) {
4314 return [ '$timeout', function($timeout){
4315 return {
4316 restrict : 'A',
4317 priority : -10, // run after normal ng-cloak
4318 compile : function( element ) {
4319 if (!config.enabled) return angular.noop;
4320
4321 // Re-add the cloak
4322 element.addClass(className);
4323
4324 return function( scope, element ) {
4325 // Wait while layout injectors configure, then uncloak
4326 // NOTE: $rAF does not delay enough... and this is a 1x-only event,
4327 // $timeout is acceptable.
4328 $timeout( function(){
4329 element.removeClass(className);
4330 }, 10, false);
4331 };
4332 }
4333 };
4334 }];
4335 }
4336
4337
4338 // *********************************************************************************
4339 //
4340 // These functions create registration functions for ngMaterial Layout attribute directives
4341 // This provides easy translation to switch ngMaterial attribute selectors to
4342 // CLASS selectors and directives; which has huge performance implications
4343 // for IE Browsers
4344 //
4345 // *********************************************************************************
4346
4347 /**
4348 * Creates a directive registration function where a possible dynamic attribute
4349 * value will be observed/watched.
4350 * @param {string} className attribute name; eg `layout-gt-md` with value ="row"
4351 */
4352 function attributeWithObserve(className) {
4353
4354 return ['$mdUtil', '$interpolate', "$log", function(_$mdUtil_, _$interpolate_, _$log_) {
4355 $mdUtil = _$mdUtil_;
4356 $interpolate = _$interpolate_;
4357 $log = _$log_;
4358
4359 return {
4360 restrict: 'A',
4361 compile: function(element, attr) {
4362 var linkFn;
4363 if (config.enabled) {
4364 // immediately replace static (non-interpolated) invalid values...
4365
4366 validateAttributeUsage(className, attr, element, $log);
4367
4368 validateAttributeValue( className,
4369 getNormalizedAttrValue(className, attr, ""),
4370 buildUpdateFn(element, className, attr)
4371 );
4372
4373 linkFn = translateWithValueToCssClass;
4374 }
4375
4376 // Use for postLink to account for transforms after ng-transclude.
4377 return linkFn || angular.noop;
4378 }
4379 };
4380 }];
4381
4382 /**
4383 * Add as transformed class selector(s), then
4384 * remove the deprecated attribute selector
4385 */
4386 function translateWithValueToCssClass(scope, element, attrs) {
4387 var updateFn = updateClassWithValue(element, className, attrs);
4388 var unwatch = attrs.$observe(attrs.$normalize(className), updateFn);
4389
4390 updateFn(getNormalizedAttrValue(className, attrs, ""));
4391 scope.$on("$destroy", function() { unwatch(); });
4392 }
4393 }
4394
4395 /**
4396 * Creates a registration function for ngMaterial Layout attribute directive.
4397 * This is a `simple` transpose of attribute usage to class usage; where we ignore
4398 * any attribute value
4399 */
4400 function attributeWithoutValue(className) {
4401 return ['$mdUtil', '$interpolate', "$log", function(_$mdUtil_, _$interpolate_, _$log_) {
4402 $mdUtil = _$mdUtil_;
4403 $interpolate = _$interpolate_;
4404 $log = _$log_;
4405
4406 return {
4407 restrict: 'A',
4408 compile: function(element, attr) {
4409 var linkFn;
4410 if (config.enabled) {
4411 // immediately replace static (non-interpolated) invalid values...
4412
4413 validateAttributeValue( className,
4414 getNormalizedAttrValue(className, attr, ""),
4415 buildUpdateFn(element, className, attr)
4416 );
4417
4418 translateToCssClass(null, element);
4419
4420 // Use for postLink to account for transforms after ng-transclude.
4421 linkFn = translateToCssClass;
4422 }
4423
4424 return linkFn || angular.noop;
4425 }
4426 };
4427 }];
4428
4429 /**
4430 * Add as transformed class selector, then
4431 * remove the deprecated attribute selector
4432 */
4433 function translateToCssClass(scope, element) {
4434 element.addClass(className);
4435 }
4436 }
4437
4438
4439
4440 /**
4441 * After link-phase, do NOT remove deprecated layout attribute selector.
4442 * Instead watch the attribute so interpolated data-bindings to layout
4443 * selectors will continue to be supported.
4444 *
4445 * $observe() the className and update with new class (after removing the last one)
4446 *
4447 * e.g. `layout="{{layoutDemo.direction}}"` will update...
4448 *
4449 * NOTE: The value must match one of the specified styles in the CSS.
4450 * For example `flex-gt-md="{{size}}` where `scope.size == 47` will NOT work since
4451 * only breakpoints for 0, 5, 10, 15... 100, 33, 34, 66, 67 are defined.
4452 *
4453 */
4454 function updateClassWithValue(element, className) {
4455 var lastClass;
4456
4457 return function updateClassFn(newValue) {
4458 var value = validateAttributeValue(className, newValue || "");
4459 if ( angular.isDefined(value) ) {
4460 if (lastClass) element.removeClass(lastClass);
4461 lastClass = !value ? className : className + "-" + value.replace(WHITESPACE, "-");
4462 element.addClass(lastClass);
4463 }
4464 };
4465 }
4466
4467 /**
4468 * Provide console warning that this layout attribute has been deprecated
4469 *
4470 */
4471 function warnAttrNotSupported(className) {
4472 var parts = className.split("-");
4473 return ["$log", function($log) {
4474 $log.warn(className + "has been deprecated. Please use a `" + parts[0] + "-gt-<xxx>` variant.");
4475 return angular.noop;
4476 }];
4477 }
4478
4479 /**
4480 * Centralize warnings for known flexbox issues (especially IE-related issues)
4481 */
4482 function validateAttributeUsage(className, attr, element, $log){
4483 var message, usage, url;
4484 var nodeName = element[0].nodeName.toLowerCase();
4485
4486 switch(className.replace(SUFFIXES,"")) {
4487 case "flex":
4488 if ((nodeName == "md-button") || (nodeName == "fieldset")){
4489 // @see https://github.com/philipwalton/flexbugs#9-some-html-elements-cant-be-flex-containers
4490 // Use <div flex> wrapper inside (preferred) or outside
4491
4492 usage = "<" + nodeName + " " + className + "></" + nodeName + ">";
4493 url = "https://github.com/philipwalton/flexbugs#9-some-html-elements-cant-be-flex-containers";
4494 message = "Markup '{0}' may not work as expected in IE Browsers. Consult '{1}' for details.";
4495
4496 $log.warn( $mdUtil.supplant(message, [usage, url]) );
4497 }
4498 }
4499
4500 }
4501
4502
4503 /**
4504 * For the Layout attribute value, validate or replace with default
4505 * fallback value
4506 */
4507 function validateAttributeValue(className, value, updateFn) {
4508 var origValue = value;
4509
4510 if (!needsInterpolation(value)) {
4511 switch (className.replace(SUFFIXES,"")) {
4512 case 'layout' :
4513 if ( !findIn(value, LAYOUT_OPTIONS) ) {
4514 value = LAYOUT_OPTIONS[0]; // 'row';
4515 }
4516 break;
4517
4518 case 'flex' :
4519 if (!findIn(value, FLEX_OPTIONS)) {
4520 if (isNaN(value)) {
4521 value = '';
4522 }
4523 }
4524 break;
4525
4526 case 'flex-offset' :
4527 case 'flex-order' :
4528 if (!value || isNaN(+value)) {
4529 value = '0';
4530 }
4531 break;
4532
4533 case 'layout-align' :
4534 var axis = extractAlignAxis(value);
4535 value = $mdUtil.supplant("{main}-{cross}",axis);
4536 break;
4537
4538 case 'layout-padding' :
4539 case 'layout-margin' :
4540 case 'layout-fill' :
4541 case 'layout-wrap' :
4542 case 'layout-nowrap' :
4543 case 'layout-nowrap' :
4544 value = '';
4545 break;
4546 }
4547
4548 if (value != origValue) {
4549 (updateFn || angular.noop)(value);
4550 }
4551 }
4552
4553 return value;
4554 }
4555
4556 /**
4557 * Replace current attribute value with fallback value
4558 */
4559 function buildUpdateFn(element, className, attrs) {
4560 return function updateAttrValue(fallback) {
4561 if (!needsInterpolation(fallback)) {
4562 // Do not modify the element's attribute value; so
4563 // uses '<ui-layout layout="/api/sidebar.html" />' will not
4564 // be affected. Just update the attrs value.
4565 attrs[attrs.$normalize(className)] = fallback;
4566 }
4567 };
4568 }
4569
4570 /**
4571 * See if the original value has interpolation symbols:
4572 * e.g. flex-gt-md="{{triggerPoint}}"
4573 */
4574 function needsInterpolation(value) {
4575 return (value || "").indexOf($interpolate.startSymbol()) > -1;
4576 }
4577
4578 function getNormalizedAttrValue(className, attrs, defaultVal) {
4579 var normalizedAttr = attrs.$normalize(className);
4580 return attrs[normalizedAttr] ? attrs[normalizedAttr].replace(WHITESPACE, "-") : defaultVal || null;
4581 }
4582
4583 function findIn(item, list, replaceWith) {
4584 item = replaceWith && item ? item.replace(WHITESPACE, replaceWith) : item;
4585
4586 var found = false;
4587 if (item) {
4588 list.forEach(function(it) {
4589 it = replaceWith ? it.replace(WHITESPACE, replaceWith) : it;
4590 found = found || (it === item);
4591 });
4592 }
4593 return found;
4594 }
4595
4596 function extractAlignAxis(attrValue) {
4597 var axis = {
4598 main : "start",
4599 cross: "stretch"
4600 }, values;
4601
4602 attrValue = (attrValue || "");
4603
4604 if ( attrValue.indexOf("-") === 0 || attrValue.indexOf(" ") === 0) {
4605 // For missing main-axis values
4606 attrValue = "none" + attrValue;
4607 }
4608
4609 values = attrValue.toLowerCase().trim().replace(WHITESPACE, "-").split("-");
4610 if ( values.length && (values[0] === "space") ) {
4611 // for main-axis values of "space-around" or "space-between"
4612 values = [ values[0]+"-"+values[1],values[2] ];
4613 }
4614
4615 if ( values.length > 0 ) axis.main = values[0] || axis.main;
4616 if ( values.length > 1 ) axis.cross = values[1] || axis.cross;
4617
4618 if ( ALIGNMENT_MAIN_AXIS.indexOf(axis.main) < 0 ) axis.main = "start";
4619 if ( ALIGNMENT_CROSS_AXIS.indexOf(axis.cross) < 0 ) axis.cross = "stretch";
4620
4621 return axis;
4622 }
4623
4624
4625})();
4626
4627})();
4628(function(){
4629"use strict";
4630
4631/**
4632 * @ngdoc module
4633 * @name material.core.liveannouncer
4634 * @description
4635 * Angular Material Live Announcer to provide accessibility for Voice Readers.
4636 */
4637MdLiveAnnouncer.$inject = ["$timeout"];
4638angular
4639 .module('material.core')
4640 .service('$mdLiveAnnouncer', MdLiveAnnouncer);
4641
4642/**
4643 * @ngdoc service
4644 * @name $mdLiveAnnouncer
4645 * @module material.core.liveannouncer
4646 *
4647 * @description
4648 *
4649 * Service to announce messages to supported screenreaders.
4650 *
4651 * > The `$mdLiveAnnouncer` service is internally used for components to provide proper accessibility.
4652 *
4653 * <hljs lang="js">
4654 * module.controller('AppCtrl', function($mdLiveAnnouncer) {
4655 * // Basic announcement (Polite Mode)
4656 * $mdLiveAnnouncer.announce('Hey Google');
4657 *
4658 * // Custom announcement (Assertive Mode)
4659 * $mdLiveAnnouncer.announce('Hey Google', 'assertive');
4660 * });
4661 * </hljs>
4662 *
4663 */
4664function MdLiveAnnouncer($timeout) {
4665 /** @private @const @type {!angular.$timeout} */
4666 this._$timeout = $timeout;
4667
4668 /** @private @const @type {!HTMLElement} */
4669 this._liveElement = this._createLiveElement();
4670
4671 /** @private @const @type {!number} */
4672 this._announceTimeout = 100;
4673}
4674
4675/**
4676 * @ngdoc method
4677 * @name $mdLiveAnnouncer#announce
4678 * @description Announces messages to supported screenreaders.
4679 * @param {string} message Message to be announced to the screenreader
4680 * @param {'off'|'polite'|'assertive'} politeness The politeness of the announcer element.
4681 */
4682MdLiveAnnouncer.prototype.announce = function(message, politeness) {
4683 if (!politeness) {
4684 politeness = 'polite';
4685 }
4686
4687 var self = this;
4688
4689 self._liveElement.textContent = '';
4690 self._liveElement.setAttribute('aria-live', politeness);
4691
4692 // This 100ms timeout is necessary for some browser + screen-reader combinations:
4693 // - Both JAWS and NVDA over IE11 will not announce anything without a non-zero timeout.
4694 // - With Chrome and IE11 with NVDA or JAWS, a repeated (identical) message won't be read a
4695 // second time without clearing and then using a non-zero delay.
4696 // (using JAWS 17 at time of this writing).
4697 self._$timeout(function() {
4698 self._liveElement.textContent = message;
4699 }, self._announceTimeout, false);
4700};
4701
4702/**
4703 * Creates a live announcer element, which listens for DOM changes and announces them
4704 * to the screenreaders.
4705 * @returns {!HTMLElement}
4706 * @private
4707 */
4708MdLiveAnnouncer.prototype._createLiveElement = function() {
4709 var liveEl = document.createElement('div');
4710
4711 liveEl.classList.add('md-visually-hidden');
4712 liveEl.setAttribute('role', 'status');
4713 liveEl.setAttribute('aria-atomic', 'true');
4714 liveEl.setAttribute('aria-live', 'polite');
4715
4716 document.body.appendChild(liveEl);
4717
4718 return liveEl;
4719};
4720
4721})();
4722(function(){
4723"use strict";
4724
4725/**
4726 * @ngdoc service
4727 * @name $$mdMeta
4728 * @module material.core.meta
4729 *
4730 * @description
4731 *
4732 * A provider and a service that simplifies meta tags access
4733 *
4734 * Note: This is intended only for use with dynamic meta tags such as browser color and title.
4735 * Tags that are only processed when the page is rendered (such as `charset`, and `http-equiv`)
4736 * will not work since `$$mdMeta` adds the tags after the page has already been loaded.
4737 *
4738 * ```js
4739 * app.config(function($$mdMetaProvider) {
4740 * var removeMeta = $$mdMetaProvider.setMeta('meta-name', 'content');
4741 * var metaValue = $$mdMetaProvider.getMeta('meta-name'); // -> 'content'
4742 *
4743 * removeMeta();
4744 * });
4745 *
4746 * app.controller('myController', function($$mdMeta) {
4747 * var removeMeta = $$mdMeta.setMeta('meta-name', 'content');
4748 * var metaValue = $$mdMeta.getMeta('meta-name'); // -> 'content'
4749 *
4750 * removeMeta();
4751 * });
4752 * ```
4753 *
4754 * @returns {$$mdMeta.$service}
4755 *
4756 */
4757angular.module('material.core.meta', [])
4758 .provider('$$mdMeta', function () {
4759 var head = angular.element(document.head);
4760 var metaElements = {};
4761
4762 /**
4763 * Checks if the requested element was written manually and maps it
4764 *
4765 * @param {string} name meta tag 'name' attribute value
4766 * @returns {boolean} returns true if there is an element with the requested name
4767 */
4768 function mapExistingElement(name) {
4769 if (metaElements[name]) {
4770 return true;
4771 }
4772
4773 var element = document.getElementsByName(name)[0];
4774
4775 if (!element) {
4776 return false;
4777 }
4778
4779 metaElements[name] = angular.element(element);
4780
4781 return true;
4782 }
4783
4784 /**
4785 * @ngdoc method
4786 * @name $$mdMeta#setMeta
4787 *
4788 * @description
4789 * Creates meta element with the 'name' and 'content' attributes,
4790 * if the meta tag is already created than we replace the 'content' value
4791 *
4792 * @param {string} name meta tag 'name' attribute value
4793 * @param {string} content meta tag 'content' attribute value
4794 * @returns {function} remove function
4795 *
4796 */
4797 function setMeta(name, content) {
4798 mapExistingElement(name);
4799
4800 if (!metaElements[name]) {
4801 var newMeta = angular.element('<meta name="' + name + '" content="' + content + '"/>');
4802 head.append(newMeta);
4803 metaElements[name] = newMeta;
4804 }
4805 else {
4806 metaElements[name].attr('content', content);
4807 }
4808
4809 return function () {
4810 metaElements[name].attr('content', '');
4811 metaElements[name].remove();
4812 delete metaElements[name];
4813 };
4814 }
4815
4816 /**
4817 * @ngdoc method
4818 * @name $$mdMeta#getMeta
4819 *
4820 * @description
4821 * Gets the 'content' attribute value of the wanted meta element
4822 *
4823 * @param {string} name meta tag 'name' attribute value
4824 * @returns {string} content attribute value
4825 */
4826 function getMeta(name) {
4827 if (!mapExistingElement(name)) {
4828 throw Error('$$mdMeta: could not find a meta tag with the name \'' + name + '\'');
4829 }
4830
4831 return metaElements[name].attr('content');
4832 }
4833
4834 var module = {
4835 setMeta: setMeta,
4836 getMeta: getMeta
4837 };
4838
4839 return angular.extend({}, module, {
4840 $get: function () {
4841 return module;
4842 }
4843 });
4844 });
4845})();
4846(function(){
4847"use strict";
4848
4849 /**
4850 * @ngdoc module
4851 * @name material.core.componentRegistry
4852 *
4853 * @description
4854 * A component instance registration service.
4855 * Note: currently this as a private service in the SideNav component.
4856 */
4857 ComponentRegistry.$inject = ["$log", "$q"];
4858 angular.module('material.core')
4859 .factory('$mdComponentRegistry', ComponentRegistry);
4860
4861 /*
4862 * @private
4863 * @ngdoc factory
4864 * @name ComponentRegistry
4865 * @module material.core.componentRegistry
4866 *
4867 */
4868 function ComponentRegistry($log, $q) {
4869
4870 var self;
4871 var instances = [ ];
4872 var pendings = { };
4873
4874 return self = {
4875 /**
4876 * Used to print an error when an instance for a handle isn't found.
4877 */
4878 notFoundError: function(handle, msgContext) {
4879 $log.error( (msgContext || "") + 'No instance found for handle', handle);
4880 },
4881 /**
4882 * Return all registered instances as an array.
4883 */
4884 getInstances: function() {
4885 return instances;
4886 },
4887
4888 /**
4889 * Get a registered instance.
4890 * @param handle the String handle to look up for a registered instance.
4891 */
4892 get: function(handle) {
4893 if ( !isValidID(handle) ) return null;
4894
4895 var i, j, instance;
4896 for(i = 0, j = instances.length; i < j; i++) {
4897 instance = instances[i];
4898 if(instance.$$mdHandle === handle) {
4899 return instance;
4900 }
4901 }
4902 return null;
4903 },
4904
4905 /**
4906 * Register an instance.
4907 * @param instance the instance to register
4908 * @param handle the handle to identify the instance under.
4909 */
4910 register: function(instance, handle) {
4911 if ( !handle ) return angular.noop;
4912
4913 instance.$$mdHandle = handle;
4914 instances.push(instance);
4915 resolveWhen();
4916
4917 return deregister;
4918
4919 /**
4920 * Remove registration for an instance
4921 */
4922 function deregister() {
4923 var index = instances.indexOf(instance);
4924 if (index !== -1) {
4925 instances.splice(index, 1);
4926 }
4927 }
4928
4929 /**
4930 * Resolve any pending promises for this instance
4931 */
4932 function resolveWhen() {
4933 var dfd = pendings[handle];
4934 if ( dfd ) {
4935 dfd.forEach(function (promise) {
4936 promise.resolve(instance);
4937 });
4938 delete pendings[handle];
4939 }
4940 }
4941 },
4942
4943 /**
4944 * Async accessor to registered component instance
4945 * If not available then a promise is created to notify
4946 * all listeners when the instance is registered.
4947 */
4948 when : function(handle) {
4949 if ( isValidID(handle) ) {
4950 var deferred = $q.defer();
4951 var instance = self.get(handle);
4952
4953 if ( instance ) {
4954 deferred.resolve( instance );
4955 } else {
4956 if (pendings[handle] === undefined) {
4957 pendings[handle] = [];
4958 }
4959 pendings[handle].push(deferred);
4960 }
4961
4962 return deferred.promise;
4963 }
4964 return $q.reject("Invalid `md-component-id` value.");
4965 }
4966
4967 };
4968
4969 function isValidID(handle){
4970 return handle && (handle !== "");
4971 }
4972
4973 }
4974
4975})();
4976(function(){
4977"use strict";
4978
4979(function() {
4980 'use strict';
4981
4982 /**
4983 * @ngdoc service
4984 * @name $mdButtonInkRipple
4985 * @module material.core
4986 *
4987 * @description
4988 * Provides ripple effects for md-button. See $mdInkRipple service for all possible configuration options.
4989 *
4990 * @param {object=} scope Scope within the current context
4991 * @param {object=} element The element the ripple effect should be applied to
4992 * @param {object=} options (Optional) Configuration options to override the default ripple configuration
4993 */
4994
4995 MdButtonInkRipple.$inject = ["$mdInkRipple"];
4996 angular.module('material.core')
4997 .factory('$mdButtonInkRipple', MdButtonInkRipple);
4998
4999 function MdButtonInkRipple($mdInkRipple) {
5000 return {
5001 attach: function attachRipple(scope, element, options) {
5002 options = angular.extend(optionsForElement(element), options);
5003
5004 return $mdInkRipple.attach(scope, element, options);
5005 }
5006 };
5007
5008 function optionsForElement(element) {
5009 if (element.hasClass('md-icon-button')) {
5010 return {
5011 isMenuItem: element.hasClass('md-menu-item'),
5012 fitRipple: true,
5013 center: true
5014 };
5015 } else {
5016 return {
5017 isMenuItem: element.hasClass('md-menu-item'),
5018 dimBackground: true
5019 };
5020 }
5021 }
5022 }
5023})();
5024
5025})();
5026(function(){
5027"use strict";
5028
5029(function() {
5030 'use strict';
5031
5032 /**
5033 * @ngdoc service
5034 * @name $mdCheckboxInkRipple
5035 * @module material.core
5036 *
5037 * @description
5038 * Provides ripple effects for md-checkbox. See $mdInkRipple service for all possible configuration options.
5039 *
5040 * @param {object=} scope Scope within the current context
5041 * @param {object=} element The element the ripple effect should be applied to
5042 * @param {object=} options (Optional) Configuration options to override the defaultripple configuration
5043 */
5044
5045 MdCheckboxInkRipple.$inject = ["$mdInkRipple"];
5046 angular.module('material.core')
5047 .factory('$mdCheckboxInkRipple', MdCheckboxInkRipple);
5048
5049 function MdCheckboxInkRipple($mdInkRipple) {
5050 return {
5051 attach: attach
5052 };
5053
5054 function attach(scope, element, options) {
5055 return $mdInkRipple.attach(scope, element, angular.extend({
5056 center: true,
5057 dimBackground: false,
5058 fitRipple: true
5059 }, options));
5060 }
5061 }
5062})();
5063
5064})();
5065(function(){
5066"use strict";
5067
5068(function() {
5069 'use strict';
5070
5071 /**
5072 * @ngdoc service
5073 * @name $mdListInkRipple
5074 * @module material.core
5075 *
5076 * @description
5077 * Provides ripple effects for md-list. See $mdInkRipple service for all possible configuration options.
5078 *
5079 * @param {object=} scope Scope within the current context
5080 * @param {object=} element The element the ripple effect should be applied to
5081 * @param {object=} options (Optional) Configuration options to override the defaultripple configuration
5082 */
5083
5084 MdListInkRipple.$inject = ["$mdInkRipple"];
5085 angular.module('material.core')
5086 .factory('$mdListInkRipple', MdListInkRipple);
5087
5088 function MdListInkRipple($mdInkRipple) {
5089 return {
5090 attach: attach
5091 };
5092
5093 function attach(scope, element, options) {
5094 return $mdInkRipple.attach(scope, element, angular.extend({
5095 center: false,
5096 dimBackground: true,
5097 outline: false,
5098 rippleSize: 'full'
5099 }, options));
5100 }
5101 }
5102})();
5103
5104})();
5105(function(){
5106"use strict";
5107
5108/**
5109 * @ngdoc module
5110 * @name material.core.ripple
5111 * @description
5112 * Ripple
5113 */
5114InkRippleCtrl.$inject = ["$scope", "$element", "rippleOptions", "$window", "$timeout", "$mdUtil", "$mdColorUtil"];
5115InkRippleDirective.$inject = ["$mdButtonInkRipple", "$mdCheckboxInkRipple"];
5116angular.module('material.core')
5117 .provider('$mdInkRipple', InkRippleProvider)
5118 .directive('mdInkRipple', InkRippleDirective)
5119 .directive('mdNoInk', attrNoDirective)
5120 .directive('mdNoBar', attrNoDirective)
5121 .directive('mdNoStretch', attrNoDirective);
5122
5123var DURATION = 450;
5124
5125/**
5126 * @ngdoc directive
5127 * @name mdInkRipple
5128 * @module material.core.ripple
5129 *
5130 * @description
5131 * The `md-ink-ripple` directive allows you to specify the ripple color or id a ripple is allowed.
5132 *
5133 * @param {string|boolean} md-ink-ripple A color string `#FF0000` or boolean (`false` or `0`) for preventing ripple
5134 *
5135 * @usage
5136 * ### String values
5137 * <hljs lang="html">
5138 * <ANY md-ink-ripple="#FF0000">
5139 * Ripples in red
5140 * </ANY>
5141 *
5142 * <ANY md-ink-ripple="false">
5143 * Not rippling
5144 * </ANY>
5145 * </hljs>
5146 *
5147 * ### Interpolated values
5148 * <hljs lang="html">
5149 * <ANY md-ink-ripple="{{ randomColor() }}">
5150 * Ripples with the return value of 'randomColor' function
5151 * </ANY>
5152 *
5153 * <ANY md-ink-ripple="{{ canRipple() }}">
5154 * Ripples if 'canRipple' function return value is not 'false' or '0'
5155 * </ANY>
5156 * </hljs>
5157 */
5158function InkRippleDirective ($mdButtonInkRipple, $mdCheckboxInkRipple) {
5159 return {
5160 controller: angular.noop,
5161 link: function (scope, element, attr) {
5162 attr.hasOwnProperty('mdInkRippleCheckbox')
5163 ? $mdCheckboxInkRipple.attach(scope, element)
5164 : $mdButtonInkRipple.attach(scope, element);
5165 }
5166 };
5167}
5168
5169/**
5170 * @ngdoc service
5171 * @name $mdInkRipple
5172 * @module material.core.ripple
5173 *
5174 * @description
5175 * `$mdInkRipple` is a service for adding ripples to any element
5176 *
5177 * @usage
5178 * <hljs lang="js">
5179 * app.factory('$myElementInkRipple', function($mdInkRipple) {
5180 * return {
5181 * attach: function (scope, element, options) {
5182 * return $mdInkRipple.attach(scope, element, angular.extend({
5183 * center: false,
5184 * dimBackground: true
5185 * }, options));
5186 * }
5187 * };
5188 * });
5189 *
5190 * app.controller('myController', function ($scope, $element, $myElementInkRipple) {
5191 * $scope.onClick = function (ev) {
5192 * $myElementInkRipple.attach($scope, angular.element(ev.target), { center: true });
5193 * }
5194 * });
5195 * </hljs>
5196 *
5197 * ### Disabling ripples globally
5198 * If you want to disable ink ripples globally, for all components, you can call the
5199 * `disableInkRipple` method in your app's config.
5200 *
5201 * <hljs lang="js">
5202 * app.config(function ($mdInkRippleProvider) {
5203 * $mdInkRippleProvider.disableInkRipple();
5204 * });
5205 */
5206
5207function InkRippleProvider () {
5208 var isDisabledGlobally = false;
5209
5210 return {
5211 disableInkRipple: disableInkRipple,
5212 $get: ["$injector", function($injector) {
5213 return { attach: attach };
5214
5215 /**
5216 * @ngdoc method
5217 * @name $mdInkRipple#attach
5218 *
5219 * @description
5220 * Attaching given scope, element and options to inkRipple controller
5221 *
5222 * @param {object=} scope Scope within the current context
5223 * @param {object=} element The element the ripple effect should be applied to
5224 * @param {object=} options (Optional) Configuration options to override the defaultRipple configuration
5225 * * `center` - Whether the ripple should start from the center of the container element
5226 * * `dimBackground` - Whether the background should be dimmed with the ripple color
5227 * * `colorElement` - The element the ripple should take its color from, defined by css property `color`
5228 * * `fitRipple` - Whether the ripple should fill the element
5229 */
5230 function attach (scope, element, options) {
5231 if (isDisabledGlobally || element.controller('mdNoInk')) return angular.noop;
5232 return $injector.instantiate(InkRippleCtrl, {
5233 $scope: scope,
5234 $element: element,
5235 rippleOptions: options
5236 });
5237 }
5238 }]
5239 };
5240
5241 /**
5242 * @ngdoc method
5243 * @name $mdInkRipple#disableInkRipple
5244 *
5245 * @description
5246 * A config-time method that, when called, disables ripples globally.
5247 */
5248 function disableInkRipple () {
5249 isDisabledGlobally = true;
5250 }
5251}
5252
5253/**
5254 * Controller used by the ripple service in order to apply ripples
5255 * @ngInject
5256 */
5257function InkRippleCtrl ($scope, $element, rippleOptions, $window, $timeout, $mdUtil, $mdColorUtil) {
5258 this.$window = $window;
5259 this.$timeout = $timeout;
5260 this.$mdUtil = $mdUtil;
5261 this.$mdColorUtil = $mdColorUtil;
5262 this.$scope = $scope;
5263 this.$element = $element;
5264 this.options = rippleOptions;
5265 this.mousedown = false;
5266 this.ripples = [];
5267 this.timeout = null; // Stores a reference to the most-recent ripple timeout
5268 this.lastRipple = null;
5269
5270 $mdUtil.valueOnUse(this, 'container', this.createContainer);
5271
5272 this.$element.addClass('md-ink-ripple');
5273
5274 // attach method for unit tests
5275 ($element.controller('mdInkRipple') || {}).createRipple = angular.bind(this, this.createRipple);
5276 ($element.controller('mdInkRipple') || {}).setColor = angular.bind(this, this.color);
5277
5278 this.bindEvents();
5279}
5280
5281
5282/**
5283 * Either remove or unlock any remaining ripples when the user mouses off of the element (either by
5284 * mouseup or mouseleave event)
5285 */
5286function autoCleanup (self, cleanupFn) {
5287
5288 if ( self.mousedown || self.lastRipple ) {
5289 self.mousedown = false;
5290 self.$mdUtil.nextTick( angular.bind(self, cleanupFn), false);
5291 }
5292
5293}
5294
5295
5296/**
5297 * Returns the color that the ripple should be (either based on CSS or hard-coded)
5298 * @returns {string}
5299 */
5300InkRippleCtrl.prototype.color = function (value) {
5301 var self = this;
5302
5303 // If assigning a color value, apply it to background and the ripple color
5304 if (angular.isDefined(value)) {
5305 self._color = self._parseColor(value);
5306 }
5307
5308 // If color lookup, use assigned, defined, or inherited
5309 return self._color || self._parseColor( self.inkRipple() ) || self._parseColor( getElementColor() );
5310
5311 /**
5312 * Finds the color element and returns its text color for use as default ripple color
5313 * @returns {string}
5314 */
5315 function getElementColor () {
5316 var items = self.options && self.options.colorElement ? self.options.colorElement : [];
5317 var elem = items.length ? items[ 0 ] : self.$element[ 0 ];
5318
5319 return elem ? self.$window.getComputedStyle(elem).color : 'rgb(0,0,0)';
5320 }
5321};
5322
5323/**
5324 * Updating the ripple colors based on the current inkRipple value
5325 * or the element's computed style color
5326 */
5327InkRippleCtrl.prototype.calculateColor = function () {
5328 return this.color();
5329};
5330
5331
5332/**
5333 * Takes a string color and converts it to RGBA format
5334 * @param color {string}
5335 * @param [multiplier] {int}
5336 * @returns {string}
5337 */
5338
5339InkRippleCtrl.prototype._parseColor = function parseColor (color, multiplier) {
5340 multiplier = multiplier || 1;
5341 var colorUtil = this.$mdColorUtil;
5342
5343 if (!color) return;
5344 if (color.indexOf('rgba') === 0) return color.replace(/\d?\.?\d*\s*\)\s*$/, (0.1 * multiplier).toString() + ')');
5345 if (color.indexOf('rgb') === 0) return colorUtil.rgbToRgba(color);
5346 if (color.indexOf('#') === 0) return colorUtil.hexToRgba(color);
5347
5348};
5349
5350/**
5351 * Binds events to the root element for
5352 */
5353InkRippleCtrl.prototype.bindEvents = function () {
5354 this.$element.on('mousedown', angular.bind(this, this.handleMousedown));
5355 this.$element.on('mouseup touchend', angular.bind(this, this.handleMouseup));
5356 this.$element.on('mouseleave', angular.bind(this, this.handleMouseup));
5357 this.$element.on('touchmove', angular.bind(this, this.handleTouchmove));
5358};
5359
5360/**
5361 * Create a new ripple on every mousedown event from the root element
5362 * @param event {MouseEvent}
5363 */
5364InkRippleCtrl.prototype.handleMousedown = function (event) {
5365 if ( this.mousedown ) return;
5366
5367 // When jQuery is loaded, we have to get the original event
5368 if (event.hasOwnProperty('originalEvent')) event = event.originalEvent;
5369 this.mousedown = true;
5370 if (this.options.center) {
5371 this.createRipple(this.container.prop('clientWidth') / 2, this.container.prop('clientWidth') / 2);
5372 } else {
5373
5374 // We need to calculate the relative coordinates if the target is a sublayer of the ripple element
5375 if (event.srcElement !== this.$element[0]) {
5376 var layerRect = this.$element[0].getBoundingClientRect();
5377 var layerX = event.clientX - layerRect.left;
5378 var layerY = event.clientY - layerRect.top;
5379
5380 this.createRipple(layerX, layerY);
5381 } else {
5382 this.createRipple(event.offsetX, event.offsetY);
5383 }
5384 }
5385};
5386
5387/**
5388 * Either remove or unlock any remaining ripples when the user mouses off of the element (either by
5389 * mouseup, touchend or mouseleave event)
5390 */
5391InkRippleCtrl.prototype.handleMouseup = function () {
5392 autoCleanup(this, this.clearRipples);
5393};
5394
5395/**
5396 * Either remove or unlock any remaining ripples when the user mouses off of the element (by
5397 * touchmove)
5398 */
5399InkRippleCtrl.prototype.handleTouchmove = function () {
5400 autoCleanup(this, this.deleteRipples);
5401};
5402
5403/**
5404 * Cycles through all ripples and attempts to remove them.
5405 */
5406InkRippleCtrl.prototype.deleteRipples = function () {
5407 for (var i = 0; i < this.ripples.length; i++) {
5408 this.ripples[ i ].remove();
5409 }
5410};
5411
5412/**
5413 * Cycles through all ripples and attempts to remove them with fade.
5414 * Depending on logic within `fadeInComplete`, some removals will be postponed.
5415 */
5416InkRippleCtrl.prototype.clearRipples = function () {
5417 for (var i = 0; i < this.ripples.length; i++) {
5418 this.fadeInComplete(this.ripples[ i ]);
5419 }
5420};
5421
5422/**
5423 * Creates the ripple container element
5424 * @returns {*}
5425 */
5426InkRippleCtrl.prototype.createContainer = function () {
5427 var container = angular.element('<div class="md-ripple-container"></div>');
5428 this.$element.append(container);
5429 return container;
5430};
5431
5432InkRippleCtrl.prototype.clearTimeout = function () {
5433 if (this.timeout) {
5434 this.$timeout.cancel(this.timeout);
5435 this.timeout = null;
5436 }
5437};
5438
5439InkRippleCtrl.prototype.isRippleAllowed = function () {
5440 var element = this.$element[0];
5441 do {
5442 if (!element.tagName || element.tagName === 'BODY') break;
5443
5444 if (element && angular.isFunction(element.hasAttribute)) {
5445 if (element.hasAttribute('disabled')) return false;
5446 if (this.inkRipple() === 'false' || this.inkRipple() === '0') return false;
5447 }
5448
5449 } while (element = element.parentNode);
5450 return true;
5451};
5452
5453/**
5454 * The attribute `md-ink-ripple` may be a static or interpolated
5455 * color value OR a boolean indicator (used to disable ripples)
5456 */
5457InkRippleCtrl.prototype.inkRipple = function () {
5458 return this.$element.attr('md-ink-ripple');
5459};
5460
5461/**
5462 * Creates a new ripple and adds it to the container. Also tracks ripple in `this.ripples`.
5463 * @param left
5464 * @param top
5465 */
5466InkRippleCtrl.prototype.createRipple = function (left, top) {
5467 if (!this.isRippleAllowed()) return;
5468
5469 var ctrl = this;
5470 var colorUtil = ctrl.$mdColorUtil;
5471 var ripple = angular.element('<div class="md-ripple"></div>');
5472 var width = this.$element.prop('clientWidth');
5473 var height = this.$element.prop('clientHeight');
5474 var x = Math.max(Math.abs(width - left), left) * 2;
5475 var y = Math.max(Math.abs(height - top), top) * 2;
5476 var size = getSize(this.options.fitRipple, x, y);
5477 var color = this.calculateColor();
5478
5479 ripple.css({
5480 left: left + 'px',
5481 top: top + 'px',
5482 background: 'black',
5483 width: size + 'px',
5484 height: size + 'px',
5485 backgroundColor: colorUtil.rgbaToRgb(color),
5486 borderColor: colorUtil.rgbaToRgb(color)
5487 });
5488 this.lastRipple = ripple;
5489
5490 // we only want one timeout to be running at a time
5491 this.clearTimeout();
5492 this.timeout = this.$timeout(function () {
5493 ctrl.clearTimeout();
5494 if (!ctrl.mousedown) ctrl.fadeInComplete(ripple);
5495 }, DURATION * 0.35, false);
5496
5497 if (this.options.dimBackground) this.container.css({ backgroundColor: color });
5498 this.container.append(ripple);
5499 this.ripples.push(ripple);
5500 ripple.addClass('md-ripple-placed');
5501
5502 this.$mdUtil.nextTick(function () {
5503
5504 ripple.addClass('md-ripple-scaled md-ripple-active');
5505 ctrl.$timeout(function () {
5506 ctrl.clearRipples();
5507 }, DURATION, false);
5508
5509 }, false);
5510
5511 function getSize (fit, x, y) {
5512 return fit
5513 ? Math.max(x, y)
5514 : Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
5515 }
5516};
5517
5518
5519
5520/**
5521 * After fadeIn finishes, either kicks off the fade-out animation or queues the element for removal on mouseup
5522 * @param ripple
5523 */
5524InkRippleCtrl.prototype.fadeInComplete = function (ripple) {
5525 if (this.lastRipple === ripple) {
5526 if (!this.timeout && !this.mousedown) {
5527 this.removeRipple(ripple);
5528 }
5529 } else {
5530 this.removeRipple(ripple);
5531 }
5532};
5533
5534/**
5535 * Kicks off the animation for removing a ripple
5536 * @param ripple {Element}
5537 */
5538InkRippleCtrl.prototype.removeRipple = function (ripple) {
5539 var ctrl = this;
5540 var index = this.ripples.indexOf(ripple);
5541 if (index < 0) return;
5542 this.ripples.splice(this.ripples.indexOf(ripple), 1);
5543 ripple.removeClass('md-ripple-active');
5544 ripple.addClass('md-ripple-remove');
5545 if (this.ripples.length === 0) this.container.css({ backgroundColor: '' });
5546 // use a 2-second timeout in order to allow for the animation to finish
5547 // we don't actually care how long the animation takes
5548 this.$timeout(function () {
5549 ctrl.fadeOutComplete(ripple);
5550 }, DURATION, false);
5551};
5552
5553/**
5554 * Removes the provided ripple from the DOM
5555 * @param ripple
5556 */
5557InkRippleCtrl.prototype.fadeOutComplete = function (ripple) {
5558 ripple.remove();
5559 this.lastRipple = null;
5560};
5561
5562/**
5563 * Used to create an empty directive. This is used to track flag-directives whose children may have
5564 * functionality based on them.
5565 *
5566 * Example: `md-no-ink` will potentially be used by all child directives.
5567 */
5568function attrNoDirective () {
5569 return { controller: angular.noop };
5570}
5571
5572})();
5573(function(){
5574"use strict";
5575
5576(function() {
5577 'use strict';
5578
5579 /**
5580 * @ngdoc service
5581 * @name $mdTabInkRipple
5582 * @module material.core
5583 *
5584 * @description
5585 * Provides ripple effects for md-tabs. See $mdInkRipple service for all possible configuration options.
5586 *
5587 * @param {object=} scope Scope within the current context
5588 * @param {object=} element The element the ripple effect should be applied to
5589 * @param {object=} options (Optional) Configuration options to override the defaultripple configuration
5590 */
5591
5592 MdTabInkRipple.$inject = ["$mdInkRipple"];
5593 angular.module('material.core')
5594 .factory('$mdTabInkRipple', MdTabInkRipple);
5595
5596 function MdTabInkRipple($mdInkRipple) {
5597 return {
5598 attach: attach
5599 };
5600
5601 function attach(scope, element, options) {
5602 return $mdInkRipple.attach(scope, element, angular.extend({
5603 center: false,
5604 dimBackground: true,
5605 outline: false,
5606 rippleSize: 'full'
5607 }, options));
5608 }
5609 }
5610})();
5611
5612})();
5613(function(){
5614"use strict";
5615
5616angular.module('material.core.theming.palette', [])
5617.constant('$mdColorPalette', {
5618 'red': {
5619 '50': '#ffebee',
5620 '100': '#ffcdd2',
5621 '200': '#ef9a9a',
5622 '300': '#e57373',
5623 '400': '#ef5350',
5624 '500': '#f44336',
5625 '600': '#e53935',
5626 '700': '#d32f2f',
5627 '800': '#c62828',
5628 '900': '#b71c1c',
5629 'A100': '#ff8a80',
5630 'A200': '#ff5252',
5631 'A400': '#ff1744',
5632 'A700': '#d50000',
5633 'contrastDefaultColor': 'light',
5634 'contrastDarkColors': '50 100 200 300 A100',
5635 'contrastStrongLightColors': '400 500 600 700 A200 A400 A700'
5636 },
5637 'pink': {
5638 '50': '#fce4ec',
5639 '100': '#f8bbd0',
5640 '200': '#f48fb1',
5641 '300': '#f06292',
5642 '400': '#ec407a',
5643 '500': '#e91e63',
5644 '600': '#d81b60',
5645 '700': '#c2185b',
5646 '800': '#ad1457',
5647 '900': '#880e4f',
5648 'A100': '#ff80ab',
5649 'A200': '#ff4081',
5650 'A400': '#f50057',
5651 'A700': '#c51162',
5652 'contrastDefaultColor': 'light',
5653 'contrastDarkColors': '50 100 200 A100',
5654 'contrastStrongLightColors': '500 600 A200 A400 A700'
5655 },
5656 'purple': {
5657 '50': '#f3e5f5',
5658 '100': '#e1bee7',
5659 '200': '#ce93d8',
5660 '300': '#ba68c8',
5661 '400': '#ab47bc',
5662 '500': '#9c27b0',
5663 '600': '#8e24aa',
5664 '700': '#7b1fa2',
5665 '800': '#6a1b9a',
5666 '900': '#4a148c',
5667 'A100': '#ea80fc',
5668 'A200': '#e040fb',
5669 'A400': '#d500f9',
5670 'A700': '#aa00ff',
5671 'contrastDefaultColor': 'light',
5672 'contrastDarkColors': '50 100 200 A100',
5673 'contrastStrongLightColors': '300 400 A200 A400 A700'
5674 },
5675 'deep-purple': {
5676 '50': '#ede7f6',
5677 '100': '#d1c4e9',
5678 '200': '#b39ddb',
5679 '300': '#9575cd',
5680 '400': '#7e57c2',
5681 '500': '#673ab7',
5682 '600': '#5e35b1',
5683 '700': '#512da8',
5684 '800': '#4527a0',
5685 '900': '#311b92',
5686 'A100': '#b388ff',
5687 'A200': '#7c4dff',
5688 'A400': '#651fff',
5689 'A700': '#6200ea',
5690 'contrastDefaultColor': 'light',
5691 'contrastDarkColors': '50 100 200 A100',
5692 'contrastStrongLightColors': '300 400 A200'
5693 },
5694 'indigo': {
5695 '50': '#e8eaf6',
5696 '100': '#c5cae9',
5697 '200': '#9fa8da',
5698 '300': '#7986cb',
5699 '400': '#5c6bc0',
5700 '500': '#3f51b5',
5701 '600': '#3949ab',
5702 '700': '#303f9f',
5703 '800': '#283593',
5704 '900': '#1a237e',
5705 'A100': '#8c9eff',
5706 'A200': '#536dfe',
5707 'A400': '#3d5afe',
5708 'A700': '#304ffe',
5709 'contrastDefaultColor': 'light',
5710 'contrastDarkColors': '50 100 200 A100',
5711 'contrastStrongLightColors': '300 400 A200 A400'
5712 },
5713 'blue': {
5714 '50': '#e3f2fd',
5715 '100': '#bbdefb',
5716 '200': '#90caf9',
5717 '300': '#64b5f6',
5718 '400': '#42a5f5',
5719 '500': '#2196f3',
5720 '600': '#1e88e5',
5721 '700': '#1976d2',
5722 '800': '#1565c0',
5723 '900': '#0d47a1',
5724 'A100': '#82b1ff',
5725 'A200': '#448aff',
5726 'A400': '#2979ff',
5727 'A700': '#2962ff',
5728 'contrastDefaultColor': 'light',
5729 'contrastDarkColors': '50 100 200 300 400 A100',
5730 'contrastStrongLightColors': '500 600 700 A200 A400 A700'
5731 },
5732 'light-blue': {
5733 '50': '#e1f5fe',
5734 '100': '#b3e5fc',
5735 '200': '#81d4fa',
5736 '300': '#4fc3f7',
5737 '400': '#29b6f6',
5738 '500': '#03a9f4',
5739 '600': '#039be5',
5740 '700': '#0288d1',
5741 '800': '#0277bd',
5742 '900': '#01579b',
5743 'A100': '#80d8ff',
5744 'A200': '#40c4ff',
5745 'A400': '#00b0ff',
5746 'A700': '#0091ea',
5747 'contrastDefaultColor': 'dark',
5748 'contrastLightColors': '600 700 800 900 A700',
5749 'contrastStrongLightColors': '600 700 800 A700'
5750 },
5751 'cyan': {
5752 '50': '#e0f7fa',
5753 '100': '#b2ebf2',
5754 '200': '#80deea',
5755 '300': '#4dd0e1',
5756 '400': '#26c6da',
5757 '500': '#00bcd4',
5758 '600': '#00acc1',
5759 '700': '#0097a7',
5760 '800': '#00838f',
5761 '900': '#006064',
5762 'A100': '#84ffff',
5763 'A200': '#18ffff',
5764 'A400': '#00e5ff',
5765 'A700': '#00b8d4',
5766 'contrastDefaultColor': 'dark',
5767 'contrastLightColors': '700 800 900',
5768 'contrastStrongLightColors': '700 800 900'
5769 },
5770 'teal': {
5771 '50': '#e0f2f1',
5772 '100': '#b2dfdb',
5773 '200': '#80cbc4',
5774 '300': '#4db6ac',
5775 '400': '#26a69a',
5776 '500': '#009688',
5777 '600': '#00897b',
5778 '700': '#00796b',
5779 '800': '#00695c',
5780 '900': '#004d40',
5781 'A100': '#a7ffeb',
5782 'A200': '#64ffda',
5783 'A400': '#1de9b6',
5784 'A700': '#00bfa5',
5785 'contrastDefaultColor': 'dark',
5786 'contrastLightColors': '500 600 700 800 900',
5787 'contrastStrongLightColors': '500 600 700'
5788 },
5789 'green': {
5790 '50': '#e8f5e9',
5791 '100': '#c8e6c9',
5792 '200': '#a5d6a7',
5793 '300': '#81c784',
5794 '400': '#66bb6a',
5795 '500': '#4caf50',
5796 '600': '#43a047',
5797 '700': '#388e3c',
5798 '800': '#2e7d32',
5799 '900': '#1b5e20',
5800 'A100': '#b9f6ca',
5801 'A200': '#69f0ae',
5802 'A400': '#00e676',
5803 'A700': '#00c853',
5804 'contrastDefaultColor': 'dark',
5805 'contrastLightColors': '500 600 700 800 900',
5806 'contrastStrongLightColors': '500 600 700'
5807 },
5808 'light-green': {
5809 '50': '#f1f8e9',
5810 '100': '#dcedc8',
5811 '200': '#c5e1a5',
5812 '300': '#aed581',
5813 '400': '#9ccc65',
5814 '500': '#8bc34a',
5815 '600': '#7cb342',
5816 '700': '#689f38',
5817 '800': '#558b2f',
5818 '900': '#33691e',
5819 'A100': '#ccff90',
5820 'A200': '#b2ff59',
5821 'A400': '#76ff03',
5822 'A700': '#64dd17',
5823 'contrastDefaultColor': 'dark',
5824 'contrastLightColors': '700 800 900',
5825 'contrastStrongLightColors': '700 800 900'
5826 },
5827 'lime': {
5828 '50': '#f9fbe7',
5829 '100': '#f0f4c3',
5830 '200': '#e6ee9c',
5831 '300': '#dce775',
5832 '400': '#d4e157',
5833 '500': '#cddc39',
5834 '600': '#c0ca33',
5835 '700': '#afb42b',
5836 '800': '#9e9d24',
5837 '900': '#827717',
5838 'A100': '#f4ff81',
5839 'A200': '#eeff41',
5840 'A400': '#c6ff00',
5841 'A700': '#aeea00',
5842 'contrastDefaultColor': 'dark',
5843 'contrastLightColors': '900',
5844 'contrastStrongLightColors': '900'
5845 },
5846 'yellow': {
5847 '50': '#fffde7',
5848 '100': '#fff9c4',
5849 '200': '#fff59d',
5850 '300': '#fff176',
5851 '400': '#ffee58',
5852 '500': '#ffeb3b',
5853 '600': '#fdd835',
5854 '700': '#fbc02d',
5855 '800': '#f9a825',
5856 '900': '#f57f17',
5857 'A100': '#ffff8d',
5858 'A200': '#ffff00',
5859 'A400': '#ffea00',
5860 'A700': '#ffd600',
5861 'contrastDefaultColor': 'dark'
5862 },
5863 'amber': {
5864 '50': '#fff8e1',
5865 '100': '#ffecb3',
5866 '200': '#ffe082',
5867 '300': '#ffd54f',
5868 '400': '#ffca28',
5869 '500': '#ffc107',
5870 '600': '#ffb300',
5871 '700': '#ffa000',
5872 '800': '#ff8f00',
5873 '900': '#ff6f00',
5874 'A100': '#ffe57f',
5875 'A200': '#ffd740',
5876 'A400': '#ffc400',
5877 'A700': '#ffab00',
5878 'contrastDefaultColor': 'dark'
5879 },
5880 'orange': {
5881 '50': '#fff3e0',
5882 '100': '#ffe0b2',
5883 '200': '#ffcc80',
5884 '300': '#ffb74d',
5885 '400': '#ffa726',
5886 '500': '#ff9800',
5887 '600': '#fb8c00',
5888 '700': '#f57c00',
5889 '800': '#ef6c00',
5890 '900': '#e65100',
5891 'A100': '#ffd180',
5892 'A200': '#ffab40',
5893 'A400': '#ff9100',
5894 'A700': '#ff6d00',
5895 'contrastDefaultColor': 'dark',
5896 'contrastLightColors': '800 900',
5897 'contrastStrongLightColors': '800 900'
5898 },
5899 'deep-orange': {
5900 '50': '#fbe9e7',
5901 '100': '#ffccbc',
5902 '200': '#ffab91',
5903 '300': '#ff8a65',
5904 '400': '#ff7043',
5905 '500': '#ff5722',
5906 '600': '#f4511e',
5907 '700': '#e64a19',
5908 '800': '#d84315',
5909 '900': '#bf360c',
5910 'A100': '#ff9e80',
5911 'A200': '#ff6e40',
5912 'A400': '#ff3d00',
5913 'A700': '#dd2c00',
5914 'contrastDefaultColor': 'light',
5915 'contrastDarkColors': '50 100 200 300 400 A100 A200',
5916 'contrastStrongLightColors': '500 600 700 800 900 A400 A700'
5917 },
5918 'brown': {
5919 '50': '#efebe9',
5920 '100': '#d7ccc8',
5921 '200': '#bcaaa4',
5922 '300': '#a1887f',
5923 '400': '#8d6e63',
5924 '500': '#795548',
5925 '600': '#6d4c41',
5926 '700': '#5d4037',
5927 '800': '#4e342e',
5928 '900': '#3e2723',
5929 'A100': '#d7ccc8',
5930 'A200': '#bcaaa4',
5931 'A400': '#8d6e63',
5932 'A700': '#5d4037',
5933 'contrastDefaultColor': 'light',
5934 'contrastDarkColors': '50 100 200 A100 A200',
5935 'contrastStrongLightColors': '300 400'
5936 },
5937 'grey': {
5938 '50': '#fafafa',
5939 '100': '#f5f5f5',
5940 '200': '#eeeeee',
5941 '300': '#e0e0e0',
5942 '400': '#bdbdbd',
5943 '500': '#9e9e9e',
5944 '600': '#757575',
5945 '700': '#616161',
5946 '800': '#424242',
5947 '900': '#212121',
5948 'A100': '#ffffff',
5949 'A200': '#000000',
5950 'A400': '#303030',
5951 'A700': '#616161',
5952 'contrastDefaultColor': 'dark',
5953 'contrastLightColors': '600 700 800 900 A200 A400 A700'
5954 },
5955 'blue-grey': {
5956 '50': '#eceff1',
5957 '100': '#cfd8dc',
5958 '200': '#b0bec5',
5959 '300': '#90a4ae',
5960 '400': '#78909c',
5961 '500': '#607d8b',
5962 '600': '#546e7a',
5963 '700': '#455a64',
5964 '800': '#37474f',
5965 '900': '#263238',
5966 'A100': '#cfd8dc',
5967 'A200': '#b0bec5',
5968 'A400': '#78909c',
5969 'A700': '#455a64',
5970 'contrastDefaultColor': 'light',
5971 'contrastDarkColors': '50 100 200 300 A100 A200',
5972 'contrastStrongLightColors': '400 500 700'
5973 }
5974});
5975
5976})();
5977(function(){
5978"use strict";
5979
5980(function(angular) {
5981 'use strict';
5982/**
5983 * @ngdoc module
5984 * @name material.core.theming
5985 * @description
5986 * Theming
5987 */
5988detectDisabledThemes.$inject = ["$mdThemingProvider"];
5989ThemingDirective.$inject = ["$mdTheming", "$interpolate", "$parse", "$mdUtil", "$q", "$log"];
5990ThemableDirective.$inject = ["$mdTheming"];
5991ThemingProvider.$inject = ["$mdColorPalette", "$$mdMetaProvider"];
5992generateAllThemes.$inject = ["$injector", "$mdTheming"];
5993angular.module('material.core.theming', ['material.core.theming.palette', 'material.core.meta'])
5994 .directive('mdTheme', ThemingDirective)
5995 .directive('mdThemable', ThemableDirective)
5996 .directive('mdThemesDisabled', disableThemesDirective )
5997 .provider('$mdTheming', ThemingProvider)
5998 .config( detectDisabledThemes )
5999 .run(generateAllThemes);
6000
6001/**
6002 * Detect if the HTML or the BODY tags has a [md-themes-disabled] attribute
6003 * If yes, then immediately disable all theme stylesheet generation and DOM injection
6004 */
6005/**
6006 * @ngInject
6007 */
6008function detectDisabledThemes($mdThemingProvider) {
6009 var isDisabled = !!document.querySelector('[md-themes-disabled]');
6010 $mdThemingProvider.disableTheming(isDisabled);
6011}
6012
6013/**
6014 * @ngdoc service
6015 * @name $mdThemingProvider
6016 * @module material.core.theming
6017 *
6018 * @description Provider to configure the `$mdTheming` service.
6019 *
6020 * ### Default Theme
6021 * The `$mdThemingProvider` uses by default the following theme configuration:
6022 *
6023 * - Primary Palette: `Blue`
6024 * - Accent Palette: `Pink`
6025 * - Warn Palette: `Deep-Orange`
6026 * - Background Palette: `Grey`
6027 *
6028 * If you don't want to use the `md-theme` directive on the elements itself, you may want to overwrite
6029 * the default theme.<br/>
6030 * This can be done by using the following markup.
6031 *
6032 * <hljs lang="js">
6033 * myAppModule.config(function($mdThemingProvider) {
6034 * $mdThemingProvider
6035 * .theme('default')
6036 * .primaryPalette('blue')
6037 * .accentPalette('teal')
6038 * .warnPalette('red')
6039 * .backgroundPalette('grey');
6040 * });
6041 * </hljs>
6042 *
6043
6044 * ### Dynamic Themes
6045 *
6046 * By default, if you change a theme at runtime, the `$mdTheming` service will not detect those changes.<br/>
6047 * If you have an application, which changes its theme on runtime, you have to enable theme watching.
6048 *
6049 * <hljs lang="js">
6050 * myAppModule.config(function($mdThemingProvider) {
6051 * // Enable theme watching.
6052 * $mdThemingProvider.alwaysWatchTheme(true);
6053 * });
6054 * </hljs>
6055 *
6056 * ### Custom Theme Styles
6057 *
6058 * Sometimes you may want to use your own theme styles for some custom components.<br/>
6059 * You are able to register your own styles by using the following markup.
6060 *
6061 * <hljs lang="js">
6062 * myAppModule.config(function($mdThemingProvider) {
6063 * // Register our custom stylesheet into the theming provider.
6064 * $mdThemingProvider.registerStyles(STYLESHEET);
6065 * });
6066 * </hljs>
6067 *
6068 * The `registerStyles` method only accepts strings as value, so you're actually not able to load an external
6069 * stylesheet file into the `$mdThemingProvider`.
6070 *
6071 * If it's necessary to load an external stylesheet, we suggest using a bundler, which supports including raw content,
6072 * like [raw-loader](https://github.com/webpack/raw-loader) for `webpack`.
6073 *
6074 * <hljs lang="js">
6075 * myAppModule.config(function($mdThemingProvider) {
6076 * // Register your custom stylesheet into the theming provider.
6077 * $mdThemingProvider.registerStyles(require('../styles/my-component.theme.css'));
6078 * });
6079 * </hljs>
6080 *
6081 * ### Browser color
6082 *
6083 * Enables browser header coloring
6084 * for more info please visit:
6085 * https://developers.google.com/web/fundamentals/design-and-ui/browser-customization/theme-color
6086 *
6087 * Options parameter: <br/>
6088 * `theme` - A defined theme via `$mdThemeProvider` to use the palettes from. Default is `default` theme. <br/>
6089 * `palette` - Can be any one of the basic material design palettes, extended defined palettes and 'primary',
6090 * 'accent', 'background' and 'warn'. Default is `primary`. <br/>
6091 * `hue` - The hue from the selected palette. Default is `800`<br/>
6092 *
6093 * <hljs lang="js">
6094 * myAppModule.config(function($mdThemingProvider) {
6095 * // Enable browser color
6096 * $mdThemingProvider.enableBrowserColor({
6097 * theme: 'myTheme', // Default is 'default'
6098 * palette: 'accent', // Default is 'primary', any basic material palette and extended palettes are available
6099 * hue: '200' // Default is '800'
6100 * });
6101 * });
6102 * </hljs>
6103 */
6104
6105/**
6106 * @ngdoc method
6107 * @name $mdThemingProvider#registerStyles
6108 * @param {string} styles The styles to be appended to Angular Material's built in theme css.
6109 */
6110/**
6111 * @ngdoc method
6112 * @name $mdThemingProvider#setNonce
6113 * @param {string} nonceValue The nonce to be added as an attribute to the theme style tags.
6114 * Setting a value allows the use of CSP policy without using the unsafe-inline directive.
6115 */
6116
6117/**
6118 * @ngdoc method
6119 * @name $mdThemingProvider#setDefaultTheme
6120 * @param {string} themeName Default theme name to be applied to elements. Default value is `default`.
6121 */
6122
6123/**
6124 * @ngdoc method
6125 * @name $mdThemingProvider#alwaysWatchTheme
6126 * @param {boolean} watch Whether or not to always watch themes for changes and re-apply
6127 * classes when they change. Default is `false`. Enabling can reduce performance.
6128 */
6129
6130/**
6131 * @ngdoc method
6132 * @name $mdThemingProvider#enableBrowserColor
6133 * @param {Object=} options Options object for the browser color<br/>
6134 * `theme` - A defined theme via `$mdThemeProvider` to use the palettes from. Default is `default` theme. <br/>
6135 * `palette` - Can be any one of the basic material design palettes, extended defined palettes and 'primary',
6136 * 'accent', 'background' and 'warn'. Default is `primary`. <br/>
6137 * `hue` - The hue from the selected palette. Default is `800`<br/>
6138 * @returns {Function} remove function of the browser color
6139 */
6140
6141/* Some Example Valid Theming Expressions
6142 * =======================================
6143 *
6144 * Intention group expansion: (valid for primary, accent, warn, background)
6145 *
6146 * {{primary-100}} - grab shade 100 from the primary palette
6147 * {{primary-100-0.7}} - grab shade 100, apply opacity of 0.7
6148 * {{primary-100-contrast}} - grab shade 100's contrast color
6149 * {{primary-hue-1}} - grab the shade assigned to hue-1 from the primary palette
6150 * {{primary-hue-1-0.7}} - apply 0.7 opacity to primary-hue-1
6151 * {{primary-color}} - Generates .md-hue-1, .md-hue-2, .md-hue-3 with configured shades set for each hue
6152 * {{primary-color-0.7}} - Apply 0.7 opacity to each of the above rules
6153 * {{primary-contrast}} - Generates .md-hue-1, .md-hue-2, .md-hue-3 with configured contrast (ie. text) color shades set for each hue
6154 * {{primary-contrast-0.7}} - Apply 0.7 opacity to each of the above rules
6155 *
6156 * Foreground expansion: Applies rgba to black/white foreground text
6157 *
6158 * {{foreground-1}} - used for primary text
6159 * {{foreground-2}} - used for secondary text/divider
6160 * {{foreground-3}} - used for disabled text
6161 * {{foreground-4}} - used for dividers
6162 *
6163 */
6164
6165// In memory generated CSS rules; registered by theme.name
6166var GENERATED = { };
6167
6168// In memory storage of defined themes and color palettes (both loaded by CSS, and user specified)
6169var PALETTES;
6170
6171// Text Colors on light and dark backgrounds
6172// @see https://www.google.com/design/spec/style/color.html#color-text-background-colors
6173var DARK_FOREGROUND = {
6174 name: 'dark',
6175 '1': 'rgba(0,0,0,0.87)',
6176 '2': 'rgba(0,0,0,0.54)',
6177 '3': 'rgba(0,0,0,0.38)',
6178 '4': 'rgba(0,0,0,0.12)'
6179};
6180var LIGHT_FOREGROUND = {
6181 name: 'light',
6182 '1': 'rgba(255,255,255,1.0)',
6183 '2': 'rgba(255,255,255,0.7)',
6184 '3': 'rgba(255,255,255,0.5)',
6185 '4': 'rgba(255,255,255,0.12)'
6186};
6187
6188var DARK_SHADOW = '1px 1px 0px rgba(0,0,0,0.4), -1px -1px 0px rgba(0,0,0,0.4)';
6189var LIGHT_SHADOW = '';
6190
6191var DARK_CONTRAST_COLOR = colorToRgbaArray('rgba(0,0,0,0.87)');
6192var LIGHT_CONTRAST_COLOR = colorToRgbaArray('rgba(255,255,255,0.87)');
6193var STRONG_LIGHT_CONTRAST_COLOR = colorToRgbaArray('rgb(255,255,255)');
6194
6195var THEME_COLOR_TYPES = ['primary', 'accent', 'warn', 'background'];
6196var DEFAULT_COLOR_TYPE = 'primary';
6197
6198// A color in a theme will use these hues by default, if not specified by user.
6199var LIGHT_DEFAULT_HUES = {
6200 'accent': {
6201 'default': 'A200',
6202 'hue-1': 'A100',
6203 'hue-2': 'A400',
6204 'hue-3': 'A700'
6205 },
6206 'background': {
6207 'default': '50',
6208 'hue-1': 'A100',
6209 'hue-2': '100',
6210 'hue-3': '300'
6211 }
6212};
6213
6214var DARK_DEFAULT_HUES = {
6215 'background': {
6216 'default': 'A400',
6217 'hue-1': '800',
6218 'hue-2': '900',
6219 'hue-3': 'A200'
6220 }
6221};
6222THEME_COLOR_TYPES.forEach(function(colorType) {
6223 // Color types with unspecified default hues will use these default hue values
6224 var defaultDefaultHues = {
6225 'default': '500',
6226 'hue-1': '300',
6227 'hue-2': '800',
6228 'hue-3': 'A100'
6229 };
6230 if (!LIGHT_DEFAULT_HUES[colorType]) LIGHT_DEFAULT_HUES[colorType] = defaultDefaultHues;
6231 if (!DARK_DEFAULT_HUES[colorType]) DARK_DEFAULT_HUES[colorType] = defaultDefaultHues;
6232});
6233
6234var VALID_HUE_VALUES = [
6235 '50', '100', '200', '300', '400', '500', '600',
6236 '700', '800', '900', 'A100', 'A200', 'A400', 'A700'
6237];
6238
6239var themeConfig = {
6240 disableTheming : false, // Generate our themes at run time; also disable stylesheet DOM injection
6241 generateOnDemand : false, // Whether or not themes are to be generated on-demand (vs. eagerly).
6242 registeredStyles : [], // Custom styles registered to be used in the theming of custom components.
6243 nonce : null // Nonce to be added as an attribute to the generated themes style tags.
6244};
6245
6246/**
6247 *
6248 */
6249function ThemingProvider($mdColorPalette, $$mdMetaProvider) {
6250 ThemingService.$inject = ["$rootScope", "$mdUtil", "$q", "$log"];
6251 PALETTES = { };
6252 var THEMES = { };
6253
6254 var themingProvider;
6255
6256 var alwaysWatchTheme = false;
6257 var defaultTheme = 'default';
6258
6259 // Load JS Defined Palettes
6260 angular.extend(PALETTES, $mdColorPalette);
6261
6262 // Default theme defined in core.js
6263
6264 /**
6265 * Adds `theme-color` and `msapplication-navbutton-color` meta tags with the color parameter
6266 * @param {string} color Hex value of the wanted browser color
6267 * @returns {Function} Remove function of the meta tags
6268 */
6269 var setBrowserColor = function (color) {
6270 // Chrome, Firefox OS and Opera
6271 var removeChrome = $$mdMetaProvider.setMeta('theme-color', color);
6272 // Windows Phone
6273 var removeWindows = $$mdMetaProvider.setMeta('msapplication-navbutton-color', color);
6274
6275 return function () {
6276 removeChrome();
6277 removeWindows();
6278 };
6279 };
6280
6281 /**
6282 * Enables browser header coloring
6283 * for more info please visit:
6284 * https://developers.google.com/web/fundamentals/design-and-ui/browser-customization/theme-color
6285 *
6286 * The default color is `800` from `primary` palette of the `default` theme
6287 *
6288 * options are:
6289 * `theme` - A defined theme via `$mdThemeProvider` to use the palettes from. Default is `default` theme
6290 * `palette` - Can be any one of the basic material design palettes, extended defined palettes and 'primary',
6291 * 'accent', 'background' and 'warn'. Default is `primary`
6292 * `hue` - The hue from the selected palette. Default is `800`
6293 *
6294 * @param {Object=} options Options object for the browser color
6295 * @returns {Function} remove function of the browser color
6296 */
6297 var enableBrowserColor = function (options) {
6298 options = angular.isObject(options) ? options : {};
6299
6300 var theme = options.theme || 'default';
6301 var hue = options.hue || '800';
6302
6303 var palette = PALETTES[options.palette] ||
6304 PALETTES[THEMES[theme].colors[options.palette || 'primary'].name];
6305
6306 var color = angular.isObject(palette[hue]) ? palette[hue].hex : palette[hue];
6307
6308 return setBrowserColor(color);
6309 };
6310
6311 return themingProvider = {
6312 definePalette: definePalette,
6313 extendPalette: extendPalette,
6314 theme: registerTheme,
6315
6316 /**
6317 * return a read-only clone of the current theme configuration
6318 */
6319 configuration : function() {
6320 return angular.extend( { }, themeConfig, {
6321 defaultTheme : defaultTheme,
6322 alwaysWatchTheme : alwaysWatchTheme,
6323 registeredStyles : [].concat(themeConfig.registeredStyles)
6324 });
6325 },
6326
6327 /**
6328 * Easy way to disable theming without having to use
6329 * `.constant("$MD_THEME_CSS","");` This disables
6330 * all dynamic theme style sheet generations and injections...
6331 */
6332 disableTheming: function(isDisabled) {
6333 themeConfig.disableTheming = angular.isUndefined(isDisabled) || !!isDisabled;
6334 },
6335
6336 registerStyles: function(styles) {
6337 themeConfig.registeredStyles.push(styles);
6338 },
6339
6340 setNonce: function(nonceValue) {
6341 themeConfig.nonce = nonceValue;
6342 },
6343
6344 generateThemesOnDemand: function(onDemand) {
6345 themeConfig.generateOnDemand = onDemand;
6346 },
6347
6348 setDefaultTheme: function(theme) {
6349 defaultTheme = theme;
6350 },
6351
6352 alwaysWatchTheme: function(alwaysWatch) {
6353 alwaysWatchTheme = alwaysWatch;
6354 },
6355
6356 enableBrowserColor: enableBrowserColor,
6357
6358 $get: ThemingService,
6359 _LIGHT_DEFAULT_HUES: LIGHT_DEFAULT_HUES,
6360 _DARK_DEFAULT_HUES: DARK_DEFAULT_HUES,
6361 _PALETTES: PALETTES,
6362 _THEMES: THEMES,
6363 _parseRules: parseRules,
6364 _rgba: rgba
6365 };
6366
6367 // Example: $mdThemingProvider.definePalette('neonRed', { 50: '#f5fafa', ... });
6368 function definePalette(name, map) {
6369 map = map || {};
6370 PALETTES[name] = checkPaletteValid(name, map);
6371 return themingProvider;
6372 }
6373
6374 // Returns an new object which is a copy of a given palette `name` with variables from
6375 // `map` overwritten
6376 // Example: var neonRedMap = $mdThemingProvider.extendPalette('red', { 50: '#f5fafafa' });
6377 function extendPalette(name, map) {
6378 return checkPaletteValid(name, angular.extend({}, PALETTES[name] || {}, map) );
6379 }
6380
6381 // Make sure that palette has all required hues
6382 function checkPaletteValid(name, map) {
6383 var missingColors = VALID_HUE_VALUES.filter(function(field) {
6384 return !map[field];
6385 });
6386 if (missingColors.length) {
6387 throw new Error("Missing colors %1 in palette %2!"
6388 .replace('%1', missingColors.join(', '))
6389 .replace('%2', name));
6390 }
6391
6392 return map;
6393 }
6394
6395 // Register a theme (which is a collection of color palettes to use with various states
6396 // ie. warn, accent, primary )
6397 // Optionally inherit from an existing theme
6398 // $mdThemingProvider.theme('custom-theme').primaryPalette('red');
6399 function registerTheme(name, inheritFrom) {
6400 if (THEMES[name]) return THEMES[name];
6401
6402 inheritFrom = inheritFrom || 'default';
6403
6404 var parentTheme = typeof inheritFrom === 'string' ? THEMES[inheritFrom] : inheritFrom;
6405 var theme = new Theme(name);
6406
6407 if (parentTheme) {
6408 angular.forEach(parentTheme.colors, function(color, colorType) {
6409 theme.colors[colorType] = {
6410 name: color.name,
6411 // Make sure a COPY of the hues is given to the child color,
6412 // not the same reference.
6413 hues: angular.extend({}, color.hues)
6414 };
6415 });
6416 }
6417 THEMES[name] = theme;
6418
6419 return theme;
6420 }
6421
6422 function Theme(name) {
6423 var self = this;
6424 self.name = name;
6425 self.colors = {};
6426
6427 self.dark = setDark;
6428 setDark(false);
6429
6430 function setDark(isDark) {
6431 isDark = arguments.length === 0 ? true : !!isDark;
6432
6433 // If no change, abort
6434 if (isDark === self.isDark) return;
6435
6436 self.isDark = isDark;
6437
6438 self.foregroundPalette = self.isDark ? LIGHT_FOREGROUND : DARK_FOREGROUND;
6439 self.foregroundShadow = self.isDark ? DARK_SHADOW : LIGHT_SHADOW;
6440
6441 // Light and dark themes have different default hues.
6442 // Go through each existing color type for this theme, and for every
6443 // hue value that is still the default hue value from the previous light/dark setting,
6444 // set it to the default hue value from the new light/dark setting.
6445 var newDefaultHues = self.isDark ? DARK_DEFAULT_HUES : LIGHT_DEFAULT_HUES;
6446 var oldDefaultHues = self.isDark ? LIGHT_DEFAULT_HUES : DARK_DEFAULT_HUES;
6447 angular.forEach(newDefaultHues, function(newDefaults, colorType) {
6448 var color = self.colors[colorType];
6449 var oldDefaults = oldDefaultHues[colorType];
6450 if (color) {
6451 for (var hueName in color.hues) {
6452 if (color.hues[hueName] === oldDefaults[hueName]) {
6453 color.hues[hueName] = newDefaults[hueName];
6454 }
6455 }
6456 }
6457 });
6458
6459 return self;
6460 }
6461
6462 THEME_COLOR_TYPES.forEach(function(colorType) {
6463 var defaultHues = (self.isDark ? DARK_DEFAULT_HUES : LIGHT_DEFAULT_HUES)[colorType];
6464 self[colorType + 'Palette'] = function setPaletteType(paletteName, hues) {
6465 var color = self.colors[colorType] = {
6466 name: paletteName,
6467 hues: angular.extend({}, defaultHues, hues)
6468 };
6469
6470 Object.keys(color.hues).forEach(function(name) {
6471 if (!defaultHues[name]) {
6472 throw new Error("Invalid hue name '%1' in theme %2's %3 color %4. Available hue names: %4"
6473 .replace('%1', name)
6474 .replace('%2', self.name)
6475 .replace('%3', paletteName)
6476 .replace('%4', Object.keys(defaultHues).join(', '))
6477 );
6478 }
6479 });
6480 Object.keys(color.hues).map(function(key) {
6481 return color.hues[key];
6482 }).forEach(function(hueValue) {
6483 if (VALID_HUE_VALUES.indexOf(hueValue) == -1) {
6484 throw new Error("Invalid hue value '%1' in theme %2's %3 color %4. Available hue values: %5"
6485 .replace('%1', hueValue)
6486 .replace('%2', self.name)
6487 .replace('%3', colorType)
6488 .replace('%4', paletteName)
6489 .replace('%5', VALID_HUE_VALUES.join(', '))
6490 );
6491 }
6492 });
6493 return self;
6494 };
6495
6496 self[colorType + 'Color'] = function() {
6497 var args = Array.prototype.slice.call(arguments);
6498 console.warn('$mdThemingProviderTheme.' + colorType + 'Color() has been deprecated. ' +
6499 'Use $mdThemingProviderTheme.' + colorType + 'Palette() instead.');
6500 return self[colorType + 'Palette'].apply(self, args);
6501 };
6502 });
6503 }
6504
6505 /**
6506 * @ngdoc service
6507 * @name $mdTheming
6508 * @module material.core.theming
6509 *
6510 * @description
6511 *
6512 * Service that makes an element apply theming related <b>classes</b> to itself.
6513 *
6514 * <hljs lang="js">
6515 * app.directive('myFancyDirective', function($mdTheming) {
6516 * return {
6517 * restrict: 'e',
6518 * link: function(scope, el, attrs) {
6519 * $mdTheming(el);
6520 * }
6521 * };
6522 * });
6523 * </hljs>
6524 * @param {element=} element to apply theming to
6525 */
6526
6527 /**
6528 * @ngdoc property
6529 * @name $mdTheming#THEMES
6530 * @description
6531 * Property to get all the themes defined
6532 * @returns {Object} All the themes defined with their properties
6533 */
6534
6535 /**
6536 * @ngdoc property
6537 * @name $mdTheming#PALETTES
6538 * @description
6539 * Property to get all the palettes defined
6540 * @returns {Object} All the palettes defined with their colors
6541 */
6542
6543 /**
6544 * @ngdoc method
6545 * @name $mdTheming#registered
6546 * @description
6547 * Determine is specified theme name is a valid, registered theme
6548 * @param {string} themeName the theme to check if registered
6549 * @returns {boolean} whether the theme is registered or not
6550 */
6551
6552 /**
6553 * @ngdoc method
6554 * @name $mdTheming#defaultTheme
6555 * @description
6556 * Returns the default theme
6557 * @returns {string} The default theme
6558 */
6559
6560 /**
6561 * @ngdoc method
6562 * @name $mdTheming#generateTheme
6563 * @description
6564 * Lazy generate themes - by default, every theme is generated when defined.
6565 * You can disable this in the configuration section using the
6566 * `$mdThemingProvider.generateThemesOnDemand(true);`
6567 *
6568 * The theme name that is passed in must match the name of the theme that was defined as part of the configuration block.
6569 *
6570 * @param name {string} theme name to generate
6571 */
6572
6573 /**
6574 * @ngdoc method
6575 * @name $mdTheming#setBrowserColor
6576 * @description
6577 * Sets browser header coloring
6578 * for more info please visit:
6579 * https://developers.google.com/web/fundamentals/design-and-ui/browser-customization/theme-color
6580 *
6581 * The default color is `800` from `primary` palette of the `default` theme
6582 *
6583 * options are:<br/>
6584 * `theme` - A defined theme via `$mdThemeProvider` to use the palettes from. Default is `default` theme.<br/>
6585 * `palette` - Can be any one of the basic material design palettes, extended defined palettes and 'primary',
6586 * 'accent', 'background' and 'warn'. Default is `primary`<br/>
6587 * `hue` - The hue from the selected palette. Default is `800`
6588 *
6589 * @param {Object} options Options object for the browser color
6590 * @returns {Function} remove function of the browser color
6591 */
6592
6593 /* @ngInject */
6594 function ThemingService($rootScope, $mdUtil, $q, $log) {
6595 // Allow us to be invoked via a linking function signature.
6596 var applyTheme = function (scope, el) {
6597 if (el === undefined) { el = scope; scope = undefined; }
6598 if (scope === undefined) { scope = $rootScope; }
6599 applyTheme.inherit(el, el);
6600 };
6601
6602 Object.defineProperty(applyTheme, 'THEMES', {
6603 get: function () {
6604 return angular.extend({}, THEMES);
6605 }
6606 });
6607 Object.defineProperty(applyTheme, 'PALETTES', {
6608 get: function () {
6609 return angular.extend({}, PALETTES);
6610 }
6611 });
6612 Object.defineProperty(applyTheme, 'ALWAYS_WATCH', {
6613 get: function () {
6614 return alwaysWatchTheme;
6615 }
6616 });
6617 applyTheme.inherit = inheritTheme;
6618 applyTheme.registered = registered;
6619 applyTheme.defaultTheme = function() { return defaultTheme; };
6620 applyTheme.generateTheme = function(name) { generateTheme(THEMES[name], name, themeConfig.nonce); };
6621 applyTheme.defineTheme = function(name, options) {
6622 options = options || {};
6623
6624 var theme = registerTheme(name);
6625
6626 if (options.primary) {
6627 theme.primaryPalette(options.primary);
6628 }
6629 if (options.accent) {
6630 theme.accentPalette(options.accent);
6631 }
6632 if (options.warn) {
6633 theme.warnPalette(options.warn);
6634 }
6635 if (options.background) {
6636 theme.backgroundPalette(options.background);
6637 }
6638 if (options.dark){
6639 theme.dark();
6640 }
6641
6642 this.generateTheme(name);
6643
6644 return $q.resolve(name);
6645 };
6646 applyTheme.setBrowserColor = enableBrowserColor;
6647
6648 return applyTheme;
6649
6650 /**
6651 * Determine is specified theme name is a valid, registered theme
6652 */
6653 function registered(themeName) {
6654 if (themeName === undefined || themeName === '') return true;
6655 return applyTheme.THEMES[themeName] !== undefined;
6656 }
6657
6658 /**
6659 * Get theme name for the element, then update with Theme CSS class
6660 */
6661 function inheritTheme (el, parent) {
6662 var ctrl = parent.controller('mdTheme') || el.data('$mdThemeController');
6663
6664 updateThemeClass(lookupThemeName());
6665
6666 if (ctrl) {
6667 var watchTheme = alwaysWatchTheme ||
6668 ctrl.$shouldWatch ||
6669 $mdUtil.parseAttributeBoolean(el.attr('md-theme-watch'));
6670
6671 var unwatch = ctrl.registerChanges(function (name) {
6672 updateThemeClass(name);
6673
6674 if (!watchTheme) {
6675 unwatch();
6676 }
6677 else {
6678 el.on('$destroy', unwatch);
6679 }
6680 });
6681 }
6682
6683 /**
6684 * Find the theme name from the parent controller or element data
6685 */
6686 function lookupThemeName() {
6687 // As a few components (dialog) add their controllers later, we should also watch for a controller init.
6688 return ctrl && ctrl.$mdTheme || (defaultTheme == 'default' ? '' : defaultTheme);
6689 }
6690
6691 /**
6692 * Remove old theme class and apply a new one
6693 * NOTE: if not a valid theme name, then the current name is not changed
6694 */
6695 function updateThemeClass(theme) {
6696 if (!theme) return;
6697 if (!registered(theme)) {
6698 $log.warn('Attempted to use unregistered theme \'' + theme + '\'. ' +
6699 'Register it with $mdThemingProvider.theme().');
6700 }
6701
6702 var oldTheme = el.data('$mdThemeName');
6703 if (oldTheme) el.removeClass('md-' + oldTheme +'-theme');
6704 el.addClass('md-' + theme + '-theme');
6705 el.data('$mdThemeName', theme);
6706 if (ctrl) {
6707 el.data('$mdThemeController', ctrl);
6708 }
6709 }
6710 }
6711
6712 }
6713}
6714
6715function ThemingDirective($mdTheming, $interpolate, $parse, $mdUtil, $q, $log) {
6716 return {
6717 priority: 101, // has to be more than 100 to be before interpolation (issue on IE)
6718 link: {
6719 pre: function(scope, el, attrs) {
6720 var registeredCallbacks = [];
6721
6722 var startSymbol = $interpolate.startSymbol();
6723 var endSymbol = $interpolate.endSymbol();
6724
6725 var theme = attrs.mdTheme.trim();
6726
6727 var hasInterpolation =
6728 theme.substr(0, startSymbol.length) === startSymbol &&
6729 theme.lastIndexOf(endSymbol) === theme.length - endSymbol.length;
6730
6731 var oneTimeOperator = '::';
6732 var oneTimeBind = attrs.mdTheme
6733 .split(startSymbol).join('')
6734 .split(endSymbol).join('')
6735 .trim()
6736 .substr(0, oneTimeOperator.length) === oneTimeOperator;
6737
6738 var ctrl = {
6739 registerChanges: function (cb, context) {
6740 if (context) {
6741 cb = angular.bind(context, cb);
6742 }
6743
6744 registeredCallbacks.push(cb);
6745
6746 return function () {
6747 var index = registeredCallbacks.indexOf(cb);
6748
6749 if (index > -1) {
6750 registeredCallbacks.splice(index, 1);
6751 }
6752 };
6753 },
6754 $setTheme: function (theme) {
6755 if (!$mdTheming.registered(theme)) {
6756 $log.warn('attempted to use unregistered theme \'' + theme + '\'');
6757 }
6758
6759 ctrl.$mdTheme = theme;
6760
6761 // Iterating backwards to support unregistering during iteration
6762 // http://stackoverflow.com/a/9882349/890293
6763 // we don't use `reverse()` of array because it mutates the array and we don't want it to get re-indexed
6764 for (var i = registeredCallbacks.length; i--;) {
6765 registeredCallbacks[i](theme);
6766 }
6767 },
6768 $shouldWatch: $mdUtil.parseAttributeBoolean(el.attr('md-theme-watch')) ||
6769 $mdTheming.ALWAYS_WATCH ||
6770 (hasInterpolation && !oneTimeBind)
6771 };
6772
6773 el.data('$mdThemeController', ctrl);
6774
6775 var getTheme = function () {
6776 var interpolation = $interpolate(attrs.mdTheme)(scope);
6777 return $parse(interpolation)(scope) || interpolation;
6778 };
6779
6780 var setParsedTheme = function (theme) {
6781 if (typeof theme === 'string') {
6782 return ctrl.$setTheme(theme);
6783 }
6784
6785 $q.when( angular.isFunction(theme) ? theme() : theme )
6786 .then(function(name){
6787 ctrl.$setTheme(name);
6788 });
6789 };
6790
6791 setParsedTheme(getTheme());
6792
6793 var unwatch = scope.$watch(getTheme, function(theme) {
6794 if (theme) {
6795 setParsedTheme(theme);
6796
6797 if (!ctrl.$shouldWatch) {
6798 unwatch();
6799 }
6800 }
6801 });
6802 }
6803 }
6804 };
6805}
6806
6807/**
6808 * Special directive that will disable ALL runtime Theme style generation and DOM injection
6809 *
6810 * <link rel="stylesheet" href="angular-material.min.css">
6811 * <link rel="stylesheet" href="angular-material.themes.css">
6812 *
6813 * <body md-themes-disabled>
6814 * ...
6815 * </body>
6816 *
6817 * Note: Using md-themes-css directive requires the developer to load external
6818 * theme stylesheets; e.g. custom themes from Material-Tools:
6819 *
6820 * `angular-material.themes.css`
6821 *
6822 * Another option is to use the ThemingProvider to configure and disable the attribute
6823 * conversions; this would obviate the use of the `md-themes-css` directive
6824 *
6825 */
6826function disableThemesDirective() {
6827 themeConfig.disableTheming = true;
6828
6829 // Return a 1x-only, first-match attribute directive
6830 return {
6831 restrict : 'A',
6832 priority : '900'
6833 };
6834}
6835
6836function ThemableDirective($mdTheming) {
6837 return $mdTheming;
6838}
6839
6840function parseRules(theme, colorType, rules) {
6841 checkValidPalette(theme, colorType);
6842
6843 rules = rules.replace(/THEME_NAME/g, theme.name);
6844 var generatedRules = [];
6845 var color = theme.colors[colorType];
6846
6847 var themeNameRegex = new RegExp('\\.md-' + theme.name + '-theme', 'g');
6848 // Matches '{{ primary-color }}', etc
6849 var hueRegex = new RegExp('(\'|")?{{\\s*(' + colorType + ')-(color|contrast)-?(\\d\\.?\\d*)?\\s*}}(\"|\')?','g');
6850 var simpleVariableRegex = /'?"?\{\{\s*([a-zA-Z]+)-(A?\d+|hue\-[0-3]|shadow|default)-?(\d\.?\d*)?(contrast)?\s*\}\}'?"?/g;
6851 var palette = PALETTES[color.name];
6852
6853 // find and replace simple variables where we use a specific hue, not an entire palette
6854 // eg. "{{primary-100}}"
6855 //\(' + THEME_COLOR_TYPES.join('\|') + '\)'
6856 rules = rules.replace(simpleVariableRegex, function(match, colorType, hue, opacity, contrast) {
6857 if (colorType === 'foreground') {
6858 if (hue == 'shadow') {
6859 return theme.foregroundShadow;
6860 } else {
6861 return theme.foregroundPalette[hue] || theme.foregroundPalette['1'];
6862 }
6863 }
6864
6865 // `default` is also accepted as a hue-value, because the background palettes are
6866 // using it as a name for the default hue.
6867 if (hue.indexOf('hue') === 0 || hue === 'default') {
6868 hue = theme.colors[colorType].hues[hue];
6869 }
6870
6871 return rgba( (PALETTES[ theme.colors[colorType].name ][hue] || '')[contrast ? 'contrast' : 'value'], opacity );
6872 });
6873
6874 // For each type, generate rules for each hue (ie. default, md-hue-1, md-hue-2, md-hue-3)
6875 angular.forEach(color.hues, function(hueValue, hueName) {
6876 var newRule = rules
6877 .replace(hueRegex, function(match, _, colorType, hueType, opacity) {
6878 return rgba(palette[hueValue][hueType === 'color' ? 'value' : 'contrast'], opacity);
6879 });
6880 if (hueName !== 'default') {
6881 newRule = newRule.replace(themeNameRegex, '.md-' + theme.name + '-theme.md-' + hueName);
6882 }
6883
6884 // Don't apply a selector rule to the default theme, making it easier to override
6885 // styles of the base-component
6886 if (theme.name == 'default') {
6887 var themeRuleRegex = /((?:\s|>|\.|\w|-|:|\(|\)|\[|\]|"|'|=)*)\.md-default-theme((?:\s|>|\.|\w|-|:|\(|\)|\[|\]|"|'|=)*)/g;
6888
6889 newRule = newRule.replace(themeRuleRegex, function(match, start, end) {
6890 return match + ', ' + start + end;
6891 });
6892 }
6893 generatedRules.push(newRule);
6894 });
6895
6896 return generatedRules;
6897}
6898
6899var rulesByType = {};
6900
6901// Generate our themes at run time given the state of THEMES and PALETTES
6902function generateAllThemes($injector, $mdTheming) {
6903 var head = document.head;
6904 var firstChild = head ? head.firstElementChild : null;
6905 var themeCss = !themeConfig.disableTheming && $injector.has('$MD_THEME_CSS') ? $injector.get('$MD_THEME_CSS') : '';
6906
6907 // Append our custom registered styles to the theme stylesheet.
6908 themeCss += themeConfig.registeredStyles.join('');
6909
6910 if ( !firstChild ) return;
6911 if (themeCss.length === 0) return; // no rules, so no point in running this expensive task
6912
6913 // Expose contrast colors for palettes to ensure that text is always readable
6914 angular.forEach(PALETTES, sanitizePalette);
6915
6916 // MD_THEME_CSS is a string generated by the build process that includes all the themable
6917 // components as templates
6918
6919 // Break the CSS into individual rules
6920 var rules = themeCss
6921 .split(/\}(?!(\}|'|"|;))/)
6922 .filter(function(rule) { return rule && rule.trim().length; })
6923 .map(function(rule) { return rule.trim() + '}'; });
6924
6925
6926 var ruleMatchRegex = new RegExp('md-(' + THEME_COLOR_TYPES.join('|') + ')', 'g');
6927
6928 THEME_COLOR_TYPES.forEach(function(type) {
6929 rulesByType[type] = '';
6930 });
6931
6932
6933 // Sort the rules based on type, allowing us to do color substitution on a per-type basis
6934 rules.forEach(function(rule) {
6935 var match = rule.match(ruleMatchRegex);
6936 // First: test that if the rule has '.md-accent', it goes into the accent set of rules
6937 for (var i = 0, type; type = THEME_COLOR_TYPES[i]; i++) {
6938 if (rule.indexOf('.md-' + type) > -1) {
6939 return rulesByType[type] += rule;
6940 }
6941 }
6942
6943 // If no eg 'md-accent' class is found, try to just find 'accent' in the rule and guess from
6944 // there
6945 for (i = 0; type = THEME_COLOR_TYPES[i]; i++) {
6946 if (rule.indexOf(type) > -1) {
6947 return rulesByType[type] += rule;
6948 }
6949 }
6950
6951 // Default to the primary array
6952 return rulesByType[DEFAULT_COLOR_TYPE] += rule;
6953 });
6954
6955 // If themes are being generated on-demand, quit here. The user will later manually
6956 // call generateTheme to do this on a theme-by-theme basis.
6957 if (themeConfig.generateOnDemand) return;
6958
6959 angular.forEach($mdTheming.THEMES, function(theme) {
6960 if (!GENERATED[theme.name] && !($mdTheming.defaultTheme() !== 'default' && theme.name === 'default')) {
6961 generateTheme(theme, theme.name, themeConfig.nonce);
6962 }
6963 });
6964
6965
6966 // *************************
6967 // Internal functions
6968 // *************************
6969
6970 // The user specifies a 'default' contrast color as either light or dark,
6971 // then explicitly lists which hues are the opposite contrast (eg. A100 has dark, A200 has light)
6972 function sanitizePalette(palette, name) {
6973 var defaultContrast = palette.contrastDefaultColor;
6974 var lightColors = palette.contrastLightColors || [];
6975 var strongLightColors = palette.contrastStrongLightColors || [];
6976 var darkColors = palette.contrastDarkColors || [];
6977
6978 // These colors are provided as space-separated lists
6979 if (typeof lightColors === 'string') lightColors = lightColors.split(' ');
6980 if (typeof strongLightColors === 'string') strongLightColors = strongLightColors.split(' ');
6981 if (typeof darkColors === 'string') darkColors = darkColors.split(' ');
6982
6983 // Cleanup after ourselves
6984 delete palette.contrastDefaultColor;
6985 delete palette.contrastLightColors;
6986 delete palette.contrastStrongLightColors;
6987 delete palette.contrastDarkColors;
6988
6989 // Change { 'A100': '#fffeee' } to { 'A100': { value: '#fffeee', contrast:DARK_CONTRAST_COLOR }
6990 angular.forEach(palette, function(hueValue, hueName) {
6991 if (angular.isObject(hueValue)) return; // Already converted
6992 // Map everything to rgb colors
6993 var rgbValue = colorToRgbaArray(hueValue);
6994 if (!rgbValue) {
6995 throw new Error("Color %1, in palette %2's hue %3, is invalid. Hex or rgb(a) color expected."
6996 .replace('%1', hueValue)
6997 .replace('%2', palette.name)
6998 .replace('%3', hueName));
6999 }
7000
7001 palette[hueName] = {
7002 hex: palette[hueName],
7003 value: rgbValue,
7004 contrast: getContrastColor()
7005 };
7006 function getContrastColor() {
7007 if (defaultContrast === 'light') {
7008 if (darkColors.indexOf(hueName) > -1) {
7009 return DARK_CONTRAST_COLOR;
7010 } else {
7011 return strongLightColors.indexOf(hueName) > -1 ? STRONG_LIGHT_CONTRAST_COLOR
7012 : LIGHT_CONTRAST_COLOR;
7013 }
7014 } else {
7015 if (lightColors.indexOf(hueName) > -1) {
7016 return strongLightColors.indexOf(hueName) > -1 ? STRONG_LIGHT_CONTRAST_COLOR
7017 : LIGHT_CONTRAST_COLOR;
7018 } else {
7019 return DARK_CONTRAST_COLOR;
7020 }
7021 }
7022 }
7023 });
7024 }
7025}
7026
7027function generateTheme(theme, name, nonce) {
7028 var head = document.head;
7029 var firstChild = head ? head.firstElementChild : null;
7030
7031 if (!GENERATED[name]) {
7032 // For each theme, use the color palettes specified for
7033 // `primary`, `warn` and `accent` to generate CSS rules.
7034 THEME_COLOR_TYPES.forEach(function(colorType) {
7035 var styleStrings = parseRules(theme, colorType, rulesByType[colorType]);
7036 while (styleStrings.length) {
7037 var styleContent = styleStrings.shift();
7038 if (styleContent) {
7039 var style = document.createElement('style');
7040 style.setAttribute('md-theme-style', '');
7041 if (nonce) {
7042 style.setAttribute('nonce', nonce);
7043 }
7044 style.appendChild(document.createTextNode(styleContent));
7045 head.insertBefore(style, firstChild);
7046 }
7047 }
7048 });
7049
7050 GENERATED[theme.name] = true;
7051 }
7052
7053}
7054
7055
7056function checkValidPalette(theme, colorType) {
7057 // If theme attempts to use a palette that doesnt exist, throw error
7058 if (!PALETTES[ (theme.colors[colorType] || {}).name ]) {
7059 throw new Error(
7060 "You supplied an invalid color palette for theme %1's %2 palette. Available palettes: %3"
7061 .replace('%1', theme.name)
7062 .replace('%2', colorType)
7063 .replace('%3', Object.keys(PALETTES).join(', '))
7064 );
7065 }
7066}
7067
7068function colorToRgbaArray(clr) {
7069 if (angular.isArray(clr) && clr.length == 3) return clr;
7070 if (/^rgb/.test(clr)) {
7071 return clr.replace(/(^\s*rgba?\(|\)\s*$)/g, '').split(',').map(function(value, i) {
7072 return i == 3 ? parseFloat(value, 10) : parseInt(value, 10);
7073 });
7074 }
7075 if (clr.charAt(0) == '#') clr = clr.substring(1);
7076 if (!/^([a-fA-F0-9]{3}){1,2}$/g.test(clr)) return;
7077
7078 var dig = clr.length / 3;
7079 var red = clr.substr(0, dig);
7080 var grn = clr.substr(dig, dig);
7081 var blu = clr.substr(dig * 2);
7082 if (dig === 1) {
7083 red += red;
7084 grn += grn;
7085 blu += blu;
7086 }
7087 return [parseInt(red, 16), parseInt(grn, 16), parseInt(blu, 16)];
7088}
7089
7090function rgba(rgbArray, opacity) {
7091 if ( !rgbArray ) return "rgb('0,0,0')";
7092
7093 if (rgbArray.length == 4) {
7094 rgbArray = angular.copy(rgbArray);
7095 opacity ? rgbArray.pop() : opacity = rgbArray.pop();
7096 }
7097 return opacity && (typeof opacity == 'number' || (typeof opacity == 'string' && opacity.length)) ?
7098 'rgba(' + rgbArray.join(',') + ',' + opacity + ')' :
7099 'rgb(' + rgbArray.join(',') + ')';
7100}
7101
7102
7103})(window.angular);
7104
7105})();
7106(function(){
7107"use strict";
7108
7109// Polyfill angular < 1.4 (provide $animateCss)
7110angular
7111 .module('material.core')
7112 .factory('$$mdAnimate', ["$q", "$timeout", "$mdConstant", "$animateCss", function($q, $timeout, $mdConstant, $animateCss){
7113
7114 // Since $$mdAnimate is injected into $mdUtil... use a wrapper function
7115 // to subsequently inject $mdUtil as an argument to the AnimateDomUtils
7116
7117 return function($mdUtil) {
7118 return AnimateDomUtils( $mdUtil, $q, $timeout, $mdConstant, $animateCss);
7119 };
7120 }]);
7121
7122/**
7123 * Factory function that requires special injections
7124 */
7125function AnimateDomUtils($mdUtil, $q, $timeout, $mdConstant, $animateCss) {
7126 var self;
7127 return self = {
7128 /**
7129 *
7130 */
7131 translate3d : function( target, from, to, options ) {
7132 return $animateCss(target, {
7133 from: from,
7134 to: to,
7135 addClass: options.transitionInClass,
7136 removeClass: options.transitionOutClass,
7137 duration: options.duration
7138 })
7139 .start()
7140 .then(function(){
7141 // Resolve with reverser function...
7142 return reverseTranslate;
7143 });
7144
7145 /**
7146 * Specific reversal of the request translate animation above...
7147 */
7148 function reverseTranslate (newFrom) {
7149 return $animateCss(target, {
7150 to: newFrom || from,
7151 addClass: options.transitionOutClass,
7152 removeClass: options.transitionInClass,
7153 duration: options.duration
7154 }).start();
7155
7156 }
7157 },
7158
7159 /**
7160 * Listen for transitionEnd event (with optional timeout)
7161 * Announce completion or failure via promise handlers
7162 */
7163 waitTransitionEnd: function (element, opts) {
7164 var TIMEOUT = 3000; // fallback is 3 secs
7165
7166 return $q(function(resolve, reject){
7167 opts = opts || { };
7168
7169 // If there is no transition is found, resolve immediately
7170 //
7171 // NOTE: using $mdUtil.nextTick() causes delays/issues
7172 if (noTransitionFound(opts.cachedTransitionStyles)) {
7173 TIMEOUT = 0;
7174 }
7175
7176 var timer = $timeout(finished, opts.timeout || TIMEOUT);
7177 element.on($mdConstant.CSS.TRANSITIONEND, finished);
7178
7179 /**
7180 * Upon timeout or transitionEnd, reject or resolve (respectively) this promise.
7181 * NOTE: Make sure this transitionEnd didn't bubble up from a child
7182 */
7183 function finished(ev) {
7184 if ( ev && ev.target !== element[0]) return;
7185
7186 if ( ev ) $timeout.cancel(timer);
7187 element.off($mdConstant.CSS.TRANSITIONEND, finished);
7188
7189 // Never reject since ngAnimate may cause timeouts due missed transitionEnd events
7190 resolve();
7191
7192 }
7193
7194 /**
7195 * Checks whether or not there is a transition.
7196 *
7197 * @param styles The cached styles to use for the calculation. If null, getComputedStyle()
7198 * will be used.
7199 *
7200 * @returns {boolean} True if there is no transition/duration; false otherwise.
7201 */
7202 function noTransitionFound(styles) {
7203 styles = styles || window.getComputedStyle(element[0]);
7204
7205 return styles.transitionDuration == '0s' || (!styles.transition && !styles.transitionProperty);
7206 }
7207
7208 });
7209 },
7210
7211 calculateTransformValues: function (element, originator) {
7212 var origin = originator.element;
7213 var bounds = originator.bounds;
7214
7215 if (origin || bounds) {
7216 var originBnds = origin ? self.clientRect(origin) || currentBounds() : self.copyRect(bounds);
7217 var dialogRect = self.copyRect(element[0].getBoundingClientRect());
7218 var dialogCenterPt = self.centerPointFor(dialogRect);
7219 var originCenterPt = self.centerPointFor(originBnds);
7220
7221 return {
7222 centerX: originCenterPt.x - dialogCenterPt.x,
7223 centerY: originCenterPt.y - dialogCenterPt.y,
7224 scaleX: Math.round(100 * Math.min(0.5, originBnds.width / dialogRect.width)) / 100,
7225 scaleY: Math.round(100 * Math.min(0.5, originBnds.height / dialogRect.height)) / 100
7226 };
7227 }
7228 return {centerX: 0, centerY: 0, scaleX: 0.5, scaleY: 0.5};
7229
7230 /**
7231 * This is a fallback if the origin information is no longer valid, then the
7232 * origin bounds simply becomes the current bounds for the dialogContainer's parent
7233 */
7234 function currentBounds() {
7235 var cntr = element ? element.parent() : null;
7236 var parent = cntr ? cntr.parent() : null;
7237
7238 return parent ? self.clientRect(parent) : null;
7239 }
7240 },
7241
7242 /**
7243 * Calculate the zoom transform from dialog to origin.
7244 *
7245 * We use this to set the dialog position immediately;
7246 * then the md-transition-in actually translates back to
7247 * `translate3d(0,0,0) scale(1.0)`...
7248 *
7249 * NOTE: all values are rounded to the nearest integer
7250 */
7251 calculateZoomToOrigin: function (element, originator) {
7252 var zoomTemplate = "translate3d( {centerX}px, {centerY}px, 0 ) scale( {scaleX}, {scaleY} )";
7253 var buildZoom = angular.bind(null, $mdUtil.supplant, zoomTemplate);
7254
7255 return buildZoom(self.calculateTransformValues(element, originator));
7256 },
7257
7258 /**
7259 * Calculate the slide transform from panel to origin.
7260 * NOTE: all values are rounded to the nearest integer
7261 */
7262 calculateSlideToOrigin: function (element, originator) {
7263 var slideTemplate = "translate3d( {centerX}px, {centerY}px, 0 )";
7264 var buildSlide = angular.bind(null, $mdUtil.supplant, slideTemplate);
7265
7266 return buildSlide(self.calculateTransformValues(element, originator));
7267 },
7268
7269 /**
7270 * Enhance raw values to represent valid css stylings...
7271 */
7272 toCss : function( raw ) {
7273 var css = { };
7274 var lookups = 'left top right bottom width height x y min-width min-height max-width max-height';
7275
7276 angular.forEach(raw, function(value,key) {
7277 if ( angular.isUndefined(value) ) return;
7278
7279 if ( lookups.indexOf(key) >= 0 ) {
7280 css[key] = value + 'px';
7281 } else {
7282 switch (key) {
7283 case 'transition':
7284 convertToVendor(key, $mdConstant.CSS.TRANSITION, value);
7285 break;
7286 case 'transform':
7287 convertToVendor(key, $mdConstant.CSS.TRANSFORM, value);
7288 break;
7289 case 'transformOrigin':
7290 convertToVendor(key, $mdConstant.CSS.TRANSFORM_ORIGIN, value);
7291 break;
7292 case 'font-size':
7293 css['font-size'] = value; // font sizes aren't always in px
7294 break;
7295 }
7296 }
7297 });
7298
7299 return css;
7300
7301 function convertToVendor(key, vendor, value) {
7302 angular.forEach(vendor.split(' '), function (key) {
7303 css[key] = value;
7304 });
7305 }
7306 },
7307
7308 /**
7309 * Convert the translate CSS value to key/value pair(s).
7310 */
7311 toTransformCss: function (transform, addTransition, transition) {
7312 var css = {};
7313 angular.forEach($mdConstant.CSS.TRANSFORM.split(' '), function (key) {
7314 css[key] = transform;
7315 });
7316
7317 if (addTransition) {
7318 transition = transition || "all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) !important";
7319 css.transition = transition;
7320 }
7321
7322 return css;
7323 },
7324
7325 /**
7326 * Clone the Rect and calculate the height/width if needed
7327 */
7328 copyRect: function (source, destination) {
7329 if (!source) return null;
7330
7331 destination = destination || {};
7332
7333 angular.forEach('left top right bottom width height'.split(' '), function (key) {
7334 destination[key] = Math.round(source[key]);
7335 });
7336
7337 destination.width = destination.width || (destination.right - destination.left);
7338 destination.height = destination.height || (destination.bottom - destination.top);
7339
7340 return destination;
7341 },
7342
7343 /**
7344 * Calculate ClientRect of element; return null if hidden or zero size
7345 */
7346 clientRect: function (element) {
7347 var bounds = angular.element(element)[0].getBoundingClientRect();
7348 var isPositiveSizeClientRect = function (rect) {
7349 return rect && (rect.width > 0) && (rect.height > 0);
7350 };
7351
7352 // If the event origin element has zero size, it has probably been hidden.
7353 return isPositiveSizeClientRect(bounds) ? self.copyRect(bounds) : null;
7354 },
7355
7356 /**
7357 * Calculate 'rounded' center point of Rect
7358 */
7359 centerPointFor: function (targetRect) {
7360 return targetRect ? {
7361 x: Math.round(targetRect.left + (targetRect.width / 2)),
7362 y: Math.round(targetRect.top + (targetRect.height / 2))
7363 } : { x : 0, y : 0 };
7364 }
7365
7366 };
7367}
7368
7369
7370})();
7371(function(){
7372"use strict";
7373
7374if (angular.version.minor >= 4) {
7375 angular.module('material.core.animate', []);
7376} else {
7377(function() {
7378 "use strict";
7379
7380 var forEach = angular.forEach;
7381
7382 var WEBKIT = angular.isDefined(document.documentElement.style.WebkitAppearance);
7383 var TRANSITION_PROP = WEBKIT ? 'WebkitTransition' : 'transition';
7384 var ANIMATION_PROP = WEBKIT ? 'WebkitAnimation' : 'animation';
7385 var PREFIX = WEBKIT ? '-webkit-' : '';
7386
7387 var TRANSITION_EVENTS = (WEBKIT ? 'webkitTransitionEnd ' : '') + 'transitionend';
7388 var ANIMATION_EVENTS = (WEBKIT ? 'webkitAnimationEnd ' : '') + 'animationend';
7389
7390 var $$ForceReflowFactory = ['$document', function($document) {
7391 return function() {
7392 return $document[0].body.clientWidth + 1;
7393 };
7394 }];
7395
7396 var $$rAFMutexFactory = ['$$rAF', function($$rAF) {
7397 return function() {
7398 var passed = false;
7399 $$rAF(function() {
7400 passed = true;
7401 });
7402 return function(fn) {
7403 passed ? fn() : $$rAF(fn);
7404 };
7405 };
7406 }];
7407
7408 var $$AnimateRunnerFactory = ['$q', '$$rAFMutex', function($q, $$rAFMutex) {
7409 var INITIAL_STATE = 0;
7410 var DONE_PENDING_STATE = 1;
7411 var DONE_COMPLETE_STATE = 2;
7412
7413 function AnimateRunner(host) {
7414 this.setHost(host);
7415
7416 this._doneCallbacks = [];
7417 this._runInAnimationFrame = $$rAFMutex();
7418 this._state = 0;
7419 }
7420
7421 AnimateRunner.prototype = {
7422 setHost: function(host) {
7423 this.host = host || {};
7424 },
7425
7426 done: function(fn) {
7427 if (this._state === DONE_COMPLETE_STATE) {
7428 fn();
7429 } else {
7430 this._doneCallbacks.push(fn);
7431 }
7432 },
7433
7434 progress: angular.noop,
7435
7436 getPromise: function() {
7437 if (!this.promise) {
7438 var self = this;
7439 this.promise = $q(function(resolve, reject) {
7440 self.done(function(status) {
7441 status === false ? reject() : resolve();
7442 });
7443 });
7444 }
7445 return this.promise;
7446 },
7447
7448 then: function(resolveHandler, rejectHandler) {
7449 return this.getPromise().then(resolveHandler, rejectHandler);
7450 },
7451
7452 'catch': function(handler) {
7453 return this.getPromise()['catch'](handler);
7454 },
7455
7456 'finally': function(handler) {
7457 return this.getPromise()['finally'](handler);
7458 },
7459
7460 pause: function() {
7461 if (this.host.pause) {
7462 this.host.pause();
7463 }
7464 },
7465
7466 resume: function() {
7467 if (this.host.resume) {
7468 this.host.resume();
7469 }
7470 },
7471
7472 end: function() {
7473 if (this.host.end) {
7474 this.host.end();
7475 }
7476 this._resolve(true);
7477 },
7478
7479 cancel: function() {
7480 if (this.host.cancel) {
7481 this.host.cancel();
7482 }
7483 this._resolve(false);
7484 },
7485
7486 complete: function(response) {
7487 var self = this;
7488 if (self._state === INITIAL_STATE) {
7489 self._state = DONE_PENDING_STATE;
7490 self._runInAnimationFrame(function() {
7491 self._resolve(response);
7492 });
7493 }
7494 },
7495
7496 _resolve: function(response) {
7497 if (this._state !== DONE_COMPLETE_STATE) {
7498 forEach(this._doneCallbacks, function(fn) {
7499 fn(response);
7500 });
7501 this._doneCallbacks.length = 0;
7502 this._state = DONE_COMPLETE_STATE;
7503 }
7504 }
7505 };
7506
7507 // Polyfill AnimateRunner.all which is used by input animations
7508 AnimateRunner.all = function(runners, callback) {
7509 var count = 0;
7510 var status = true;
7511 forEach(runners, function(runner) {
7512 runner.done(onProgress);
7513 });
7514
7515 function onProgress(response) {
7516 status = status && response;
7517 if (++count === runners.length) {
7518 callback(status);
7519 }
7520 }
7521 };
7522
7523 return AnimateRunner;
7524 }];
7525
7526 angular
7527 .module('material.core.animate', [])
7528 .factory('$$forceReflow', $$ForceReflowFactory)
7529 .factory('$$AnimateRunner', $$AnimateRunnerFactory)
7530 .factory('$$rAFMutex', $$rAFMutexFactory)
7531 .factory('$animateCss', ['$window', '$$rAF', '$$AnimateRunner', '$$forceReflow', '$$jqLite', '$timeout', '$animate',
7532 function($window, $$rAF, $$AnimateRunner, $$forceReflow, $$jqLite, $timeout, $animate) {
7533
7534 function init(element, options) {
7535
7536 var temporaryStyles = [];
7537 var node = getDomNode(element);
7538 var areAnimationsAllowed = node && $animate.enabled();
7539
7540 var hasCompleteStyles = false;
7541 var hasCompleteClasses = false;
7542
7543 if (areAnimationsAllowed) {
7544 if (options.transitionStyle) {
7545 temporaryStyles.push([PREFIX + 'transition', options.transitionStyle]);
7546 }
7547
7548 if (options.keyframeStyle) {
7549 temporaryStyles.push([PREFIX + 'animation', options.keyframeStyle]);
7550 }
7551
7552 if (options.delay) {
7553 temporaryStyles.push([PREFIX + 'transition-delay', options.delay + 's']);
7554 }
7555
7556 if (options.duration) {
7557 temporaryStyles.push([PREFIX + 'transition-duration', options.duration + 's']);
7558 }
7559
7560 hasCompleteStyles = options.keyframeStyle ||
7561 (options.to && (options.duration > 0 || options.transitionStyle));
7562 hasCompleteClasses = !!options.addClass || !!options.removeClass;
7563
7564 blockTransition(element, true);
7565 }
7566
7567 var hasCompleteAnimation = areAnimationsAllowed && (hasCompleteStyles || hasCompleteClasses);
7568
7569 applyAnimationFromStyles(element, options);
7570
7571 var animationClosed = false;
7572 var events, eventFn;
7573
7574 return {
7575 close: $window.close,
7576 start: function() {
7577 var runner = new $$AnimateRunner();
7578 waitUntilQuiet(function() {
7579 blockTransition(element, false);
7580 if (!hasCompleteAnimation) {
7581 return close();
7582 }
7583
7584 forEach(temporaryStyles, function(entry) {
7585 var key = entry[0];
7586 var value = entry[1];
7587 node.style[camelCase(key)] = value;
7588 });
7589
7590 applyClasses(element, options);
7591
7592 var timings = computeTimings(element);
7593 if (timings.duration === 0) {
7594 return close();
7595 }
7596
7597 var moreStyles = [];
7598
7599 if (options.easing) {
7600 if (timings.transitionDuration) {
7601 moreStyles.push([PREFIX + 'transition-timing-function', options.easing]);
7602 }
7603 if (timings.animationDuration) {
7604 moreStyles.push([PREFIX + 'animation-timing-function', options.easing]);
7605 }
7606 }
7607
7608 if (options.delay && timings.animationDelay) {
7609 moreStyles.push([PREFIX + 'animation-delay', options.delay + 's']);
7610 }
7611
7612 if (options.duration && timings.animationDuration) {
7613 moreStyles.push([PREFIX + 'animation-duration', options.duration + 's']);
7614 }
7615
7616 forEach(moreStyles, function(entry) {
7617 var key = entry[0];
7618 var value = entry[1];
7619 node.style[camelCase(key)] = value;
7620 temporaryStyles.push(entry);
7621 });
7622
7623 var maxDelay = timings.delay;
7624 var maxDelayTime = maxDelay * 1000;
7625 var maxDuration = timings.duration;
7626 var maxDurationTime = maxDuration * 1000;
7627 var startTime = Date.now();
7628
7629 events = [];
7630 if (timings.transitionDuration) {
7631 events.push(TRANSITION_EVENTS);
7632 }
7633 if (timings.animationDuration) {
7634 events.push(ANIMATION_EVENTS);
7635 }
7636 events = events.join(' ');
7637 eventFn = function(event) {
7638 event.stopPropagation();
7639 var ev = event.originalEvent || event;
7640 var timeStamp = ev.timeStamp || Date.now();
7641 var elapsedTime = parseFloat(ev.elapsedTime.toFixed(3));
7642 if (Math.max(timeStamp - startTime, 0) >= maxDelayTime && elapsedTime >= maxDuration) {
7643 close();
7644 }
7645 };
7646 element.on(events, eventFn);
7647
7648 applyAnimationToStyles(element, options);
7649
7650 $timeout(close, maxDelayTime + maxDurationTime * 1.5, false);
7651 });
7652
7653 return runner;
7654
7655 function close() {
7656 if (animationClosed) return;
7657 animationClosed = true;
7658
7659 if (events && eventFn) {
7660 element.off(events, eventFn);
7661 }
7662 applyClasses(element, options);
7663 applyAnimationStyles(element, options);
7664 forEach(temporaryStyles, function(entry) {
7665 node.style[camelCase(entry[0])] = '';
7666 });
7667 runner.complete(true);
7668 return runner;
7669 }
7670 }
7671 };
7672 }
7673
7674 function applyClasses(element, options) {
7675 if (options.addClass) {
7676 $$jqLite.addClass(element, options.addClass);
7677 options.addClass = null;
7678 }
7679 if (options.removeClass) {
7680 $$jqLite.removeClass(element, options.removeClass);
7681 options.removeClass = null;
7682 }
7683 }
7684
7685 function computeTimings(element) {
7686 var node = getDomNode(element);
7687 var cs = $window.getComputedStyle(node);
7688 var tdr = parseMaxTime(cs[prop('transitionDuration')]);
7689 var adr = parseMaxTime(cs[prop('animationDuration')]);
7690 var tdy = parseMaxTime(cs[prop('transitionDelay')]);
7691 var ady = parseMaxTime(cs[prop('animationDelay')]);
7692
7693 adr *= (parseInt(cs[prop('animationIterationCount')], 10) || 1);
7694 var duration = Math.max(adr, tdr);
7695 var delay = Math.max(ady, tdy);
7696
7697 return {
7698 duration: duration,
7699 delay: delay,
7700 animationDuration: adr,
7701 transitionDuration: tdr,
7702 animationDelay: ady,
7703 transitionDelay: tdy
7704 };
7705
7706 function prop(key) {
7707 return WEBKIT ? 'Webkit' + key.charAt(0).toUpperCase() + key.substr(1)
7708 : key;
7709 }
7710 }
7711
7712 function parseMaxTime(str) {
7713 var maxValue = 0;
7714 var values = (str || "").split(/\s*,\s*/);
7715 forEach(values, function(value) {
7716 // it's always safe to consider only second values and omit `ms` values since
7717 // getComputedStyle will always handle the conversion for us
7718 if (value.charAt(value.length - 1) == 's') {
7719 value = value.substring(0, value.length - 1);
7720 }
7721 value = parseFloat(value) || 0;
7722 maxValue = maxValue ? Math.max(value, maxValue) : value;
7723 });
7724 return maxValue;
7725 }
7726
7727 var cancelLastRAFRequest;
7728 var rafWaitQueue = [];
7729 function waitUntilQuiet(callback) {
7730 if (cancelLastRAFRequest) {
7731 cancelLastRAFRequest(); //cancels the request
7732 }
7733 rafWaitQueue.push(callback);
7734 cancelLastRAFRequest = $$rAF(function() {
7735 cancelLastRAFRequest = null;
7736
7737 // DO NOT REMOVE THIS LINE OR REFACTOR OUT THE `pageWidth` variable.
7738 // PLEASE EXAMINE THE `$$forceReflow` service to understand why.
7739 var pageWidth = $$forceReflow();
7740
7741 // we use a for loop to ensure that if the queue is changed
7742 // during this looping then it will consider new requests
7743 for (var i = 0; i < rafWaitQueue.length; i++) {
7744 rafWaitQueue[i](pageWidth);
7745 }
7746 rafWaitQueue.length = 0;
7747 });
7748 }
7749
7750 function applyAnimationStyles(element, options) {
7751 applyAnimationFromStyles(element, options);
7752 applyAnimationToStyles(element, options);
7753 }
7754
7755 function applyAnimationFromStyles(element, options) {
7756 if (options.from) {
7757 element.css(options.from);
7758 options.from = null;
7759 }
7760 }
7761
7762 function applyAnimationToStyles(element, options) {
7763 if (options.to) {
7764 element.css(options.to);
7765 options.to = null;
7766 }
7767 }
7768
7769 function getDomNode(element) {
7770 for (var i = 0; i < element.length; i++) {
7771 if (element[i].nodeType === 1) return element[i];
7772 }
7773 }
7774
7775 function blockTransition(element, bool) {
7776 var node = getDomNode(element);
7777 var key = camelCase(PREFIX + 'transition-delay');
7778 node.style[key] = bool ? '-9999s' : '';
7779 }
7780
7781 return init;
7782 }]);
7783
7784 /**
7785 * Older browsers [FF31] expect camelCase
7786 * property keys.
7787 * e.g.
7788 * animation-duration --> animationDuration
7789 */
7790 function camelCase(str) {
7791 return str.replace(/-[a-z]/g, function(str) {
7792 return str.charAt(1).toUpperCase();
7793 });
7794 }
7795
7796})();
7797
7798}
7799
7800})();
7801(function(){
7802"use strict";
7803
7804/**
7805 * @ngdoc module
7806 * @name material.components.autocomplete
7807 */
7808/*
7809 * @see js folder for autocomplete implementation
7810 */
7811angular.module('material.components.autocomplete', [
7812 'material.core',
7813 'material.components.icon',
7814 'material.components.virtualRepeat'
7815]);
7816
7817})();
7818(function(){
7819"use strict";
7820
7821/*
7822 * @ngdoc module
7823 * @name material.components.backdrop
7824 * @description Backdrop
7825 */
7826
7827/**
7828 * @ngdoc directive
7829 * @name mdBackdrop
7830 * @module material.components.backdrop
7831 *
7832 * @restrict E
7833 *
7834 * @description
7835 * `<md-backdrop>` is a backdrop element used by other components, such as dialog and bottom sheet.
7836 * Apply class `opaque` to make the backdrop use the theme backdrop color.
7837 *
7838 */
7839
7840angular
7841 .module('material.components.backdrop', ['material.core'])
7842 .directive('mdBackdrop', ["$mdTheming", "$mdUtil", "$animate", "$rootElement", "$window", "$log", "$$rAF", "$document", function BackdropDirective($mdTheming, $mdUtil, $animate, $rootElement, $window, $log, $$rAF, $document) {
7843 var ERROR_CSS_POSITION = '<md-backdrop> may not work properly in a scrolled, static-positioned parent container.';
7844
7845 return {
7846 restrict: 'E',
7847 link: postLink
7848 };
7849
7850 function postLink(scope, element, attrs) {
7851 // backdrop may be outside the $rootElement, tell ngAnimate to animate regardless
7852 if ($animate.pin) $animate.pin(element, $rootElement);
7853
7854 var bodyStyles;
7855
7856 $$rAF(function() {
7857 // If body scrolling has been disabled using mdUtil.disableBodyScroll(),
7858 // adjust the 'backdrop' height to account for the fixed 'body' top offset.
7859 // Note that this can be pretty expensive and is better done inside the $$rAF.
7860 bodyStyles = $window.getComputedStyle($document[0].body);
7861
7862 if (bodyStyles.position === 'fixed') {
7863 var resizeHandler = $mdUtil.debounce(function(){
7864 bodyStyles = $window.getComputedStyle($document[0].body);
7865 resize();
7866 }, 60, null, false);
7867
7868 resize();
7869 angular.element($window).on('resize', resizeHandler);
7870
7871 scope.$on('$destroy', function() {
7872 angular.element($window).off('resize', resizeHandler);
7873 });
7874 }
7875
7876 // Often $animate.enter() is used to append the backDrop element
7877 // so let's wait until $animate is done...
7878 var parent = element.parent();
7879
7880 if (parent.length) {
7881 if (parent[0].nodeName === 'BODY') {
7882 element.css('position', 'fixed');
7883 }
7884
7885 var styles = $window.getComputedStyle(parent[0]);
7886
7887 if (styles.position === 'static') {
7888 // backdrop uses position:absolute and will not work properly with parent position:static (default)
7889 $log.warn(ERROR_CSS_POSITION);
7890 }
7891
7892 // Only inherit the parent if the backdrop has a parent.
7893 $mdTheming.inherit(element, parent);
7894 }
7895 });
7896
7897 function resize() {
7898 var viewportHeight = parseInt(bodyStyles.height, 10) + Math.abs(parseInt(bodyStyles.top, 10));
7899 element.css('height', viewportHeight + 'px');
7900 }
7901 }
7902
7903 }]);
7904
7905})();
7906(function(){
7907"use strict";
7908
7909/**
7910 * @ngdoc module
7911 * @name material.components.bottomSheet
7912 * @description
7913 * BottomSheet
7914 */
7915MdBottomSheetDirective.$inject = ["$mdBottomSheet"];
7916MdBottomSheetProvider.$inject = ["$$interimElementProvider"];
7917angular
7918 .module('material.components.bottomSheet', [
7919 'material.core',
7920 'material.components.backdrop'
7921 ])
7922 .directive('mdBottomSheet', MdBottomSheetDirective)
7923 .provider('$mdBottomSheet', MdBottomSheetProvider);
7924
7925/* @ngInject */
7926function MdBottomSheetDirective($mdBottomSheet) {
7927 return {
7928 restrict: 'E',
7929 link : function postLink(scope, element) {
7930 element.addClass('_md'); // private md component indicator for styling
7931
7932 // When navigation force destroys an interimElement, then
7933 // listen and $destroy() that interim instance...
7934 scope.$on('$destroy', function() {
7935 $mdBottomSheet.destroy();
7936 });
7937 }
7938 };
7939}
7940
7941
7942/**
7943 * @ngdoc service
7944 * @name $mdBottomSheet
7945 * @module material.components.bottomSheet
7946 *
7947 * @description
7948 * `$mdBottomSheet` opens a bottom sheet over the app and provides a simple promise API.
7949 *
7950 * ## Restrictions
7951 *
7952 * - The bottom sheet's template must have an outer `<md-bottom-sheet>` element.
7953 * - Add the `md-grid` class to the bottom sheet for a grid layout.
7954 * - Add the `md-list` class to the bottom sheet for a list layout.
7955 *
7956 * @usage
7957 * <hljs lang="html">
7958 * <div ng-controller="MyController">
7959 * <md-button ng-click="openBottomSheet()">
7960 * Open a Bottom Sheet!
7961 * </md-button>
7962 * </div>
7963 * </hljs>
7964 * <hljs lang="js">
7965 * var app = angular.module('app', ['ngMaterial']);
7966 * app.controller('MyController', function($scope, $mdBottomSheet) {
7967 * $scope.openBottomSheet = function() {
7968 * $mdBottomSheet.show({
7969 * template: '<md-bottom-sheet>Hello!</md-bottom-sheet>'
7970 * });
7971 * };
7972 * });
7973 * </hljs>
7974 */
7975
7976 /**
7977 * @ngdoc method
7978 * @name $mdBottomSheet#show
7979 *
7980 * @description
7981 * Show a bottom sheet with the specified options.
7982 *
7983 * @param {object} options An options object, with the following properties:
7984 *
7985 * - `templateUrl` - `{string=}`: The url of an html template file that will
7986 * be used as the content of the bottom sheet. Restrictions: the template must
7987 * have an outer `md-bottom-sheet` element.
7988 * - `template` - `{string=}`: Same as templateUrl, except this is an actual
7989 * template string.
7990 * - `scope` - `{object=}`: the scope to link the template / controller to. If none is specified, it will create a new child scope.
7991 * This scope will be destroyed when the bottom sheet is removed unless `preserveScope` is set to true.
7992 * - `preserveScope` - `{boolean=}`: whether to preserve the scope when the element is removed. Default is false
7993 * - `controller` - `{string=}`: The controller to associate with this bottom sheet.
7994 * - `locals` - `{string=}`: An object containing key/value pairs. The keys will
7995 * be used as names of values to inject into the controller. For example,
7996 * `locals: {three: 3}` would inject `three` into the controller with the value
7997 * of 3.
7998 * - `clickOutsideToClose` - `{boolean=}`: Whether the user can click outside the bottom sheet to
7999 * close it. Default true.
8000 * - `bindToController` - `{boolean=}`: When set to true, the locals will be bound to the controller instance.
8001 * - `disableBackdrop` - `{boolean=}`: When set to true, the bottomsheet will not show a backdrop.
8002 * - `escapeToClose` - `{boolean=}`: Whether the user can press escape to close the bottom sheet.
8003 * Default true.
8004 * - `resolve` - `{object=}`: Similar to locals, except it takes promises as values
8005 * and the bottom sheet will not open until the promises resolve.
8006 * - `controllerAs` - `{string=}`: An alias to assign the controller to on the scope.
8007 * - `parent` - `{element=}`: The element to append the bottom sheet to. The `parent` may be a `function`, `string`,
8008 * `object`, or null. Defaults to appending to the body of the root element (or the root element) of the application.
8009 * e.g. angular.element(document.getElementById('content')) or "#content"
8010 * - `disableParentScroll` - `{boolean=}`: Whether to disable scrolling while the bottom sheet is open.
8011 * Default true.
8012 *
8013 * @returns {promise} A promise that can be resolved with `$mdBottomSheet.hide()` or
8014 * rejected with `$mdBottomSheet.cancel()`.
8015 */
8016
8017/**
8018 * @ngdoc method
8019 * @name $mdBottomSheet#hide
8020 *
8021 * @description
8022 * Hide the existing bottom sheet and resolve the promise returned from
8023 * `$mdBottomSheet.show()`. This call will close the most recently opened/current bottomsheet (if any).
8024 *
8025 * @param {*=} response An argument for the resolved promise.
8026 *
8027 */
8028
8029/**
8030 * @ngdoc method
8031 * @name $mdBottomSheet#cancel
8032 *
8033 * @description
8034 * Hide the existing bottom sheet and reject the promise returned from
8035 * `$mdBottomSheet.show()`.
8036 *
8037 * @param {*=} response An argument for the rejected promise.
8038 *
8039 */
8040
8041function MdBottomSheetProvider($$interimElementProvider) {
8042 // how fast we need to flick down to close the sheet, pixels/ms
8043 bottomSheetDefaults.$inject = ["$animate", "$mdConstant", "$mdUtil", "$mdTheming", "$mdBottomSheet", "$rootElement", "$mdGesture", "$log"];
8044 var CLOSING_VELOCITY = 0.5;
8045 var PADDING = 80; // same as css
8046
8047 return $$interimElementProvider('$mdBottomSheet')
8048 .setDefaults({
8049 methods: ['disableParentScroll', 'escapeToClose', 'clickOutsideToClose'],
8050 options: bottomSheetDefaults
8051 });
8052
8053 /* @ngInject */
8054 function bottomSheetDefaults($animate, $mdConstant, $mdUtil, $mdTheming, $mdBottomSheet, $rootElement,
8055 $mdGesture, $log) {
8056 var backdrop;
8057
8058 return {
8059 themable: true,
8060 onShow: onShow,
8061 onRemove: onRemove,
8062 disableBackdrop: false,
8063 escapeToClose: true,
8064 clickOutsideToClose: true,
8065 disableParentScroll: true
8066 };
8067
8068
8069 function onShow(scope, element, options, controller) {
8070
8071 element = $mdUtil.extractElementByName(element, 'md-bottom-sheet');
8072
8073 // prevent tab focus or click focus on the bottom-sheet container
8074 element.attr('tabindex',"-1");
8075
8076 // Once the md-bottom-sheet has `ng-cloak` applied on his template the opening animation will not work properly.
8077 // This is a very common problem, so we have to notify the developer about this.
8078 if (element.hasClass('ng-cloak')) {
8079 var message = '$mdBottomSheet: using `<md-bottom-sheet ng-cloak >` will affect the bottom-sheet opening animations.';
8080 $log.warn( message, element[0] );
8081 }
8082
8083 if (!options.disableBackdrop) {
8084 // Add a backdrop that will close on click
8085 backdrop = $mdUtil.createBackdrop(scope, "md-bottom-sheet-backdrop md-opaque");
8086
8087 // Prevent mouse focus on backdrop; ONLY programatic focus allowed.
8088 // This allows clicks on backdrop to propogate to the $rootElement and
8089 // ESC key events to be detected properly.
8090
8091 backdrop[0].tabIndex = -1;
8092
8093 if (options.clickOutsideToClose) {
8094 backdrop.on('click', function() {
8095 $mdUtil.nextTick($mdBottomSheet.cancel,true);
8096 });
8097 }
8098
8099 $mdTheming.inherit(backdrop, options.parent);
8100
8101 $animate.enter(backdrop, options.parent, null);
8102 }
8103
8104 var bottomSheet = new BottomSheet(element, options.parent);
8105 options.bottomSheet = bottomSheet;
8106
8107 $mdTheming.inherit(bottomSheet.element, options.parent);
8108
8109 if (options.disableParentScroll) {
8110 options.restoreScroll = $mdUtil.disableScrollAround(bottomSheet.element, options.parent);
8111 }
8112
8113 return $animate.enter(bottomSheet.element, options.parent, backdrop)
8114 .then(function() {
8115 var focusable = $mdUtil.findFocusTarget(element) || angular.element(
8116 element[0].querySelector('button') ||
8117 element[0].querySelector('a') ||
8118 element[0].querySelector($mdUtil.prefixer('ng-click', true))
8119 ) || backdrop;
8120
8121 if (options.escapeToClose) {
8122 options.rootElementKeyupCallback = function(e) {
8123 if (e.keyCode === $mdConstant.KEY_CODE.ESCAPE) {
8124 $mdUtil.nextTick($mdBottomSheet.cancel,true);
8125 }
8126 };
8127
8128 $rootElement.on('keyup', options.rootElementKeyupCallback);
8129 focusable && focusable.focus();
8130 }
8131 });
8132
8133 }
8134
8135 function onRemove(scope, element, options) {
8136
8137 var bottomSheet = options.bottomSheet;
8138
8139 if (!options.disableBackdrop) $animate.leave(backdrop);
8140 return $animate.leave(bottomSheet.element).then(function() {
8141 if (options.disableParentScroll) {
8142 options.restoreScroll();
8143 delete options.restoreScroll;
8144 }
8145
8146 bottomSheet.cleanup();
8147 });
8148 }
8149
8150 /**
8151 * BottomSheet class to apply bottom-sheet behavior to an element
8152 */
8153 function BottomSheet(element, parent) {
8154 var deregister = $mdGesture.register(parent, 'drag', { horizontal: false });
8155 parent.on('$md.dragstart', onDragStart)
8156 .on('$md.drag', onDrag)
8157 .on('$md.dragend', onDragEnd);
8158
8159 return {
8160 element: element,
8161 cleanup: function cleanup() {
8162 deregister();
8163 parent.off('$md.dragstart', onDragStart);
8164 parent.off('$md.drag', onDrag);
8165 parent.off('$md.dragend', onDragEnd);
8166 }
8167 };
8168
8169 function onDragStart(ev) {
8170 // Disable transitions on transform so that it feels fast
8171 element.css($mdConstant.CSS.TRANSITION_DURATION, '0ms');
8172 }
8173
8174 function onDrag(ev) {
8175 var transform = ev.pointer.distanceY;
8176 if (transform < 5) {
8177 // Slow down drag when trying to drag up, and stop after PADDING
8178 transform = Math.max(-PADDING, transform / 2);
8179 }
8180 element.css($mdConstant.CSS.TRANSFORM, 'translate3d(0,' + (PADDING + transform) + 'px,0)');
8181 }
8182
8183 function onDragEnd(ev) {
8184 if (ev.pointer.distanceY > 0 &&
8185 (ev.pointer.distanceY > 20 || Math.abs(ev.pointer.velocityY) > CLOSING_VELOCITY)) {
8186 var distanceRemaining = element.prop('offsetHeight') - ev.pointer.distanceY;
8187 var transitionDuration = Math.min(distanceRemaining / ev.pointer.velocityY * 0.75, 500);
8188 element.css($mdConstant.CSS.TRANSITION_DURATION, transitionDuration + 'ms');
8189 $mdUtil.nextTick($mdBottomSheet.cancel,true);
8190 } else {
8191 element.css($mdConstant.CSS.TRANSITION_DURATION, '');
8192 element.css($mdConstant.CSS.TRANSFORM, '');
8193 }
8194 }
8195 }
8196
8197 }
8198
8199}
8200
8201})();
8202(function(){
8203"use strict";
8204
8205/**
8206 * @ngdoc module
8207 * @name material.components.button
8208 * @description
8209 *
8210 * Button
8211 */
8212MdButtonDirective.$inject = ["$mdButtonInkRipple", "$mdTheming", "$mdAria", "$mdInteraction"];
8213MdAnchorDirective.$inject = ["$mdTheming"];
8214angular
8215 .module('material.components.button', [ 'material.core' ])
8216 .directive('mdButton', MdButtonDirective)
8217 .directive('a', MdAnchorDirective);
8218
8219
8220/**
8221 * @private
8222 * @restrict E
8223 *
8224 * @description
8225 * `a` is an anchor directive used to inherit theme colors for md-primary, md-accent, etc.
8226 *
8227 * @usage
8228 *
8229 * <hljs lang="html">
8230 * <md-content md-theme="myTheme">
8231 * <a href="#chapter1" class="md-accent"></a>
8232 * </md-content>
8233 * </hljs>
8234 */
8235function MdAnchorDirective($mdTheming) {
8236 return {
8237 restrict : 'E',
8238 link : function postLink(scope, element) {
8239 // Make sure to inherit theme so stand-alone anchors
8240 // support theme colors for md-primary, md-accent, etc.
8241 $mdTheming(element);
8242 }
8243 };
8244}
8245
8246
8247/**
8248 * @ngdoc directive
8249 * @name mdButton
8250 * @module material.components.button
8251 *
8252 * @restrict E
8253 *
8254 * @description
8255 * `<md-button>` is a button directive with optional ink ripples (default enabled).
8256 *
8257 * If you supply a `href` or `ng-href` attribute, it will become an `<a>` element. Otherwise, it
8258 * will become a `<button>` element. As per the
8259 * [Material Design specifications](https://material.google.com/style/color.html#color-color-palette)
8260 * the FAB button background is filled with the accent color [by default]. The primary color palette
8261 * may be used with the `md-primary` class.
8262 *
8263 * Developers can also change the color palette of the button, by using the following classes
8264 * - `md-primary`
8265 * - `md-accent`
8266 * - `md-warn`
8267 *
8268 * See for example
8269 *
8270 * <hljs lang="html">
8271 * <md-button class="md-primary">Primary Button</md-button>
8272 * </hljs>
8273 *
8274 * Button can be also raised, which means that they will use the current color palette to fill the button.
8275 *
8276 * <hljs lang="html">
8277 * <md-button class="md-accent md-raised">Raised and Accent Button</md-button>
8278 * </hljs>
8279 *
8280 * It is also possible to disable the focus effect on the button, by using the following markup.
8281 *
8282 * <hljs lang="html">
8283 * <md-button class="md-no-focus">No Focus Style</md-button>
8284 * </hljs>
8285 *
8286 * @param {boolean=} md-no-ink If present, disable ripple ink effects.
8287 * @param {expression=} ng-disabled En/Disable based on the expression
8288 * @param {string=} md-ripple-size Overrides the default ripple size logic. Options: `full`, `partial`, `auto`
8289 * @param {string=} aria-label Adds alternative text to button for accessibility, useful for icon buttons.
8290 * If no default text is found, a warning will be logged.
8291 *
8292 * @usage
8293 *
8294 * Regular buttons:
8295 *
8296 * <hljs lang="html">
8297 * <md-button> Flat Button </md-button>
8298 * <md-button href="http://google.com"> Flat link </md-button>
8299 * <md-button class="md-raised"> Raised Button </md-button>
8300 * <md-button ng-disabled="true"> Disabled Button </md-button>
8301 * <md-button>
8302 * <md-icon md-svg-src="your/icon.svg"></md-icon>
8303 * Register Now
8304 * </md-button>
8305 * </hljs>
8306 *
8307 * FAB buttons:
8308 *
8309 * <hljs lang="html">
8310 * <md-button class="md-fab" aria-label="FAB">
8311 * <md-icon md-svg-src="your/icon.svg"></md-icon>
8312 * </md-button>
8313 * <!-- mini-FAB -->
8314 * <md-button class="md-fab md-mini" aria-label="Mini FAB">
8315 * <md-icon md-svg-src="your/icon.svg"></md-icon>
8316 * </md-button>
8317 * <!-- Button with SVG Icon -->
8318 * <md-button class="md-icon-button" aria-label="Custom Icon Button">
8319 * <md-icon md-svg-icon="path/to/your.svg"></md-icon>
8320 * </md-button>
8321 * </hljs>
8322 */
8323function MdButtonDirective($mdButtonInkRipple, $mdTheming, $mdAria, $mdInteraction) {
8324
8325 return {
8326 restrict: 'EA',
8327 replace: true,
8328 transclude: true,
8329 template: getTemplate,
8330 link: postLink
8331 };
8332
8333 function isAnchor(attr) {
8334 return angular.isDefined(attr.href) || angular.isDefined(attr.ngHref) || angular.isDefined(attr.ngLink) || angular.isDefined(attr.uiSref);
8335 }
8336
8337 function getTemplate(element, attr) {
8338 if (isAnchor(attr)) {
8339 return '<a class="md-button" ng-transclude></a>';
8340 } else {
8341 //If buttons don't have type="button", they will submit forms automatically.
8342 var btnType = (typeof attr.type === 'undefined') ? 'button' : attr.type;
8343 return '<button class="md-button" type="' + btnType + '" ng-transclude></button>';
8344 }
8345 }
8346
8347 function postLink(scope, element, attr) {
8348 $mdTheming(element);
8349 $mdButtonInkRipple.attach(scope, element);
8350
8351 // Use async expect to support possible bindings in the button label
8352 $mdAria.expectWithoutText(element, 'aria-label');
8353
8354 // For anchor elements, we have to set tabindex manually when the
8355 // element is disabled
8356 if (isAnchor(attr) && angular.isDefined(attr.ngDisabled) ) {
8357 scope.$watch(attr.ngDisabled, function(isDisabled) {
8358 element.attr('tabindex', isDisabled ? -1 : 0);
8359 });
8360 }
8361
8362 // disabling click event when disabled is true
8363 element.on('click', function(e){
8364 if (attr.disabled === true) {
8365 e.preventDefault();
8366 e.stopImmediatePropagation();
8367 }
8368 });
8369
8370 if (!element.hasClass('md-no-focus')) {
8371
8372 element.on('focus', function() {
8373
8374 // Only show the focus effect when being focused through keyboard interaction or programmatically
8375 if (!$mdInteraction.isUserInvoked() || $mdInteraction.getLastInteractionType() === 'keyboard') {
8376 element.addClass('md-focused');
8377 }
8378
8379 });
8380
8381 element.on('blur', function() {
8382 element.removeClass('md-focused');
8383 });
8384 }
8385
8386 }
8387
8388}
8389
8390})();
8391(function(){
8392"use strict";
8393
8394/**
8395 * @ngdoc module
8396 * @name material.components.card
8397 *
8398 * @description
8399 * Card components.
8400 */
8401mdCardDirective.$inject = ["$mdTheming"];
8402angular.module('material.components.card', [
8403 'material.core'
8404 ])
8405 .directive('mdCard', mdCardDirective);
8406
8407
8408/**
8409 * @ngdoc directive
8410 * @name mdCard
8411 * @module material.components.card
8412 *
8413 * @restrict E
8414 *
8415 * @description
8416 * The `<md-card>` directive is a container element used within `<md-content>` containers.
8417 *
8418 * An image included as a direct descendant will fill the card's width. If you want to avoid this,
8419 * you can add the `md-image-no-fill` class to the parent element. The `<md-card-content>`
8420 * container will wrap text content and provide padding. An `<md-card-footer>` element can be
8421 * optionally included to put content flush against the bottom edge of the card.
8422 *
8423 * Action buttons can be included in an `<md-card-actions>` element, similar to `<md-dialog-actions>`.
8424 * You can then position buttons using layout attributes.
8425 *
8426 * Card is built with:
8427 * * `<md-card-header>` - Header for the card, holds avatar, text and squared image
8428 * - `<md-card-avatar>` - Card avatar
8429 * - `md-user-avatar` - Class for user image
8430 * - `<md-icon>`
8431 * - `<md-card-header-text>` - Contains elements for the card description
8432 * - `md-title` - Class for the card title
8433 * - `md-subhead` - Class for the card sub header
8434 * * `<img>` - Image for the card
8435 * * `<md-card-title>` - Card content title
8436 * - `<md-card-title-text>`
8437 * - `md-headline` - Class for the card content title
8438 * - `md-subhead` - Class for the card content sub header
8439 * - `<md-card-title-media>` - Squared image within the title
8440 * - `md-media-sm` - Class for small image
8441 * - `md-media-md` - Class for medium image
8442 * - `md-media-lg` - Class for large image
8443 * - `md-media-xl` - Class for extra large image
8444 * * `<md-card-content>` - Card content
8445 * * `<md-card-actions>` - Card actions
8446 * - `<md-card-icon-actions>` - Icon actions
8447 *
8448 * Cards have constant width and variable heights; where the maximum height is limited to what can
8449 * fit within a single view on a platform, but it can temporarily expand as needed.
8450 *
8451 * @usage
8452 * ### Card with optional footer
8453 * <hljs lang="html">
8454 * <md-card>
8455 * <img src="card-image.png" class="md-card-image" alt="image caption">
8456 * <md-card-content>
8457 * <h2>Card headline</h2>
8458 * <p>Card content</p>
8459 * </md-card-content>
8460 * <md-card-footer>
8461 * Card footer
8462 * </md-card-footer>
8463 * </md-card>
8464 * </hljs>
8465 *
8466 * ### Card with actions
8467 * <hljs lang="html">
8468 * <md-card>
8469 * <img src="card-image.png" class="md-card-image" alt="image caption">
8470 * <md-card-content>
8471 * <h2>Card headline</h2>
8472 * <p>Card content</p>
8473 * </md-card-content>
8474 * <md-card-actions layout="row" layout-align="end center">
8475 * <md-button>Action 1</md-button>
8476 * <md-button>Action 2</md-button>
8477 * </md-card-actions>
8478 * </md-card>
8479 * </hljs>
8480 *
8481 * ### Card with header, image, title actions and content
8482 * <hljs lang="html">
8483 * <md-card>
8484 * <md-card-header>
8485 * <md-card-avatar>
8486 * <img class="md-user-avatar" src="avatar.png"/>
8487 * </md-card-avatar>
8488 * <md-card-header-text>
8489 * <span class="md-title">Title</span>
8490 * <span class="md-subhead">Sub header</span>
8491 * </md-card-header-text>
8492 * </md-card-header>
8493 * <img ng-src="card-image.png" class="md-card-image" alt="image caption">
8494 * <md-card-title>
8495 * <md-card-title-text>
8496 * <span class="md-headline">Card headline</span>
8497 * <span class="md-subhead">Card subheader</span>
8498 * </md-card-title-text>
8499 * </md-card-title>
8500 * <md-card-actions layout="row" layout-align="start center">
8501 * <md-button>Action 1</md-button>
8502 * <md-button>Action 2</md-button>
8503 * <md-card-icon-actions>
8504 * <md-button class="md-icon-button" aria-label="icon">
8505 * <md-icon md-svg-icon="icon"></md-icon>
8506 * </md-button>
8507 * </md-card-icon-actions>
8508 * </md-card-actions>
8509 * <md-card-content>
8510 * <p>
8511 * Card content
8512 * </p>
8513 * </md-card-content>
8514 * </md-card>
8515 * </hljs>
8516 */
8517function mdCardDirective($mdTheming) {
8518 return {
8519 restrict: 'E',
8520 link: function ($scope, $element, attr) {
8521 $element.addClass('_md'); // private md component indicator for styling
8522 $mdTheming($element);
8523 }
8524 };
8525}
8526
8527})();
8528(function(){
8529"use strict";
8530
8531/**
8532 * @ngdoc module
8533 * @name material.components.checkbox
8534 * @description Checkbox module!
8535 */
8536MdCheckboxDirective.$inject = ["inputDirective", "$mdAria", "$mdConstant", "$mdTheming", "$mdUtil", "$mdInteraction"];
8537angular
8538 .module('material.components.checkbox', ['material.core'])
8539 .directive('mdCheckbox', MdCheckboxDirective);
8540
8541/**
8542 * @ngdoc directive
8543 * @name mdCheckbox
8544 * @module material.components.checkbox
8545 * @restrict E
8546 *
8547 * @description
8548 * The checkbox directive is used like the normal [angular checkbox](https://docs.angularjs.org/api/ng/input/input%5Bcheckbox%5D).
8549 *
8550 * As per the [material design spec](http://www.google.com/design/spec/style/color.html#color-color-schemes)
8551 * the checkbox is in the accent color by default. The primary color palette may be used with
8552 * the `md-primary` class.
8553 *
8554 * @param {string} ng-model Assignable angular expression to data-bind to.
8555 * @param {string=} name Property name of the form under which the control is published.
8556 * @param {expression=} ng-true-value The value to which the expression should be set when selected.
8557 * @param {expression=} ng-false-value The value to which the expression should be set when not selected.
8558 * @param {string=} ng-change Angular expression to be executed when input changes due to user interaction with the input element.
8559 * @param {boolean=} md-no-ink Use of attribute indicates use of ripple ink effects
8560 * @param {string=} aria-label Adds label to checkbox for accessibility.
8561 * Defaults to checkbox's text. If no default text is found, a warning will be logged.
8562 * @param {expression=} md-indeterminate This determines when the checkbox should be rendered as 'indeterminate'.
8563 * If a truthy expression or no value is passed in the checkbox renders in the md-indeterminate state.
8564 * If falsy expression is passed in it just looks like a normal unchecked checkbox.
8565 * The indeterminate, checked, and unchecked states are mutually exclusive. A box cannot be in any two states at the same time.
8566 * Adding the 'md-indeterminate' attribute overrides any checked/unchecked rendering logic.
8567 * When using the 'md-indeterminate' attribute use 'ng-checked' to define rendering logic instead of using 'ng-model'.
8568 * @param {expression=} ng-checked If this expression evaluates as truthy, the 'md-checked' css class is added to the checkbox and it
8569 * will appear checked.
8570 *
8571 * @usage
8572 * <hljs lang="html">
8573 * <md-checkbox ng-model="isChecked" aria-label="Finished?">
8574 * Finished ?
8575 * </md-checkbox>
8576 *
8577 * <md-checkbox md-no-ink ng-model="hasInk" aria-label="No Ink Effects">
8578 * No Ink Effects
8579 * </md-checkbox>
8580 *
8581 * <md-checkbox ng-disabled="true" ng-model="isDisabled" aria-label="Disabled">
8582 * Disabled
8583 * </md-checkbox>
8584 *
8585 * </hljs>
8586 *
8587 */
8588function MdCheckboxDirective(inputDirective, $mdAria, $mdConstant, $mdTheming, $mdUtil, $mdInteraction) {
8589 inputDirective = inputDirective[0];
8590
8591 return {
8592 restrict: 'E',
8593 transclude: true,
8594 require: ['^?mdInputContainer', '?ngModel', '?^form'],
8595 priority: $mdConstant.BEFORE_NG_ARIA,
8596 template:
8597 '<div class="md-container" md-ink-ripple md-ink-ripple-checkbox>' +
8598 '<div class="md-icon"></div>' +
8599 '</div>' +
8600 '<div ng-transclude class="md-label"></div>',
8601 compile: compile
8602 };
8603
8604 // **********************************************************
8605 // Private Methods
8606 // **********************************************************
8607
8608 function compile (tElement, tAttrs) {
8609 tAttrs.$set('tabindex', tAttrs.tabindex || '0');
8610 tAttrs.$set('type', 'checkbox');
8611 tAttrs.$set('role', tAttrs.type);
8612
8613 return {
8614 pre: function(scope, element) {
8615 // Attach a click handler during preLink, in order to immediately stop propagation
8616 // (especially for ng-click) when the checkbox is disabled.
8617 element.on('click', function(e) {
8618 if (this.hasAttribute('disabled')) {
8619 e.stopImmediatePropagation();
8620 }
8621 });
8622 },
8623 post: postLink
8624 };
8625
8626 function postLink(scope, element, attr, ctrls) {
8627 var isIndeterminate;
8628 var containerCtrl = ctrls[0];
8629 var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel();
8630 var formCtrl = ctrls[2];
8631
8632 if (containerCtrl) {
8633 var isErrorGetter = containerCtrl.isErrorGetter || function() {
8634 return ngModelCtrl.$invalid && (ngModelCtrl.$touched || (formCtrl && formCtrl.$submitted));
8635 };
8636
8637 containerCtrl.input = element;
8638
8639 scope.$watch(isErrorGetter, containerCtrl.setInvalid);
8640 }
8641
8642 $mdTheming(element);
8643
8644 // Redirect focus events to the root element, because IE11 is always focusing the container element instead
8645 // of the md-checkbox element. This causes issues when using ngModelOptions: `updateOnBlur`
8646 element.children().on('focus', function() {
8647 element.focus();
8648 });
8649
8650 if ($mdUtil.parseAttributeBoolean(attr.mdIndeterminate)) {
8651 setIndeterminateState();
8652 scope.$watch(attr.mdIndeterminate, setIndeterminateState);
8653 }
8654
8655 if (attr.ngChecked) {
8656 scope.$watch(scope.$eval.bind(scope, attr.ngChecked), function(value) {
8657 ngModelCtrl.$setViewValue(value);
8658 ngModelCtrl.$render();
8659 });
8660 }
8661
8662 $$watchExpr('ngDisabled', 'tabindex', {
8663 true: '-1',
8664 false: attr.tabindex
8665 });
8666
8667 $mdAria.expectWithText(element, 'aria-label');
8668
8669 // Reuse the original input[type=checkbox] directive from Angular core.
8670 // This is a bit hacky as we need our own event listener and own render
8671 // function.
8672 inputDirective.link.pre(scope, {
8673 on: angular.noop,
8674 0: {}
8675 }, attr, [ngModelCtrl]);
8676
8677 element.on('click', listener)
8678 .on('keypress', keypressHandler)
8679 .on('focus', function() {
8680 if ($mdInteraction.getLastInteractionType() === 'keyboard') {
8681 element.addClass('md-focused');
8682 }
8683 })
8684 .on('blur', function() {
8685 element.removeClass('md-focused');
8686 });
8687
8688 ngModelCtrl.$render = render;
8689
8690 function $$watchExpr(expr, htmlAttr, valueOpts) {
8691 if (attr[expr]) {
8692 scope.$watch(attr[expr], function(val) {
8693 if (valueOpts[val]) {
8694 element.attr(htmlAttr, valueOpts[val]);
8695 }
8696 });
8697 }
8698 }
8699
8700 function keypressHandler(ev) {
8701 var keyCode = ev.which || ev.keyCode;
8702 if (keyCode === $mdConstant.KEY_CODE.SPACE || keyCode === $mdConstant.KEY_CODE.ENTER) {
8703 ev.preventDefault();
8704 element.addClass('md-focused');
8705 listener(ev);
8706 }
8707 }
8708
8709 function listener(ev) {
8710 // skipToggle boolean is used by the switch directive to prevent the click event
8711 // when releasing the drag. There will be always a click if releasing the drag over the checkbox
8712 if (element[0].hasAttribute('disabled') || scope.skipToggle) {
8713 return;
8714 }
8715
8716 scope.$apply(function() {
8717 // Toggle the checkbox value...
8718 var viewValue = attr.ngChecked ? attr.checked : !ngModelCtrl.$viewValue;
8719
8720 ngModelCtrl.$setViewValue(viewValue, ev && ev.type);
8721 ngModelCtrl.$render();
8722 });
8723 }
8724
8725 function render() {
8726 // Cast the $viewValue to a boolean since it could be undefined
8727 element.toggleClass('md-checked', !!ngModelCtrl.$viewValue && !isIndeterminate);
8728 }
8729
8730 function setIndeterminateState(newValue) {
8731 isIndeterminate = newValue !== false;
8732 if (isIndeterminate) {
8733 element.attr('aria-checked', 'mixed');
8734 }
8735 element.toggleClass('md-indeterminate', isIndeterminate);
8736 }
8737 }
8738 }
8739}
8740
8741})();
8742(function(){
8743"use strict";
8744
8745/**
8746 * @ngdoc module
8747 * @name material.components.chips
8748 */
8749/*
8750 * @see js folder for chips implementation
8751 */
8752angular.module('material.components.chips', [
8753 'material.core',
8754 'material.components.autocomplete'
8755]);
8756
8757})();
8758(function(){
8759"use strict";
8760
8761(function () {
8762 "use strict";
8763
8764 /**
8765 * Use a RegExp to check if the `md-colors="<expression>"` is static string
8766 * or one that should be observed and dynamically interpolated.
8767 */
8768 MdColorsDirective.$inject = ["$mdColors", "$mdUtil", "$log", "$parse"];
8769 MdColorsService.$inject = ["$mdTheming", "$mdUtil", "$log"];
8770 var STATIC_COLOR_EXPRESSION = /^{((\s|,)*?["'a-zA-Z-]+?\s*?:\s*?('|")[a-zA-Z0-9-.]*('|"))+\s*}$/;
8771 var colorPalettes = null;
8772
8773 /**
8774 * @ngdoc module
8775 * @name material.components.colors
8776 *
8777 * @description
8778 * Define $mdColors service and a `md-colors=""` attribute directive
8779 */
8780 angular
8781 .module('material.components.colors', ['material.core'])
8782 .directive('mdColors', MdColorsDirective)
8783 .service('$mdColors', MdColorsService);
8784
8785 /**
8786 * @ngdoc service
8787 * @name $mdColors
8788 * @module material.components.colors
8789 *
8790 * @description
8791 * With only defining themes, one couldn't get non ngMaterial elements colored with Material colors,
8792 * `$mdColors` service is used by the md-color directive to convert the 1..n color expressions to RGBA values and will apply
8793 * those values to element as CSS property values.
8794 *
8795 * @usage
8796 * <hljs lang="js">
8797 * angular.controller('myCtrl', function ($mdColors) {
8798 * var color = $mdColors.getThemeColor('myTheme-red-200-0.5');
8799 * ...
8800 * });
8801 * </hljs>
8802 *
8803 */
8804 function MdColorsService($mdTheming, $mdUtil, $log) {
8805 colorPalettes = colorPalettes || Object.keys($mdTheming.PALETTES);
8806
8807 // Publish service instance
8808 return {
8809 applyThemeColors: applyThemeColors,
8810 getThemeColor: getThemeColor,
8811 hasTheme: hasTheme
8812 };
8813
8814 // ********************************************
8815 // Internal Methods
8816 // ********************************************
8817
8818 /**
8819 * @ngdoc method
8820 * @name $mdColors#applyThemeColors
8821 *
8822 * @description
8823 * Gets a color json object, keys are css properties and values are string of the wanted color
8824 * Then calculate the rgba() values based on the theme color parts
8825 *
8826 * @param {DOMElement} element the element to apply the styles on.
8827 * @param {object} colorExpression json object, keys are css properties and values are string of the wanted color,
8828 * for example: `{color: 'red-A200-0.3'}`.
8829 *
8830 * @usage
8831 * <hljs lang="js">
8832 * app.directive('myDirective', function($mdColors) {
8833 * return {
8834 * ...
8835 * link: function (scope, elem) {
8836 * $mdColors.applyThemeColors(elem, {color: 'red'});
8837 * }
8838 * }
8839 * });
8840 * </hljs>
8841 */
8842 function applyThemeColors(element, colorExpression) {
8843 try {
8844 if (colorExpression) {
8845 // Assign the calculate RGBA color values directly as inline CSS
8846 element.css(interpolateColors(colorExpression));
8847 }
8848 } catch (e) {
8849 $log.error(e.message);
8850 }
8851
8852 }
8853
8854 /**
8855 * @ngdoc method
8856 * @name $mdColors#getThemeColor
8857 *
8858 * @description
8859 * Get parsed color from expression
8860 *
8861 * @param {string} expression string of a color expression (for instance `'red-700-0.8'`)
8862 *
8863 * @returns {string} a css color expression (for instance `rgba(211, 47, 47, 0.8)`)
8864 *
8865 * @usage
8866 * <hljs lang="js">
8867 * angular.controller('myCtrl', function ($mdColors) {
8868 * var color = $mdColors.getThemeColor('myTheme-red-200-0.5');
8869 * ...
8870 * });
8871 * </hljs>
8872 */
8873 function getThemeColor(expression) {
8874 var color = extractColorOptions(expression);
8875
8876 return parseColor(color);
8877 }
8878
8879 /**
8880 * Return the parsed color
8881 * @param color hashmap of color definitions
8882 * @param contrast whether use contrast color for foreground
8883 * @returns rgba color string
8884 */
8885 function parseColor(color, contrast) {
8886 contrast = contrast || false;
8887 var rgbValues = $mdTheming.PALETTES[color.palette][color.hue];
8888
8889 rgbValues = contrast ? rgbValues.contrast : rgbValues.value;
8890
8891 return $mdUtil.supplant('rgba({0}, {1}, {2}, {3})',
8892 [rgbValues[0], rgbValues[1], rgbValues[2], rgbValues[3] || color.opacity]
8893 );
8894 }
8895
8896 /**
8897 * Convert the color expression into an object with scope-interpolated values
8898 * Then calculate the rgba() values based on the theme color parts
8899 *
8900 * @results Hashmap of CSS properties with associated `rgba( )` string vales
8901 *
8902 *
8903 */
8904 function interpolateColors(themeColors) {
8905 var rgbColors = {};
8906
8907 var hasColorProperty = themeColors.hasOwnProperty('color');
8908
8909 angular.forEach(themeColors, function (value, key) {
8910 var color = extractColorOptions(value);
8911 var hasBackground = key.indexOf('background') > -1;
8912
8913 rgbColors[key] = parseColor(color);
8914 if (hasBackground && !hasColorProperty) {
8915 rgbColors.color = parseColor(color, true);
8916 }
8917 });
8918
8919 return rgbColors;
8920 }
8921
8922 /**
8923 * Check if expression has defined theme
8924 * e.g.
8925 * 'myTheme-primary' => true
8926 * 'red-800' => false
8927 */
8928 function hasTheme(expression) {
8929 return angular.isDefined($mdTheming.THEMES[expression.split('-')[0]]);
8930 }
8931
8932 /**
8933 * For the evaluated expression, extract the color parts into a hash map
8934 */
8935 function extractColorOptions(expression) {
8936 var parts = expression.split('-');
8937 var hasTheme = angular.isDefined($mdTheming.THEMES[parts[0]]);
8938 var theme = hasTheme ? parts.splice(0, 1)[0] : $mdTheming.defaultTheme();
8939
8940 return {
8941 theme: theme,
8942 palette: extractPalette(parts, theme),
8943 hue: extractHue(parts, theme),
8944 opacity: parts[2] || 1
8945 };
8946 }
8947
8948 /**
8949 * Calculate the theme palette name
8950 */
8951 function extractPalette(parts, theme) {
8952 // If the next section is one of the palettes we assume it's a two word palette
8953 // Two word palette can be also written in camelCase, forming camelCase to dash-case
8954
8955 var isTwoWord = parts.length > 1 && colorPalettes.indexOf(parts[1]) !== -1;
8956 var palette = parts[0].replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
8957
8958 if (isTwoWord) palette = parts[0] + '-' + parts.splice(1, 1);
8959
8960 if (colorPalettes.indexOf(palette) === -1) {
8961 // If the palette is not in the palette list it's one of primary/accent/warn/background
8962 var scheme = $mdTheming.THEMES[theme].colors[palette];
8963 if (!scheme) {
8964 throw new Error($mdUtil.supplant('mdColors: couldn\'t find \'{palette}\' in the palettes.', {palette: palette}));
8965 }
8966 palette = scheme.name;
8967 }
8968
8969 return palette;
8970 }
8971
8972 function extractHue(parts, theme) {
8973 var themeColors = $mdTheming.THEMES[theme].colors;
8974
8975 if (parts[1] === 'hue') {
8976 var hueNumber = parseInt(parts.splice(2, 1)[0], 10);
8977
8978 if (hueNumber < 1 || hueNumber > 3) {
8979 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}));
8980 }
8981 parts[1] = 'hue-' + hueNumber;
8982
8983 if (!(parts[0] in themeColors)) {
8984 throw new Error($mdUtil.supplant('mdColors: \'hue-x\' can only be used with [{availableThemes}], but was used with \'{usedTheme}\'', {
8985 availableThemes: Object.keys(themeColors).join(', '),
8986 usedTheme: parts[0]
8987 }));
8988 }
8989
8990 return themeColors[parts[0]].hues[parts[1]];
8991 }
8992
8993 return parts[1] || themeColors[parts[0] in themeColors ? parts[0] : 'primary'].hues['default'];
8994 }
8995 }
8996
8997 /**
8998 * @ngdoc directive
8999 * @name mdColors
9000 * @module material.components.colors
9001 *
9002 * @restrict A
9003 *
9004 * @description
9005 * `mdColors` directive will apply the theme-based color expression as RGBA CSS style values.
9006 *
9007 * The format will be similar to our color defining in the scss files:
9008 *
9009 * ## `[?theme]-[palette]-[?hue]-[?opacity]`
9010 * - [theme] - default value is the default theme
9011 * - [palette] - can be either palette name or primary/accent/warn/background
9012 * - [hue] - default is 500 (hue-x can be used with primary/accent/warn/background)
9013 * - [opacity] - default is 1
9014 *
9015 * > `?` indicates optional parameter
9016 *
9017 * @usage
9018 * <hljs lang="html">
9019 * <div md-colors="{background: 'myTheme-accent-900-0.43'}">
9020 * <div md-colors="{color: 'red-A100', 'border-color': 'primary-600'}">
9021 * <span>Color demo</span>
9022 * </div>
9023 * </div>
9024 * </hljs>
9025 *
9026 * `mdColors` directive will automatically watch for changes in the expression if it recognizes an interpolation
9027 * expression or a function. For performance options, you can use `::` prefix to the `md-colors` expression
9028 * to indicate a one-time data binding.
9029 * <hljs lang="html">
9030 * <md-card md-colors="::{background: '{{theme}}-primary-700'}">
9031 * </md-card>
9032 * </hljs>
9033 *
9034 */
9035 function MdColorsDirective($mdColors, $mdUtil, $log, $parse) {
9036 return {
9037 restrict: 'A',
9038 require: ['^?mdTheme'],
9039 compile: function (tElem, tAttrs) {
9040 var shouldWatch = shouldColorsWatch();
9041
9042 return function (scope, element, attrs, ctrl) {
9043 var mdThemeController = ctrl[0];
9044
9045 var lastColors = {};
9046
9047 var parseColors = function (theme) {
9048 if (typeof theme !== 'string') {
9049 theme = '';
9050 }
9051
9052 if (!attrs.mdColors) {
9053 attrs.mdColors = '{}';
9054 }
9055
9056 /**
9057 * Json.parse() does not work because the keys are not quoted;
9058 * use $parse to convert to a hash map
9059 */
9060 var colors = $parse(attrs.mdColors)(scope);
9061
9062 /**
9063 * If mdTheme is defined up the DOM tree
9064 * we add mdTheme theme to colors who doesn't specified a theme
9065 *
9066 * # example
9067 * <hljs lang="html">
9068 * <div md-theme="myTheme">
9069 * <div md-colors="{background: 'primary-600'}">
9070 * <span md-colors="{background: 'mySecondTheme-accent-200'}">Color demo</span>
9071 * </div>
9072 * </div>
9073 * </hljs>
9074 *
9075 * 'primary-600' will be 'myTheme-primary-600',
9076 * but 'mySecondTheme-accent-200' will stay the same cause it has a theme prefix
9077 */
9078 if (mdThemeController) {
9079 Object.keys(colors).forEach(function (prop) {
9080 var color = colors[prop];
9081 if (!$mdColors.hasTheme(color)) {
9082 colors[prop] = (theme || mdThemeController.$mdTheme) + '-' + color;
9083 }
9084 });
9085 }
9086
9087 cleanElement(colors);
9088
9089 return colors;
9090 };
9091
9092 var cleanElement = function (colors) {
9093 if (!angular.equals(colors, lastColors)) {
9094 var keys = Object.keys(lastColors);
9095
9096 if (lastColors.background && !keys.color) {
9097 keys.push('color');
9098 }
9099
9100 keys.forEach(function (key) {
9101 element.css(key, '');
9102 });
9103 }
9104
9105 lastColors = colors;
9106 };
9107
9108 /**
9109 * Registering for mgTheme changes and asking mdTheme controller run our callback whenever a theme changes
9110 */
9111 var unregisterChanges = angular.noop;
9112
9113 if (mdThemeController) {
9114 unregisterChanges = mdThemeController.registerChanges(function (theme) {
9115 $mdColors.applyThemeColors(element, parseColors(theme));
9116 });
9117 }
9118
9119 scope.$on('$destroy', function () {
9120 unregisterChanges();
9121 });
9122
9123 try {
9124 if (shouldWatch) {
9125 scope.$watch(parseColors, angular.bind(this,
9126 $mdColors.applyThemeColors, element
9127 ), true);
9128 }
9129 else {
9130 $mdColors.applyThemeColors(element, parseColors());
9131 }
9132
9133 }
9134 catch (e) {
9135 $log.error(e.message);
9136 }
9137
9138 };
9139
9140 function shouldColorsWatch() {
9141 // Simulate 1x binding and mark mdColorsWatch == false
9142 var rawColorExpression = tAttrs.mdColors;
9143 var bindOnce = rawColorExpression.indexOf('::') > -1;
9144 var isStatic = bindOnce ? true : STATIC_COLOR_EXPRESSION.test(tAttrs.mdColors);
9145
9146 // Remove it for the postLink...
9147 tAttrs.mdColors = rawColorExpression.replace('::', '');
9148
9149 var hasWatchAttr = angular.isDefined(tAttrs.mdColorsWatch);
9150
9151 return (bindOnce || isStatic) ? false :
9152 hasWatchAttr ? $mdUtil.parseAttributeBoolean(tAttrs.mdColorsWatch) : true;
9153 }
9154 }
9155 };
9156
9157 }
9158
9159
9160})();
9161
9162})();
9163(function(){
9164"use strict";
9165
9166/**
9167 * @ngdoc module
9168 * @name material.components.content
9169 *
9170 * @description
9171 * Scrollable content
9172 */
9173mdContentDirective.$inject = ["$mdTheming"];
9174angular.module('material.components.content', [
9175 'material.core'
9176])
9177 .directive('mdContent', mdContentDirective);
9178
9179/**
9180 * @ngdoc directive
9181 * @name mdContent
9182 * @module material.components.content
9183 *
9184 * @restrict E
9185 *
9186 * @description
9187 *
9188 * The `<md-content>` directive is a container element useful for scrollable content. It achieves
9189 * this by setting the CSS `overflow` property to `auto` so that content can properly scroll.
9190 *
9191 * In general, `<md-content>` components are not designed to be nested inside one another. If
9192 * possible, it is better to make them siblings. This often results in a better user experience as
9193 * having nested scrollbars may confuse the user.
9194 *
9195 * ## Troubleshooting
9196 *
9197 * In some cases, you may wish to apply the `md-no-momentum` class to ensure that Safari's
9198 * momentum scrolling is disabled. Momentum scrolling can cause flickering issues while scrolling
9199 * SVG icons and some other components.
9200 *
9201 * Additionally, we now also offer the `md-no-flicker` class which can be applied to any element
9202 * and uses a Webkit-specific filter of `blur(0px)` that forces GPU rendering of all elements
9203 * inside (which eliminates the flicker on iOS devices).
9204 *
9205 * _<b>Note:</b> Forcing an element to render on the GPU can have unintended side-effects, especially
9206 * related to the z-index of elements. Please use with caution and only on the elements needed._
9207 *
9208 * @usage
9209 *
9210 * Add the `[layout-padding]` attribute to make the content padded.
9211 *
9212 * <hljs lang="html">
9213 * <md-content layout-padding>
9214 * Lorem ipsum dolor sit amet, ne quod novum mei.
9215 * </md-content>
9216 * </hljs>
9217 */
9218
9219function mdContentDirective($mdTheming) {
9220 return {
9221 restrict: 'E',
9222 controller: ['$scope', '$element', ContentController],
9223 link: function(scope, element) {
9224 element.addClass('_md'); // private md component indicator for styling
9225
9226 $mdTheming(element);
9227 scope.$broadcast('$mdContentLoaded', element);
9228
9229 iosScrollFix(element[0]);
9230 }
9231 };
9232
9233 function ContentController($scope, $element) {
9234 this.$scope = $scope;
9235 this.$element = $element;
9236 }
9237}
9238
9239function iosScrollFix(node) {
9240 // IOS FIX:
9241 // If we scroll where there is no more room for the webview to scroll,
9242 // by default the webview itself will scroll up and down, this looks really
9243 // bad. So if we are scrolling to the very top or bottom, add/subtract one
9244 angular.element(node).on('$md.pressdown', function(ev) {
9245 // Only touch events
9246 if (ev.pointer.type !== 't') return;
9247 // Don't let a child content's touchstart ruin it for us.
9248 if (ev.$materialScrollFixed) return;
9249 ev.$materialScrollFixed = true;
9250
9251 if (node.scrollTop === 0) {
9252 node.scrollTop = 1;
9253 } else if (node.scrollHeight === node.scrollTop + node.offsetHeight) {
9254 node.scrollTop -= 1;
9255 }
9256 });
9257}
9258
9259})();
9260(function(){
9261"use strict";
9262
9263/**
9264 * @ngdoc module
9265 * @name material.components.datepicker
9266 * @description Module for the datepicker component.
9267 */
9268
9269angular.module('material.components.datepicker', [
9270 'material.core',
9271 'material.components.icon',
9272 'material.components.virtualRepeat'
9273]);
9274
9275})();
9276(function(){
9277"use strict";
9278
9279/**
9280 * @ngdoc module
9281 * @name material.components.dialog
9282 */
9283MdDialogDirective.$inject = ["$$rAF", "$mdTheming", "$mdDialog"];
9284MdDialogProvider.$inject = ["$$interimElementProvider"];
9285angular
9286 .module('material.components.dialog', [
9287 'material.core',
9288 'material.components.backdrop'
9289 ])
9290 .directive('mdDialog', MdDialogDirective)
9291 .provider('$mdDialog', MdDialogProvider);
9292
9293/**
9294 * @ngdoc directive
9295 * @name mdDialog
9296 * @module material.components.dialog
9297 *
9298 * @restrict E
9299 *
9300 * @description
9301 * `<md-dialog>` - The dialog's template must be inside this element.
9302 *
9303 * Inside, use an `<md-dialog-content>` element for the dialog's content, and use
9304 * an `<md-dialog-actions>` element for the dialog's actions.
9305 *
9306 * ## CSS
9307 * - `.md-dialog-content` - class that sets the padding on the content as the spec file
9308 *
9309 * ## Notes
9310 * - If you specify an `id` for the `<md-dialog>`, the `<md-dialog-content>` will have the same `id`
9311 * prefixed with `dialogContent_`.
9312 *
9313 * @usage
9314 * ### Dialog template
9315 * <hljs lang="html">
9316 * <md-dialog aria-label="List dialog">
9317 * <md-dialog-content>
9318 * <md-list>
9319 * <md-list-item ng-repeat="item in items">
9320 * <p>Number {{item}}</p>
9321 * </md-list-item>
9322 * </md-list>
9323 * </md-dialog-content>
9324 * <md-dialog-actions>
9325 * <md-button ng-click="closeDialog()" class="md-primary">Close Dialog</md-button>
9326 * </md-dialog-actions>
9327 * </md-dialog>
9328 * </hljs>
9329 */
9330function MdDialogDirective($$rAF, $mdTheming, $mdDialog) {
9331 return {
9332 restrict: 'E',
9333 link: function(scope, element) {
9334 element.addClass('_md'); // private md component indicator for styling
9335
9336 $mdTheming(element);
9337 $$rAF(function() {
9338 var images;
9339 var content = element[0].querySelector('md-dialog-content');
9340
9341 if (content) {
9342 images = content.getElementsByTagName('img');
9343 addOverflowClass();
9344 //-- delayed image loading may impact scroll height, check after images are loaded
9345 angular.element(images).on('load', addOverflowClass);
9346 }
9347
9348 scope.$on('$destroy', function() {
9349 $mdDialog.destroy(element);
9350 });
9351
9352 /**
9353 *
9354 */
9355 function addOverflowClass() {
9356 element.toggleClass('md-content-overflow', content.scrollHeight > content.clientHeight);
9357 }
9358
9359
9360 });
9361 }
9362 };
9363}
9364
9365/**
9366 * @ngdoc service
9367 * @name $mdDialog
9368 * @module material.components.dialog
9369 *
9370 * @description
9371 * `$mdDialog` opens a dialog over the app to inform users about critical information or require
9372 * them to make decisions. There are two approaches for setup: a simple promise API
9373 * and regular object syntax.
9374 *
9375 * ## Restrictions
9376 *
9377 * - The dialog is always given an isolate scope.
9378 * - The dialog's template must have an outer `<md-dialog>` element.
9379 * Inside, use an `<md-dialog-content>` element for the dialog's content, and use
9380 * an `<md-dialog-actions>` element for the dialog's actions.
9381 * - Dialogs must cover the entire application to keep interactions inside of them.
9382 * Use the `parent` option to change where dialogs are appended.
9383 *
9384 * ## Sizing
9385 * - Complex dialogs can be sized with `flex="percentage"`, i.e. `flex="66"`.
9386 * - Default max-width is 80% of the `rootElement` or `parent`.
9387 *
9388 * ## CSS
9389 * - `.md-dialog-content` - class that sets the padding on the content as the spec file
9390 *
9391 * @usage
9392 * <hljs lang="html">
9393 * <div ng-app="demoApp" ng-controller="EmployeeController">
9394 * <div>
9395 * <md-button ng-click="showAlert()" class="md-raised md-warn">
9396 * Employee Alert!
9397 * </md-button>
9398 * </div>
9399 * <div>
9400 * <md-button ng-click="showDialog($event)" class="md-raised">
9401 * Custom Dialog
9402 * </md-button>
9403 * </div>
9404 * <div>
9405 * <md-button ng-click="closeAlert()" ng-disabled="!hasAlert()" class="md-raised">
9406 * Close Alert
9407 * </md-button>
9408 * </div>
9409 * <div>
9410 * <md-button ng-click="showGreeting($event)" class="md-raised md-primary" >
9411 * Greet Employee
9412 * </md-button>
9413 * </div>
9414 * </div>
9415 * </hljs>
9416 *
9417 * ### JavaScript: object syntax
9418 * <hljs lang="js">
9419 * (function(angular, undefined){
9420 * "use strict";
9421 *
9422 * angular
9423 * .module('demoApp', ['ngMaterial'])
9424 * .controller('AppCtrl', AppController);
9425 *
9426 * function AppController($scope, $mdDialog) {
9427 * var alert;
9428 * $scope.showAlert = showAlert;
9429 * $scope.showDialog = showDialog;
9430 * $scope.items = [1, 2, 3];
9431 *
9432 * // Internal method
9433 * function showAlert() {
9434 * alert = $mdDialog.alert({
9435 * title: 'Attention',
9436 * textContent: 'This is an example of how easy dialogs can be!',
9437 * ok: 'Close'
9438 * });
9439 *
9440 * $mdDialog
9441 * .show( alert )
9442 * .finally(function() {
9443 * alert = undefined;
9444 * });
9445 * }
9446 *
9447 * function showDialog($event) {
9448 * var parentEl = angular.element(document.body);
9449 * $mdDialog.show({
9450 * parent: parentEl,
9451 * targetEvent: $event,
9452 * template:
9453 * '<md-dialog aria-label="List dialog">' +
9454 * ' <md-dialog-content>'+
9455 * ' <md-list>'+
9456 * ' <md-list-item ng-repeat="item in items">'+
9457 * ' <p>Number {{item}}</p>' +
9458 * ' </md-item>'+
9459 * ' </md-list>'+
9460 * ' </md-dialog-content>' +
9461 * ' <md-dialog-actions>' +
9462 * ' <md-button ng-click="closeDialog()" class="md-primary">' +
9463 * ' Close Dialog' +
9464 * ' </md-button>' +
9465 * ' </md-dialog-actions>' +
9466 * '</md-dialog>',
9467 * locals: {
9468 * items: $scope.items
9469 * },
9470 * controller: DialogController
9471 * });
9472 * function DialogController($scope, $mdDialog, items) {
9473 * $scope.items = items;
9474 * $scope.closeDialog = function() {
9475 * $mdDialog.hide();
9476 * }
9477 * }
9478 * }
9479 * }
9480 * })(angular);
9481 * </hljs>
9482 *
9483 * ### Multiple Dialogs
9484 * Using the `multiple` option for the `$mdDialog` service allows developers to show multiple dialogs
9485 * at the same time.
9486 *
9487 * <hljs lang="js">
9488 * // From plain options
9489 * $mdDialog.show({
9490 * multiple: true
9491 * });
9492 *
9493 * // From a dialog preset
9494 * $mdDialog.show(
9495 * $mdDialog
9496 * .alert()
9497 * .multiple(true)
9498 * );
9499 *
9500 * </hljs>
9501 *
9502 * ### Pre-Rendered Dialogs
9503 * By using the `contentElement` option, it is possible to use an already existing element in the DOM.
9504 *
9505 * > Pre-rendered dialogs will be not linked to any scope and will not instantiate any new controller.<br/>
9506 * > You can manually link the elements to a scope or instantiate a controller from the template (`ng-controller`)
9507 *
9508 * <hljs lang="js">
9509 * $scope.showPrerenderedDialog = function() {
9510 * $mdDialog.show({
9511 * contentElement: '#myStaticDialog',
9512 * parent: angular.element(document.body)
9513 * });
9514 * };
9515 * </hljs>
9516 *
9517 * When using a string as value, `$mdDialog` will automatically query the DOM for the specified CSS selector.
9518 *
9519 * <hljs lang="html">
9520 * <div style="visibility: hidden">
9521 * <div class="md-dialog-container" id="myStaticDialog">
9522 * <md-dialog>
9523 * This is a pre-rendered dialog.
9524 * </md-dialog>
9525 * </div>
9526 * </div>
9527 * </hljs>
9528 *
9529 * **Notice**: It is important, to use the `.md-dialog-container` as the content element, otherwise the dialog
9530 * will not show up.
9531 *
9532 * It also possible to use a DOM element for the `contentElement` option.
9533 * - `contentElement: document.querySelector('#myStaticDialog')`
9534 * - `contentElement: angular.element(TEMPLATE)`
9535 *
9536 * When using a `template` as content element, it will be not compiled upon open.
9537 * This allows you to compile the element yourself and use it each time the dialog opens.
9538 *
9539 * ### Custom Presets
9540 * Developers are also able to create their own preset, which can be easily used without repeating
9541 * their options each time.
9542 *
9543 * <hljs lang="js">
9544 * $mdDialogProvider.addPreset('testPreset', {
9545 * options: function() {
9546 * return {
9547 * template:
9548 * '<md-dialog>' +
9549 * 'This is a custom preset' +
9550 * '</md-dialog>',
9551 * controllerAs: 'dialog',
9552 * bindToController: true,
9553 * clickOutsideToClose: true,
9554 * escapeToClose: true
9555 * };
9556 * }
9557 * });
9558 * </hljs>
9559 *
9560 * After you created your preset at config phase, you can easily access it.
9561 *
9562 * <hljs lang="js">
9563 * $mdDialog.show(
9564 * $mdDialog.testPreset()
9565 * );
9566 * </hljs>
9567 *
9568 * ### JavaScript: promise API syntax, custom dialog template
9569 * <hljs lang="js">
9570 * (function(angular, undefined){
9571 * "use strict";
9572 *
9573 * angular
9574 * .module('demoApp', ['ngMaterial'])
9575 * .controller('EmployeeController', EmployeeEditor)
9576 * .controller('GreetingController', GreetingController);
9577 *
9578 * // Fictitious Employee Editor to show how to use simple and complex dialogs.
9579 *
9580 * function EmployeeEditor($scope, $mdDialog) {
9581 * var alert;
9582 *
9583 * $scope.showAlert = showAlert;
9584 * $scope.closeAlert = closeAlert;
9585 * $scope.showGreeting = showCustomGreeting;
9586 *
9587 * $scope.hasAlert = function() { return !!alert };
9588 * $scope.userName = $scope.userName || 'Bobby';
9589 *
9590 * // Dialog #1 - Show simple alert dialog and cache
9591 * // reference to dialog instance
9592 *
9593 * function showAlert() {
9594 * alert = $mdDialog.alert()
9595 * .title('Attention, ' + $scope.userName)
9596 * .textContent('This is an example of how easy dialogs can be!')
9597 * .ok('Close');
9598 *
9599 * $mdDialog
9600 * .show( alert )
9601 * .finally(function() {
9602 * alert = undefined;
9603 * });
9604 * }
9605 *
9606 * // Close the specified dialog instance and resolve with 'finished' flag
9607 * // Normally this is not needed, just use '$mdDialog.hide()' to close
9608 * // the most recent dialog popup.
9609 *
9610 * function closeAlert() {
9611 * $mdDialog.hide( alert, "finished" );
9612 * alert = undefined;
9613 * }
9614 *
9615 * // Dialog #2 - Demonstrate more complex dialogs construction and popup.
9616 *
9617 * function showCustomGreeting($event) {
9618 * $mdDialog.show({
9619 * targetEvent: $event,
9620 * template:
9621 * '<md-dialog>' +
9622 *
9623 * ' <md-dialog-content>Hello {{ employee }}!</md-dialog-content>' +
9624 *
9625 * ' <md-dialog-actions>' +
9626 * ' <md-button ng-click="closeDialog()" class="md-primary">' +
9627 * ' Close Greeting' +
9628 * ' </md-button>' +
9629 * ' </md-dialog-actions>' +
9630 * '</md-dialog>',
9631 * controller: 'GreetingController',
9632 * onComplete: afterShowAnimation,
9633 * locals: { employee: $scope.userName }
9634 * });
9635 *
9636 * // When the 'enter' animation finishes...
9637 *
9638 * function afterShowAnimation(scope, element, options) {
9639 * // post-show code here: DOM element focus, etc.
9640 * }
9641 * }
9642 *
9643 * // Dialog #3 - Demonstrate use of ControllerAs and passing $scope to dialog
9644 * // Here we used ng-controller="GreetingController as vm" and
9645 * // $scope.vm === <controller instance>
9646 *
9647 * function showCustomGreeting() {
9648 *
9649 * $mdDialog.show({
9650 * clickOutsideToClose: true,
9651 *
9652 * scope: $scope, // use parent scope in template
9653 * preserveScope: true, // do not forget this if use parent scope
9654
9655 * // Since GreetingController is instantiated with ControllerAs syntax
9656 * // AND we are passing the parent '$scope' to the dialog, we MUST
9657 * // use 'vm.<xxx>' in the template markup
9658 *
9659 * template: '<md-dialog>' +
9660 * ' <md-dialog-content>' +
9661 * ' Hi There {{vm.employee}}' +
9662 * ' </md-dialog-content>' +
9663 * '</md-dialog>',
9664 *
9665 * controller: function DialogController($scope, $mdDialog) {
9666 * $scope.closeDialog = function() {
9667 * $mdDialog.hide();
9668 * }
9669 * }
9670 * });
9671 * }
9672 *
9673 * }
9674 *
9675 * // Greeting controller used with the more complex 'showCustomGreeting()' custom dialog
9676 *
9677 * function GreetingController($scope, $mdDialog, employee) {
9678 * // Assigned from construction <code>locals</code> options...
9679 * $scope.employee = employee;
9680 *
9681 * $scope.closeDialog = function() {
9682 * // Easily hides most recent dialog shown...
9683 * // no specific instance reference is needed.
9684 * $mdDialog.hide();
9685 * };
9686 * }
9687 *
9688 * })(angular);
9689 * </hljs>
9690 */
9691
9692/**
9693 * @ngdoc method
9694 * @name $mdDialog#alert
9695 *
9696 * @description
9697 * Builds a preconfigured dialog with the specified message.
9698 *
9699 * @returns {obj} an `$mdDialogPreset` with the chainable configuration methods:
9700 *
9701 * - $mdDialogPreset#title(string) - Sets the alert title.
9702 * - $mdDialogPreset#textContent(string) - Sets the alert message.
9703 * - $mdDialogPreset#htmlContent(string) - Sets the alert message as HTML. Requires ngSanitize
9704 * module to be loaded. HTML is not run through Angular's compiler.
9705 * - $mdDialogPreset#ok(string) - Sets the alert "Okay" button text.
9706 * - $mdDialogPreset#theme(string) - Sets the theme of the alert dialog.
9707 * - $mdDialogPreset#targetEvent(DOMClickEvent=) - A click's event object. When passed in as an option,
9708 * the location of the click will be used as the starting point for the opening animation
9709 * of the the dialog.
9710 *
9711 */
9712
9713/**
9714 * @ngdoc method
9715 * @name $mdDialog#confirm
9716 *
9717 * @description
9718 * Builds a preconfigured dialog with the specified message. You can call show and the promise returned
9719 * will be resolved only if the user clicks the confirm action on the dialog.
9720 *
9721 * @returns {obj} an `$mdDialogPreset` with the chainable configuration methods:
9722 *
9723 * Additionally, it supports the following methods:
9724 *
9725 * - $mdDialogPreset#title(string) - Sets the confirm title.
9726 * - $mdDialogPreset#textContent(string) - Sets the confirm message.
9727 * - $mdDialogPreset#htmlContent(string) - Sets the confirm message as HTML. Requires ngSanitize
9728 * module to be loaded. HTML is not run through Angular's compiler.
9729 * - $mdDialogPreset#ok(string) - Sets the confirm "Okay" button text.
9730 * - $mdDialogPreset#cancel(string) - Sets the confirm "Cancel" button text.
9731 * - $mdDialogPreset#theme(string) - Sets the theme of the confirm dialog.
9732 * - $mdDialogPreset#targetEvent(DOMClickEvent=) - A click's event object. When passed in as an option,
9733 * the location of the click will be used as the starting point for the opening animation
9734 * of the the dialog.
9735 *
9736 */
9737
9738/**
9739 * @ngdoc method
9740 * @name $mdDialog#prompt
9741 *
9742 * @description
9743 * Builds a preconfigured dialog with the specified message and input box. You can call show and the promise returned
9744 * will be resolved only if the user clicks the prompt action on the dialog, passing the input value as the first argument.
9745 *
9746 * @returns {obj} an `$mdDialogPreset` with the chainable configuration methods:
9747 *
9748 * Additionally, it supports the following methods:
9749 *
9750 * - $mdDialogPreset#title(string) - Sets the prompt title.
9751 * - $mdDialogPreset#textContent(string) - Sets the prompt message.
9752 * - $mdDialogPreset#htmlContent(string) - Sets the prompt message as HTML. Requires ngSanitize
9753 * module to be loaded. HTML is not run through Angular's compiler.
9754 * - $mdDialogPreset#placeholder(string) - Sets the placeholder text for the input.
9755 * - $mdDialogPreset#initialValue(string) - Sets the initial value for the prompt input.
9756 * - $mdDialogPreset#ok(string) - Sets the prompt "Okay" button text.
9757 * - $mdDialogPreset#cancel(string) - Sets the prompt "Cancel" button text.
9758 * - $mdDialogPreset#theme(string) - Sets the theme of the prompt dialog.
9759 * - $mdDialogPreset#targetEvent(DOMClickEvent=) - A click's event object. When passed in as an option,
9760 * the location of the click will be used as the starting point for the opening animation
9761 * of the the dialog.
9762 *
9763 */
9764
9765/**
9766 * @ngdoc method
9767 * @name $mdDialog#show
9768 *
9769 * @description
9770 * Show a dialog with the specified options.
9771 *
9772 * @param {object} optionsOrPreset Either provide an `$mdDialogPreset` returned from `alert()`, and
9773 * `confirm()`, or an options object with the following properties:
9774 * - `templateUrl` - `{string=}`: The url of a template that will be used as the content
9775 * of the dialog.
9776 * - `template` - `{string=}`: HTML template to show in the dialog. This **must** be trusted HTML
9777 * with respect to Angular's [$sce service](https://docs.angularjs.org/api/ng/service/$sce).
9778 * This template should **never** be constructed with any kind of user input or user data.
9779 * - `contentElement` - `{string|Element}`: Instead of using a template, which will be compiled each time a
9780 * dialog opens, you can also use a DOM element.<br/>
9781 * * When specifying an element, which is present on the DOM, `$mdDialog` will temporary fetch the element into
9782 * the dialog and restores it at the old DOM position upon close.
9783 * * When specifying a string, the string be used as a CSS selector, to lookup for the element in the DOM.
9784 * - `autoWrap` - `{boolean=}`: Whether or not to automatically wrap the template with a
9785 * `<md-dialog>` tag if one is not provided. Defaults to true. Can be disabled if you provide a
9786 * custom dialog directive.
9787 * - `targetEvent` - `{DOMClickEvent=}`: A click's event object. When passed in as an option,
9788 * the location of the click will be used as the starting point for the opening animation
9789 * of the the dialog.
9790 * - `openFrom` - `{string|Element|object}`: The query selector, DOM element or the Rect object
9791 * that is used to determine the bounds (top, left, height, width) from which the Dialog will
9792 * originate.
9793 * - `closeTo` - `{string|Element|object}`: The query selector, DOM element or the Rect object
9794 * that is used to determine the bounds (top, left, height, width) to which the Dialog will
9795 * target.
9796 * - `scope` - `{object=}`: the scope to link the template / controller to. If none is specified,
9797 * it will create a new isolate scope.
9798 * This scope will be destroyed when the dialog is removed unless `preserveScope` is set to true.
9799 * - `preserveScope` - `{boolean=}`: whether to preserve the scope when the element is removed. Default is false
9800 * - `disableParentScroll` - `{boolean=}`: Whether to disable scrolling while the dialog is open.
9801 * Default true.
9802 * - `hasBackdrop` - `{boolean=}`: Whether there should be an opaque backdrop behind the dialog.
9803 * Default true.
9804 * - `clickOutsideToClose` - `{boolean=}`: Whether the user can click outside the dialog to
9805 * close it. Default false.
9806 * - `escapeToClose` - `{boolean=}`: Whether the user can press escape to close the dialog.
9807 * Default true.
9808 * - `focusOnOpen` - `{boolean=}`: An option to override focus behavior on open. Only disable if
9809 * focusing some other way, as focus management is required for dialogs to be accessible.
9810 * Defaults to true.
9811 * - `controller` - `{function|string=}`: The controller to associate with the dialog. The controller
9812 * will be injected with the local `$mdDialog`, which passes along a scope for the dialog.
9813 * - `locals` - `{object=}`: An object containing key/value pairs. The keys will be used as names
9814 * of values to inject into the controller. For example, `locals: {three: 3}` would inject
9815 * `three` into the controller, with the value 3. If `bindToController` is true, they will be
9816 * copied to the controller instead.
9817 * - `bindToController` - `bool`: bind the locals to the controller, instead of passing them in.
9818 * - `resolve` - `{object=}`: Similar to locals, except it takes promises as values, and the
9819 * dialog will not open until all of the promises resolve.
9820 * - `controllerAs` - `{string=}`: An alias to assign the controller to on the scope.
9821 * - `parent` - `{element=}`: The element to append the dialog to. Defaults to appending
9822 * to the root element of the application.
9823 * - `onShowing` - `function(scope, element)`: Callback function used to announce the show() action is
9824 * starting.
9825 * - `onComplete` - `function(scope, element)`: Callback function used to announce when the show() action is
9826 * finished.
9827 * - `onRemoving` - `function(element, removePromise)`: Callback function used to announce the
9828 * close/hide() action is starting. This allows developers to run custom animations
9829 * in parallel the close animations.
9830 * - `fullscreen` `{boolean=}`: An option to toggle whether the dialog should show in fullscreen
9831 * or not. Defaults to `false`.
9832 * @returns {promise} A promise that can be resolved with `$mdDialog.hide()` or
9833 * rejected with `$mdDialog.cancel()`.
9834 */
9835
9836/**
9837 * @ngdoc method
9838 * @name $mdDialog#hide
9839 *
9840 * @description
9841 * Hide an existing dialog and resolve the promise returned from `$mdDialog.show()`.
9842 *
9843 * @param {*=} response An argument for the resolved promise.
9844 *
9845 * @returns {promise} A promise that is resolved when the dialog has been closed.
9846 */
9847
9848/**
9849 * @ngdoc method
9850 * @name $mdDialog#cancel
9851 *
9852 * @description
9853 * Hide an existing dialog and reject the promise returned from `$mdDialog.show()`.
9854 *
9855 * @param {*=} response An argument for the rejected promise.
9856 *
9857 * @returns {promise} A promise that is resolved when the dialog has been closed.
9858 */
9859
9860function MdDialogProvider($$interimElementProvider) {
9861 // Elements to capture and redirect focus when the user presses tab at the dialog boundary.
9862 advancedDialogOptions.$inject = ["$mdDialog", "$mdConstant"];
9863 dialogDefaultOptions.$inject = ["$mdDialog", "$mdAria", "$mdUtil", "$mdConstant", "$animate", "$document", "$window", "$rootElement", "$log", "$injector", "$mdTheming", "$interpolate", "$mdInteraction"];
9864 var topFocusTrap, bottomFocusTrap;
9865
9866 return $$interimElementProvider('$mdDialog')
9867 .setDefaults({
9868 methods: ['disableParentScroll', 'hasBackdrop', 'clickOutsideToClose', 'escapeToClose',
9869 'targetEvent', 'closeTo', 'openFrom', 'parent', 'fullscreen', 'multiple'],
9870 options: dialogDefaultOptions
9871 })
9872 .addPreset('alert', {
9873 methods: ['title', 'htmlContent', 'textContent', 'content', 'ariaLabel', 'ok', 'theme',
9874 'css'],
9875 options: advancedDialogOptions
9876 })
9877 .addPreset('confirm', {
9878 methods: ['title', 'htmlContent', 'textContent', 'content', 'ariaLabel', 'ok', 'cancel',
9879 'theme', 'css'],
9880 options: advancedDialogOptions
9881 })
9882 .addPreset('prompt', {
9883 methods: ['title', 'htmlContent', 'textContent', 'initialValue', 'content', 'placeholder', 'ariaLabel',
9884 'ok', 'cancel', 'theme', 'css'],
9885 options: advancedDialogOptions
9886 });
9887
9888 /* @ngInject */
9889 function advancedDialogOptions($mdDialog, $mdConstant) {
9890 return {
9891 template: [
9892 '<md-dialog md-theme="{{ dialog.theme || dialog.defaultTheme }}" aria-label="{{ dialog.ariaLabel }}" ng-class="dialog.css">',
9893 ' <md-dialog-content class="md-dialog-content" role="document" tabIndex="-1">',
9894 ' <h2 class="md-title">{{ dialog.title }}</h2>',
9895 ' <div ng-if="::dialog.mdHtmlContent" class="md-dialog-content-body" ',
9896 ' ng-bind-html="::dialog.mdHtmlContent"></div>',
9897 ' <div ng-if="::!dialog.mdHtmlContent" class="md-dialog-content-body">',
9898 ' <p>{{::dialog.mdTextContent}}</p>',
9899 ' </div>',
9900 ' <md-input-container md-no-float ng-if="::dialog.$type == \'prompt\'" class="md-prompt-input-container">',
9901 ' <input ng-keypress="dialog.keypress($event)" md-autofocus ng-model="dialog.result" ' +
9902 ' placeholder="{{::dialog.placeholder}}">',
9903 ' </md-input-container>',
9904 ' </md-dialog-content>',
9905 ' <md-dialog-actions>',
9906 ' <md-button ng-if="dialog.$type === \'confirm\' || dialog.$type === \'prompt\'"' +
9907 ' ng-click="dialog.abort()" class="md-primary md-cancel-button">',
9908 ' {{ dialog.cancel }}',
9909 ' </md-button>',
9910 ' <md-button ng-click="dialog.hide()" class="md-primary md-confirm-button" md-autofocus="dialog.$type===\'alert\'">',
9911 ' {{ dialog.ok }}',
9912 ' </md-button>',
9913 ' </md-dialog-actions>',
9914 '</md-dialog>'
9915 ].join('').replace(/\s\s+/g, ''),
9916 controller: function mdDialogCtrl() {
9917 var isPrompt = this.$type == 'prompt';
9918
9919 if (isPrompt && this.initialValue) {
9920 this.result = this.initialValue;
9921 }
9922
9923 this.hide = function() {
9924 $mdDialog.hide(isPrompt ? this.result : true);
9925 };
9926 this.abort = function() {
9927 $mdDialog.cancel();
9928 };
9929 this.keypress = function($event) {
9930 if ($event.keyCode === $mdConstant.KEY_CODE.ENTER) {
9931 $mdDialog.hide(this.result);
9932 }
9933 };
9934 },
9935 controllerAs: 'dialog',
9936 bindToController: true,
9937 };
9938 }
9939
9940 /* @ngInject */
9941 function dialogDefaultOptions($mdDialog, $mdAria, $mdUtil, $mdConstant, $animate, $document, $window, $rootElement,
9942 $log, $injector, $mdTheming, $interpolate, $mdInteraction) {
9943
9944 return {
9945 hasBackdrop: true,
9946 isolateScope: true,
9947 onCompiling: beforeCompile,
9948 onShow: onShow,
9949 onShowing: beforeShow,
9950 onRemove: onRemove,
9951 clickOutsideToClose: false,
9952 escapeToClose: true,
9953 targetEvent: null,
9954 closeTo: null,
9955 openFrom: null,
9956 focusOnOpen: true,
9957 disableParentScroll: true,
9958 autoWrap: true,
9959 fullscreen: false,
9960 transformTemplate: function(template, options) {
9961 // Make the dialog container focusable, because otherwise the focus will be always redirected to
9962 // an element outside of the container, and the focus trap won't work probably..
9963 // Also the tabindex is needed for the `escapeToClose` functionality, because
9964 // the keyDown event can't be triggered when the focus is outside of the container.
9965 var startSymbol = $interpolate.startSymbol();
9966 var endSymbol = $interpolate.endSymbol();
9967 var theme = startSymbol + (options.themeWatch ? '' : '::') + 'theme' + endSymbol;
9968 return '<div class="md-dialog-container" tabindex="-1" md-theme="' + theme + '">' + validatedTemplate(template) + '</div>';
9969
9970 /**
9971 * The specified template should contain a <md-dialog> wrapper element....
9972 */
9973 function validatedTemplate(template) {
9974 if (options.autoWrap && !/<\/md-dialog>/g.test(template)) {
9975 return '<md-dialog>' + (template || '') + '</md-dialog>';
9976 } else {
9977 return template || '';
9978 }
9979 }
9980 }
9981 };
9982
9983 function beforeCompile(options) {
9984 // Automatically apply the theme, if the user didn't specify a theme explicitly.
9985 // Those option changes need to be done, before the compilation has started, because otherwise
9986 // the option changes will be not available in the $mdCompilers locales.
9987 options.defaultTheme = $mdTheming.defaultTheme();
9988
9989 detectTheming(options);
9990 }
9991
9992 function beforeShow(scope, element, options, controller) {
9993
9994 if (controller) {
9995 var mdHtmlContent = controller.htmlContent || options.htmlContent || '';
9996 var mdTextContent = controller.textContent || options.textContent ||
9997 controller.content || options.content || '';
9998
9999 if (mdHtmlContent && !$injector.has('$sanitize')) {
10000 throw Error('The ngSanitize module must be loaded in order to use htmlContent.');
10001 }
10002
10003 if (mdHtmlContent && mdTextContent) {
10004 throw Error('md-dialog cannot have both `htmlContent` and `textContent`');
10005 }
10006
10007 // Only assign the content if nothing throws, otherwise it'll still be compiled.
10008 controller.mdHtmlContent = mdHtmlContent;
10009 controller.mdTextContent = mdTextContent;
10010 }
10011 }
10012
10013 /** Show method for dialogs */
10014 function onShow(scope, element, options, controller) {
10015 angular.element($document[0].body).addClass('md-dialog-is-showing');
10016
10017 var dialogElement = element.find('md-dialog');
10018
10019 // Once a dialog has `ng-cloak` applied on his template the dialog animation will not work properly.
10020 // This is a very common problem, so we have to notify the developer about this.
10021 if (dialogElement.hasClass('ng-cloak')) {
10022 var message = '$mdDialog: using `<md-dialog ng-cloak>` will affect the dialog opening animations.';
10023 $log.warn( message, element[0] );
10024 }
10025
10026 captureParentAndFromToElements(options);
10027 configureAria(dialogElement, options);
10028 showBackdrop(scope, element, options);
10029 activateListeners(element, options);
10030
10031 return dialogPopIn(element, options)
10032 .then(function() {
10033 lockScreenReader(element, options);
10034 warnDeprecatedActions();
10035 focusOnOpen();
10036 });
10037
10038 /**
10039 * Check to see if they used the deprecated .md-actions class and log a warning
10040 */
10041 function warnDeprecatedActions() {
10042 if (element[0].querySelector('.md-actions')) {
10043 $log.warn('Using a class of md-actions is deprecated, please use <md-dialog-actions>.');
10044 }
10045 }
10046
10047 /**
10048 * For alerts, focus on content... otherwise focus on
10049 * the close button (or equivalent)
10050 */
10051 function focusOnOpen() {
10052 if (options.focusOnOpen) {
10053 var target = $mdUtil.findFocusTarget(element) || findCloseButton() || dialogElement;
10054 target.focus();
10055 }
10056
10057 /**
10058 * If no element with class dialog-close, try to find the last
10059 * button child in md-actions and assume it is a close button.
10060 *
10061 * If we find no actions at all, log a warning to the console.
10062 */
10063 function findCloseButton() {
10064 return element[0].querySelector('.dialog-close, md-dialog-actions button:last-child');
10065 }
10066 }
10067 }
10068
10069 /**
10070 * Remove function for all dialogs
10071 */
10072 function onRemove(scope, element, options) {
10073 options.deactivateListeners();
10074 options.unlockScreenReader();
10075 options.hideBackdrop(options.$destroy);
10076
10077 // Remove the focus traps that we added earlier for keeping focus within the dialog.
10078 if (topFocusTrap && topFocusTrap.parentNode) {
10079 topFocusTrap.parentNode.removeChild(topFocusTrap);
10080 }
10081
10082 if (bottomFocusTrap && bottomFocusTrap.parentNode) {
10083 bottomFocusTrap.parentNode.removeChild(bottomFocusTrap);
10084 }
10085
10086 // For navigation $destroy events, do a quick, non-animated removal,
10087 // but for normal closes (from clicks, etc) animate the removal
10088 return !!options.$destroy ? detachAndClean() : animateRemoval().then( detachAndClean );
10089
10090 /**
10091 * For normal closes, animate the removal.
10092 * For forced closes (like $destroy events), skip the animations
10093 */
10094 function animateRemoval() {
10095 return dialogPopOut(element, options);
10096 }
10097
10098 /**
10099 * Detach the element
10100 */
10101 function detachAndClean() {
10102 angular.element($document[0].body).removeClass('md-dialog-is-showing');
10103
10104 // Reverse the container stretch if using a content element.
10105 if (options.contentElement) {
10106 options.reverseContainerStretch();
10107 }
10108
10109 // Exposed cleanup function from the $mdCompiler.
10110 options.cleanupElement();
10111
10112 // Restores the focus to the origin element if the last interaction upon opening was a keyboard.
10113 if (!options.$destroy && options.originInteraction === 'keyboard') {
10114 options.origin.focus();
10115 }
10116 }
10117 }
10118
10119 function detectTheming(options) {
10120 // Once the user specifies a targetEvent, we will automatically try to find the correct
10121 // nested theme.
10122 var targetEl;
10123 if (options.targetEvent && options.targetEvent.target) {
10124 targetEl = angular.element(options.targetEvent.target);
10125 }
10126
10127 var themeCtrl = targetEl && targetEl.controller('mdTheme');
10128
10129 if (!themeCtrl) {
10130 return;
10131 }
10132
10133 options.themeWatch = themeCtrl.$shouldWatch;
10134
10135 var theme = options.theme || themeCtrl.$mdTheme;
10136
10137 if (theme) {
10138 options.scope.theme = theme;
10139 }
10140
10141 var unwatch = themeCtrl.registerChanges(function (newTheme) {
10142 options.scope.theme = newTheme;
10143
10144 if (!options.themeWatch) {
10145 unwatch();
10146 }
10147 });
10148 }
10149
10150 /**
10151 * Capture originator/trigger/from/to element information (if available)
10152 * and the parent container for the dialog; defaults to the $rootElement
10153 * unless overridden in the options.parent
10154 */
10155 function captureParentAndFromToElements(options) {
10156 options.origin = angular.extend({
10157 element: null,
10158 bounds: null,
10159 focus: angular.noop
10160 }, options.origin || {});
10161
10162 options.parent = getDomElement(options.parent, $rootElement);
10163 options.closeTo = getBoundingClientRect(getDomElement(options.closeTo));
10164 options.openFrom = getBoundingClientRect(getDomElement(options.openFrom));
10165
10166 if ( options.targetEvent ) {
10167 options.origin = getBoundingClientRect(options.targetEvent.target, options.origin);
10168 options.originInteraction = $mdInteraction.getLastInteractionType();
10169 }
10170
10171
10172 /**
10173 * Identify the bounding RECT for the target element
10174 *
10175 */
10176 function getBoundingClientRect (element, orig) {
10177 var source = angular.element((element || {}));
10178 if (source && source.length) {
10179 // Compute and save the target element's bounding rect, so that if the
10180 // element is hidden when the dialog closes, we can shrink the dialog
10181 // back to the same position it expanded from.
10182 //
10183 // Checking if the source is a rect object or a DOM element
10184 var bounds = {top:0,left:0,height:0,width:0};
10185 var hasFn = angular.isFunction(source[0].getBoundingClientRect);
10186
10187 return angular.extend(orig || {}, {
10188 element : hasFn ? source : undefined,
10189 bounds : hasFn ? source[0].getBoundingClientRect() : angular.extend({}, bounds, source[0]),
10190 focus : angular.bind(source, source.focus),
10191 });
10192 }
10193 }
10194
10195 /**
10196 * If the specifier is a simple string selector, then query for
10197 * the DOM element.
10198 */
10199 function getDomElement(element, defaultElement) {
10200 if (angular.isString(element)) {
10201 element = $document[0].querySelector(element);
10202 }
10203
10204 // If we have a reference to a raw dom element, always wrap it in jqLite
10205 return angular.element(element || defaultElement);
10206 }
10207
10208 }
10209
10210 /**
10211 * Listen for escape keys and outside clicks to auto close
10212 */
10213 function activateListeners(element, options) {
10214 var window = angular.element($window);
10215 var onWindowResize = $mdUtil.debounce(function() {
10216 stretchDialogContainerToViewport(element, options);
10217 }, 60);
10218
10219 var removeListeners = [];
10220 var smartClose = function() {
10221 // Only 'confirm' dialogs have a cancel button... escape/clickOutside will
10222 // cancel or fallback to hide.
10223 var closeFn = ( options.$type == 'alert' ) ? $mdDialog.hide : $mdDialog.cancel;
10224 $mdUtil.nextTick(closeFn, true);
10225 };
10226
10227 if (options.escapeToClose) {
10228 var parentTarget = options.parent;
10229 var keyHandlerFn = function(ev) {
10230 if (ev.keyCode === $mdConstant.KEY_CODE.ESCAPE) {
10231 ev.stopPropagation();
10232 ev.preventDefault();
10233
10234 smartClose();
10235 }
10236 };
10237
10238 // Add keydown listeners
10239 element.on('keydown', keyHandlerFn);
10240 parentTarget.on('keydown', keyHandlerFn);
10241
10242 // Queue remove listeners function
10243 removeListeners.push(function() {
10244
10245 element.off('keydown', keyHandlerFn);
10246 parentTarget.off('keydown', keyHandlerFn);
10247
10248 });
10249 }
10250
10251 // Register listener to update dialog on window resize
10252 window.on('resize', onWindowResize);
10253
10254 removeListeners.push(function() {
10255 window.off('resize', onWindowResize);
10256 });
10257
10258 if (options.clickOutsideToClose) {
10259 var target = element;
10260 var sourceElem;
10261
10262 // Keep track of the element on which the mouse originally went down
10263 // so that we can only close the backdrop when the 'click' started on it.
10264 // A simple 'click' handler does not work,
10265 // it sets the target object as the element the mouse went down on.
10266 var mousedownHandler = function(ev) {
10267 sourceElem = ev.target;
10268 };
10269
10270 // We check if our original element and the target is the backdrop
10271 // because if the original was the backdrop and the target was inside the dialog
10272 // we don't want to dialog to close.
10273 var mouseupHandler = function(ev) {
10274 if (sourceElem === target[0] && ev.target === target[0]) {
10275 ev.stopPropagation();
10276 ev.preventDefault();
10277
10278 smartClose();
10279 }
10280 };
10281
10282 // Add listeners
10283 target.on('mousedown', mousedownHandler);
10284 target.on('mouseup', mouseupHandler);
10285
10286 // Queue remove listeners function
10287 removeListeners.push(function() {
10288 target.off('mousedown', mousedownHandler);
10289 target.off('mouseup', mouseupHandler);
10290 });
10291 }
10292
10293 // Attach specific `remove` listener handler
10294 options.deactivateListeners = function() {
10295 removeListeners.forEach(function(removeFn) {
10296 removeFn();
10297 });
10298 options.deactivateListeners = null;
10299 };
10300 }
10301
10302 /**
10303 * Show modal backdrop element...
10304 */
10305 function showBackdrop(scope, element, options) {
10306
10307 if (options.disableParentScroll) {
10308 // !! DO this before creating the backdrop; since disableScrollAround()
10309 // configures the scroll offset; which is used by mdBackDrop postLink()
10310 options.restoreScroll = $mdUtil.disableScrollAround(element, options.parent);
10311 }
10312
10313 if (options.hasBackdrop) {
10314 options.backdrop = $mdUtil.createBackdrop(scope, "md-dialog-backdrop md-opaque");
10315 $animate.enter(options.backdrop, options.parent);
10316 }
10317
10318 /**
10319 * Hide modal backdrop element...
10320 */
10321 options.hideBackdrop = function hideBackdrop($destroy) {
10322 if (options.backdrop) {
10323 if ( !!$destroy ) options.backdrop.remove();
10324 else $animate.leave(options.backdrop);
10325 }
10326
10327
10328 if (options.disableParentScroll) {
10329 options.restoreScroll && options.restoreScroll();
10330 delete options.restoreScroll;
10331 }
10332
10333 options.hideBackdrop = null;
10334 };
10335 }
10336
10337 /**
10338 * Inject ARIA-specific attributes appropriate for Dialogs
10339 */
10340 function configureAria(element, options) {
10341
10342 var role = (options.$type === 'alert') ? 'alertdialog' : 'dialog';
10343 var dialogContent = element.find('md-dialog-content');
10344 var existingDialogId = element.attr('id');
10345 var dialogContentId = 'dialogContent_' + (existingDialogId || $mdUtil.nextUid());
10346
10347 element.attr({
10348 'role': role,
10349 'tabIndex': '-1'
10350 });
10351
10352 if (dialogContent.length === 0) {
10353 dialogContent = element;
10354 // If the dialog element already had an ID, don't clobber it.
10355 if (existingDialogId) {
10356 dialogContentId = existingDialogId;
10357 }
10358 }
10359
10360 dialogContent.attr('id', dialogContentId);
10361 element.attr('aria-describedby', dialogContentId);
10362
10363 if (options.ariaLabel) {
10364 $mdAria.expect(element, 'aria-label', options.ariaLabel);
10365 }
10366 else {
10367 $mdAria.expectAsync(element, 'aria-label', function() {
10368 var words = dialogContent.text().split(/\s+/);
10369 if (words.length > 3) words = words.slice(0, 3).concat('...');
10370 return words.join(' ');
10371 });
10372 }
10373
10374 // Set up elements before and after the dialog content to capture focus and
10375 // redirect back into the dialog.
10376 topFocusTrap = document.createElement('div');
10377 topFocusTrap.classList.add('md-dialog-focus-trap');
10378 topFocusTrap.tabIndex = 0;
10379
10380 bottomFocusTrap = topFocusTrap.cloneNode(false);
10381
10382 // When focus is about to move out of the dialog, we want to intercept it and redirect it
10383 // back to the dialog element.
10384 var focusHandler = function() {
10385 element.focus();
10386 };
10387 topFocusTrap.addEventListener('focus', focusHandler);
10388 bottomFocusTrap.addEventListener('focus', focusHandler);
10389
10390 // The top focus trap inserted immeidately before the md-dialog element (as a sibling).
10391 // The bottom focus trap is inserted at the very end of the md-dialog element (as a child).
10392 element[0].parentNode.insertBefore(topFocusTrap, element[0]);
10393 element.after(bottomFocusTrap);
10394 }
10395
10396 /**
10397 * Prevents screen reader interaction behind modal window
10398 * on swipe interfaces
10399 */
10400 function lockScreenReader(element, options) {
10401 var isHidden = true;
10402
10403 // get raw DOM node
10404 walkDOM(element[0]);
10405
10406 options.unlockScreenReader = function() {
10407 isHidden = false;
10408 walkDOM(element[0]);
10409
10410 options.unlockScreenReader = null;
10411 };
10412
10413 /**
10414 * Walk DOM to apply or remove aria-hidden on sibling nodes
10415 * and parent sibling nodes
10416 *
10417 */
10418 function walkDOM(element) {
10419 while (element.parentNode) {
10420 if (element === document.body) {
10421 return;
10422 }
10423 var children = element.parentNode.children;
10424 for (var i = 0; i < children.length; i++) {
10425 // skip over child if it is an ascendant of the dialog
10426 // or a script or style tag
10427 if (element !== children[i] && !isNodeOneOf(children[i], ['SCRIPT', 'STYLE'])) {
10428 children[i].setAttribute('aria-hidden', isHidden);
10429 }
10430 }
10431
10432 walkDOM(element = element.parentNode);
10433 }
10434 }
10435 }
10436
10437 /**
10438 * Ensure the dialog container fill-stretches to the viewport
10439 */
10440 function stretchDialogContainerToViewport(container, options) {
10441 var isFixed = $window.getComputedStyle($document[0].body).position == 'fixed';
10442 var backdrop = options.backdrop ? $window.getComputedStyle(options.backdrop[0]) : null;
10443 var height = backdrop ? Math.min($document[0].body.clientHeight, Math.ceil(Math.abs(parseInt(backdrop.height, 10)))) : 0;
10444
10445 var previousStyles = {
10446 top: container.css('top'),
10447 height: container.css('height')
10448 };
10449
10450 // If the body is fixed, determine the distance to the viewport in relative from the parent.
10451 var parentTop = Math.abs(options.parent[0].getBoundingClientRect().top);
10452
10453 container.css({
10454 top: (isFixed ? parentTop : 0) + 'px',
10455 height: height ? height + 'px' : '100%'
10456 });
10457
10458 return function() {
10459 // Reverts the modified styles back to the previous values.
10460 // This is needed for contentElements, which should have the same styles after close
10461 // as before.
10462 container.css(previousStyles);
10463 };
10464 }
10465
10466 /**
10467 * Dialog open and pop-in animation
10468 */
10469 function dialogPopIn(container, options) {
10470 // Add the `md-dialog-container` to the DOM
10471 options.parent.append(container);
10472 options.reverseContainerStretch = stretchDialogContainerToViewport(container, options);
10473
10474 var dialogEl = container.find('md-dialog');
10475 var animator = $mdUtil.dom.animator;
10476 var buildTranslateToOrigin = animator.calculateZoomToOrigin;
10477 var translateOptions = {transitionInClass: 'md-transition-in', transitionOutClass: 'md-transition-out'};
10478 var from = animator.toTransformCss(buildTranslateToOrigin(dialogEl, options.openFrom || options.origin));
10479 var to = animator.toTransformCss(""); // defaults to center display (or parent or $rootElement)
10480
10481 dialogEl.toggleClass('md-dialog-fullscreen', !!options.fullscreen);
10482
10483 return animator
10484 .translate3d(dialogEl, from, to, translateOptions)
10485 .then(function(animateReversal) {
10486
10487 // Build a reversal translate function synced to this translation...
10488 options.reverseAnimate = function() {
10489 delete options.reverseAnimate;
10490
10491 if (options.closeTo) {
10492 // Using the opposite classes to create a close animation to the closeTo element
10493 translateOptions = {transitionInClass: 'md-transition-out', transitionOutClass: 'md-transition-in'};
10494 from = to;
10495 to = animator.toTransformCss(buildTranslateToOrigin(dialogEl, options.closeTo));
10496
10497 return animator
10498 .translate3d(dialogEl, from, to,translateOptions);
10499 }
10500
10501 return animateReversal(
10502 to = animator.toTransformCss(
10503 // in case the origin element has moved or is hidden,
10504 // let's recalculate the translateCSS
10505 buildTranslateToOrigin(dialogEl, options.origin)
10506 )
10507 );
10508
10509 };
10510
10511 // Function to revert the generated animation styles on the dialog element.
10512 // Useful when using a contentElement instead of a template.
10513 options.clearAnimate = function() {
10514 delete options.clearAnimate;
10515
10516 // Remove the transition classes, added from $animateCSS, since those can't be removed
10517 // by reversely running the animator.
10518 dialogEl.removeClass([
10519 translateOptions.transitionOutClass,
10520 translateOptions.transitionInClass
10521 ].join(' '));
10522
10523 // Run the animation reversely to remove the previous added animation styles.
10524 return animator.translate3d(dialogEl, to, animator.toTransformCss(''), {});
10525 };
10526
10527 return true;
10528 });
10529 }
10530
10531 /**
10532 * Dialog close and pop-out animation
10533 */
10534 function dialogPopOut(container, options) {
10535 return options.reverseAnimate().then(function() {
10536 if (options.contentElement) {
10537 // When we use a contentElement, we want the element to be the same as before.
10538 // That means, that we have to clear all the animation properties, like transform.
10539 options.clearAnimate();
10540 }
10541 });
10542 }
10543
10544 /**
10545 * Utility function to filter out raw DOM nodes
10546 */
10547 function isNodeOneOf(elem, nodeTypeArray) {
10548 if (nodeTypeArray.indexOf(elem.nodeName) !== -1) {
10549 return true;
10550 }
10551 }
10552
10553 }
10554}
10555
10556})();
10557(function(){
10558"use strict";
10559
10560/**
10561 * @ngdoc module
10562 * @name material.components.divider
10563 * @description Divider module!
10564 */
10565MdDividerDirective.$inject = ["$mdTheming"];
10566angular.module('material.components.divider', [
10567 'material.core'
10568])
10569 .directive('mdDivider', MdDividerDirective);
10570
10571/**
10572 * @ngdoc directive
10573 * @name mdDivider
10574 * @module material.components.divider
10575 * @restrict E
10576 *
10577 * @description
10578 * 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.
10579 *
10580 * @param {boolean=} md-inset Add this attribute to activate the inset divider style.
10581 * @usage
10582 * <hljs lang="html">
10583 * <md-divider></md-divider>
10584 *
10585 * <md-divider md-inset></md-divider>
10586 * </hljs>
10587 *
10588 */
10589function MdDividerDirective($mdTheming) {
10590 return {
10591 restrict: 'E',
10592 link: $mdTheming
10593 };
10594}
10595
10596})();
10597(function(){
10598"use strict";
10599
10600(function() {
10601 'use strict';
10602
10603 /**
10604 * @ngdoc module
10605 * @name material.components.fabActions
10606 */
10607 MdFabActionsDirective.$inject = ["$mdUtil"];
10608 angular
10609 .module('material.components.fabActions', ['material.core'])
10610 .directive('mdFabActions', MdFabActionsDirective);
10611
10612 /**
10613 * @ngdoc directive
10614 * @name mdFabActions
10615 * @module material.components.fabActions
10616 *
10617 * @restrict E
10618 *
10619 * @description
10620 * The `<md-fab-actions>` directive is used inside of a `<md-fab-speed-dial>` or
10621 * `<md-fab-toolbar>` directive to mark an element (or elements) as the actions and setup the
10622 * proper event listeners.
10623 *
10624 * @usage
10625 * See the `<md-fab-speed-dial>` or `<md-fab-toolbar>` directives for example usage.
10626 */
10627 function MdFabActionsDirective($mdUtil) {
10628 return {
10629 restrict: 'E',
10630
10631 require: ['^?mdFabSpeedDial', '^?mdFabToolbar'],
10632
10633 compile: function(element, attributes) {
10634 var children = element.children();
10635
10636 var hasNgRepeat = $mdUtil.prefixer().hasAttribute(children, 'ng-repeat');
10637
10638 // Support both ng-repeat and static content
10639 if (hasNgRepeat) {
10640 children.addClass('md-fab-action-item');
10641 } else {
10642 // Wrap every child in a new div and add a class that we can scale/fling independently
10643 children.wrap('<div class="md-fab-action-item">');
10644 }
10645 }
10646 };
10647 }
10648
10649})();
10650
10651})();
10652(function(){
10653"use strict";
10654
10655(function() {
10656 'use strict';
10657
10658 MdFabController.$inject = ["$scope", "$element", "$animate", "$mdUtil", "$mdConstant", "$timeout"];
10659 angular.module('material.components.fabShared', ['material.core'])
10660 .controller('MdFabController', MdFabController);
10661
10662 function MdFabController($scope, $element, $animate, $mdUtil, $mdConstant, $timeout) {
10663 var vm = this;
10664 var initialAnimationAttempts = 0;
10665
10666 // NOTE: We use async eval(s) below to avoid conflicts with any existing digest loops
10667
10668 vm.open = function() {
10669 $scope.$evalAsync("vm.isOpen = true");
10670 };
10671
10672 vm.close = function() {
10673 // Async eval to avoid conflicts with existing digest loops
10674 $scope.$evalAsync("vm.isOpen = false");
10675
10676 // Focus the trigger when the element closes so users can still tab to the next item
10677 $element.find('md-fab-trigger')[0].focus();
10678 };
10679
10680 // Toggle the open/close state when the trigger is clicked
10681 vm.toggle = function() {
10682 $scope.$evalAsync("vm.isOpen = !vm.isOpen");
10683 };
10684
10685 /*
10686 * Angular Lifecycle hook for newer Angular versions.
10687 * Bindings are not guaranteed to have been assigned in the controller, but they are in the $onInit hook.
10688 */
10689 vm.$onInit = function() {
10690 setupDefaults();
10691 setupListeners();
10692 setupWatchers();
10693
10694 fireInitialAnimations();
10695 };
10696
10697 // For Angular 1.4 and older, where there are no lifecycle hooks but bindings are pre-assigned,
10698 // manually call the $onInit hook.
10699 if (angular.version.major === 1 && angular.version.minor <= 4) {
10700 this.$onInit();
10701 }
10702
10703 function setupDefaults() {
10704 // Set the default direction to 'down' if none is specified
10705 vm.direction = vm.direction || 'down';
10706
10707 // Set the default to be closed
10708 vm.isOpen = vm.isOpen || false;
10709
10710 // Start the keyboard interaction at the first action
10711 resetActionIndex();
10712
10713 // Add an animations waiting class so we know not to run
10714 $element.addClass('md-animations-waiting');
10715 }
10716
10717 function setupListeners() {
10718 var eventTypes = [
10719 'click', 'focusin', 'focusout'
10720 ];
10721
10722 // Add our listeners
10723 angular.forEach(eventTypes, function(eventType) {
10724 $element.on(eventType, parseEvents);
10725 });
10726
10727 // Remove our listeners when destroyed
10728 $scope.$on('$destroy', function() {
10729 angular.forEach(eventTypes, function(eventType) {
10730 $element.off(eventType, parseEvents);
10731 });
10732
10733 // remove any attached keyboard handlers in case element is removed while
10734 // speed dial is open
10735 disableKeyboard();
10736 });
10737 }
10738
10739 var closeTimeout;
10740 function parseEvents(event) {
10741 // If the event is a click, just handle it
10742 if (event.type == 'click') {
10743 handleItemClick(event);
10744 }
10745
10746 // If we focusout, set a timeout to close the element
10747 if (event.type == 'focusout' && !closeTimeout) {
10748 closeTimeout = $timeout(function() {
10749 vm.close();
10750 }, 100, false);
10751 }
10752
10753 // If we see a focusin and there is a timeout about to run, cancel it so we stay open
10754 if (event.type == 'focusin' && closeTimeout) {
10755 $timeout.cancel(closeTimeout);
10756 closeTimeout = null;
10757 }
10758 }
10759
10760 function resetActionIndex() {
10761 vm.currentActionIndex = -1;
10762 }
10763
10764 function setupWatchers() {
10765 // Watch for changes to the direction and update classes/attributes
10766 $scope.$watch('vm.direction', function(newDir, oldDir) {
10767 // Add the appropriate classes so we can target the direction in the CSS
10768 $animate.removeClass($element, 'md-' + oldDir);
10769 $animate.addClass($element, 'md-' + newDir);
10770
10771 // Reset the action index since it may have changed
10772 resetActionIndex();
10773 });
10774
10775 var trigger, actions;
10776
10777 // Watch for changes to md-open
10778 $scope.$watch('vm.isOpen', function(isOpen) {
10779 // Reset the action index since it may have changed
10780 resetActionIndex();
10781
10782 // We can't get the trigger/actions outside of the watch because the component hasn't been
10783 // linked yet, so we wait until the first watch fires to cache them.
10784 if (!trigger || !actions) {
10785 trigger = getTriggerElement();
10786 actions = getActionsElement();
10787 }
10788
10789 if (isOpen) {
10790 enableKeyboard();
10791 } else {
10792 disableKeyboard();
10793 }
10794
10795 var toAdd = isOpen ? 'md-is-open' : '';
10796 var toRemove = isOpen ? '' : 'md-is-open';
10797
10798 // Set the proper ARIA attributes
10799 trigger.attr('aria-haspopup', true);
10800 trigger.attr('aria-expanded', isOpen);
10801 actions.attr('aria-hidden', !isOpen);
10802
10803 // Animate the CSS classes
10804 $animate.setClass($element, toAdd, toRemove);
10805 });
10806 }
10807
10808 function fireInitialAnimations() {
10809 // If the element is actually visible on the screen
10810 if ($element[0].scrollHeight > 0) {
10811 // Fire our animation
10812 $animate.addClass($element, '_md-animations-ready').then(function() {
10813 // Remove the waiting class
10814 $element.removeClass('md-animations-waiting');
10815 });
10816 }
10817
10818 // Otherwise, try for up to 1 second before giving up
10819 else if (initialAnimationAttempts < 10) {
10820 $timeout(fireInitialAnimations, 100);
10821
10822 // Increment our counter
10823 initialAnimationAttempts = initialAnimationAttempts + 1;
10824 }
10825 }
10826
10827 function enableKeyboard() {
10828 $element.on('keydown', keyPressed);
10829
10830 // On the next tick, setup a check for outside clicks; we do this on the next tick to avoid
10831 // clicks/touches that result in the isOpen attribute changing (e.g. a bound radio button)
10832 $mdUtil.nextTick(function() {
10833 angular.element(document).on('click touchend', checkForOutsideClick);
10834 });
10835
10836 // TODO: On desktop, we should be able to reset the indexes so you cannot tab through, but
10837 // this breaks accessibility, especially on mobile, since you have no arrow keys to press
10838 //resetActionTabIndexes();
10839 }
10840
10841 function disableKeyboard() {
10842 $element.off('keydown', keyPressed);
10843 angular.element(document).off('click touchend', checkForOutsideClick);
10844 }
10845
10846 function checkForOutsideClick(event) {
10847 if (event.target) {
10848 var closestTrigger = $mdUtil.getClosest(event.target, 'md-fab-trigger');
10849 var closestActions = $mdUtil.getClosest(event.target, 'md-fab-actions');
10850
10851 if (!closestTrigger && !closestActions) {
10852 vm.close();
10853 }
10854 }
10855 }
10856
10857 function keyPressed(event) {
10858 switch (event.which) {
10859 case $mdConstant.KEY_CODE.ESCAPE: vm.close(); event.preventDefault(); return false;
10860 case $mdConstant.KEY_CODE.LEFT_ARROW: doKeyLeft(event); return false;
10861 case $mdConstant.KEY_CODE.UP_ARROW: doKeyUp(event); return false;
10862 case $mdConstant.KEY_CODE.RIGHT_ARROW: doKeyRight(event); return false;
10863 case $mdConstant.KEY_CODE.DOWN_ARROW: doKeyDown(event); return false;
10864 }
10865 }
10866
10867 function doActionPrev(event) {
10868 focusAction(event, -1);
10869 }
10870
10871 function doActionNext(event) {
10872 focusAction(event, 1);
10873 }
10874
10875 function focusAction(event, direction) {
10876 var actions = resetActionTabIndexes();
10877
10878 // Increment/decrement the counter with restrictions
10879 vm.currentActionIndex = vm.currentActionIndex + direction;
10880 vm.currentActionIndex = Math.min(actions.length - 1, vm.currentActionIndex);
10881 vm.currentActionIndex = Math.max(0, vm.currentActionIndex);
10882
10883 // Focus the element
10884 var focusElement = angular.element(actions[vm.currentActionIndex]).children()[0];
10885 angular.element(focusElement).attr('tabindex', 0);
10886 focusElement.focus();
10887
10888 // Make sure the event doesn't bubble and cause something else
10889 event.preventDefault();
10890 event.stopImmediatePropagation();
10891 }
10892
10893 function resetActionTabIndexes() {
10894 // Grab all of the actions
10895 var actions = getActionsElement()[0].querySelectorAll('.md-fab-action-item');
10896
10897 // Disable all other actions for tabbing
10898 angular.forEach(actions, function(action) {
10899 angular.element(angular.element(action).children()[0]).attr('tabindex', -1);
10900 });
10901
10902 return actions;
10903 }
10904
10905 function doKeyLeft(event) {
10906 if (vm.direction === 'left') {
10907 doActionNext(event);
10908 } else {
10909 doActionPrev(event);
10910 }
10911 }
10912
10913 function doKeyUp(event) {
10914 if (vm.direction === 'down') {
10915 doActionPrev(event);
10916 } else {
10917 doActionNext(event);
10918 }
10919 }
10920
10921 function doKeyRight(event) {
10922 if (vm.direction === 'left') {
10923 doActionPrev(event);
10924 } else {
10925 doActionNext(event);
10926 }
10927 }
10928
10929 function doKeyDown(event) {
10930 if (vm.direction === 'up') {
10931 doActionPrev(event);
10932 } else {
10933 doActionNext(event);
10934 }
10935 }
10936
10937 function isTrigger(element) {
10938 return $mdUtil.getClosest(element, 'md-fab-trigger');
10939 }
10940
10941 function isAction(element) {
10942 return $mdUtil.getClosest(element, 'md-fab-actions');
10943 }
10944
10945 function handleItemClick(event) {
10946 if (isTrigger(event.target)) {
10947 vm.toggle();
10948 }
10949
10950 if (isAction(event.target)) {
10951 vm.close();
10952 }
10953 }
10954
10955 function getTriggerElement() {
10956 return $element.find('md-fab-trigger');
10957 }
10958
10959 function getActionsElement() {
10960 return $element.find('md-fab-actions');
10961 }
10962 }
10963})();
10964
10965})();
10966(function(){
10967"use strict";
10968
10969(function() {
10970 'use strict';
10971
10972 /**
10973 * The duration of the CSS animation in milliseconds.
10974 *
10975 * @type {number}
10976 */
10977 MdFabSpeedDialFlingAnimation.$inject = ["$timeout"];
10978 MdFabSpeedDialScaleAnimation.$inject = ["$timeout"];
10979 var cssAnimationDuration = 300;
10980
10981 /**
10982 * @ngdoc module
10983 * @name material.components.fabSpeedDial
10984 */
10985 angular
10986 // Declare our module
10987 .module('material.components.fabSpeedDial', [
10988 'material.core',
10989 'material.components.fabShared',
10990 'material.components.fabActions'
10991 ])
10992
10993 // Register our directive
10994 .directive('mdFabSpeedDial', MdFabSpeedDialDirective)
10995
10996 // Register our custom animations
10997 .animation('.md-fling', MdFabSpeedDialFlingAnimation)
10998 .animation('.md-scale', MdFabSpeedDialScaleAnimation)
10999
11000 // Register a service for each animation so that we can easily inject them into unit tests
11001 .service('mdFabSpeedDialFlingAnimation', MdFabSpeedDialFlingAnimation)
11002 .service('mdFabSpeedDialScaleAnimation', MdFabSpeedDialScaleAnimation);
11003
11004 /**
11005 * @ngdoc directive
11006 * @name mdFabSpeedDial
11007 * @module material.components.fabSpeedDial
11008 *
11009 * @restrict E
11010 *
11011 * @description
11012 * The `<md-fab-speed-dial>` directive is used to present a series of popup elements (usually
11013 * `<md-button>`s) for quick access to common actions.
11014 *
11015 * There are currently two animations available by applying one of the following classes to
11016 * the component:
11017 *
11018 * - `md-fling` - The speed dial items appear from underneath the trigger and move into their
11019 * appropriate positions.
11020 * - `md-scale` - The speed dial items appear in their proper places by scaling from 0% to 100%.
11021 *
11022 * You may also easily position the trigger by applying one one of the following classes to the
11023 * `<md-fab-speed-dial>` element:
11024 * - `md-fab-top-left`
11025 * - `md-fab-top-right`
11026 * - `md-fab-bottom-left`
11027 * - `md-fab-bottom-right`
11028 *
11029 * These CSS classes use `position: absolute`, so you need to ensure that the container element
11030 * also uses `position: absolute` or `position: relative` in order for them to work.
11031 *
11032 * Additionally, you may use the standard `ng-mouseenter` and `ng-mouseleave` directives to
11033 * open or close the speed dial. However, if you wish to allow users to hover over the empty
11034 * space where the actions will appear, you must also add the `md-hover-full` class to the speed
11035 * dial element. Without this, the hover effect will only occur on top of the trigger.
11036 *
11037 * See the demos for more information.
11038 *
11039 * ## Troubleshooting
11040 *
11041 * If your speed dial shows the closing animation upon launch, you may need to use `ng-cloak` on
11042 * the parent container to ensure that it is only visible once ready. We have plans to remove this
11043 * necessity in the future.
11044 *
11045 * @usage
11046 * <hljs lang="html">
11047 * <md-fab-speed-dial md-direction="up" class="md-fling">
11048 * <md-fab-trigger>
11049 * <md-button aria-label="Add..."><md-icon md-svg-src="/img/icons/plus.svg"></md-icon></md-button>
11050 * </md-fab-trigger>
11051 *
11052 * <md-fab-actions>
11053 * <md-button aria-label="Add User">
11054 * <md-icon md-svg-src="/img/icons/user.svg"></md-icon>
11055 * </md-button>
11056 *
11057 * <md-button aria-label="Add Group">
11058 * <md-icon md-svg-src="/img/icons/group.svg"></md-icon>
11059 * </md-button>
11060 * </md-fab-actions>
11061 * </md-fab-speed-dial>
11062 * </hljs>
11063 *
11064 * @param {string} md-direction From which direction you would like the speed dial to appear
11065 * relative to the trigger element.
11066 * @param {expression=} md-open Programmatically control whether or not the speed-dial is visible.
11067 */
11068 function MdFabSpeedDialDirective() {
11069 return {
11070 restrict: 'E',
11071
11072 scope: {
11073 direction: '@?mdDirection',
11074 isOpen: '=?mdOpen'
11075 },
11076
11077 bindToController: true,
11078 controller: 'MdFabController',
11079 controllerAs: 'vm',
11080
11081 link: FabSpeedDialLink
11082 };
11083
11084 function FabSpeedDialLink(scope, element) {
11085 // Prepend an element to hold our CSS variables so we can use them in the animations below
11086 element.prepend('<div class="_md-css-variables"></div>');
11087 }
11088 }
11089
11090 function MdFabSpeedDialFlingAnimation($timeout) {
11091 function delayDone(done) { $timeout(done, cssAnimationDuration, false); }
11092
11093 function runAnimation(element) {
11094 // Don't run if we are still waiting and we are not ready
11095 if (element.hasClass('md-animations-waiting') && !element.hasClass('_md-animations-ready')) {
11096 return;
11097 }
11098
11099 var el = element[0];
11100 var ctrl = element.controller('mdFabSpeedDial');
11101 var items = el.querySelectorAll('.md-fab-action-item');
11102
11103 // Grab our trigger element
11104 var triggerElement = el.querySelector('md-fab-trigger');
11105
11106 // Grab our element which stores CSS variables
11107 var variablesElement = el.querySelector('._md-css-variables');
11108
11109 // Setup JS variables based on our CSS variables
11110 var startZIndex = parseInt(window.getComputedStyle(variablesElement).zIndex);
11111
11112 // Always reset the items to their natural position/state
11113 angular.forEach(items, function(item, index) {
11114 var styles = item.style;
11115
11116 styles.transform = styles.webkitTransform = '';
11117 styles.transitionDelay = '';
11118 styles.opacity = 1;
11119
11120 // Make the items closest to the trigger have the highest z-index
11121 styles.zIndex = (items.length - index) + startZIndex;
11122 });
11123
11124 // Set the trigger to be above all of the actions so they disappear behind it.
11125 triggerElement.style.zIndex = startZIndex + items.length + 1;
11126
11127 // If the control is closed, hide the items behind the trigger
11128 if (!ctrl.isOpen) {
11129 angular.forEach(items, function(item, index) {
11130 var newPosition, axis;
11131 var styles = item.style;
11132
11133 // Make sure to account for differences in the dimensions of the trigger verses the items
11134 // so that we can properly center everything; this helps hide the item's shadows behind
11135 // the trigger.
11136 var triggerItemHeightOffset = (triggerElement.clientHeight - item.clientHeight) / 2;
11137 var triggerItemWidthOffset = (triggerElement.clientWidth - item.clientWidth) / 2;
11138
11139 switch (ctrl.direction) {
11140 case 'up':
11141 newPosition = (item.scrollHeight * (index + 1) + triggerItemHeightOffset);
11142 axis = 'Y';
11143 break;
11144 case 'down':
11145 newPosition = -(item.scrollHeight * (index + 1) + triggerItemHeightOffset);
11146 axis = 'Y';
11147 break;
11148 case 'left':
11149 newPosition = (item.scrollWidth * (index + 1) + triggerItemWidthOffset);
11150 axis = 'X';
11151 break;
11152 case 'right':
11153 newPosition = -(item.scrollWidth * (index + 1) + triggerItemWidthOffset);
11154 axis = 'X';
11155 break;
11156 }
11157
11158 var newTranslate = 'translate' + axis + '(' + newPosition + 'px)';
11159
11160 styles.transform = styles.webkitTransform = newTranslate;
11161 });
11162 }
11163 }
11164
11165 return {
11166 addClass: function(element, className, done) {
11167 if (element.hasClass('md-fling')) {
11168 runAnimation(element);
11169 delayDone(done);
11170 } else {
11171 done();
11172 }
11173 },
11174 removeClass: function(element, className, done) {
11175 runAnimation(element);
11176 delayDone(done);
11177 }
11178 };
11179 }
11180
11181 function MdFabSpeedDialScaleAnimation($timeout) {
11182 function delayDone(done) { $timeout(done, cssAnimationDuration, false); }
11183
11184 var delay = 65;
11185
11186 function runAnimation(element) {
11187 var el = element[0];
11188 var ctrl = element.controller('mdFabSpeedDial');
11189 var items = el.querySelectorAll('.md-fab-action-item');
11190
11191 // Grab our element which stores CSS variables
11192 var variablesElement = el.querySelector('._md-css-variables');
11193
11194 // Setup JS variables based on our CSS variables
11195 var startZIndex = parseInt(window.getComputedStyle(variablesElement).zIndex);
11196
11197 // Always reset the items to their natural position/state
11198 angular.forEach(items, function(item, index) {
11199 var styles = item.style,
11200 offsetDelay = index * delay;
11201
11202 styles.opacity = ctrl.isOpen ? 1 : 0;
11203 styles.transform = styles.webkitTransform = ctrl.isOpen ? 'scale(1)' : 'scale(0)';
11204 styles.transitionDelay = (ctrl.isOpen ? offsetDelay : (items.length - offsetDelay)) + 'ms';
11205
11206 // Make the items closest to the trigger have the highest z-index
11207 styles.zIndex = (items.length - index) + startZIndex;
11208 });
11209 }
11210
11211 return {
11212 addClass: function(element, className, done) {
11213 runAnimation(element);
11214 delayDone(done);
11215 },
11216
11217 removeClass: function(element, className, done) {
11218 runAnimation(element);
11219 delayDone(done);
11220 }
11221 };
11222 }
11223})();
11224
11225})();
11226(function(){
11227"use strict";
11228
11229(function() {
11230 'use strict';
11231
11232 /**
11233 * @ngdoc module
11234 * @name material.components.fabToolbar
11235 */
11236 angular
11237 // Declare our module
11238 .module('material.components.fabToolbar', [
11239 'material.core',
11240 'material.components.fabShared',
11241 'material.components.fabActions'
11242 ])
11243
11244 // Register our directive
11245 .directive('mdFabToolbar', MdFabToolbarDirective)
11246
11247 // Register our custom animations
11248 .animation('.md-fab-toolbar', MdFabToolbarAnimation)
11249
11250 // Register a service for the animation so that we can easily inject it into unit tests
11251 .service('mdFabToolbarAnimation', MdFabToolbarAnimation);
11252
11253 /**
11254 * @ngdoc directive
11255 * @name mdFabToolbar
11256 * @module material.components.fabToolbar
11257 *
11258 * @restrict E
11259 *
11260 * @description
11261 *
11262 * The `<md-fab-toolbar>` directive is used to present a toolbar of elements (usually `<md-button>`s)
11263 * for quick access to common actions when a floating action button is activated (via click or
11264 * keyboard navigation).
11265 *
11266 * You may also easily position the trigger by applying one one of the following classes to the
11267 * `<md-fab-toolbar>` element:
11268 * - `md-fab-top-left`
11269 * - `md-fab-top-right`
11270 * - `md-fab-bottom-left`
11271 * - `md-fab-bottom-right`
11272 *
11273 * These CSS classes use `position: absolute`, so you need to ensure that the container element
11274 * also uses `position: absolute` or `position: relative` in order for them to work.
11275 *
11276 * @usage
11277 *
11278 * <hljs lang="html">
11279 * <md-fab-toolbar md-direction='left'>
11280 * <md-fab-trigger>
11281 * <md-button aria-label="Add..."><md-icon md-svg-src="/img/icons/plus.svg"></md-icon></md-button>
11282 * </md-fab-trigger>
11283 *
11284 * <md-toolbar>
11285 * <md-fab-actions>
11286 * <md-button aria-label="Add User">
11287 * <md-icon md-svg-src="/img/icons/user.svg"></md-icon>
11288 * </md-button>
11289 *
11290 * <md-button aria-label="Add Group">
11291 * <md-icon md-svg-src="/img/icons/group.svg"></md-icon>
11292 * </md-button>
11293 * </md-fab-actions>
11294 * </md-toolbar>
11295 * </md-fab-toolbar>
11296 * </hljs>
11297 *
11298 * @param {string} md-direction From which direction you would like the toolbar items to appear
11299 * relative to the trigger element. Supports `left` and `right` directions.
11300 * @param {expression=} md-open Programmatically control whether or not the toolbar is visible.
11301 */
11302 function MdFabToolbarDirective() {
11303 return {
11304 restrict: 'E',
11305 transclude: true,
11306 template: '<div class="md-fab-toolbar-wrapper">' +
11307 ' <div class="md-fab-toolbar-content" ng-transclude></div>' +
11308 '</div>',
11309
11310 scope: {
11311 direction: '@?mdDirection',
11312 isOpen: '=?mdOpen'
11313 },
11314
11315 bindToController: true,
11316 controller: 'MdFabController',
11317 controllerAs: 'vm',
11318
11319 link: link
11320 };
11321
11322 function link(scope, element, attributes) {
11323 // Add the base class for animations
11324 element.addClass('md-fab-toolbar');
11325
11326 // Prepend the background element to the trigger's button
11327 element.find('md-fab-trigger').find('button')
11328 .prepend('<div class="md-fab-toolbar-background"></div>');
11329 }
11330 }
11331
11332 function MdFabToolbarAnimation() {
11333
11334 function runAnimation(element, className, done) {
11335 // If no className was specified, don't do anything
11336 if (!className) {
11337 return;
11338 }
11339
11340 var el = element[0];
11341 var ctrl = element.controller('mdFabToolbar');
11342
11343 // Grab the relevant child elements
11344 var backgroundElement = el.querySelector('.md-fab-toolbar-background');
11345 var triggerElement = el.querySelector('md-fab-trigger button');
11346 var toolbarElement = el.querySelector('md-toolbar');
11347 var iconElement = el.querySelector('md-fab-trigger button md-icon');
11348 var actions = element.find('md-fab-actions').children();
11349
11350 // If we have both elements, use them to position the new background
11351 if (triggerElement && backgroundElement) {
11352 // Get our variables
11353 var color = window.getComputedStyle(triggerElement).getPropertyValue('background-color');
11354 var width = el.offsetWidth;
11355 var height = el.offsetHeight;
11356
11357 // Make it twice as big as it should be since we scale from the center
11358 var scale = 2 * (width / triggerElement.offsetWidth);
11359
11360 // Set some basic styles no matter what animation we're doing
11361 backgroundElement.style.backgroundColor = color;
11362 backgroundElement.style.borderRadius = width + 'px';
11363
11364 // If we're open
11365 if (ctrl.isOpen) {
11366 // Turn on toolbar pointer events when closed
11367 toolbarElement.style.pointerEvents = 'inherit';
11368
11369 backgroundElement.style.width = triggerElement.offsetWidth + 'px';
11370 backgroundElement.style.height = triggerElement.offsetHeight + 'px';
11371 backgroundElement.style.transform = 'scale(' + scale + ')';
11372
11373 // Set the next close animation to have the proper delays
11374 backgroundElement.style.transitionDelay = '0ms';
11375 iconElement && (iconElement.style.transitionDelay = '.3s');
11376
11377 // Apply a transition delay to actions
11378 angular.forEach(actions, function(action, index) {
11379 action.style.transitionDelay = (actions.length - index) * 25 + 'ms';
11380 });
11381 } else {
11382 // Turn off toolbar pointer events when closed
11383 toolbarElement.style.pointerEvents = 'none';
11384
11385 // Scale it back down to the trigger's size
11386 backgroundElement.style.transform = 'scale(1)';
11387
11388 // Reset the position
11389 backgroundElement.style.top = '0';
11390
11391 if (element.hasClass('md-right')) {
11392 backgroundElement.style.left = '0';
11393 backgroundElement.style.right = null;
11394 }
11395
11396 if (element.hasClass('md-left')) {
11397 backgroundElement.style.right = '0';
11398 backgroundElement.style.left = null;
11399 }
11400
11401 // Set the next open animation to have the proper delays
11402 backgroundElement.style.transitionDelay = '200ms';
11403 iconElement && (iconElement.style.transitionDelay = '0ms');
11404
11405 // Apply a transition delay to actions
11406 angular.forEach(actions, function(action, index) {
11407 action.style.transitionDelay = 200 + (index * 25) + 'ms';
11408 });
11409 }
11410 }
11411 }
11412
11413 return {
11414 addClass: function(element, className, done) {
11415 runAnimation(element, className, done);
11416 done();
11417 },
11418
11419 removeClass: function(element, className, done) {
11420 runAnimation(element, className, done);
11421 done();
11422 }
11423 };
11424 }
11425})();
11426
11427})();
11428(function(){
11429"use strict";
11430
11431/**
11432 * @ngdoc module
11433 * @name material.components.gridList
11434 */
11435GridListController.$inject = ["$mdUtil"];
11436GridLayoutFactory.$inject = ["$mdUtil"];
11437GridListDirective.$inject = ["$interpolate", "$mdConstant", "$mdGridLayout", "$mdMedia"];
11438GridTileDirective.$inject = ["$mdMedia"];
11439angular.module('material.components.gridList', ['material.core'])
11440 .directive('mdGridList', GridListDirective)
11441 .directive('mdGridTile', GridTileDirective)
11442 .directive('mdGridTileFooter', GridTileCaptionDirective)
11443 .directive('mdGridTileHeader', GridTileCaptionDirective)
11444 .factory('$mdGridLayout', GridLayoutFactory);
11445
11446/**
11447 * @ngdoc directive
11448 * @name mdGridList
11449 * @module material.components.gridList
11450 * @restrict E
11451 * @description
11452 * Grid lists are an alternative to standard list views. Grid lists are distinct
11453 * from grids used for layouts and other visual presentations.
11454 *
11455 * A grid list is best suited to presenting a homogenous data type, typically
11456 * images, and is optimized for visual comprehension and differentiating between
11457 * like data types.
11458 *
11459 * A grid list is a continuous element consisting of tessellated, regular
11460 * subdivisions called cells that contain tiles (`md-grid-tile`).
11461 *
11462 * <img src="//material-design.storage.googleapis.com/publish/v_2/material_ext_publish/0Bx4BSt6jniD7OVlEaXZ5YmU1Xzg/components_grids_usage2.png"
11463 * style="width: 300px; height: auto; margin-right: 16px;" alt="Concept of grid explained visually">
11464 * <img src="//material-design.storage.googleapis.com/publish/v_2/material_ext_publish/0Bx4BSt6jniD7VGhsOE5idWlJWXM/components_grids_usage3.png"
11465 * style="width: 300px; height: auto;" alt="Grid concepts legend">
11466 *
11467 * Cells are arrayed vertically and horizontally within the grid.
11468 *
11469 * Tiles hold content and can span one or more cells vertically or horizontally.
11470 *
11471 * ### Responsive Attributes
11472 *
11473 * The `md-grid-list` directive supports "responsive" attributes, which allow
11474 * different `md-cols`, `md-gutter` and `md-row-height` values depending on the
11475 * currently matching media query.
11476 *
11477 * In order to set a responsive attribute, first define the fallback value with
11478 * the standard attribute name, then add additional attributes with the
11479 * following convention: `{base-attribute-name}-{media-query-name}="{value}"`
11480 * (ie. `md-cols-lg="8"`)
11481 *
11482 * @param {number} md-cols Number of columns in the grid.
11483 * @param {string} md-row-height One of
11484 * <ul>
11485 * <li>CSS length - Fixed height rows (eg. `8px` or `1rem`)</li>
11486 * <li>`{width}:{height}` - Ratio of width to height (eg.
11487 * `md-row-height="16:9"`)</li>
11488 * <li>`"fit"` - Height will be determined by subdividing the available
11489 * height by the number of rows</li>
11490 * </ul>
11491 * @param {string=} md-gutter The amount of space between tiles in CSS units
11492 * (default 1px)
11493 * @param {expression=} md-on-layout Expression to evaluate after layout. Event
11494 * object is available as `$event`, and contains performance information.
11495 *
11496 * @usage
11497 * Basic:
11498 * <hljs lang="html">
11499 * <md-grid-list md-cols="5" md-gutter="1em" md-row-height="4:3">
11500 * <md-grid-tile></md-grid-tile>
11501 * </md-grid-list>
11502 * </hljs>
11503 *
11504 * Fixed-height rows:
11505 * <hljs lang="html">
11506 * <md-grid-list md-cols="4" md-row-height="200px" ...>
11507 * <md-grid-tile></md-grid-tile>
11508 * </md-grid-list>
11509 * </hljs>
11510 *
11511 * Fit rows:
11512 * <hljs lang="html">
11513 * <md-grid-list md-cols="4" md-row-height="fit" style="height: 400px;" ...>
11514 * <md-grid-tile></md-grid-tile>
11515 * </md-grid-list>
11516 * </hljs>
11517 *
11518 * Using responsive attributes:
11519 * <hljs lang="html">
11520 * <md-grid-list
11521 * md-cols-sm="2"
11522 * md-cols-md="4"
11523 * md-cols-lg="8"
11524 * md-cols-gt-lg="12"
11525 * ...>
11526 * <md-grid-tile></md-grid-tile>
11527 * </md-grid-list>
11528 * </hljs>
11529 */
11530function GridListDirective($interpolate, $mdConstant, $mdGridLayout, $mdMedia) {
11531 return {
11532 restrict: 'E',
11533 controller: GridListController,
11534 scope: {
11535 mdOnLayout: '&'
11536 },
11537 link: postLink
11538 };
11539
11540 function postLink(scope, element, attrs, ctrl) {
11541 element.addClass('_md'); // private md component indicator for styling
11542
11543 // Apply semantics
11544 element.attr('role', 'list');
11545
11546 // Provide the controller with a way to trigger layouts.
11547 ctrl.layoutDelegate = layoutDelegate;
11548
11549 var invalidateLayout = angular.bind(ctrl, ctrl.invalidateLayout),
11550 unwatchAttrs = watchMedia();
11551 scope.$on('$destroy', unwatchMedia);
11552
11553 /**
11554 * Watches for changes in media, invalidating layout as necessary.
11555 */
11556 function watchMedia() {
11557 for (var mediaName in $mdConstant.MEDIA) {
11558 $mdMedia(mediaName); // initialize
11559 $mdMedia.getQuery($mdConstant.MEDIA[mediaName])
11560 .addListener(invalidateLayout);
11561 }
11562 return $mdMedia.watchResponsiveAttributes(
11563 ['md-cols', 'md-row-height', 'md-gutter'], attrs, layoutIfMediaMatch);
11564 }
11565
11566 function unwatchMedia() {
11567 ctrl.layoutDelegate = angular.noop;
11568
11569 unwatchAttrs();
11570 for (var mediaName in $mdConstant.MEDIA) {
11571 $mdMedia.getQuery($mdConstant.MEDIA[mediaName])
11572 .removeListener(invalidateLayout);
11573 }
11574 }
11575
11576 /**
11577 * Performs grid layout if the provided mediaName matches the currently
11578 * active media type.
11579 */
11580 function layoutIfMediaMatch(mediaName) {
11581 if (mediaName == null) {
11582 // TODO(shyndman): It would be nice to only layout if we have
11583 // instances of attributes using this media type
11584 ctrl.invalidateLayout();
11585 } else if ($mdMedia(mediaName)) {
11586 ctrl.invalidateLayout();
11587 }
11588 }
11589
11590 var lastLayoutProps;
11591
11592 /**
11593 * Invokes the layout engine, and uses its results to lay out our
11594 * tile elements.
11595 *
11596 * @param {boolean} tilesInvalidated Whether tiles have been
11597 * added/removed/moved since the last layout. This is to avoid situations
11598 * where tiles are replaced with properties identical to their removed
11599 * counterparts.
11600 */
11601 function layoutDelegate(tilesInvalidated) {
11602 var tiles = getTileElements();
11603 var props = {
11604 tileSpans: getTileSpans(tiles),
11605 colCount: getColumnCount(),
11606 rowMode: getRowMode(),
11607 rowHeight: getRowHeight(),
11608 gutter: getGutter()
11609 };
11610
11611 if (!tilesInvalidated && angular.equals(props, lastLayoutProps)) {
11612 return;
11613 }
11614
11615 var performance =
11616 $mdGridLayout(props.colCount, props.tileSpans, tiles)
11617 .map(function(tilePositions, rowCount) {
11618 return {
11619 grid: {
11620 element: element,
11621 style: getGridStyle(props.colCount, rowCount,
11622 props.gutter, props.rowMode, props.rowHeight)
11623 },
11624 tiles: tilePositions.map(function(ps, i) {
11625 return {
11626 element: angular.element(tiles[i]),
11627 style: getTileStyle(ps.position, ps.spans,
11628 props.colCount, rowCount,
11629 props.gutter, props.rowMode, props.rowHeight)
11630 }
11631 })
11632 }
11633 })
11634 .reflow()
11635 .performance();
11636
11637 // Report layout
11638 scope.mdOnLayout({
11639 $event: {
11640 performance: performance
11641 }
11642 });
11643
11644 lastLayoutProps = props;
11645 }
11646
11647 // Use $interpolate to do some simple string interpolation as a convenience.
11648
11649 var startSymbol = $interpolate.startSymbol();
11650 var endSymbol = $interpolate.endSymbol();
11651
11652 // Returns an expression wrapped in the interpolator's start and end symbols.
11653 function expr(exprStr) {
11654 return startSymbol + exprStr + endSymbol;
11655 }
11656
11657 // The amount of space a single 1x1 tile would take up (either width or height), used as
11658 // a basis for other calculations. This consists of taking the base size percent (as would be
11659 // if evenly dividing the size between cells), and then subtracting the size of one gutter.
11660 // However, since there are no gutters on the edges, each tile only uses a fration
11661 // (gutterShare = numGutters / numCells) of the gutter size. (Imagine having one gutter per
11662 // tile, and then breaking up the extra gutter on the edge evenly among the cells).
11663 var UNIT = $interpolate(expr('share') + '% - (' + expr('gutter') + ' * ' + expr('gutterShare') + ')');
11664
11665 // The horizontal or vertical position of a tile, e.g., the 'top' or 'left' property value.
11666 // The position comes the size of a 1x1 tile plus gutter for each previous tile in the
11667 // row/column (offset).
11668 var POSITION = $interpolate('calc((' + expr('unit') + ' + ' + expr('gutter') + ') * ' + expr('offset') + ')');
11669
11670 // The actual size of a tile, e.g., width or height, taking rowSpan or colSpan into account.
11671 // This is computed by multiplying the base unit by the rowSpan/colSpan, and then adding back
11672 // in the space that the gutter would normally have used (which was already accounted for in
11673 // the base unit calculation).
11674 var DIMENSION = $interpolate('calc((' + expr('unit') + ') * ' + expr('span') + ' + (' + expr('span') + ' - 1) * ' + expr('gutter') + ')');
11675
11676 /**
11677 * Gets the styles applied to a tile element described by the given parameters.
11678 * @param {{row: number, col: number}} position The row and column indices of the tile.
11679 * @param {{row: number, col: number}} spans The rowSpan and colSpan of the tile.
11680 * @param {number} colCount The number of columns.
11681 * @param {number} rowCount The number of rows.
11682 * @param {string} gutter The amount of space between tiles. This will be something like
11683 * '5px' or '2em'.
11684 * @param {string} rowMode The row height mode. Can be one of:
11685 * 'fixed': all rows have a fixed size, given by rowHeight,
11686 * 'ratio': row height defined as a ratio to width, or
11687 * 'fit': fit to the grid-list element height, divinding evenly among rows.
11688 * @param {string|number} rowHeight The height of a row. This is only used for 'fixed' mode and
11689 * for 'ratio' mode. For 'ratio' mode, this is the *ratio* of width-to-height (e.g., 0.75).
11690 * @returns {Object} Map of CSS properties to be applied to the style element. Will define
11691 * values for top, left, width, height, marginTop, and paddingTop.
11692 */
11693 function getTileStyle(position, spans, colCount, rowCount, gutter, rowMode, rowHeight) {
11694 // TODO(shyndman): There are style caching opportunities here.
11695
11696 // Percent of the available horizontal space that one column takes up.
11697 var hShare = (1 / colCount) * 100;
11698
11699 // Fraction of the gutter size that each column takes up.
11700 var hGutterShare = (colCount - 1) / colCount;
11701
11702 // Base horizontal size of a column.
11703 var hUnit = UNIT({share: hShare, gutterShare: hGutterShare, gutter: gutter});
11704
11705 // The width and horizontal position of each tile is always calculated the same way, but the
11706 // height and vertical position depends on the rowMode.
11707 var style = {
11708 left: POSITION({ unit: hUnit, offset: position.col, gutter: gutter }),
11709 width: DIMENSION({ unit: hUnit, span: spans.col, gutter: gutter }),
11710 // resets
11711 paddingTop: '',
11712 marginTop: '',
11713 top: '',
11714 height: ''
11715 };
11716
11717 switch (rowMode) {
11718 case 'fixed':
11719 // In fixed mode, simply use the given rowHeight.
11720 style.top = POSITION({ unit: rowHeight, offset: position.row, gutter: gutter });
11721 style.height = DIMENSION({ unit: rowHeight, span: spans.row, gutter: gutter });
11722 break;
11723
11724 case 'ratio':
11725 // Percent of the available vertical space that one row takes up. Here, rowHeight holds
11726 // the ratio value. For example, if the width:height ratio is 4:3, rowHeight = 1.333.
11727 var vShare = hShare / rowHeight;
11728
11729 // Base veritcal size of a row.
11730 var vUnit = UNIT({ share: vShare, gutterShare: hGutterShare, gutter: gutter });
11731
11732 // padidngTop and marginTop are used to maintain the given aspect ratio, as
11733 // a percentage-based value for these properties is applied to the *width* of the
11734 // containing block. See http://www.w3.org/TR/CSS2/box.html#margin-properties
11735 style.paddingTop = DIMENSION({ unit: vUnit, span: spans.row, gutter: gutter});
11736 style.marginTop = POSITION({ unit: vUnit, offset: position.row, gutter: gutter });
11737 break;
11738
11739 case 'fit':
11740 // Fraction of the gutter size that each column takes up.
11741 var vGutterShare = (rowCount - 1) / rowCount;
11742
11743 // Percent of the available vertical space that one row takes up.
11744 var vShare = (1 / rowCount) * 100;
11745
11746 // Base vertical size of a row.
11747 var vUnit = UNIT({share: vShare, gutterShare: vGutterShare, gutter: gutter});
11748
11749 style.top = POSITION({unit: vUnit, offset: position.row, gutter: gutter});
11750 style.height = DIMENSION({unit: vUnit, span: spans.row, gutter: gutter});
11751 break;
11752 }
11753
11754 return style;
11755 }
11756
11757 function getGridStyle(colCount, rowCount, gutter, rowMode, rowHeight) {
11758 var style = {};
11759
11760 switch(rowMode) {
11761 case 'fixed':
11762 style.height = DIMENSION({ unit: rowHeight, span: rowCount, gutter: gutter });
11763 style.paddingBottom = '';
11764 break;
11765
11766 case 'ratio':
11767 // rowHeight is width / height
11768 var hGutterShare = colCount === 1 ? 0 : (colCount - 1) / colCount,
11769 hShare = (1 / colCount) * 100,
11770 vShare = hShare * (1 / rowHeight),
11771 vUnit = UNIT({ share: vShare, gutterShare: hGutterShare, gutter: gutter });
11772
11773 style.height = '';
11774 style.paddingBottom = DIMENSION({ unit: vUnit, span: rowCount, gutter: gutter});
11775 break;
11776
11777 case 'fit':
11778 // noop, as the height is user set
11779 break;
11780 }
11781
11782 return style;
11783 }
11784
11785 function getTileElements() {
11786 return [].filter.call(element.children(), function(ele) {
11787 return ele.tagName == 'MD-GRID-TILE' && !ele.$$mdDestroyed;
11788 });
11789 }
11790
11791 /**
11792 * Gets an array of objects containing the rowspan and colspan for each tile.
11793 * @returns {Array<{row: number, col: number}>}
11794 */
11795 function getTileSpans(tileElements) {
11796 return [].map.call(tileElements, function(ele) {
11797 var ctrl = angular.element(ele).controller('mdGridTile');
11798 return {
11799 row: parseInt(
11800 $mdMedia.getResponsiveAttribute(ctrl.$attrs, 'md-rowspan'), 10) || 1,
11801 col: parseInt(
11802 $mdMedia.getResponsiveAttribute(ctrl.$attrs, 'md-colspan'), 10) || 1
11803 };
11804 });
11805 }
11806
11807 function getColumnCount() {
11808 var colCount = parseInt($mdMedia.getResponsiveAttribute(attrs, 'md-cols'), 10);
11809 if (isNaN(colCount)) {
11810 throw 'md-grid-list: md-cols attribute was not found, or contained a non-numeric value';
11811 }
11812 return colCount;
11813 }
11814
11815 function getGutter() {
11816 return applyDefaultUnit($mdMedia.getResponsiveAttribute(attrs, 'md-gutter') || 1);
11817 }
11818
11819 function getRowHeight() {
11820 var rowHeight = $mdMedia.getResponsiveAttribute(attrs, 'md-row-height');
11821 if (!rowHeight) {
11822 throw 'md-grid-list: md-row-height attribute was not found';
11823 }
11824
11825 switch (getRowMode()) {
11826 case 'fixed':
11827 return applyDefaultUnit(rowHeight);
11828 case 'ratio':
11829 var whRatio = rowHeight.split(':');
11830 return parseFloat(whRatio[0]) / parseFloat(whRatio[1]);
11831 case 'fit':
11832 return 0; // N/A
11833 }
11834 }
11835
11836 function getRowMode() {
11837 var rowHeight = $mdMedia.getResponsiveAttribute(attrs, 'md-row-height');
11838 if (!rowHeight) {
11839 throw 'md-grid-list: md-row-height attribute was not found';
11840 }
11841
11842 if (rowHeight == 'fit') {
11843 return 'fit';
11844 } else if (rowHeight.indexOf(':') !== -1) {
11845 return 'ratio';
11846 } else {
11847 return 'fixed';
11848 }
11849 }
11850
11851 function applyDefaultUnit(val) {
11852 return /\D$/.test(val) ? val : val + 'px';
11853 }
11854 }
11855}
11856
11857/* @ngInject */
11858function GridListController($mdUtil) {
11859 this.layoutInvalidated = false;
11860 this.tilesInvalidated = false;
11861 this.$timeout_ = $mdUtil.nextTick;
11862 this.layoutDelegate = angular.noop;
11863}
11864
11865GridListController.prototype = {
11866 invalidateTiles: function() {
11867 this.tilesInvalidated = true;
11868 this.invalidateLayout();
11869 },
11870
11871 invalidateLayout: function() {
11872 if (this.layoutInvalidated) {
11873 return;
11874 }
11875 this.layoutInvalidated = true;
11876 this.$timeout_(angular.bind(this, this.layout));
11877 },
11878
11879 layout: function() {
11880 try {
11881 this.layoutDelegate(this.tilesInvalidated);
11882 } finally {
11883 this.layoutInvalidated = false;
11884 this.tilesInvalidated = false;
11885 }
11886 }
11887};
11888
11889
11890/* @ngInject */
11891function GridLayoutFactory($mdUtil) {
11892 var defaultAnimator = GridTileAnimator;
11893
11894 /**
11895 * Set the reflow animator callback
11896 */
11897 GridLayout.animateWith = function(customAnimator) {
11898 defaultAnimator = !angular.isFunction(customAnimator) ? GridTileAnimator : customAnimator;
11899 };
11900
11901 return GridLayout;
11902
11903 /**
11904 * Publish layout function
11905 */
11906 function GridLayout(colCount, tileSpans) {
11907 var self, layoutInfo, gridStyles, layoutTime, mapTime, reflowTime;
11908
11909 layoutTime = $mdUtil.time(function() {
11910 layoutInfo = calculateGridFor(colCount, tileSpans);
11911 });
11912
11913 return self = {
11914
11915 /**
11916 * An array of objects describing each tile's position in the grid.
11917 */
11918 layoutInfo: function() {
11919 return layoutInfo;
11920 },
11921
11922 /**
11923 * Maps grid positioning to an element and a set of styles using the
11924 * provided updateFn.
11925 */
11926 map: function(updateFn) {
11927 mapTime = $mdUtil.time(function() {
11928 var info = self.layoutInfo();
11929 gridStyles = updateFn(info.positioning, info.rowCount);
11930 });
11931 return self;
11932 },
11933
11934 /**
11935 * Default animator simply sets the element.css( <styles> ). An alternate
11936 * animator can be provided as an argument. The function has the following
11937 * signature:
11938 *
11939 * function({grid: {element: JQLite, style: Object}, tiles: Array<{element: JQLite, style: Object}>)
11940 */
11941 reflow: function(animatorFn) {
11942 reflowTime = $mdUtil.time(function() {
11943 var animator = animatorFn || defaultAnimator;
11944 animator(gridStyles.grid, gridStyles.tiles);
11945 });
11946 return self;
11947 },
11948
11949 /**
11950 * Timing for the most recent layout run.
11951 */
11952 performance: function() {
11953 return {
11954 tileCount: tileSpans.length,
11955 layoutTime: layoutTime,
11956 mapTime: mapTime,
11957 reflowTime: reflowTime,
11958 totalTime: layoutTime + mapTime + reflowTime
11959 };
11960 }
11961 };
11962 }
11963
11964 /**
11965 * Default Gridlist animator simple sets the css for each element;
11966 * NOTE: any transitions effects must be manually set in the CSS.
11967 * e.g.
11968 *
11969 * md-grid-tile {
11970 * transition: all 700ms ease-out 50ms;
11971 * }
11972 *
11973 */
11974 function GridTileAnimator(grid, tiles) {
11975 grid.element.css(grid.style);
11976 tiles.forEach(function(t) {
11977 t.element.css(t.style);
11978 })
11979 }
11980
11981 /**
11982 * Calculates the positions of tiles.
11983 *
11984 * The algorithm works as follows:
11985 * An Array<Number> with length colCount (spaceTracker) keeps track of
11986 * available tiling positions, where elements of value 0 represents an
11987 * empty position. Space for a tile is reserved by finding a sequence of
11988 * 0s with length <= than the tile's colspan. When such a space has been
11989 * found, the occupied tile positions are incremented by the tile's
11990 * rowspan value, as these positions have become unavailable for that
11991 * many rows.
11992 *
11993 * If the end of a row has been reached without finding space for the
11994 * tile, spaceTracker's elements are each decremented by 1 to a minimum
11995 * of 0. Rows are searched in this fashion until space is found.
11996 */
11997 function calculateGridFor(colCount, tileSpans) {
11998 var curCol = 0,
11999 curRow = 0,
12000 spaceTracker = newSpaceTracker();
12001
12002 return {
12003 positioning: tileSpans.map(function(spans, i) {
12004 return {
12005 spans: spans,
12006 position: reserveSpace(spans, i)
12007 };
12008 }),
12009 rowCount: curRow + Math.max.apply(Math, spaceTracker)
12010 };
12011
12012 function reserveSpace(spans, i) {
12013 if (spans.col > colCount) {
12014 throw 'md-grid-list: Tile at position ' + i + ' has a colspan ' +
12015 '(' + spans.col + ') that exceeds the column count ' +
12016 '(' + colCount + ')';
12017 }
12018
12019 var start = 0,
12020 end = 0;
12021
12022 // TODO(shyndman): This loop isn't strictly necessary if you can
12023 // determine the minimum number of rows before a space opens up. To do
12024 // this, recognize that you've iterated across an entire row looking for
12025 // space, and if so fast-forward by the minimum rowSpan count. Repeat
12026 // until the required space opens up.
12027 while (end - start < spans.col) {
12028 if (curCol >= colCount) {
12029 nextRow();
12030 continue;
12031 }
12032
12033 start = spaceTracker.indexOf(0, curCol);
12034 if (start === -1 || (end = findEnd(start + 1)) === -1) {
12035 start = end = 0;
12036 nextRow();
12037 continue;
12038 }
12039
12040 curCol = end + 1;
12041 }
12042
12043 adjustRow(start, spans.col, spans.row);
12044 curCol = start + spans.col;
12045
12046 return {
12047 col: start,
12048 row: curRow
12049 };
12050 }
12051
12052 function nextRow() {
12053 curCol = 0;
12054 curRow++;
12055 adjustRow(0, colCount, -1); // Decrement row spans by one
12056 }
12057
12058 function adjustRow(from, cols, by) {
12059 for (var i = from; i < from + cols; i++) {
12060 spaceTracker[i] = Math.max(spaceTracker[i] + by, 0);
12061 }
12062 }
12063
12064 function findEnd(start) {
12065 var i;
12066 for (i = start; i < spaceTracker.length; i++) {
12067 if (spaceTracker[i] !== 0) {
12068 return i;
12069 }
12070 }
12071
12072 if (i === spaceTracker.length) {
12073 return i;
12074 }
12075 }
12076
12077 function newSpaceTracker() {
12078 var tracker = [];
12079 for (var i = 0; i < colCount; i++) {
12080 tracker.push(0);
12081 }
12082 return tracker;
12083 }
12084 }
12085}
12086
12087/**
12088 * @ngdoc directive
12089 * @name mdGridTile
12090 * @module material.components.gridList
12091 * @restrict E
12092 * @description
12093 * Tiles contain the content of an `md-grid-list`. They span one or more grid
12094 * cells vertically or horizontally, and use `md-grid-tile-{footer,header}` to
12095 * display secondary content.
12096 *
12097 * ### Responsive Attributes
12098 *
12099 * The `md-grid-tile` directive supports "responsive" attributes, which allow
12100 * different `md-rowspan` and `md-colspan` values depending on the currently
12101 * matching media query.
12102 *
12103 * In order to set a responsive attribute, first define the fallback value with
12104 * the standard attribute name, then add additional attributes with the
12105 * following convention: `{base-attribute-name}-{media-query-name}="{value}"`
12106 * (ie. `md-colspan-sm="4"`)
12107 *
12108 * @param {number=} md-colspan The number of columns to span (default 1). Cannot
12109 * exceed the number of columns in the grid. Supports interpolation.
12110 * @param {number=} md-rowspan The number of rows to span (default 1). Supports
12111 * interpolation.
12112 *
12113 * @usage
12114 * With header:
12115 * <hljs lang="html">
12116 * <md-grid-tile>
12117 * <md-grid-tile-header>
12118 * <h3>This is a header</h3>
12119 * </md-grid-tile-header>
12120 * </md-grid-tile>
12121 * </hljs>
12122 *
12123 * With footer:
12124 * <hljs lang="html">
12125 * <md-grid-tile>
12126 * <md-grid-tile-footer>
12127 * <h3>This is a footer</h3>
12128 * </md-grid-tile-footer>
12129 * </md-grid-tile>
12130 * </hljs>
12131 *
12132 * Spanning multiple rows/columns:
12133 * <hljs lang="html">
12134 * <md-grid-tile md-colspan="2" md-rowspan="3">
12135 * </md-grid-tile>
12136 * </hljs>
12137 *
12138 * Responsive attributes:
12139 * <hljs lang="html">
12140 * <md-grid-tile md-colspan="1" md-colspan-sm="3" md-colspan-md="5">
12141 * </md-grid-tile>
12142 * </hljs>
12143 */
12144function GridTileDirective($mdMedia) {
12145 return {
12146 restrict: 'E',
12147 require: '^mdGridList',
12148 template: '<figure ng-transclude></figure>',
12149 transclude: true,
12150 scope: {},
12151 // Simple controller that exposes attributes to the grid directive
12152 controller: ["$attrs", function($attrs) {
12153 this.$attrs = $attrs;
12154 }],
12155 link: postLink
12156 };
12157
12158 function postLink(scope, element, attrs, gridCtrl) {
12159 // Apply semantics
12160 element.attr('role', 'listitem');
12161
12162 // If our colspan or rowspan changes, trigger a layout
12163 var unwatchAttrs = $mdMedia.watchResponsiveAttributes(['md-colspan', 'md-rowspan'],
12164 attrs, angular.bind(gridCtrl, gridCtrl.invalidateLayout));
12165
12166 // Tile registration/deregistration
12167 gridCtrl.invalidateTiles();
12168 scope.$on('$destroy', function() {
12169 // Mark the tile as destroyed so it is no longer considered in layout,
12170 // even if the DOM element sticks around (like during a leave animation)
12171 element[0].$$mdDestroyed = true;
12172 unwatchAttrs();
12173 gridCtrl.invalidateLayout();
12174 });
12175
12176 if (angular.isDefined(scope.$parent.$index)) {
12177 scope.$watch(function() { return scope.$parent.$index; },
12178 function indexChanged(newIdx, oldIdx) {
12179 if (newIdx === oldIdx) {
12180 return;
12181 }
12182 gridCtrl.invalidateTiles();
12183 });
12184 }
12185 }
12186}
12187
12188
12189function GridTileCaptionDirective() {
12190 return {
12191 template: '<figcaption ng-transclude></figcaption>',
12192 transclude: true
12193 };
12194}
12195
12196})();
12197(function(){
12198"use strict";
12199
12200/**
12201 * @ngdoc module
12202 * @name material.components.icon
12203 * @description
12204 * Icon
12205 */
12206angular.module('material.components.icon', ['material.core']);
12207
12208})();
12209(function(){
12210"use strict";
12211
12212/**
12213 * @ngdoc module
12214 * @name material.components.input
12215 */
12216mdInputContainerDirective.$inject = ["$mdTheming", "$parse"];
12217inputTextareaDirective.$inject = ["$mdUtil", "$window", "$mdAria", "$timeout", "$mdGesture"];
12218mdMaxlengthDirective.$inject = ["$animate", "$mdUtil"];
12219placeholderDirective.$inject = ["$compile"];
12220ngMessageDirective.$inject = ["$mdUtil"];
12221mdSelectOnFocusDirective.$inject = ["$timeout"];
12222mdInputInvalidMessagesAnimation.$inject = ["$$AnimateRunner", "$animateCss", "$mdUtil", "$log"];
12223ngMessagesAnimation.$inject = ["$$AnimateRunner", "$animateCss", "$mdUtil", "$log"];
12224ngMessageAnimation.$inject = ["$$AnimateRunner", "$animateCss", "$mdUtil", "$log"];
12225var inputModule = angular.module('material.components.input', [
12226 'material.core'
12227 ])
12228 .directive('mdInputContainer', mdInputContainerDirective)
12229 .directive('label', labelDirective)
12230 .directive('input', inputTextareaDirective)
12231 .directive('textarea', inputTextareaDirective)
12232 .directive('mdMaxlength', mdMaxlengthDirective)
12233 .directive('placeholder', placeholderDirective)
12234 .directive('ngMessages', ngMessagesDirective)
12235 .directive('ngMessage', ngMessageDirective)
12236 .directive('ngMessageExp', ngMessageDirective)
12237 .directive('mdSelectOnFocus', mdSelectOnFocusDirective)
12238
12239 .animation('.md-input-invalid', mdInputInvalidMessagesAnimation)
12240 .animation('.md-input-messages-animation', ngMessagesAnimation)
12241 .animation('.md-input-message-animation', ngMessageAnimation);
12242
12243// If we are running inside of tests; expose some extra services so that we can test them
12244if (window._mdMocksIncluded) {
12245 inputModule.service('$$mdInput', function() {
12246 return {
12247 // special accessor to internals... useful for testing
12248 messages: {
12249 show : showInputMessages,
12250 hide : hideInputMessages,
12251 getElement : getMessagesElement
12252 }
12253 }
12254 })
12255
12256 // Register a service for each animation so that we can easily inject them into unit tests
12257 .service('mdInputInvalidAnimation', mdInputInvalidMessagesAnimation)
12258 .service('mdInputMessagesAnimation', ngMessagesAnimation)
12259 .service('mdInputMessageAnimation', ngMessageAnimation);
12260}
12261
12262/**
12263 * @ngdoc directive
12264 * @name mdInputContainer
12265 * @module material.components.input
12266 *
12267 * @restrict E
12268 *
12269 * @description
12270 * `<md-input-container>` is the parent of any input or textarea element.
12271 *
12272 * Input and textarea elements will not behave properly unless the md-input-container
12273 * parent is provided.
12274 *
12275 * A single `<md-input-container>` should contain only one `<input>` element, otherwise it will throw an error.
12276 *
12277 * <b>Exception:</b> Hidden inputs (`<input type="hidden" />`) are ignored and will not throw an error, so
12278 * you may combine these with other inputs.
12279 *
12280 * <b>Note:</b> When using `ngMessages` with your input element, make sure the message and container elements
12281 * are *block* elements, otherwise animations applied to the messages will not look as intended. Either use a `div` and
12282 * apply the `ng-message` and `ng-messages` classes respectively, or use the `md-block` class on your element.
12283 *
12284 * @param md-is-error {expression=} When the given expression evaluates to true, the input container
12285 * will go into error state. Defaults to erroring if the input has been touched and is invalid.
12286 * @param md-no-float {boolean=} When present, `placeholder` attributes on the input will not be converted to floating
12287 * labels.
12288 *
12289 * @usage
12290 * <hljs lang="html">
12291 *
12292 * <md-input-container>
12293 * <label>Username</label>
12294 * <input type="text" ng-model="user.name">
12295 * </md-input-container>
12296 *
12297 * <md-input-container>
12298 * <label>Description</label>
12299 * <textarea ng-model="user.description"></textarea>
12300 * </md-input-container>
12301 *
12302 * </hljs>
12303 *
12304 * <h3>When disabling floating labels</h3>
12305 * <hljs lang="html">
12306 *
12307 * <md-input-container md-no-float>
12308 * <input type="text" placeholder="Non-Floating Label">
12309 * </md-input-container>
12310 *
12311 * </hljs>
12312 */
12313function mdInputContainerDirective($mdTheming, $parse) {
12314
12315 ContainerCtrl.$inject = ["$scope", "$element", "$attrs", "$animate"];
12316 var INPUT_TAGS = ['INPUT', 'TEXTAREA', 'SELECT', 'MD-SELECT'];
12317
12318 var LEFT_SELECTORS = INPUT_TAGS.reduce(function(selectors, isel) {
12319 return selectors.concat(['md-icon ~ ' + isel, '.md-icon ~ ' + isel]);
12320 }, []).join(",");
12321
12322 var RIGHT_SELECTORS = INPUT_TAGS.reduce(function(selectors, isel) {
12323 return selectors.concat([isel + ' ~ md-icon', isel + ' ~ .md-icon']);
12324 }, []).join(",");
12325
12326 return {
12327 restrict: 'E',
12328 compile: compile,
12329 controller: ContainerCtrl
12330 };
12331
12332 function compile(tElement) {
12333 // Check for both a left & right icon
12334 var leftIcon = tElement[0].querySelector(LEFT_SELECTORS);
12335 var rightIcon = tElement[0].querySelector(RIGHT_SELECTORS);
12336
12337 if (leftIcon) { tElement.addClass('md-icon-left'); }
12338 if (rightIcon) { tElement.addClass('md-icon-right'); }
12339
12340 return function postLink(scope, element) {
12341 $mdTheming(element);
12342 };
12343 }
12344
12345 function ContainerCtrl($scope, $element, $attrs, $animate) {
12346 var self = this;
12347
12348 self.isErrorGetter = $attrs.mdIsError && $parse($attrs.mdIsError);
12349
12350 self.delegateClick = function() {
12351 self.input.focus();
12352 };
12353 self.element = $element;
12354 self.setFocused = function(isFocused) {
12355 $element.toggleClass('md-input-focused', !!isFocused);
12356 };
12357 self.setHasValue = function(hasValue) {
12358 $element.toggleClass('md-input-has-value', !!hasValue);
12359 };
12360 self.setHasPlaceholder = function(hasPlaceholder) {
12361 $element.toggleClass('md-input-has-placeholder', !!hasPlaceholder);
12362 };
12363 self.setInvalid = function(isInvalid) {
12364 if (isInvalid) {
12365 $animate.addClass($element, 'md-input-invalid');
12366 } else {
12367 $animate.removeClass($element, 'md-input-invalid');
12368 }
12369 };
12370 $scope.$watch(function() {
12371 return self.label && self.input;
12372 }, function(hasLabelAndInput) {
12373 if (hasLabelAndInput && !self.label.attr('for')) {
12374 self.label.attr('for', self.input.attr('id'));
12375 }
12376 });
12377 }
12378}
12379
12380function labelDirective() {
12381 return {
12382 restrict: 'E',
12383 require: '^?mdInputContainer',
12384 link: function(scope, element, attr, containerCtrl) {
12385 if (!containerCtrl || attr.mdNoFloat || element.hasClass('md-container-ignore')) return;
12386
12387 containerCtrl.label = element;
12388 scope.$on('$destroy', function() {
12389 containerCtrl.label = null;
12390 });
12391 }
12392 };
12393}
12394
12395/**
12396 * @ngdoc directive
12397 * @name mdInput
12398 * @restrict E
12399 * @module material.components.input
12400 *
12401 * @description
12402 * You can use any `<input>` or `<textarea>` element as a child of an `<md-input-container>`. This
12403 * allows you to build complex forms for data entry.
12404 *
12405 * When the input is required and uses a floating label, then the label will automatically contain
12406 * an asterisk (`*`).<br/>
12407 * This behavior can be disabled by using the `md-no-asterisk` attribute.
12408 *
12409 * @param {number=} md-maxlength The maximum number of characters allowed in this input. If this is
12410 * specified, a character counter will be shown underneath the input.<br/><br/>
12411 * The purpose of **`md-maxlength`** is exactly to show the max length counter text. If you don't
12412 * want the counter text and only need "plain" validation, you can use the "simple" `ng-maxlength`
12413 * or maxlength attributes.<br/><br/>
12414 * **Note:** Only valid for text/string inputs (not numeric).
12415 *
12416 * @param {string=} aria-label Aria-label is required when no label is present. A warning message
12417 * will be logged in the console if not present.
12418 * @param {string=} placeholder An alternative approach to using aria-label when the label is not
12419 * PRESENT. The placeholder text is copied to the aria-label attribute.
12420 * @param md-no-autogrow {boolean=} When present, textareas will not grow automatically.
12421 * @param md-no-asterisk {boolean=} When present, an asterisk will not be appended to the inputs floating label
12422 * @param md-no-resize {boolean=} Disables the textarea resize handle.
12423 * @param {number=} max-rows The maximum amount of rows for a textarea.
12424 * @param md-detect-hidden {boolean=} When present, textareas will be sized properly when they are
12425 * revealed after being hidden. This is off by default for performance reasons because it
12426 * guarantees a reflow every digest cycle.
12427 *
12428 * @usage
12429 * <hljs lang="html">
12430 * <md-input-container>
12431 * <label>Color</label>
12432 * <input type="text" ng-model="color" required md-maxlength="10">
12433 * </md-input-container>
12434 * </hljs>
12435 *
12436 * <h3>With Errors</h3>
12437 *
12438 * `md-input-container` also supports errors using the standard `ng-messages` directives and
12439 * animates the messages when they become visible using from the `ngEnter`/`ngLeave` events or
12440 * the `ngShow`/`ngHide` events.
12441 *
12442 * By default, the messages will be hidden until the input is in an error state. This is based off
12443 * of the `md-is-error` expression of the `md-input-container`. This gives the user a chance to
12444 * fill out the form before the errors become visible.
12445 *
12446 * <hljs lang="html">
12447 * <form name="colorForm">
12448 * <md-input-container>
12449 * <label>Favorite Color</label>
12450 * <input name="favoriteColor" ng-model="favoriteColor" required>
12451 * <div ng-messages="colorForm.favoriteColor.$error">
12452 * <div ng-message="required">This is required!</div>
12453 * </div>
12454 * </md-input-container>
12455 * </form>
12456 * </hljs>
12457 *
12458 * We automatically disable this auto-hiding functionality if you provide any of the following
12459 * visibility directives on the `ng-messages` container:
12460 *
12461 * - `ng-if`
12462 * - `ng-show`/`ng-hide`
12463 * - `ng-switch-when`/`ng-switch-default`
12464 *
12465 * You can also disable this functionality manually by adding the `md-auto-hide="false"` expression
12466 * to the `ng-messages` container. This may be helpful if you always want to see the error messages
12467 * or if you are building your own visibilty directive.
12468 *
12469 * _<b>Note:</b> The `md-auto-hide` attribute is a static string that is only checked upon
12470 * initialization of the `ng-messages` directive to see if it equals the string `false`._
12471 *
12472 * <hljs lang="html">
12473 * <form name="userForm">
12474 * <md-input-container>
12475 * <label>Last Name</label>
12476 * <input name="lastName" ng-model="lastName" required md-maxlength="10" minlength="4">
12477 * <div ng-messages="userForm.lastName.$error" ng-show="userForm.lastName.$dirty">
12478 * <div ng-message="required">This is required!</div>
12479 * <div ng-message="md-maxlength">That's too long!</div>
12480 * <div ng-message="minlength">That's too short!</div>
12481 * </div>
12482 * </md-input-container>
12483 * <md-input-container>
12484 * <label>Biography</label>
12485 * <textarea name="bio" ng-model="biography" required md-maxlength="150"></textarea>
12486 * <div ng-messages="userForm.bio.$error" ng-show="userForm.bio.$dirty">
12487 * <div ng-message="required">This is required!</div>
12488 * <div ng-message="md-maxlength">That's too long!</div>
12489 * </div>
12490 * </md-input-container>
12491 * <md-input-container>
12492 * <input aria-label='title' ng-model='title'>
12493 * </md-input-container>
12494 * <md-input-container>
12495 * <input placeholder='title' ng-model='title'>
12496 * </md-input-container>
12497 * </form>
12498 * </hljs>
12499 *
12500 * <h3>Notes</h3>
12501 *
12502 * - Requires [ngMessages](https://docs.angularjs.org/api/ngMessages).
12503 * - Behaves like the [AngularJS input directive](https://docs.angularjs.org/api/ng/directive/input).
12504 *
12505 * The `md-input` and `md-input-container` directives use very specific positioning to achieve the
12506 * error animation effects. Therefore, it is *not* advised to use the Layout system inside of the
12507 * `<md-input-container>` tags. Instead, use relative or absolute positioning.
12508 *
12509 *
12510 * <h3>Textarea directive</h3>
12511 * The `textarea` element within a `md-input-container` has the following specific behavior:
12512 * - By default the `textarea` grows as the user types. This can be disabled via the `md-no-autogrow`
12513 * attribute.
12514 * - If a `textarea` has the `rows` attribute, it will treat the `rows` as the minimum height and will
12515 * continue growing as the user types. For example a textarea with `rows="3"` will be 3 lines of text
12516 * high initially. If no rows are specified, the directive defaults to 1.
12517 * - The textarea's height gets set on initialization, as well as while the user is typing. In certain situations
12518 * (e.g. while animating) the directive might have been initialized, before the element got it's final height. In
12519 * those cases, you can trigger a resize manually by broadcasting a `md-resize-textarea` event on the scope.
12520 * - If you wan't a `textarea` to stop growing at a certain point, you can specify the `max-rows` attribute.
12521 * - The textarea's bottom border acts as a handle which users can drag, in order to resize the element vertically.
12522 * Once the user has resized a `textarea`, the autogrowing functionality becomes disabled. If you don't want a
12523 * `textarea` to be resizeable by the user, you can add the `md-no-resize` attribute.
12524 */
12525
12526function inputTextareaDirective($mdUtil, $window, $mdAria, $timeout, $mdGesture) {
12527 return {
12528 restrict: 'E',
12529 require: ['^?mdInputContainer', '?ngModel', '?^form'],
12530 link: postLink
12531 };
12532
12533 function postLink(scope, element, attr, ctrls) {
12534
12535 var containerCtrl = ctrls[0];
12536 var hasNgModel = !!ctrls[1];
12537 var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel();
12538 var parentForm = ctrls[2];
12539 var isReadonly = angular.isDefined(attr.readonly);
12540 var mdNoAsterisk = $mdUtil.parseAttributeBoolean(attr.mdNoAsterisk);
12541 var tagName = element[0].tagName.toLowerCase();
12542
12543
12544 if (!containerCtrl) return;
12545 if (attr.type === 'hidden') {
12546 element.attr('aria-hidden', 'true');
12547 return;
12548 } else if (containerCtrl.input) {
12549 if (containerCtrl.input[0].contains(element[0])) {
12550 return;
12551 } else {
12552 throw new Error("<md-input-container> can only have *one* <input>, <textarea> or <md-select> child element!");
12553 }
12554 }
12555 containerCtrl.input = element;
12556
12557 setupAttributeWatchers();
12558
12559 // Add an error spacer div after our input to provide space for the char counter and any ng-messages
12560 var errorsSpacer = angular.element('<div class="md-errors-spacer">');
12561 element.after(errorsSpacer);
12562
12563 if (!containerCtrl.label) {
12564 $mdAria.expect(element, 'aria-label', attr.placeholder);
12565 }
12566
12567 element.addClass('md-input');
12568 if (!element.attr('id')) {
12569 element.attr('id', 'input_' + $mdUtil.nextUid());
12570 }
12571
12572 // This works around a Webkit issue where number inputs, placed in a flexbox, that have
12573 // a `min` and `max` will collapse to about 1/3 of their proper width. Please check #7349
12574 // for more info. Also note that we don't override the `step` if the user has specified it,
12575 // in order to prevent some unexpected behaviour.
12576 if (tagName === 'input' && attr.type === 'number' && attr.min && attr.max && !attr.step) {
12577 element.attr('step', 'any');
12578 } else if (tagName === 'textarea') {
12579 setupTextarea();
12580 }
12581
12582 // If the input doesn't have an ngModel, it may have a static value. For that case,
12583 // we have to do one initial check to determine if the container should be in the
12584 // "has a value" state.
12585 if (!hasNgModel) {
12586 inputCheckValue();
12587 }
12588
12589 var isErrorGetter = containerCtrl.isErrorGetter || function() {
12590 return ngModelCtrl.$invalid && (ngModelCtrl.$touched || (parentForm && parentForm.$submitted));
12591 };
12592
12593 scope.$watch(isErrorGetter, containerCtrl.setInvalid);
12594
12595 // When the developer uses the ngValue directive for the input, we have to observe the attribute, because
12596 // Angular's ngValue directive is just setting the `value` attribute.
12597 if (attr.ngValue) {
12598 attr.$observe('value', inputCheckValue);
12599 }
12600
12601 ngModelCtrl.$parsers.push(ngModelPipelineCheckValue);
12602 ngModelCtrl.$formatters.push(ngModelPipelineCheckValue);
12603
12604 element.on('input', inputCheckValue);
12605
12606 if (!isReadonly) {
12607 element
12608 .on('focus', function(ev) {
12609 $mdUtil.nextTick(function() {
12610 containerCtrl.setFocused(true);
12611 });
12612 })
12613 .on('blur', function(ev) {
12614 $mdUtil.nextTick(function() {
12615 containerCtrl.setFocused(false);
12616 inputCheckValue();
12617 });
12618 });
12619 }
12620
12621 scope.$on('$destroy', function() {
12622 containerCtrl.setFocused(false);
12623 containerCtrl.setHasValue(false);
12624 containerCtrl.input = null;
12625 });
12626
12627 /** Gets run through ngModel's pipeline and set the `has-value` class on the container. */
12628 function ngModelPipelineCheckValue(arg) {
12629 containerCtrl.setHasValue(!ngModelCtrl.$isEmpty(arg));
12630 return arg;
12631 }
12632
12633 function setupAttributeWatchers() {
12634 if (containerCtrl.label) {
12635 attr.$observe('required', function (value) {
12636 // We don't need to parse the required value, it's always a boolean because of angular's
12637 // required directive.
12638 containerCtrl.label.toggleClass('md-required', value && !mdNoAsterisk);
12639 });
12640 }
12641 }
12642
12643 function inputCheckValue() {
12644 // An input's value counts if its length > 0,
12645 // or if the input's validity state says it has bad input (eg string in a number input)
12646 containerCtrl.setHasValue(element.val().length > 0 || (element[0].validity || {}).badInput);
12647 }
12648
12649 function setupTextarea() {
12650 var isAutogrowing = !attr.hasOwnProperty('mdNoAutogrow');
12651
12652 attachResizeHandle();
12653
12654 if (!isAutogrowing) return;
12655
12656 // Can't check if height was or not explicity set,
12657 // so rows attribute will take precedence if present
12658 var minRows = attr.hasOwnProperty('rows') ? parseInt(attr.rows) : NaN;
12659 var maxRows = attr.hasOwnProperty('maxRows') ? parseInt(attr.maxRows) : NaN;
12660 var scopeResizeListener = scope.$on('md-resize-textarea', growTextarea);
12661 var lineHeight = null;
12662 var node = element[0];
12663
12664 // This timeout is necessary, because the browser needs a little bit
12665 // of time to calculate the `clientHeight` and `scrollHeight`.
12666 $timeout(function() {
12667 $mdUtil.nextTick(growTextarea);
12668 }, 10, false);
12669
12670 // We could leverage ngModel's $parsers here, however it
12671 // isn't reliable, because Angular trims the input by default,
12672 // which means that growTextarea won't fire when newlines and
12673 // spaces are added.
12674 element.on('input', growTextarea);
12675
12676 // We should still use the $formatters, because they fire when
12677 // the value was changed from outside the textarea.
12678 if (hasNgModel) {
12679 ngModelCtrl.$formatters.push(formattersListener);
12680 }
12681
12682 if (!minRows) {
12683 element.attr('rows', 1);
12684 }
12685
12686 angular.element($window).on('resize', growTextarea);
12687 scope.$on('$destroy', disableAutogrow);
12688
12689 function growTextarea() {
12690 // temporarily disables element's flex so its height 'runs free'
12691 element
12692 .attr('rows', 1)
12693 .css('height', 'auto')
12694 .addClass('md-no-flex');
12695
12696 var height = getHeight();
12697
12698 if (!lineHeight) {
12699 // offsetHeight includes padding which can throw off our value
12700 var originalPadding = element[0].style.padding || '';
12701 lineHeight = element.css('padding', 0).prop('offsetHeight');
12702 element[0].style.padding = originalPadding;
12703 }
12704
12705 if (minRows && lineHeight) {
12706 height = Math.max(height, lineHeight * minRows);
12707 }
12708
12709 if (maxRows && lineHeight) {
12710 var maxHeight = lineHeight * maxRows;
12711
12712 if (maxHeight < height) {
12713 element.attr('md-no-autogrow', '');
12714 height = maxHeight;
12715 } else {
12716 element.removeAttr('md-no-autogrow');
12717 }
12718 }
12719
12720 if (lineHeight) {
12721 element.attr('rows', Math.round(height / lineHeight));
12722 }
12723
12724 element
12725 .css('height', height + 'px')
12726 .removeClass('md-no-flex');
12727 }
12728
12729 function getHeight() {
12730 var offsetHeight = node.offsetHeight;
12731 var line = node.scrollHeight - offsetHeight;
12732 return offsetHeight + Math.max(line, 0);
12733 }
12734
12735 function formattersListener(value) {
12736 $mdUtil.nextTick(growTextarea);
12737 return value;
12738 }
12739
12740 function disableAutogrow() {
12741 if (!isAutogrowing) return;
12742
12743 isAutogrowing = false;
12744 angular.element($window).off('resize', growTextarea);
12745 scopeResizeListener && scopeResizeListener();
12746 element
12747 .attr('md-no-autogrow', '')
12748 .off('input', growTextarea);
12749
12750 if (hasNgModel) {
12751 var listenerIndex = ngModelCtrl.$formatters.indexOf(formattersListener);
12752
12753 if (listenerIndex > -1) {
12754 ngModelCtrl.$formatters.splice(listenerIndex, 1);
12755 }
12756 }
12757 }
12758
12759 function attachResizeHandle() {
12760 if (attr.hasOwnProperty('mdNoResize')) return;
12761
12762 var handle = angular.element('<div class="md-resize-handle"></div>');
12763 var isDragging = false;
12764 var dragStart = null;
12765 var startHeight = 0;
12766 var container = containerCtrl.element;
12767 var dragGestureHandler = $mdGesture.register(handle, 'drag', { horizontal: false });
12768
12769
12770 element.wrap('<div class="md-resize-wrapper">').after(handle);
12771 handle.on('mousedown', onMouseDown);
12772
12773 container
12774 .on('$md.dragstart', onDragStart)
12775 .on('$md.drag', onDrag)
12776 .on('$md.dragend', onDragEnd);
12777
12778 scope.$on('$destroy', function() {
12779 handle
12780 .off('mousedown', onMouseDown)
12781 .remove();
12782
12783 container
12784 .off('$md.dragstart', onDragStart)
12785 .off('$md.drag', onDrag)
12786 .off('$md.dragend', onDragEnd);
12787
12788 dragGestureHandler();
12789 handle = null;
12790 container = null;
12791 dragGestureHandler = null;
12792 });
12793
12794 function onMouseDown(ev) {
12795 ev.preventDefault();
12796 isDragging = true;
12797 dragStart = ev.clientY;
12798 startHeight = parseFloat(element.css('height')) || element.prop('offsetHeight');
12799 }
12800
12801 function onDragStart(ev) {
12802 if (!isDragging) return;
12803 ev.preventDefault();
12804 disableAutogrow();
12805 container.addClass('md-input-resized');
12806 }
12807
12808 function onDrag(ev) {
12809 if (!isDragging) return;
12810
12811 element.css('height', (startHeight + ev.pointer.distanceY) + 'px');
12812 }
12813
12814 function onDragEnd(ev) {
12815 if (!isDragging) return;
12816 isDragging = false;
12817 container.removeClass('md-input-resized');
12818 }
12819 }
12820
12821 // Attach a watcher to detect when the textarea gets shown.
12822 if (attr.hasOwnProperty('mdDetectHidden')) {
12823
12824 var handleHiddenChange = function() {
12825 var wasHidden = false;
12826
12827 return function() {
12828 var isHidden = node.offsetHeight === 0;
12829
12830 if (isHidden === false && wasHidden === true) {
12831 growTextarea();
12832 }
12833
12834 wasHidden = isHidden;
12835 };
12836 }();
12837
12838 // Check every digest cycle whether the visibility of the textarea has changed.
12839 // Queue up to run after the digest cycle is complete.
12840 scope.$watch(function() {
12841 $mdUtil.nextTick(handleHiddenChange, false);
12842 return true;
12843 });
12844 }
12845 }
12846 }
12847}
12848
12849function mdMaxlengthDirective($animate, $mdUtil) {
12850 return {
12851 restrict: 'A',
12852 require: ['ngModel', '^mdInputContainer'],
12853 link: postLink
12854 };
12855
12856 function postLink(scope, element, attr, ctrls) {
12857 var maxlength;
12858 var ngModelCtrl = ctrls[0];
12859 var containerCtrl = ctrls[1];
12860 var charCountEl, errorsSpacer;
12861
12862 // Wait until the next tick to ensure that the input has setup the errors spacer where we will
12863 // append our counter
12864 $mdUtil.nextTick(function() {
12865 errorsSpacer = angular.element(containerCtrl.element[0].querySelector('.md-errors-spacer'));
12866 charCountEl = angular.element('<div class="md-char-counter">');
12867
12868 // Append our character counter inside the errors spacer
12869 errorsSpacer.append(charCountEl);
12870
12871 // Stop model from trimming. This makes it so whitespace
12872 // over the maxlength still counts as invalid.
12873 attr.$set('ngTrim', 'false');
12874
12875 scope.$watch(attr.mdMaxlength, function(value) {
12876 maxlength = value;
12877 if (angular.isNumber(value) && value > 0) {
12878 if (!charCountEl.parent().length) {
12879 $animate.enter(charCountEl, errorsSpacer);
12880 }
12881 renderCharCount();
12882 } else {
12883 $animate.leave(charCountEl);
12884 }
12885 });
12886
12887 ngModelCtrl.$validators['md-maxlength'] = function(modelValue, viewValue) {
12888 if (!angular.isNumber(maxlength) || maxlength < 0) {
12889 return true;
12890 }
12891
12892 // We always update the char count, when the modelValue has changed.
12893 // Using the $validators for triggering the update works very well.
12894 renderCharCount();
12895
12896 return ( modelValue || element.val() || viewValue || '' ).length <= maxlength;
12897 };
12898 });
12899
12900 function renderCharCount(value) {
12901 // If we have not been appended to the body yet; do not render
12902 if (!charCountEl.parent) {
12903 return value;
12904 }
12905
12906 // Force the value into a string since it may be a number,
12907 // which does not have a length property.
12908 charCountEl.text(String(element.val() || value || '').length + ' / ' + maxlength);
12909 return value;
12910 }
12911 }
12912}
12913
12914function placeholderDirective($compile) {
12915 return {
12916 restrict: 'A',
12917 require: '^^?mdInputContainer',
12918 priority: 200,
12919 link: {
12920 // Note that we need to do this in the pre-link, as opposed to the post link, if we want to
12921 // support data bindings in the placeholder. This is necessary, because we have a case where
12922 // we transfer the placeholder value to the `<label>` and we remove it from the original `<input>`.
12923 // If we did this in the post-link, Angular would have set up the observers already and would be
12924 // re-adding the attribute, even though we removed it from the element.
12925 pre: preLink
12926 }
12927 };
12928
12929 function preLink(scope, element, attr, inputContainer) {
12930 // If there is no input container, just return
12931 if (!inputContainer) return;
12932
12933 var label = inputContainer.element.find('label');
12934 var noFloat = inputContainer.element.attr('md-no-float');
12935
12936 // If we have a label, or they specify the md-no-float attribute, just return
12937 if ((label && label.length) || noFloat === '' || scope.$eval(noFloat)) {
12938 // Add a placeholder class so we can target it in the CSS
12939 inputContainer.setHasPlaceholder(true);
12940 return;
12941 }
12942
12943 // md-select handles placeholders on it's own
12944 if (element[0].nodeName != 'MD-SELECT') {
12945 // Move the placeholder expression to the label
12946 var newLabel = angular.element('<label ng-click="delegateClick()" tabindex="-1">' + attr.placeholder + '</label>');
12947
12948 // Note that we unset it via `attr`, in order to get Angular
12949 // to remove any observers that it might have set up. Otherwise
12950 // the attribute will be added on the next digest.
12951 attr.$set('placeholder', null);
12952
12953 // We need to compile the label manually in case it has any bindings.
12954 // A gotcha here is that we first add the element to the DOM and we compile
12955 // it later. This is necessary, because if we compile the element beforehand,
12956 // it won't be able to find the `mdInputContainer` controller.
12957 inputContainer.element
12958 .addClass('md-icon-float')
12959 .prepend(newLabel);
12960
12961 $compile(newLabel)(scope);
12962 }
12963 }
12964}
12965
12966/**
12967 * @ngdoc directive
12968 * @name mdSelectOnFocus
12969 * @module material.components.input
12970 *
12971 * @restrict A
12972 *
12973 * @description
12974 * The `md-select-on-focus` directive allows you to automatically select the element's input text on focus.
12975 *
12976 * <h3>Notes</h3>
12977 * - The use of `md-select-on-focus` is restricted to `<input>` and `<textarea>` elements.
12978 *
12979 * @usage
12980 * <h3>Using with an Input</h3>
12981 * <hljs lang="html">
12982 *
12983 * <md-input-container>
12984 * <label>Auto Select</label>
12985 * <input type="text" md-select-on-focus>
12986 * </md-input-container>
12987 * </hljs>
12988 *
12989 * <h3>Using with a Textarea</h3>
12990 * <hljs lang="html">
12991 *
12992 * <md-input-container>
12993 * <label>Auto Select</label>
12994 * <textarea md-select-on-focus>This text will be selected on focus.</textarea>
12995 * </md-input-container>
12996 *
12997 * </hljs>
12998 */
12999function mdSelectOnFocusDirective($timeout) {
13000
13001 return {
13002 restrict: 'A',
13003 link: postLink
13004 };
13005
13006 function postLink(scope, element, attr) {
13007 if (element[0].nodeName !== 'INPUT' && element[0].nodeName !== "TEXTAREA") return;
13008
13009 var preventMouseUp = false;
13010
13011 element
13012 .on('focus', onFocus)
13013 .on('mouseup', onMouseUp);
13014
13015 scope.$on('$destroy', function() {
13016 element
13017 .off('focus', onFocus)
13018 .off('mouseup', onMouseUp);
13019 });
13020
13021 function onFocus() {
13022 preventMouseUp = true;
13023
13024 $timeout(function() {
13025 // Use HTMLInputElement#select to fix firefox select issues.
13026 // The debounce is here for Edge's sake, otherwise the selection doesn't work.
13027 element[0].select();
13028
13029 // This should be reset from inside the `focus`, because the event might
13030 // have originated from something different than a click, e.g. a keyboard event.
13031 preventMouseUp = false;
13032 }, 1, false);
13033 }
13034
13035 // Prevents the default action of the first `mouseup` after a focus.
13036 // This is necessary, because browsers fire a `mouseup` right after the element
13037 // has been focused. In some browsers (Firefox in particular) this can clear the
13038 // selection. There are examples of the problem in issue #7487.
13039 function onMouseUp(event) {
13040 if (preventMouseUp) {
13041 event.preventDefault();
13042 }
13043 }
13044 }
13045}
13046
13047var visibilityDirectives = ['ngIf', 'ngShow', 'ngHide', 'ngSwitchWhen', 'ngSwitchDefault'];
13048function ngMessagesDirective() {
13049 return {
13050 restrict: 'EA',
13051 link: postLink,
13052
13053 // This is optional because we don't want target *all* ngMessage instances, just those inside of
13054 // mdInputContainer.
13055 require: '^^?mdInputContainer'
13056 };
13057
13058 function postLink(scope, element, attrs, inputContainer) {
13059 // If we are not a child of an input container, don't do anything
13060 if (!inputContainer) return;
13061
13062 // Add our animation class
13063 element.toggleClass('md-input-messages-animation', true);
13064
13065 // Add our md-auto-hide class to automatically hide/show messages when container is invalid
13066 element.toggleClass('md-auto-hide', true);
13067
13068 // If we see some known visibility directives, remove the md-auto-hide class
13069 if (attrs.mdAutoHide == 'false' || hasVisibiltyDirective(attrs)) {
13070 element.toggleClass('md-auto-hide', false);
13071 }
13072 }
13073
13074 function hasVisibiltyDirective(attrs) {
13075 return visibilityDirectives.some(function(attr) {
13076 return attrs[attr];
13077 });
13078 }
13079}
13080
13081function ngMessageDirective($mdUtil) {
13082 return {
13083 restrict: 'EA',
13084 compile: compile,
13085 priority: 100
13086 };
13087
13088 function compile(tElement) {
13089 if (!isInsideInputContainer(tElement)) {
13090
13091 // When the current element is inside of a document fragment, then we need to check for an input-container
13092 // in the postLink, because the element will be later added to the DOM and is currently just in a temporary
13093 // fragment, which causes the input-container check to fail.
13094 if (isInsideFragment()) {
13095 return function (scope, element) {
13096 if (isInsideInputContainer(element)) {
13097 // Inside of the postLink function, a ngMessage directive will be a comment element, because it's
13098 // currently hidden. To access the shown element, we need to use the element from the compile function.
13099 initMessageElement(tElement);
13100 }
13101 };
13102 }
13103 } else {
13104 initMessageElement(tElement);
13105 }
13106
13107 function isInsideFragment() {
13108 var nextNode = tElement[0];
13109 while (nextNode = nextNode.parentNode) {
13110 if (nextNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
13111 return true;
13112 }
13113 }
13114 return false;
13115 }
13116
13117 function isInsideInputContainer(element) {
13118 return !!$mdUtil.getClosest(element, "md-input-container");
13119 }
13120
13121 function initMessageElement(element) {
13122 // Add our animation class
13123 element.toggleClass('md-input-message-animation', true);
13124 }
13125 }
13126}
13127
13128var $$AnimateRunner, $animateCss, $mdUtil, $log;
13129
13130function mdInputInvalidMessagesAnimation($$AnimateRunner, $animateCss, $mdUtil, $log) {
13131 saveSharedServices($$AnimateRunner, $animateCss, $mdUtil, $log);
13132
13133 return {
13134 addClass: function(element, className, done) {
13135 showInputMessages(element, done);
13136 }
13137
13138 // NOTE: We do not need the removeClass method, because the message ng-leave animation will fire
13139 };
13140}
13141
13142function ngMessagesAnimation($$AnimateRunner, $animateCss, $mdUtil, $log) {
13143 saveSharedServices($$AnimateRunner, $animateCss, $mdUtil, $log);
13144
13145 return {
13146 enter: function(element, done) {
13147 showInputMessages(element, done);
13148 },
13149
13150 leave: function(element, done) {
13151 hideInputMessages(element, done);
13152 },
13153
13154 addClass: function(element, className, done) {
13155 if (className == "ng-hide") {
13156 hideInputMessages(element, done);
13157 } else {
13158 done();
13159 }
13160 },
13161
13162 removeClass: function(element, className, done) {
13163 if (className == "ng-hide") {
13164 showInputMessages(element, done);
13165 } else {
13166 done();
13167 }
13168 }
13169 };
13170}
13171
13172function ngMessageAnimation($$AnimateRunner, $animateCss, $mdUtil, $log) {
13173 saveSharedServices($$AnimateRunner, $animateCss, $mdUtil, $log);
13174
13175 return {
13176 enter: function(element, done) {
13177 var animator = showMessage(element);
13178
13179 animator.start().done(done);
13180 },
13181
13182 leave: function(element, done) {
13183 var animator = hideMessage(element);
13184
13185 animator.start().done(done);
13186 }
13187 };
13188}
13189
13190function showInputMessages(element, done) {
13191 var animators = [], animator;
13192 var messages = getMessagesElement(element);
13193 var children = messages.children();
13194
13195 if (messages.length == 0 || children.length == 0) {
13196 $log.warn('mdInput messages show animation called on invalid messages element: ', element);
13197 done();
13198 return;
13199 }
13200
13201 angular.forEach(children, function(child) {
13202 animator = showMessage(angular.element(child));
13203
13204 animators.push(animator.start());
13205 });
13206
13207 $$AnimateRunner.all(animators, done);
13208}
13209
13210function hideInputMessages(element, done) {
13211 var animators = [], animator;
13212 var messages = getMessagesElement(element);
13213 var children = messages.children();
13214
13215 if (messages.length == 0 || children.length == 0) {
13216 $log.warn('mdInput messages hide animation called on invalid messages element: ', element);
13217 done();
13218 return;
13219 }
13220
13221 angular.forEach(children, function(child) {
13222 animator = hideMessage(angular.element(child));
13223
13224 animators.push(animator.start());
13225 });
13226
13227 $$AnimateRunner.all(animators, done);
13228}
13229
13230function showMessage(element) {
13231 var height = parseInt(window.getComputedStyle(element[0]).height);
13232 var topMargin = parseInt(window.getComputedStyle(element[0]).marginTop);
13233
13234 var messages = getMessagesElement(element);
13235 var container = getInputElement(element);
13236
13237 // Check to see if the message is already visible so we can skip
13238 var alreadyVisible = (topMargin > -height);
13239
13240 // If we have the md-auto-hide class, the md-input-invalid animation will fire, so we can skip
13241 if (alreadyVisible || (messages.hasClass('md-auto-hide') && !container.hasClass('md-input-invalid'))) {
13242 return $animateCss(element, {});
13243 }
13244
13245 return $animateCss(element, {
13246 event: 'enter',
13247 structural: true,
13248 from: {"opacity": 0, "margin-top": -height + "px"},
13249 to: {"opacity": 1, "margin-top": "0"},
13250 duration: 0.3
13251 });
13252}
13253
13254function hideMessage(element) {
13255 var height = element[0].offsetHeight;
13256 var styles = window.getComputedStyle(element[0]);
13257
13258 // If we are already hidden, just return an empty animation
13259 if (parseInt(styles.opacity) === 0) {
13260 return $animateCss(element, {});
13261 }
13262
13263 // Otherwise, animate
13264 return $animateCss(element, {
13265 event: 'leave',
13266 structural: true,
13267 from: {"opacity": 1, "margin-top": 0},
13268 to: {"opacity": 0, "margin-top": -height + "px"},
13269 duration: 0.3
13270 });
13271}
13272
13273function getInputElement(element) {
13274 var inputContainer = element.controller('mdInputContainer');
13275
13276 return inputContainer.element;
13277}
13278
13279function getMessagesElement(element) {
13280 // If we ARE the messages element, just return ourself
13281 if (element.hasClass('md-input-messages-animation')) {
13282 return element;
13283 }
13284
13285 // If we are a ng-message element, we need to traverse up the DOM tree
13286 if (element.hasClass('md-input-message-animation')) {
13287 return angular.element($mdUtil.getClosest(element, function(node) {
13288 return node.classList.contains('md-input-messages-animation');
13289 }));
13290 }
13291
13292 // Otherwise, we can traverse down
13293 return angular.element(element[0].querySelector('.md-input-messages-animation'));
13294}
13295
13296function saveSharedServices(_$$AnimateRunner_, _$animateCss_, _$mdUtil_, _$log_) {
13297 $$AnimateRunner = _$$AnimateRunner_;
13298 $animateCss = _$animateCss_;
13299 $mdUtil = _$mdUtil_;
13300 $log = _$log_;
13301}
13302
13303})();
13304(function(){
13305"use strict";
13306
13307/**
13308 * @ngdoc module
13309 * @name material.components.list
13310 * @description
13311 * List module
13312 */
13313MdListController.$inject = ["$scope", "$element", "$mdListInkRipple"];
13314mdListDirective.$inject = ["$mdTheming"];
13315mdListItemDirective.$inject = ["$mdAria", "$mdConstant", "$mdUtil", "$timeout"];
13316angular.module('material.components.list', [
13317 'material.core'
13318])
13319 .controller('MdListController', MdListController)
13320 .directive('mdList', mdListDirective)
13321 .directive('mdListItem', mdListItemDirective);
13322
13323/**
13324 * @ngdoc directive
13325 * @name mdList
13326 * @module material.components.list
13327 *
13328 * @restrict E
13329 *
13330 * @description
13331 * The `<md-list>` directive is a list container for 1..n `<md-list-item>` tags.
13332 *
13333 * @usage
13334 * <hljs lang="html">
13335 * <md-list>
13336 * <md-list-item class="md-2-line" ng-repeat="item in todos">
13337 * <md-checkbox ng-model="item.done"></md-checkbox>
13338 * <div class="md-list-item-text">
13339 * <h3>{{item.title}}</h3>
13340 * <p>{{item.description}}</p>
13341 * </div>
13342 * </md-list-item>
13343 * </md-list>
13344 * </hljs>
13345 */
13346
13347function mdListDirective($mdTheming) {
13348 return {
13349 restrict: 'E',
13350 compile: function(tEl) {
13351 tEl[0].setAttribute('role', 'list');
13352 return $mdTheming;
13353 }
13354 };
13355}
13356/**
13357 * @ngdoc directive
13358 * @name mdListItem
13359 * @module material.components.list
13360 *
13361 * @restrict E
13362 *
13363 * @description
13364 * A `md-list-item` element can be used to represent some information in a row.<br/>
13365 *
13366 * @usage
13367 * ### Single Row Item
13368 * <hljs lang="html">
13369 * <md-list-item>
13370 * <span>Single Row Item</span>
13371 * </md-list-item>
13372 * </hljs>
13373 *
13374 * ### Multiple Lines
13375 * By using the following markup, you will be able to have two lines inside of one `md-list-item`.
13376 *
13377 * <hljs lang="html">
13378 * <md-list-item class="md-2-line">
13379 * <div class="md-list-item-text" layout="column">
13380 * <p>First Line</p>
13381 * <p>Second Line</p>
13382 * </div>
13383 * </md-list-item>
13384 * </hljs>
13385 *
13386 * It is also possible to have three lines inside of one list item.
13387 *
13388 * <hljs lang="html">
13389 * <md-list-item class="md-3-line">
13390 * <div class="md-list-item-text" layout="column">
13391 * <p>First Line</p>
13392 * <p>Second Line</p>
13393 * <p>Third Line</p>
13394 * </div>
13395 * </md-list-item>
13396 * </hljs>
13397 *
13398 * ### Secondary Items
13399 * Secondary items are elements which will be aligned at the end of the `md-list-item`.
13400 *
13401 * <hljs lang="html">
13402 * <md-list-item>
13403 * <span>Single Row Item</span>
13404 * <md-button class="md-secondary">
13405 * Secondary Button
13406 * </md-button>
13407 * </md-list-item>
13408 * </hljs>
13409 *
13410 * It also possible to have multiple secondary items inside of one `md-list-item`.
13411 *
13412 * <hljs lang="html">
13413 * <md-list-item>
13414 * <span>Single Row Item</span>
13415 * <md-button class="md-secondary">First Button</md-button>
13416 * <md-button class="md-secondary">Second Button</md-button>
13417 * </md-list-item>
13418 * </hljs>
13419 *
13420 * ### Proxy Item
13421 * Proxies are elements, which will execute their specific action on click<br/>
13422 * Currently supported proxy items are
13423 * - `md-checkbox` (Toggle)
13424 * - `md-switch` (Toggle)
13425 * - `md-menu` (Open)
13426 *
13427 * This means, when using a supported proxy item inside of `md-list-item`, the list item will
13428 * automatically become clickable and executes the associated action of the proxy element on click.
13429 *
13430 * It is possible to disable this behavior by applying the `md-no-proxy` class to the list item.
13431 *
13432 * <hljs lang="html">
13433 * <md-list-item class="md-no-proxy">
13434 * <span>No Proxy List</span>
13435 * <md-checkbox class="md-secondary"></md-checkbox>
13436 * </md-list-item>
13437 * </hljs>
13438 *
13439 * Here are a few examples of proxy elements inside of a list item.
13440 *
13441 * <hljs lang="html">
13442 * <md-list-item>
13443 * <span>First Line</span>
13444 * <md-checkbox class="md-secondary"></md-checkbox>
13445 * </md-list-item>
13446 * </hljs>
13447 *
13448 * The `md-checkbox` element will be automatically detected as a proxy element and will toggle on click.
13449 *
13450 * <hljs lang="html">
13451 * <md-list-item>
13452 * <span>First Line</span>
13453 * <md-switch class="md-secondary"></md-switch>
13454 * </md-list-item>
13455 * </hljs>
13456 *
13457 * The recognized `md-switch` will toggle its state, when the user clicks on the `md-list-item`.
13458 *
13459 * It is also possible to have a `md-menu` inside of a `md-list-item`.
13460 * <hljs lang="html">
13461 * <md-list-item>
13462 * <p>Click anywhere to fire the secondary action</p>
13463 * <md-menu class="md-secondary">
13464 * <md-button class="md-icon-button">
13465 * <md-icon md-svg-icon="communication:message"></md-icon>
13466 * </md-button>
13467 * <md-menu-content width="4">
13468 * <md-menu-item>
13469 * <md-button>
13470 * Redial
13471 * </md-button>
13472 * </md-menu-item>
13473 * <md-menu-item>
13474 * <md-button>
13475 * Check voicemail
13476 * </md-button>
13477 * </md-menu-item>
13478 * <md-menu-divider></md-menu-divider>
13479 * <md-menu-item>
13480 * <md-button>
13481 * Notifications
13482 * </md-button>
13483 * </md-menu-item>
13484 * </md-menu-content>
13485 * </md-menu>
13486 * </md-list-item>
13487 * </hljs>
13488 *
13489 * The menu will automatically open, when the users clicks on the `md-list-item`.<br/>
13490 *
13491 * If the developer didn't specify any position mode on the menu, the `md-list-item` will automatically detect the
13492 * position mode and applies it to the `md-menu`.
13493 *
13494 * ### Avatars
13495 * Sometimes you may want to have some avatars inside of the `md-list-item `.<br/>
13496 * You are able to create a optimized icon for the list item, by applying the `.md-avatar` class on the `<img>` element.
13497 *
13498 * <hljs lang="html">
13499 * <md-list-item>
13500 * <img src="my-avatar.png" class="md-avatar">
13501 * <span>Alan Turing</span>
13502 * </hljs>
13503 *
13504 * When using `<md-icon>` for an avatar, you have to use the `.md-avatar-icon` class.
13505 * <hljs lang="html">
13506 * <md-list-item>
13507 * <md-icon class="md-avatar-icon" md-svg-icon="avatars:timothy"></md-icon>
13508 * <span>Timothy Kopra</span>
13509 * </md-list-item>
13510 * </hljs>
13511 *
13512 * In cases, you have a `md-list-item`, which doesn't have any avatar,
13513 * but you want to align it with the other avatar items, you have to use the `.md-offset` class.
13514 *
13515 * <hljs lang="html">
13516 * <md-list-item class="md-offset">
13517 * <span>Jon Doe</span>
13518 * </md-list-item>
13519 * </hljs>
13520 *
13521 * ### DOM modification
13522 * The `md-list-item` component automatically detects if the list item should be clickable.
13523 *
13524 * ---
13525 * If the `md-list-item` is clickable, we wrap all content inside of a `<div>` and create
13526 * an overlaying button, which will will execute the given actions (like `ng-href`, `ng-click`)
13527 *
13528 * We create an overlaying button, instead of wrapping all content inside of the button,
13529 * because otherwise some elements may not be clickable inside of the button.
13530 *
13531 * ---
13532 * When using a secondary item inside of your list item, the `md-list-item` component will automatically create
13533 * a secondary container at the end of the `md-list-item`, which contains all secondary items.
13534 *
13535 * The secondary item container is not static, because otherwise the overflow will not work properly on the
13536 * list item.
13537 *
13538 */
13539function mdListItemDirective($mdAria, $mdConstant, $mdUtil, $timeout) {
13540 var proxiedTypes = ['md-checkbox', 'md-switch', 'md-menu'];
13541 return {
13542 restrict: 'E',
13543 controller: 'MdListController',
13544 compile: function(tEl, tAttrs) {
13545
13546 // Check for proxy controls (no ng-click on parent, and a control inside)
13547 var secondaryItems = tEl[0].querySelectorAll('.md-secondary');
13548 var hasProxiedElement;
13549 var proxyElement;
13550 var itemContainer = tEl;
13551
13552 tEl[0].setAttribute('role', 'listitem');
13553
13554 if (tAttrs.ngClick || tAttrs.ngDblclick || tAttrs.ngHref || tAttrs.href || tAttrs.uiSref || tAttrs.ngAttrUiSref) {
13555 wrapIn('button');
13556 } else if (!tEl.hasClass('md-no-proxy')) {
13557
13558 for (var i = 0, type; type = proxiedTypes[i]; ++i) {
13559 if (proxyElement = tEl[0].querySelector(type)) {
13560 hasProxiedElement = true;
13561 break;
13562 }
13563 }
13564
13565 if (hasProxiedElement) {
13566 wrapIn('div');
13567 } else {
13568 tEl.addClass('md-no-proxy');
13569 }
13570
13571 }
13572
13573 wrapSecondaryItems();
13574 setupToggleAria();
13575
13576 if (hasProxiedElement && proxyElement.nodeName === "MD-MENU") {
13577 setupProxiedMenu();
13578 }
13579
13580 function setupToggleAria() {
13581 var toggleTypes = ['md-switch', 'md-checkbox'];
13582 var toggle;
13583
13584 for (var i = 0, toggleType; toggleType = toggleTypes[i]; ++i) {
13585 if (toggle = tEl.find(toggleType)[0]) {
13586 if (!toggle.hasAttribute('aria-label')) {
13587 var p = tEl.find('p')[0];
13588 if (!p) return;
13589 toggle.setAttribute('aria-label', 'Toggle ' + p.textContent);
13590 }
13591 }
13592 }
13593 }
13594
13595 function setupProxiedMenu() {
13596 var menuEl = angular.element(proxyElement);
13597
13598 var isEndAligned = menuEl.parent().hasClass('md-secondary-container') ||
13599 proxyElement.parentNode.firstElementChild !== proxyElement;
13600
13601 var xAxisPosition = 'left';
13602
13603 if (isEndAligned) {
13604 // When the proxy item is aligned at the end of the list, we have to set the origin to the end.
13605 xAxisPosition = 'right';
13606 }
13607
13608 // Set the position mode / origin of the proxied menu.
13609 if (!menuEl.attr('md-position-mode')) {
13610 menuEl.attr('md-position-mode', xAxisPosition + ' target');
13611 }
13612
13613 // Apply menu open binding to menu button
13614 var menuOpenButton = menuEl.children().eq(0);
13615 if (!hasClickEvent(menuOpenButton[0])) {
13616 menuOpenButton.attr('ng-click', '$mdMenu.open($event)');
13617 }
13618
13619 if (!menuOpenButton.attr('aria-label')) {
13620 menuOpenButton.attr('aria-label', 'Open List Menu');
13621 }
13622 }
13623
13624 function wrapIn(type) {
13625 if (type == 'div') {
13626 itemContainer = angular.element('<div class="md-no-style md-list-item-inner">');
13627 itemContainer.append(tEl.contents());
13628 tEl.addClass('md-proxy-focus');
13629 } else {
13630 // Element which holds the default list-item content.
13631 itemContainer = angular.element(
13632 '<div class="md-button md-no-style">'+
13633 ' <div class="md-list-item-inner"></div>'+
13634 '</div>'
13635 );
13636
13637 // Button which shows ripple and executes primary action.
13638 var buttonWrap = angular.element(
13639 '<md-button class="md-no-style"></md-button>'
13640 );
13641
13642 copyAttributes(tEl[0], buttonWrap[0]);
13643
13644 // If there is no aria-label set on the button (previously copied over if present)
13645 // we determine the label from the content and copy it to the button.
13646 if (!buttonWrap.attr('aria-label')) {
13647 buttonWrap.attr('aria-label', $mdAria.getText(tEl));
13648 }
13649
13650 // We allow developers to specify the `md-no-focus` class, to disable the focus style
13651 // on the button executor. Once more classes should be forwarded, we should probably make the
13652 // class forward more generic.
13653 if (tEl.hasClass('md-no-focus')) {
13654 buttonWrap.addClass('md-no-focus');
13655 }
13656
13657 // Append the button wrap before our list-item content, because it will overlay in relative.
13658 itemContainer.prepend(buttonWrap);
13659 itemContainer.children().eq(1).append(tEl.contents());
13660
13661 tEl.addClass('_md-button-wrap');
13662 }
13663
13664 tEl[0].setAttribute('tabindex', '-1');
13665 tEl.append(itemContainer);
13666 }
13667
13668 function wrapSecondaryItems() {
13669 var secondaryItemsWrapper = angular.element('<div class="md-secondary-container">');
13670
13671 angular.forEach(secondaryItems, function(secondaryItem) {
13672 wrapSecondaryItem(secondaryItem, secondaryItemsWrapper);
13673 });
13674
13675 itemContainer.append(secondaryItemsWrapper);
13676 }
13677
13678 function wrapSecondaryItem(secondaryItem, container) {
13679 // If the current secondary item is not a button, but contains a ng-click attribute,
13680 // the secondary item will be automatically wrapped inside of a button.
13681 if (secondaryItem && !isButton(secondaryItem) && secondaryItem.hasAttribute('ng-click')) {
13682
13683 $mdAria.expect(secondaryItem, 'aria-label');
13684 var buttonWrapper = angular.element('<md-button class="md-secondary md-icon-button">');
13685
13686 // Copy the attributes from the secondary item to the generated button.
13687 // We also support some additional attributes from the secondary item,
13688 // because some developers may use a ngIf, ngHide, ngShow on their item.
13689 copyAttributes(secondaryItem, buttonWrapper[0], ['ng-if', 'ng-hide', 'ng-show']);
13690
13691 secondaryItem.setAttribute('tabindex', '-1');
13692 buttonWrapper.append(secondaryItem);
13693
13694 secondaryItem = buttonWrapper[0];
13695 }
13696
13697 if (secondaryItem && (!hasClickEvent(secondaryItem) || (!tAttrs.ngClick && isProxiedElement(secondaryItem)))) {
13698 // In this case we remove the secondary class, so we can identify it later, when we searching for the
13699 // proxy items.
13700 angular.element(secondaryItem).removeClass('md-secondary');
13701 }
13702
13703 tEl.addClass('md-with-secondary');
13704 container.append(secondaryItem);
13705 }
13706
13707 /**
13708 * Copies attributes from a source element to the destination element
13709 * By default the function will copy the most necessary attributes, supported
13710 * by the button executor for clickable list items.
13711 * @param source Element with the specified attributes
13712 * @param destination Element which will retrieve the attributes
13713 * @param extraAttrs Additional attributes, which will be copied over.
13714 */
13715 function copyAttributes(source, destination, extraAttrs) {
13716 var copiedAttrs = $mdUtil.prefixer([
13717 'ng-if', 'ng-click', 'ng-dblclick', 'aria-label', 'ng-disabled', 'ui-sref',
13718 'href', 'ng-href', 'target', 'ng-attr-ui-sref', 'ui-sref-opts'
13719 ]);
13720
13721 if (extraAttrs) {
13722 copiedAttrs = copiedAttrs.concat($mdUtil.prefixer(extraAttrs));
13723 }
13724
13725 angular.forEach(copiedAttrs, function(attr) {
13726 if (source.hasAttribute(attr)) {
13727 destination.setAttribute(attr, source.getAttribute(attr));
13728 source.removeAttribute(attr);
13729 }
13730 });
13731 }
13732
13733 function isProxiedElement(el) {
13734 return proxiedTypes.indexOf(el.nodeName.toLowerCase()) != -1;
13735 }
13736
13737 function isButton(el) {
13738 var nodeName = el.nodeName.toUpperCase();
13739
13740 return nodeName == "MD-BUTTON" || nodeName == "BUTTON";
13741 }
13742
13743 function hasClickEvent (element) {
13744 var attr = element.attributes;
13745 for (var i = 0; i < attr.length; i++) {
13746 if (tAttrs.$normalize(attr[i].name) === 'ngClick') return true;
13747 }
13748 return false;
13749 }
13750
13751 return postLink;
13752
13753 function postLink($scope, $element, $attr, ctrl) {
13754 $element.addClass('_md'); // private md component indicator for styling
13755
13756 var proxies = [],
13757 firstElement = $element[0].firstElementChild,
13758 isButtonWrap = $element.hasClass('_md-button-wrap'),
13759 clickChild = isButtonWrap ? firstElement.firstElementChild : firstElement,
13760 hasClick = clickChild && hasClickEvent(clickChild),
13761 noProxies = $element.hasClass('md-no-proxy');
13762
13763 computeProxies();
13764 computeClickable();
13765
13766 if (proxies.length) {
13767 angular.forEach(proxies, function(proxy) {
13768 proxy = angular.element(proxy);
13769
13770 $scope.mouseActive = false;
13771 proxy.on('mousedown', function() {
13772 $scope.mouseActive = true;
13773 $timeout(function(){
13774 $scope.mouseActive = false;
13775 }, 100);
13776 })
13777 .on('focus', function() {
13778 if ($scope.mouseActive === false) { $element.addClass('md-focused'); }
13779 proxy.on('blur', function proxyOnBlur() {
13780 $element.removeClass('md-focused');
13781 proxy.off('blur', proxyOnBlur);
13782 });
13783 });
13784 });
13785 }
13786
13787
13788 function computeProxies() {
13789
13790 if (firstElement && firstElement.children && !hasClick && !noProxies) {
13791
13792 angular.forEach(proxiedTypes, function(type) {
13793
13794 // All elements which are not capable for being used a proxy have the .md-secondary class
13795 // applied. These items had been sorted out in the secondary wrap function.
13796 angular.forEach(firstElement.querySelectorAll(type + ':not(.md-secondary)'), function(child) {
13797 proxies.push(child);
13798 });
13799 });
13800
13801 }
13802 }
13803
13804 function computeClickable() {
13805 if (proxies.length == 1 || hasClick) {
13806 $element.addClass('md-clickable');
13807
13808 if (!hasClick) {
13809 ctrl.attachRipple($scope, angular.element($element[0].querySelector('.md-no-style')));
13810 }
13811 }
13812 }
13813
13814 function isEventFromControl(event) {
13815 var forbiddenControls = ['md-slider'];
13816
13817 // If there is no path property in the event, then we can assume that the event was not bubbled.
13818 if (!event.path) {
13819 return forbiddenControls.indexOf(event.target.tagName.toLowerCase()) !== -1;
13820 }
13821
13822 // We iterate the event path up and check for a possible component.
13823 // Our maximum index to search, is the list item root.
13824 var maxPath = event.path.indexOf($element.children()[0]);
13825
13826 for (var i = 0; i < maxPath; i++) {
13827 if (forbiddenControls.indexOf(event.path[i].tagName.toLowerCase()) !== -1) {
13828 return true;
13829 }
13830 }
13831 }
13832
13833 var clickChildKeypressListener = function(e) {
13834 if (e.target.nodeName != 'INPUT' && e.target.nodeName != 'TEXTAREA' && !e.target.isContentEditable) {
13835 var keyCode = e.which || e.keyCode;
13836 if (keyCode == $mdConstant.KEY_CODE.SPACE) {
13837 if (clickChild) {
13838 clickChild.click();
13839 e.preventDefault();
13840 e.stopPropagation();
13841 }
13842 }
13843 }
13844 };
13845
13846 if (!hasClick && !proxies.length) {
13847 clickChild && clickChild.addEventListener('keypress', clickChildKeypressListener);
13848 }
13849
13850 $element.off('click');
13851 $element.off('keypress');
13852
13853 if (proxies.length == 1 && clickChild) {
13854 $element.children().eq(0).on('click', function(e) {
13855 // When the event is coming from an control and it should not trigger the proxied element
13856 // then we are skipping.
13857 if (isEventFromControl(e)) return;
13858
13859 var parentButton = $mdUtil.getClosest(e.target, 'BUTTON');
13860 if (!parentButton && clickChild.contains(e.target)) {
13861 angular.forEach(proxies, function(proxy) {
13862 if (e.target !== proxy && !proxy.contains(e.target)) {
13863 if (proxy.nodeName === 'MD-MENU') {
13864 proxy = proxy.children[0];
13865 }
13866 angular.element(proxy).triggerHandler('click');
13867 }
13868 });
13869 }
13870 });
13871 }
13872
13873 $scope.$on('$destroy', function () {
13874 clickChild && clickChild.removeEventListener('keypress', clickChildKeypressListener);
13875 });
13876 }
13877 }
13878 };
13879}
13880
13881/*
13882 * @private
13883 * @ngdoc controller
13884 * @name MdListController
13885 * @module material.components.list
13886 *
13887 */
13888function MdListController($scope, $element, $mdListInkRipple) {
13889 var ctrl = this;
13890 ctrl.attachRipple = attachRipple;
13891
13892 function attachRipple (scope, element) {
13893 var options = {};
13894 $mdListInkRipple.attach(scope, element, options);
13895 }
13896}
13897
13898})();
13899(function(){
13900"use strict";
13901
13902/**
13903 * @ngdoc module
13904 * @name material.components.menu
13905 */
13906
13907angular.module('material.components.menu', [
13908 'material.core',
13909 'material.components.backdrop'
13910]);
13911
13912})();
13913(function(){
13914"use strict";
13915
13916/**
13917 * @ngdoc module
13918 * @name material.components.menuBar
13919 */
13920
13921angular.module('material.components.menuBar', [
13922 'material.core',
13923 'material.components.icon',
13924 'material.components.menu'
13925]);
13926
13927})();
13928(function(){
13929"use strict";
13930
13931/**
13932 * @ngdoc module
13933 * @name material.components.panel
13934 */
13935MdPanelService.$inject = ["presets", "$rootElement", "$rootScope", "$injector", "$window"];
13936angular
13937 .module('material.components.panel', [
13938 'material.core',
13939 'material.components.backdrop'
13940 ])
13941 .provider('$mdPanel', MdPanelProvider);
13942
13943
13944/*****************************************************************************
13945 * PUBLIC DOCUMENTATION *
13946 *****************************************************************************/
13947
13948
13949/**
13950 * @ngdoc service
13951 * @name $mdPanelProvider
13952 * @module material.components.panel
13953 *
13954 * @description
13955 * `$mdPanelProvider` allows users to create configuration presets that will be
13956 * stored within a cached presets object. When the configuration is needed, the
13957 * user can request the preset by passing it as the first parameter in the
13958 * `$mdPanel.create` or `$mdPanel.open` methods.
13959 *
13960 * @usage
13961 * <hljs lang="js">
13962 * (function(angular, undefined) {
13963 * 'use strict';
13964 *
13965 * angular
13966 * .module('demoApp', ['ngMaterial'])
13967 * .config(DemoConfig)
13968 * .controller('DemoCtrl', DemoCtrl)
13969 * .controller('DemoMenuCtrl', DemoMenuCtrl);
13970 *
13971 * function DemoConfig($mdPanelProvider) {
13972 * $mdPanelProvider.definePreset('demoPreset', {
13973 * attachTo: angular.element(document.body),
13974 * controller: DemoMenuCtrl,
13975 * controllerAs: 'ctrl',
13976 * template: '' +
13977 * '<div class="menu-panel" md-whiteframe="4">' +
13978 * ' <div class="menu-content">' +
13979 * ' <div class="menu-item" ng-repeat="item in ctrl.items">' +
13980 * ' <button class="md-button">' +
13981 * ' <span>{{item}}</span>' +
13982 * ' </button>' +
13983 * ' </div>' +
13984 * ' <md-divider></md-divider>' +
13985 * ' <div class="menu-item">' +
13986 * ' <button class="md-button" ng-click="ctrl.closeMenu()">' +
13987 * ' <span>Close Menu</span>' +
13988 * ' </button>' +
13989 * ' </div>' +
13990 * ' </div>' +
13991 * '</div>',
13992 * panelClass: 'menu-panel-container',
13993 * focusOnOpen: false,
13994 * zIndex: 100,
13995 * propagateContainerEvents: true,
13996 * groupName: 'menus'
13997 * });
13998 * }
13999 *
14000 * function PanelProviderCtrl($mdPanel) {
14001 * this.navigation = {
14002 * name: 'navigation',
14003 * items: [
14004 * 'Home',
14005 * 'About',
14006 * 'Contact'
14007 * ]
14008 * };
14009 * this.favorites = {
14010 * name: 'favorites',
14011 * items: [
14012 * 'Add to Favorites'
14013 * ]
14014 * };
14015 * this.more = {
14016 * name: 'more',
14017 * items: [
14018 * 'Account',
14019 * 'Sign Out'
14020 * ]
14021 * };
14022 *
14023 * $mdPanel.newPanelGroup('menus', {
14024 * maxOpen: 2
14025 * });
14026 *
14027 * this.showMenu = function($event, menu) {
14028 * $mdPanel.open('demoPreset', {
14029 * id: 'menu_' + menu.name,
14030 * position: $mdPanel.newPanelPosition()
14031 * .relativeTo($event.srcElement)
14032 * .addPanelPosition(
14033 * $mdPanel.xPosition.ALIGN_START,
14034 * $mdPanel.yPosition.BELOW
14035 * ),
14036 * locals: {
14037 * items: menu.items
14038 * },
14039 * openFrom: $event
14040 * });
14041 * };
14042 * }
14043 *
14044 * function PanelMenuCtrl(mdPanelRef) {
14045 * this.closeMenu = function() {
14046 * mdPanelRef && mdPanelRef.close();
14047 * };
14048 * }
14049 * })(angular);
14050 * </hljs>
14051 */
14052
14053/**
14054 * @ngdoc method
14055 * @name $mdPanelProvider#definePreset
14056 * @description
14057 * Takes the passed in preset name and preset configuration object and adds it
14058 * to the `_presets` object of the provider. This `_presets` object is then
14059 * passed along to the `$mdPanel` service.
14060 *
14061 * @param {string} name Preset name.
14062 * @param {!Object} preset Specific configuration object that can contain any
14063 * and all of the parameters avaialble within the `$mdPanel.create` method.
14064 * However, parameters that pertain to id, position, animation, and user
14065 * interaction are not allowed and will be removed from the preset
14066 * configuration.
14067 */
14068
14069
14070/*****************************************************************************
14071 * MdPanel Service *
14072 *****************************************************************************/
14073
14074
14075/**
14076 * @ngdoc service
14077 * @name $mdPanel
14078 * @module material.components.panel
14079 *
14080 * @description
14081 * `$mdPanel` is a robust, low-level service for creating floating panels on
14082 * the screen. It can be used to implement tooltips, dialogs, pop-ups, etc.
14083 *
14084 * @usage
14085 * <hljs lang="js">
14086 * (function(angular, undefined) {
14087 * 'use strict';
14088 *
14089 * angular
14090 * .module('demoApp', ['ngMaterial'])
14091 * .controller('DemoDialogController', DialogController);
14092 *
14093 * var panelRef;
14094 *
14095 * function showPanel($event) {
14096 * var panelPosition = $mdPanel.newPanelPosition()
14097 * .absolute()
14098 * .top('50%')
14099 * .left('50%');
14100 *
14101 * var panelAnimation = $mdPanel.newPanelAnimation()
14102 * .targetEvent($event)
14103 * .defaultAnimation('md-panel-animate-fly')
14104 * .closeTo('.show-button');
14105 *
14106 * var config = {
14107 * attachTo: angular.element(document.body),
14108 * controller: DialogController,
14109 * controllerAs: 'ctrl',
14110 * position: panelPosition,
14111 * animation: panelAnimation,
14112 * targetEvent: $event,
14113 * templateUrl: 'dialog-template.html',
14114 * clickOutsideToClose: true,
14115 * escapeToClose: true,
14116 * focusOnOpen: true
14117 * }
14118 *
14119 * $mdPanel.open(config)
14120 * .then(function(result) {
14121 * panelRef = result;
14122 * });
14123 * }
14124 *
14125 * function DialogController(MdPanelRef) {
14126 * function closeDialog() {
14127 * if (MdPanelRef) MdPanelRef.close();
14128 * }
14129 * }
14130 * })(angular);
14131 * </hljs>
14132 */
14133
14134/**
14135 * @ngdoc method
14136 * @name $mdPanel#create
14137 * @description
14138 * Creates a panel with the specified options.
14139 *
14140 * @param config {!Object=} Specific configuration object that may contain the
14141 * following properties:
14142 *
14143 * - `id` - `{string=}`: An ID to track the panel by. When an ID is provided,
14144 * the created panel is added to a tracked panels object. Any subsequent
14145 * requests made to create a panel with that ID are ignored. This is useful
14146 * in having the panel service not open multiple panels from the same user
14147 * interaction when there is no backdrop and events are propagated. Defaults
14148 * to an arbitrary string that is not tracked.
14149 * - `template` - `{string=}`: HTML template to show in the panel. This
14150 * **must** be trusted HTML with respect to Angular’s
14151 * [$sce service](https://docs.angularjs.org/api/ng/service/$sce).
14152 * - `templateUrl` - `{string=}`: The URL that will be used as the content of
14153 * the panel.
14154 * - `contentElement` - `{(string|!angular.JQLite|!Element)=}`: Pre-compiled
14155 * element to be used as the panel's content.
14156 * - `controller` - `{(function|string)=}`: The controller to associate with
14157 * the panel. The controller can inject a reference to the returned
14158 * panelRef, which allows the panel to be closed, hidden, and shown. Any
14159 * fields passed in through locals or resolve will be bound to the
14160 * controller.
14161 * - `controllerAs` - `{string=}`: An alias to assign the controller to on
14162 * the scope.
14163 * - `bindToController` - `{boolean=}`: Binds locals to the controller
14164 * instead of passing them in. Defaults to true, as this is a best
14165 * practice.
14166 * - `locals` - `{Object=}`: An object containing key/value pairs. The keys
14167 * will be used as names of values to inject into the controller. For
14168 * example, `locals: {three: 3}` would inject `three` into the controller,
14169 * with the value 3.
14170 * - `resolve` - `{Object=}`: Similar to locals, except it takes promises as
14171 * values. The panel will not open until all of the promises resolve.
14172 * - `attachTo` - `{(string|!angular.JQLite|!Element)=}`: The element to
14173 * attach the panel to. Defaults to appending to the root element of the
14174 * application.
14175 * - `propagateContainerEvents` - `{boolean=}`: Whether pointer or touch
14176 * events should be allowed to propagate 'go through' the container, aka the
14177 * wrapper, of the panel. Defaults to false.
14178 * - `panelClass` - `{string=}`: A css class to apply to the panel element.
14179 * This class should define any borders, box-shadow, etc. for the panel.
14180 * - `zIndex` - `{number=}`: The z-index to place the panel at.
14181 * Defaults to 80.
14182 * - `position` - `{MdPanelPosition=}`: An MdPanelPosition object that
14183 * specifies the alignment of the panel. For more information, see
14184 * `MdPanelPosition`.
14185 * - `clickOutsideToClose` - `{boolean=}`: Whether the user can click
14186 * outside the panel to close it. Defaults to false.
14187 * - `escapeToClose` - `{boolean=}`: Whether the user can press escape to
14188 * close the panel. Defaults to false.
14189 * - `onCloseSuccess` - `{function(!panelRef, string)=}`: Function that is
14190 * called after the close successfully finishes. The first parameter passed
14191 * into this function is the current panelRef and the 2nd is an optional
14192 * string explaining the close reason. The currently supported closeReasons
14193 * can be found in the MdPanelRef.closeReasons enum. These are by default
14194 * passed along by the panel.
14195 * - `trapFocus` - `{boolean=}`: Whether focus should be trapped within the
14196 * panel. If `trapFocus` is true, the user will not be able to interact
14197 * with the rest of the page until the panel is dismissed. Defaults to
14198 * false.
14199 * - `focusOnOpen` - `{boolean=}`: An option to override focus behavior on
14200 * open. Only disable if focusing some other way, as focus management is
14201 * required for panels to be accessible. Defaults to true.
14202 * - `fullscreen` - `{boolean=}`: Whether the panel should be full screen.
14203 * Applies the class `._md-panel-fullscreen` to the panel on open. Defaults
14204 * to false.
14205 * - `animation` - `{MdPanelAnimation=}`: An MdPanelAnimation object that
14206 * specifies the animation of the panel. For more information, see
14207 * `MdPanelAnimation`.
14208 * - `hasBackdrop` - `{boolean=}`: Whether there should be an opaque backdrop
14209 * behind the panel. Defaults to false.
14210 * - `disableParentScroll` - `{boolean=}`: Whether the user can scroll the
14211 * page behind the panel. Defaults to false.
14212 * - `onDomAdded` - `{function=}`: Callback function used to announce when
14213 * the panel is added to the DOM.
14214 * - `onOpenComplete` - `{function=}`: Callback function used to announce
14215 * when the open() action is finished.
14216 * - `onRemoving` - `{function=}`: Callback function used to announce the
14217 * close/hide() action is starting.
14218 * - `onDomRemoved` - `{function=}`: Callback function used to announce when
14219 * the panel is removed from the DOM.
14220 * - `origin` - `{(string|!angular.JQLite|!Element)=}`: The element to focus
14221 * on when the panel closes. This is commonly the element which triggered
14222 * the opening of the panel. If you do not use `origin`, you need to control
14223 * the focus manually.
14224 * - `groupName` - `{(string|!Array<string>)=}`: A group name or an array of
14225 * group names. The group name is used for creating a group of panels. The
14226 * group is used for configuring the number of open panels and identifying
14227 * specific behaviors for groups. For instance, all tooltips could be
14228 * identified using the same groupName.
14229 *
14230 * @returns {!MdPanelRef} panelRef
14231 */
14232
14233/**
14234 * @ngdoc method
14235 * @name $mdPanel#open
14236 * @description
14237 * Calls the create method above, then opens the panel. This is a shortcut for
14238 * creating and then calling open manually. If custom methods need to be
14239 * called when the panel is added to the DOM or opened, do not use this method.
14240 * Instead create the panel, chain promises on the domAdded and openComplete
14241 * methods, and call open from the returned panelRef.
14242 *
14243 * @param {!Object=} config Specific configuration object that may contain
14244 * the properties defined in `$mdPanel.create`.
14245 * @returns {!angular.$q.Promise<!MdPanelRef>} panelRef A promise that resolves
14246 * to an instance of the panel.
14247 */
14248
14249/**
14250 * @ngdoc method
14251 * @name $mdPanel#newPanelPosition
14252 * @description
14253 * Returns a new instance of the MdPanelPosition object. Use this to create
14254 * the position config object.
14255 *
14256 * @returns {!MdPanelPosition} panelPosition
14257 */
14258
14259/**
14260 * @ngdoc method
14261 * @name $mdPanel#newPanelAnimation
14262 * @description
14263 * Returns a new instance of the MdPanelAnimation object. Use this to create
14264 * the animation config object.
14265 *
14266 * @returns {!MdPanelAnimation} panelAnimation
14267 */
14268
14269/**
14270 * @ngdoc method
14271 * @name $mdPanel#newPanelGroup
14272 * @description
14273 * Creates a panel group and adds it to a tracked list of panel groups.
14274 *
14275 * @param {string} groupName Name of the group to create.
14276 * @param {!Object=} config Specific configuration object that may contain the
14277 * following properties:
14278 *
14279 * - `maxOpen` - `{number=}`: The maximum number of panels that are allowed to
14280 * be open within a defined panel group.
14281 *
14282 * @returns {!Object<string,
14283 * {panels: !Array<!MdPanelRef>,
14284 * openPanels: !Array<!MdPanelRef>,
14285 * maxOpen: number}>} panelGroup
14286 */
14287
14288/**
14289 * @ngdoc method
14290 * @name $mdPanel#setGroupMaxOpen
14291 * @description
14292 * Sets the maximum number of panels in a group that can be opened at a given
14293 * time.
14294 *
14295 * @param {string} groupName The name of the group to configure.
14296 * @param {number} maxOpen The maximum number of panels that can be
14297 * opened. Infinity can be passed in to remove the maxOpen limit.
14298 */
14299
14300
14301/*****************************************************************************
14302 * MdPanelRef *
14303 *****************************************************************************/
14304
14305
14306/**
14307 * @ngdoc type
14308 * @name MdPanelRef
14309 * @module material.components.panel
14310 * @description
14311 * A reference to a created panel. This reference contains a unique id for the
14312 * panel, along with the following properties:
14313 *
14314 * - `id` - `{string}`: The unique id for the panel. This id is used to track
14315 * when a panel was interacted with.
14316 * - `config` - `{!Object=}`: The entire config object that was used in
14317 * create.
14318 * - `isAttached` - `{boolean}`: Whether the panel is attached to the DOM.
14319 * Visibility to the user does not factor into isAttached.
14320 * - `panelContainer` - `{angular.JQLite}`: The wrapper element containing the
14321 * panel. This property is added in order to have access to the `addClass`,
14322 * `removeClass`, `toggleClass`, etc methods.
14323 * - `panelEl` - `{angular.JQLite}`: The panel element. This property is added
14324 * in order to have access to the `addClass`, `removeClass`, `toggleClass`,
14325 * etc methods.
14326 */
14327
14328/**
14329 * @ngdoc method
14330 * @name MdPanelRef#open
14331 * @description
14332 * Attaches and shows the panel.
14333 *
14334 * @returns {!angular.$q.Promise} A promise that is resolved when the panel is
14335 * opened.
14336 */
14337
14338/**
14339 * @ngdoc method
14340 * @name MdPanelRef#close
14341 * @description
14342 * Hides and detaches the panel. Note that this will **not** destroy the panel.
14343 * If you don't intend on using the panel again, call the {@link #destroy
14344 * destroy} method afterwards.
14345 *
14346 * @returns {!angular.$q.Promise} A promise that is resolved when the panel is
14347 * closed.
14348 */
14349
14350/**
14351 * @ngdoc method
14352 * @name MdPanelRef#attach
14353 * @description
14354 * Create the panel elements and attach them to the DOM. The panel will be
14355 * hidden by default.
14356 *
14357 * @returns {!angular.$q.Promise} A promise that is resolved when the panel is
14358 * attached.
14359 */
14360
14361/**
14362 * @ngdoc method
14363 * @name MdPanelRef#detach
14364 * @description
14365 * Removes the panel from the DOM. This will NOT hide the panel before removing
14366 * it.
14367 *
14368 * @returns {!angular.$q.Promise} A promise that is resolved when the panel is
14369 * detached.
14370 */
14371
14372/**
14373 * @ngdoc method
14374 * @name MdPanelRef#show
14375 * @description
14376 * Shows the panel.
14377 *
14378 * @returns {!angular.$q.Promise} A promise that is resolved when the panel has
14379 * shown and animations are completed.
14380 */
14381
14382/**
14383 * @ngdoc method
14384 * @name MdPanelRef#hide
14385 * @description
14386 * Hides the panel.
14387 *
14388 * @returns {!angular.$q.Promise} A promise that is resolved when the panel has
14389 * hidden and animations are completed.
14390 */
14391
14392/**
14393 * @ngdoc method
14394 * @name MdPanelRef#destroy
14395 * @description
14396 * Destroys the panel. The panel cannot be opened again after this is called.
14397 */
14398
14399/**
14400 * @ngdoc method
14401 * @name MdPanelRef#addClass
14402 * @deprecated
14403 * This method is in the process of being deprecated in favor of using the panel
14404 * and container JQLite elements that are referenced in the MdPanelRef object.
14405 * Full deprecation is scheduled for material 1.2.
14406 * @description
14407 * Adds a class to the panel. DO NOT use this hide/show the panel.
14408 *
14409 * @param {string} newClass class to be added.
14410 * @param {boolean} toElement Whether or not to add the class to the panel
14411 * element instead of the container.
14412 */
14413
14414/**
14415 * @ngdoc method
14416 * @name MdPanelRef#removeClass
14417 * @deprecated
14418 * This method is in the process of being deprecated in favor of using the panel
14419 * and container JQLite elements that are referenced in the MdPanelRef object.
14420 * Full deprecation is scheduled for material 1.2.
14421 * @description
14422 * Removes a class from the panel. DO NOT use this to hide/show the panel.
14423 *
14424 * @param {string} oldClass Class to be removed.
14425 * @param {boolean} fromElement Whether or not to remove the class from the
14426 * panel element instead of the container.
14427 */
14428
14429/**
14430 * @ngdoc method
14431 * @name MdPanelRef#toggleClass
14432 * @deprecated
14433 * This method is in the process of being deprecated in favor of using the panel
14434 * and container JQLite elements that are referenced in the MdPanelRef object.
14435 * Full deprecation is scheduled for material 1.2.
14436 * @description
14437 * Toggles a class on the panel. DO NOT use this to hide/show the panel.
14438 *
14439 * @param {string} toggleClass Class to be toggled.
14440 * @param {boolean} onElement Whether or not to remove the class from the panel
14441 * element instead of the container.
14442 */
14443
14444/**
14445 * @ngdoc method
14446 * @name MdPanelRef#updatePosition
14447 * @description
14448 * Updates the position configuration of a panel. Use this to update the
14449 * position of a panel that is open, without having to close and re-open the
14450 * panel.
14451 *
14452 * @param {!MdPanelPosition} position
14453 */
14454
14455/**
14456 * @ngdoc method
14457 * @name MdPanelRef#addToGroup
14458 * @description
14459 * Adds a panel to a group if the panel does not exist within the group already.
14460 * A panel can only exist within a single group.
14461 *
14462 * @param {string} groupName The name of the group to add the panel to.
14463 */
14464
14465/**
14466 * @ngdoc method
14467 * @name MdPanelRef#removeFromGroup
14468 * @description
14469 * Removes a panel from a group if the panel exists within that group. The group
14470 * must be created ahead of time.
14471 *
14472 * @param {string} groupName The name of the group.
14473 */
14474
14475/**
14476 * @ngdoc method
14477 * @name MdPanelRef#registerInterceptor
14478 * @description
14479 * Registers an interceptor with the panel. The callback should return a promise,
14480 * which will allow the action to continue when it gets resolved, or will
14481 * prevent an action if it is rejected. The interceptors are called sequentially
14482 * and it reverse order. `type` must be one of the following
14483 * values available on `$mdPanel.interceptorTypes`:
14484 * * `CLOSE` - Gets called before the panel begins closing.
14485 *
14486 * @param {string} type Type of interceptor.
14487 * @param {!angular.$q.Promise<any>} callback Callback to be registered.
14488 * @returns {!MdPanelRef}
14489 */
14490
14491/**
14492 * @ngdoc method
14493 * @name MdPanelRef#removeInterceptor
14494 * @description
14495 * Removes a registered interceptor.
14496 *
14497 * @param {string} type Type of interceptor to be removed.
14498 * @param {function(): !angular.$q.Promise<any>} callback Interceptor to be removed.
14499 * @returns {!MdPanelRef}
14500 */
14501
14502/**
14503 * @ngdoc method
14504 * @name MdPanelRef#removeAllInterceptors
14505 * @description
14506 * Removes all interceptors. If a type is supplied, only the
14507 * interceptors of that type will be cleared.
14508 *
14509 * @param {string=} type Type of interceptors to be removed.
14510 * @returns {!MdPanelRef}
14511 */
14512
14513/**
14514 * @ngdoc method
14515 * @name MdPanelRef#updateAnimation
14516 * @description
14517 * Updates the animation configuration for a panel. You can use this to change
14518 * the panel's animation without having to re-create it.
14519 *
14520 * @param {!MdPanelAnimation} animation
14521 */
14522
14523
14524/*****************************************************************************
14525 * MdPanelPosition *
14526 *****************************************************************************/
14527
14528
14529/**
14530 * @ngdoc type
14531 * @name MdPanelPosition
14532 * @module material.components.panel
14533 * @description
14534 *
14535 * Object for configuring the position of the panel.
14536 *
14537 * @usage
14538 *
14539 * #### Centering the panel
14540 *
14541 * <hljs lang="js">
14542 * new MdPanelPosition().absolute().center();
14543 * </hljs>
14544 *
14545 * #### Overlapping the panel with an element
14546 *
14547 * <hljs lang="js">
14548 * new MdPanelPosition()
14549 * .relativeTo(someElement)
14550 * .addPanelPosition(
14551 * $mdPanel.xPosition.ALIGN_START,
14552 * $mdPanel.yPosition.ALIGN_TOPS
14553 * );
14554 * </hljs>
14555 *
14556 * #### Aligning the panel with the bottom of an element
14557 *
14558 * <hljs lang="js">
14559 * new MdPanelPosition()
14560 * .relativeTo(someElement)
14561 * .addPanelPosition($mdPanel.xPosition.CENTER, $mdPanel.yPosition.BELOW);
14562 * </hljs>
14563 */
14564
14565/**
14566 * @ngdoc method
14567 * @name MdPanelPosition#absolute
14568 * @description
14569 * Positions the panel absolutely relative to the parent element. If the parent
14570 * is document.body, this is equivalent to positioning the panel absolutely
14571 * within the viewport.
14572 *
14573 * @returns {!MdPanelPosition}
14574 */
14575
14576/**
14577 * @ngdoc method
14578 * @name MdPanelPosition#relativeTo
14579 * @description
14580 * Positions the panel relative to a specific element.
14581 *
14582 * @param {string|!Element|!angular.JQLite} element Query selector, DOM element,
14583 * or angular element to position the panel with respect to.
14584 * @returns {!MdPanelPosition}
14585 */
14586
14587/**
14588 * @ngdoc method
14589 * @name MdPanelPosition#top
14590 * @description
14591 * Sets the value of `top` for the panel. Clears any previously set vertical
14592 * position.
14593 *
14594 * @param {string=} top Value of `top`. Defaults to '0'.
14595 * @returns {!MdPanelPosition}
14596 */
14597
14598/**
14599 * @ngdoc method
14600 * @name MdPanelPosition#bottom
14601 * @description
14602 * Sets the value of `bottom` for the panel. Clears any previously set vertical
14603 * position.
14604 *
14605 * @param {string=} bottom Value of `bottom`. Defaults to '0'.
14606 * @returns {!MdPanelPosition}
14607 */
14608
14609/**
14610 * @ngdoc method
14611 * @name MdPanelPosition#start
14612 * @description
14613 * Sets the panel to the start of the page - `left` if `ltr` or `right` for
14614 * `rtl`. Clears any previously set horizontal position.
14615 *
14616 * @param {string=} start Value of position. Defaults to '0'.
14617 * @returns {!MdPanelPosition}
14618 */
14619
14620/**
14621 * @ngdoc method
14622 * @name MdPanelPosition#end
14623 * @description
14624 * Sets the panel to the end of the page - `right` if `ltr` or `left` for `rtl`.
14625 * Clears any previously set horizontal position.
14626 *
14627 * @param {string=} end Value of position. Defaults to '0'.
14628 * @returns {!MdPanelPosition}
14629 */
14630
14631/**
14632 * @ngdoc method
14633 * @name MdPanelPosition#left
14634 * @description
14635 * Sets the value of `left` for the panel. Clears any previously set
14636 * horizontal position.
14637 *
14638 * @param {string=} left Value of `left`. Defaults to '0'.
14639 * @returns {!MdPanelPosition}
14640 */
14641
14642/**
14643 * @ngdoc method
14644 * @name MdPanelPosition#right
14645 * @description
14646 * Sets the value of `right` for the panel. Clears any previously set
14647 * horizontal position.
14648 *
14649 * @param {string=} right Value of `right`. Defaults to '0'.
14650 * @returns {!MdPanelPosition}
14651 */
14652
14653/**
14654 * @ngdoc method
14655 * @name MdPanelPosition#centerHorizontally
14656 * @description
14657 * Centers the panel horizontally in the viewport. Clears any previously set
14658 * horizontal position.
14659 *
14660 * @returns {!MdPanelPosition}
14661 */
14662
14663/**
14664 * @ngdoc method
14665 * @name MdPanelPosition#centerVertically
14666 * @description
14667 * Centers the panel vertically in the viewport. Clears any previously set
14668 * vertical position.
14669 *
14670 * @returns {!MdPanelPosition}
14671 */
14672
14673/**
14674 * @ngdoc method
14675 * @name MdPanelPosition#center
14676 * @description
14677 * Centers the panel horizontally and vertically in the viewport. This is
14678 * equivalent to calling both `centerHorizontally` and `centerVertically`.
14679 * Clears any previously set horizontal and vertical positions.
14680 *
14681 * @returns {!MdPanelPosition}
14682 */
14683
14684/**
14685 * @ngdoc method
14686 * @name MdPanelPosition#addPanelPosition
14687 * @description
14688 * Sets the x and y position for the panel relative to another element. Can be
14689 * called multiple times to specify an ordered list of panel positions. The
14690 * first position which allows the panel to be completely on-screen will be
14691 * chosen; the last position will be chose whether it is on-screen or not.
14692 *
14693 * xPosition must be one of the following values available on
14694 * $mdPanel.xPosition:
14695 *
14696 *
14697 * CENTER | ALIGN_START | ALIGN_END | OFFSET_START | OFFSET_END
14698 *
14699 * <pre>
14700 * *************
14701 * * *
14702 * * PANEL *
14703 * * *
14704 * *************
14705 * A B C D E
14706 *
14707 * A: OFFSET_START (for LTR displays)
14708 * B: ALIGN_START (for LTR displays)
14709 * C: CENTER
14710 * D: ALIGN_END (for LTR displays)
14711 * E: OFFSET_END (for LTR displays)
14712 * </pre>
14713 *
14714 * yPosition must be one of the following values available on
14715 * $mdPanel.yPosition:
14716 *
14717 * CENTER | ALIGN_TOPS | ALIGN_BOTTOMS | ABOVE | BELOW
14718 *
14719 * <pre>
14720 * F
14721 * G *************
14722 * * *
14723 * H * PANEL *
14724 * * *
14725 * I *************
14726 * J
14727 *
14728 * F: BELOW
14729 * G: ALIGN_TOPS
14730 * H: CENTER
14731 * I: ALIGN_BOTTOMS
14732 * J: ABOVE
14733 * </pre>
14734 *
14735 * @param {string} xPosition
14736 * @param {string} yPosition
14737 * @returns {!MdPanelPosition}
14738 */
14739
14740/**
14741 * @ngdoc method
14742 * @name MdPanelPosition#withOffsetX
14743 * @description
14744 * Sets the value of the offset in the x-direction.
14745 *
14746 * @param {string} offsetX
14747 * @returns {!MdPanelPosition}
14748 */
14749
14750/**
14751 * @ngdoc method
14752 * @name MdPanelPosition#withOffsetY
14753 * @description
14754 * Sets the value of the offset in the y-direction.
14755 *
14756 * @param {string} offsetY
14757 * @returns {!MdPanelPosition}
14758 */
14759
14760
14761/*****************************************************************************
14762 * MdPanelAnimation *
14763 *****************************************************************************/
14764
14765
14766/**
14767 * @ngdoc type
14768 * @name MdPanelAnimation
14769 * @module material.components.panel
14770 * @description
14771 * Animation configuration object. To use, create an MdPanelAnimation with the
14772 * desired properties, then pass the object as part of $mdPanel creation.
14773 *
14774 * @usage
14775 *
14776 * <hljs lang="js">
14777 * var panelAnimation = new MdPanelAnimation()
14778 * .openFrom(myButtonEl)
14779 * .duration(1337)
14780 * .closeTo('.my-button')
14781 * .withAnimation($mdPanel.animation.SCALE);
14782 *
14783 * $mdPanel.create({
14784 * animation: panelAnimation
14785 * });
14786 * </hljs>
14787 */
14788
14789/**
14790 * @ngdoc method
14791 * @name MdPanelAnimation#openFrom
14792 * @description
14793 * Specifies where to start the open animation. `openFrom` accepts a
14794 * click event object, query selector, DOM element, or a Rect object that
14795 * is used to determine the bounds. When passed a click event, the location
14796 * of the click will be used as the position to start the animation.
14797 *
14798 * @param {string|!Element|!Event|{top: number, left: number}}
14799 * @returns {!MdPanelAnimation}
14800 */
14801
14802/**
14803 * @ngdoc method
14804 * @name MdPanelAnimation#closeTo
14805 * @description
14806 * Specifies where to animate the panel close. `closeTo` accepts a
14807 * query selector, DOM element, or a Rect object that is used to determine
14808 * the bounds.
14809 *
14810 * @param {string|!Element|{top: number, left: number}}
14811 * @returns {!MdPanelAnimation}
14812 */
14813
14814/**
14815 * @ngdoc method
14816 * @name MdPanelAnimation#withAnimation
14817 * @description
14818 * Specifies the animation class.
14819 *
14820 * There are several default animations that can be used:
14821 * ($mdPanel.animation)
14822 * SLIDE: The panel slides in and out from the specified
14823 * elements. It will not fade in or out.
14824 * SCALE: The panel scales in and out. Slide and fade are
14825 * included in this animation.
14826 * FADE: The panel fades in and out.
14827 *
14828 * Custom classes will by default fade in and out unless
14829 * "transition: opacity 1ms" is added to the to custom class.
14830 *
14831 * @param {string|{open: string, close: string}} cssClass
14832 * @returns {!MdPanelAnimation}
14833 */
14834
14835/**
14836 * @ngdoc method
14837 * @name MdPanelAnimation#duration
14838 * @description
14839 * Specifies the duration of the animation in milliseconds. The `duration`
14840 * method accepts either a number or an object with separate open and close
14841 * durations.
14842 *
14843 * @param {number|{open: number, close: number}} duration
14844 * @returns {!MdPanelAnimation}
14845 */
14846
14847
14848/*****************************************************************************
14849 * PUBLIC DOCUMENTATION *
14850 *****************************************************************************/
14851
14852
14853var MD_PANEL_Z_INDEX = 80;
14854var MD_PANEL_HIDDEN = '_md-panel-hidden';
14855var FOCUS_TRAP_TEMPLATE = angular.element(
14856 '<div class="_md-panel-focus-trap" tabindex="0"></div>');
14857
14858var _presets = {};
14859
14860
14861/**
14862 * A provider that is used for creating presets for the panel API.
14863 * @final @constructor @ngInject
14864 */
14865function MdPanelProvider() {
14866 return {
14867 'definePreset': definePreset,
14868 'getAllPresets': getAllPresets,
14869 'clearPresets': clearPresets,
14870 '$get': $getProvider()
14871 };
14872}
14873
14874
14875/**
14876 * Takes the passed in panel configuration object and adds it to the `_presets`
14877 * object at the specified name.
14878 * @param {string} name Name of the preset to set.
14879 * @param {!Object} preset Specific configuration object that can contain any
14880 * and all of the parameters avaialble within the `$mdPanel.create` method.
14881 * However, parameters that pertain to id, position, animation, and user
14882 * interaction are not allowed and will be removed from the preset
14883 * configuration.
14884 */
14885function definePreset(name, preset) {
14886 if (!name || !preset) {
14887 throw new Error('mdPanelProvider: The panel preset definition is ' +
14888 'malformed. The name and preset object are required.');
14889 } else if (_presets.hasOwnProperty(name)) {
14890 throw new Error('mdPanelProvider: The panel preset you have requested ' +
14891 'has already been defined.');
14892 }
14893
14894 // Delete any property on the preset that is not allowed.
14895 delete preset.id;
14896 delete preset.position;
14897 delete preset.animation;
14898
14899 _presets[name] = preset;
14900}
14901
14902
14903/**
14904 * Gets a clone of the `_presets`.
14905 * @return {!Object}
14906 */
14907function getAllPresets() {
14908 return angular.copy(_presets);
14909}
14910
14911
14912/**
14913 * Clears all of the stored presets.
14914 */
14915function clearPresets() {
14916 _presets = {};
14917}
14918
14919
14920/**
14921 * Represents the `$get` method of the Angular provider. From here, a new
14922 * reference to the MdPanelService is returned where the needed arguments are
14923 * passed in including the MdPanelProvider `_presets`.
14924 * @param {!Object} _presets
14925 * @param {!angular.JQLite} $rootElement
14926 * @param {!angular.Scope} $rootScope
14927 * @param {!angular.$injector} $injector
14928 * @param {!angular.$window} $window
14929 */
14930function $getProvider() {
14931 return [
14932 '$rootElement', '$rootScope', '$injector', '$window',
14933 function($rootElement, $rootScope, $injector, $window) {
14934 return new MdPanelService(_presets, $rootElement, $rootScope,
14935 $injector, $window);
14936 }
14937 ];
14938}
14939
14940
14941/*****************************************************************************
14942 * MdPanel Service *
14943 *****************************************************************************/
14944
14945
14946/**
14947 * A service that is used for controlling/displaying panels on the screen.
14948 * @param {!Object} presets
14949 * @param {!angular.JQLite} $rootElement
14950 * @param {!angular.Scope} $rootScope
14951 * @param {!angular.$injector} $injector
14952 * @param {!angular.$window} $window
14953 * @final @constructor @ngInject
14954 */
14955function MdPanelService(presets, $rootElement, $rootScope, $injector, $window) {
14956 /**
14957 * Default config options for the panel.
14958 * Anything angular related needs to be done later. Therefore
14959 * scope: $rootScope.$new(true),
14960 * attachTo: $rootElement,
14961 * are added later.
14962 * @private {!Object}
14963 */
14964 this._defaultConfigOptions = {
14965 bindToController: true,
14966 clickOutsideToClose: false,
14967 disableParentScroll: false,
14968 escapeToClose: false,
14969 focusOnOpen: true,
14970 fullscreen: false,
14971 hasBackdrop: false,
14972 propagateContainerEvents: false,
14973 transformTemplate: angular.bind(this, this._wrapTemplate),
14974 trapFocus: false,
14975 zIndex: MD_PANEL_Z_INDEX
14976 };
14977
14978 /** @private {!Object} */
14979 this._config = {};
14980
14981 /** @private {!Object} */
14982 this._presets = presets;
14983
14984 /** @private @const */
14985 this._$rootElement = $rootElement;
14986
14987 /** @private @const */
14988 this._$rootScope = $rootScope;
14989
14990 /** @private @const */
14991 this._$injector = $injector;
14992
14993 /** @private @const */
14994 this._$window = $window;
14995
14996 /** @private @const */
14997 this._$mdUtil = this._$injector.get('$mdUtil');
14998
14999 /** @private {!Object<string, !MdPanelRef>} */
15000 this._trackedPanels = {};
15001
15002 /**
15003 * @private {!Object<string,
15004 * {panels: !Array<!MdPanelRef>,
15005 * openPanels: !Array<!MdPanelRef>,
15006 * maxOpen: number}>}
15007 */
15008 this._groups = Object.create(null);
15009
15010 /**
15011 * Default animations that can be used within the panel.
15012 * @type {enum}
15013 */
15014 this.animation = MdPanelAnimation.animation;
15015
15016 /**
15017 * Possible values of xPosition for positioning the panel relative to
15018 * another element.
15019 * @type {enum}
15020 */
15021 this.xPosition = MdPanelPosition.xPosition;
15022
15023 /**
15024 * Possible values of yPosition for positioning the panel relative to
15025 * another element.
15026 * @type {enum}
15027 */
15028 this.yPosition = MdPanelPosition.yPosition;
15029
15030 /**
15031 * Possible values for the interceptors that can be registered on a panel.
15032 * @type {enum}
15033 */
15034 this.interceptorTypes = MdPanelRef.interceptorTypes;
15035
15036 /**
15037 * Possible values for closing of a panel.
15038 * @type {enum}
15039 */
15040 this.closeReasons = MdPanelRef.closeReasons;
15041
15042 /**
15043 * Possible values of absolute position.
15044 * @type {enum}
15045 */
15046 this.absPosition = MdPanelPosition.absPosition;
15047}
15048
15049
15050/**
15051 * Creates a panel with the specified options.
15052 * @param {string=} preset Name of a preset configuration that can be used to
15053 * extend the panel configuration.
15054 * @param {!Object=} config Configuration object for the panel.
15055 * @returns {!MdPanelRef}
15056 */
15057MdPanelService.prototype.create = function(preset, config) {
15058 if (typeof preset === 'string') {
15059 preset = this._getPresetByName(preset);
15060 } else if (typeof preset === 'object' &&
15061 (angular.isUndefined(config) || !config)) {
15062 config = preset;
15063 preset = {};
15064 }
15065
15066 preset = preset || {};
15067 config = config || {};
15068
15069 // If the passed-in config contains an ID and the ID is within _trackedPanels,
15070 // return the tracked panel after updating its config with the passed-in
15071 // config.
15072 if (angular.isDefined(config.id) && this._trackedPanels[config.id]) {
15073 var trackedPanel = this._trackedPanels[config.id];
15074 angular.extend(trackedPanel.config, config);
15075 return trackedPanel;
15076 }
15077
15078 // Combine the passed-in config, the _defaultConfigOptions, and the preset
15079 // configuration into the `_config`.
15080 this._config = angular.extend({
15081 // If no ID is set within the passed-in config, then create an arbitrary ID.
15082 id: config.id || 'panel_' + this._$mdUtil.nextUid(),
15083 scope: this._$rootScope.$new(true),
15084 attachTo: this._$rootElement
15085 }, this._defaultConfigOptions, config, preset);
15086
15087 // Create the panelRef and add it to the `_trackedPanels` object.
15088 var panelRef = new MdPanelRef(this._config, this._$injector);
15089 this._trackedPanels[config.id] = panelRef;
15090
15091 // Add the panel to each of its requested groups.
15092 if (this._config.groupName) {
15093 if (angular.isString(this._config.groupName)) {
15094 this._config.groupName = [this._config.groupName];
15095 }
15096 angular.forEach(this._config.groupName, function(group) {
15097 panelRef.addToGroup(group);
15098 });
15099 }
15100
15101 this._config.scope.$on('$destroy', angular.bind(panelRef, panelRef.detach));
15102
15103 return panelRef;
15104};
15105
15106
15107/**
15108 * Creates and opens a panel with the specified options.
15109 * @param {string=} preset Name of a preset configuration that can be used to
15110 * extend the panel configuration.
15111 * @param {!Object=} config Configuration object for the panel.
15112 * @returns {!angular.$q.Promise<!MdPanelRef>} The panel created from create.
15113 */
15114MdPanelService.prototype.open = function(preset, config) {
15115 var panelRef = this.create(preset, config);
15116 return panelRef.open().then(function() {
15117 return panelRef;
15118 });
15119};
15120
15121
15122/**
15123 * Gets a specific preset configuration object saved within `_presets`.
15124 * @param {string} preset Name of the preset to search for.
15125 * @returns {!Object} The preset configuration object.
15126 */
15127MdPanelService.prototype._getPresetByName = function(preset) {
15128 if (!this._presets[preset]) {
15129 throw new Error('mdPanel: The panel preset configuration that you ' +
15130 'requested does not exist. Use the $mdPanelProvider to create a ' +
15131 'preset before requesting one.');
15132 }
15133 return this._presets[preset];
15134};
15135
15136
15137/**
15138 * Returns a new instance of the MdPanelPosition. Use this to create the
15139 * positioning object.
15140 * @returns {!MdPanelPosition}
15141 */
15142MdPanelService.prototype.newPanelPosition = function() {
15143 return new MdPanelPosition(this._$injector);
15144};
15145
15146
15147/**
15148 * Returns a new instance of the MdPanelAnimation. Use this to create the
15149 * animation object.
15150 * @returns {!MdPanelAnimation}
15151 */
15152MdPanelService.prototype.newPanelAnimation = function() {
15153 return new MdPanelAnimation(this._$injector);
15154};
15155
15156
15157/**
15158 * Creates a panel group and adds it to a tracked list of panel groups.
15159 * @param groupName {string} Name of the group to create.
15160 * @param config {!Object=} Specific configuration object that may contain the
15161 * following properties:
15162 *
15163 * - `maxOpen` - `{number=}`: The maximum number of panels that are allowed
15164 * open within a defined panel group.
15165 *
15166 * @returns {!Object<string,
15167 * {panels: !Array<!MdPanelRef>,
15168 * openPanels: !Array<!MdPanelRef>,
15169 * maxOpen: number}>} panelGroup
15170 */
15171MdPanelService.prototype.newPanelGroup = function(groupName, config) {
15172 if (!this._groups[groupName]) {
15173 config = config || {};
15174 var group = {
15175 panels: [],
15176 openPanels: [],
15177 maxOpen: config.maxOpen > 0 ? config.maxOpen : Infinity
15178 };
15179 this._groups[groupName] = group;
15180 }
15181 return this._groups[groupName];
15182};
15183
15184
15185/**
15186 * Sets the maximum number of panels in a group that can be opened at a given
15187 * time.
15188 * @param {string} groupName The name of the group to configure.
15189 * @param {number} maxOpen The maximum number of panels that can be
15190 * opened. Infinity can be passed in to remove the maxOpen limit.
15191 */
15192MdPanelService.prototype.setGroupMaxOpen = function(groupName, maxOpen) {
15193 if (this._groups[groupName]) {
15194 this._groups[groupName].maxOpen = maxOpen;
15195 } else {
15196 throw new Error('mdPanel: Group does not exist yet. Call newPanelGroup().');
15197 }
15198};
15199
15200
15201/**
15202 * Determines if the current number of open panels within a group exceeds the
15203 * limit of allowed open panels.
15204 * @param {string} groupName The name of the group to check.
15205 * @returns {boolean} true if open count does exceed maxOpen and false if not.
15206 * @private
15207 */
15208MdPanelService.prototype._openCountExceedsMaxOpen = function(groupName) {
15209 if (this._groups[groupName]) {
15210 var group = this._groups[groupName];
15211 return group.maxOpen > 0 && group.openPanels.length > group.maxOpen;
15212 }
15213 return false;
15214};
15215
15216
15217/**
15218 * Closes the first open panel within a specific group.
15219 * @param {string} groupName The name of the group.
15220 * @private
15221 */
15222MdPanelService.prototype._closeFirstOpenedPanel = function(groupName) {
15223 this._groups[groupName].openPanels[0].close();
15224};
15225
15226
15227/**
15228 * Wraps the users template in two elements, md-panel-outer-wrapper, which
15229 * covers the entire attachTo element, and md-panel, which contains only the
15230 * template. This allows the panel control over positioning, animations,
15231 * and similar properties.
15232 * @param {string} origTemplate The original template.
15233 * @returns {string} The wrapped template.
15234 * @private
15235 */
15236MdPanelService.prototype._wrapTemplate = function(origTemplate) {
15237 var template = origTemplate || '';
15238
15239 // The panel should be initially rendered offscreen so we can calculate
15240 // height and width for positioning.
15241 return '' +
15242 '<div class="md-panel-outer-wrapper">' +
15243 ' <div class="md-panel" style="left: -9999px;">' + template + '</div>' +
15244 '</div>';
15245};
15246
15247
15248/**
15249 * Wraps a content element in a md-panel-outer wrapper and
15250 * positions it off-screen. Allows for proper control over positoning
15251 * and animations.
15252 * @param {!angular.JQLite} contentElement Element to be wrapped.
15253 * @return {!angular.JQLite} Wrapper element.
15254 * @private
15255 */
15256MdPanelService.prototype._wrapContentElement = function(contentElement) {
15257 var wrapper = angular.element('<div class="md-panel-outer-wrapper">');
15258
15259 contentElement.addClass('md-panel').css('left', '-9999px');
15260 wrapper.append(contentElement);
15261
15262 return wrapper;
15263};
15264
15265
15266/*****************************************************************************
15267 * MdPanelRef *
15268 *****************************************************************************/
15269
15270
15271/**
15272 * A reference to a created panel. This reference contains a unique id for the
15273 * panel, along with properties/functions used to control the panel.
15274 * @param {!Object} config
15275 * @param {!angular.$injector} $injector
15276 * @final @constructor
15277 */
15278function MdPanelRef(config, $injector) {
15279 // Injected variables.
15280 /** @private @const {!angular.$q} */
15281 this._$q = $injector.get('$q');
15282
15283 /** @private @const {!angular.$mdCompiler} */
15284 this._$mdCompiler = $injector.get('$mdCompiler');
15285
15286 /** @private @const {!angular.$mdConstant} */
15287 this._$mdConstant = $injector.get('$mdConstant');
15288
15289 /** @private @const {!angular.$mdUtil} */
15290 this._$mdUtil = $injector.get('$mdUtil');
15291
15292 /** @private @const {!angular.$mdTheming} */
15293 this._$mdTheming = $injector.get('$mdTheming');
15294
15295 /** @private @const {!angular.Scope} */
15296 this._$rootScope = $injector.get('$rootScope');
15297
15298 /** @private @const {!angular.$animate} */
15299 this._$animate = $injector.get('$animate');
15300
15301 /** @private @const {!MdPanelRef} */
15302 this._$mdPanel = $injector.get('$mdPanel');
15303
15304 /** @private @const {!angular.$log} */
15305 this._$log = $injector.get('$log');
15306
15307 /** @private @const {!angular.$window} */
15308 this._$window = $injector.get('$window');
15309
15310 /** @private @const {!Function} */
15311 this._$$rAF = $injector.get('$$rAF');
15312
15313 // Public variables.
15314 /**
15315 * Unique id for the panelRef.
15316 * @type {string}
15317 */
15318 this.id = config.id;
15319
15320 /** @type {!Object} */
15321 this.config = config;
15322
15323 /** @type {!angular.JQLite|undefined} */
15324 this.panelContainer;
15325
15326 /** @type {!angular.JQLite|undefined} */
15327 this.panelEl;
15328
15329 /**
15330 * Whether the panel is attached. This is synchronous. When attach is called,
15331 * isAttached is set to true. When detach is called, isAttached is set to
15332 * false.
15333 * @type {boolean}
15334 */
15335 this.isAttached = false;
15336
15337 // Private variables.
15338 /** @private {Array<function()>} */
15339 this._removeListeners = [];
15340
15341 /** @private {!angular.JQLite|undefined} */
15342 this._topFocusTrap;
15343
15344 /** @private {!angular.JQLite|undefined} */
15345 this._bottomFocusTrap;
15346
15347 /** @private {!$mdPanel|undefined} */
15348 this._backdropRef;
15349
15350 /** @private {Function?} */
15351 this._restoreScroll = null;
15352
15353 /**
15354 * Keeps track of all the panel interceptors.
15355 * @private {!Object}
15356 */
15357 this._interceptors = Object.create(null);
15358
15359 /**
15360 * Cleanup function, provided by `$mdCompiler` and assigned after the element
15361 * has been compiled. When `contentElement` is used, the function is used to
15362 * restore the element to it's proper place in the DOM.
15363 * @private {!Function}
15364 */
15365 this._compilerCleanup = null;
15366
15367 /**
15368 * Cache for saving and restoring element inline styles, CSS classes etc.
15369 * @type {{styles: string, classes: string}}
15370 */
15371 this._restoreCache = {
15372 styles: '',
15373 classes: ''
15374 };
15375}
15376
15377
15378MdPanelRef.interceptorTypes = {
15379 CLOSE: 'onClose'
15380};
15381
15382
15383/**
15384 * Opens an already created and configured panel. If the panel is already
15385 * visible, does nothing.
15386 * @returns {!angular.$q.Promise<!MdPanelRef>} A promise that is resolved when
15387 * the panel is opened and animations finish.
15388 */
15389MdPanelRef.prototype.open = function() {
15390 var self = this;
15391 return this._$q(function(resolve, reject) {
15392 var done = self._done(resolve, self);
15393 var show = self._simpleBind(self.show, self);
15394 var checkGroupMaxOpen = function() {
15395 if (self.config.groupName) {
15396 angular.forEach(self.config.groupName, function(group) {
15397 if (self._$mdPanel._openCountExceedsMaxOpen(group)) {
15398 self._$mdPanel._closeFirstOpenedPanel(group);
15399 }
15400 });
15401 }
15402 };
15403
15404 self.attach()
15405 .then(show)
15406 .then(checkGroupMaxOpen)
15407 .then(done)
15408 .catch(reject);
15409 });
15410};
15411
15412
15413/**
15414 * Closes the panel.
15415 * @param {string} closeReason The event type that triggered the close.
15416 * @returns {!angular.$q.Promise<!MdPanelRef>} A promise that is resolved when
15417 * the panel is closed and animations finish.
15418 */
15419MdPanelRef.prototype.close = function(closeReason) {
15420 var self = this;
15421
15422 return this._$q(function(resolve, reject) {
15423 self._callInterceptors(MdPanelRef.interceptorTypes.CLOSE).then(function() {
15424 var done = self._done(resolve, self);
15425 var detach = self._simpleBind(self.detach, self);
15426 var onCloseSuccess = self.config['onCloseSuccess'] || angular.noop;
15427 onCloseSuccess = angular.bind(self, onCloseSuccess, self, closeReason);
15428
15429 self.hide()
15430 .then(detach)
15431 .then(done)
15432 .then(onCloseSuccess)
15433 .catch(reject);
15434 }, reject);
15435 });
15436};
15437
15438
15439/**
15440 * Attaches the panel. The panel will be hidden afterwards.
15441 * @returns {!angular.$q.Promise<!MdPanelRef>} A promise that is resolved when
15442 * the panel is attached.
15443 */
15444MdPanelRef.prototype.attach = function() {
15445 if (this.isAttached && this.panelEl) {
15446 return this._$q.when(this);
15447 }
15448
15449 var self = this;
15450 return this._$q(function(resolve, reject) {
15451 var done = self._done(resolve, self);
15452 var onDomAdded = self.config['onDomAdded'] || angular.noop;
15453 var addListeners = function(response) {
15454 self.isAttached = true;
15455 self._addEventListeners();
15456 return response;
15457 };
15458
15459 self._$q.all([
15460 self._createBackdrop(),
15461 self._createPanel()
15462 .then(addListeners)
15463 .catch(reject)
15464 ]).then(onDomAdded)
15465 .then(done)
15466 .catch(reject);
15467 });
15468};
15469
15470
15471/**
15472 * Only detaches the panel. Will NOT hide the panel first.
15473 * @returns {!angular.$q.Promise<!MdPanelRef>} A promise that is resolved when
15474 * the panel is detached.
15475 */
15476MdPanelRef.prototype.detach = function() {
15477 if (!this.isAttached) {
15478 return this._$q.when(this);
15479 }
15480
15481 var self = this;
15482 var onDomRemoved = self.config['onDomRemoved'] || angular.noop;
15483
15484 var detachFn = function() {
15485 self._removeEventListeners();
15486
15487 // Remove the focus traps that we added earlier for keeping focus within
15488 // the panel.
15489 if (self._topFocusTrap && self._topFocusTrap.parentNode) {
15490 self._topFocusTrap.parentNode.removeChild(self._topFocusTrap);
15491 }
15492
15493 if (self._bottomFocusTrap && self._bottomFocusTrap.parentNode) {
15494 self._bottomFocusTrap.parentNode.removeChild(self._bottomFocusTrap);
15495 }
15496
15497 if (self._restoreCache.classes) {
15498 self.panelEl[0].className = self._restoreCache.classes;
15499 }
15500
15501 // Either restore the saved styles or clear the ones set by mdPanel.
15502 self.panelEl[0].style.cssText = self._restoreCache.styles || '';
15503
15504 self._compilerCleanup();
15505 self.panelContainer.remove();
15506 self.isAttached = false;
15507 return self._$q.when(self);
15508 };
15509
15510 if (this._restoreScroll) {
15511 this._restoreScroll();
15512 this._restoreScroll = null;
15513 }
15514
15515 return this._$q(function(resolve, reject) {
15516 var done = self._done(resolve, self);
15517
15518 self._$q.all([
15519 detachFn(),
15520 self._backdropRef ? self._backdropRef.detach() : true
15521 ]).then(onDomRemoved)
15522 .then(done)
15523 .catch(reject);
15524 });
15525};
15526
15527
15528/**
15529 * Destroys the panel. The Panel cannot be opened again after this.
15530 */
15531MdPanelRef.prototype.destroy = function() {
15532 var self = this;
15533 if (this.config.groupName) {
15534 angular.forEach(this.config.groupName, function(group) {
15535 self.removeFromGroup(group);
15536 });
15537 }
15538 this.config.scope.$destroy();
15539 this.config.locals = null;
15540 this._interceptors = null;
15541};
15542
15543
15544/**
15545 * Shows the panel.
15546 * @returns {!angular.$q.Promise<!MdPanelRef>} A promise that is resolved when
15547 * the panel has shown and animations finish.
15548 */
15549MdPanelRef.prototype.show = function() {
15550 if (!this.panelContainer) {
15551 return this._$q(function(resolve, reject) {
15552 reject('mdPanel: Panel does not exist yet. Call open() or attach().');
15553 });
15554 }
15555
15556 if (!this.panelContainer.hasClass(MD_PANEL_HIDDEN)) {
15557 return this._$q.when(this);
15558 }
15559
15560 var self = this;
15561 var animatePromise = function() {
15562 self.panelContainer.removeClass(MD_PANEL_HIDDEN);
15563 return self._animateOpen();
15564 };
15565
15566 return this._$q(function(resolve, reject) {
15567 var done = self._done(resolve, self);
15568 var onOpenComplete = self.config['onOpenComplete'] || angular.noop;
15569 var addToGroupOpen = function() {
15570 if (self.config.groupName) {
15571 angular.forEach(self.config.groupName, function(group) {
15572 self._$mdPanel._groups[group].openPanels.push(self);
15573 });
15574 }
15575 };
15576
15577 self._$q.all([
15578 self._backdropRef ? self._backdropRef.show() : self,
15579 animatePromise().then(function() { self._focusOnOpen(); }, reject)
15580 ]).then(onOpenComplete)
15581 .then(addToGroupOpen)
15582 .then(done)
15583 .catch(reject);
15584 });
15585};
15586
15587
15588/**
15589 * Hides the panel.
15590 * @returns {!angular.$q.Promise<!MdPanelRef>} A promise that is resolved when
15591 * the panel has hidden and animations finish.
15592 */
15593MdPanelRef.prototype.hide = function() {
15594 if (!this.panelContainer) {
15595 return this._$q(function(resolve, reject) {
15596 reject('mdPanel: Panel does not exist yet. Call open() or attach().');
15597 });
15598 }
15599
15600 if (this.panelContainer.hasClass(MD_PANEL_HIDDEN)) {
15601 return this._$q.when(this);
15602 }
15603
15604 var self = this;
15605
15606 return this._$q(function(resolve, reject) {
15607 var done = self._done(resolve, self);
15608 var onRemoving = self.config['onRemoving'] || angular.noop;
15609 var hidePanel = function() {
15610 self.panelContainer.addClass(MD_PANEL_HIDDEN);
15611 };
15612 var removeFromGroupOpen = function() {
15613 if (self.config.groupName) {
15614 var group, index;
15615 angular.forEach(self.config.groupName, function(group) {
15616 group = self._$mdPanel._groups[group];
15617 index = group.openPanels.indexOf(self);
15618 if (index > -1) {
15619 group.openPanels.splice(index, 1);
15620 }
15621 });
15622 }
15623 };
15624 var focusOnOrigin = function() {
15625 var origin = self.config['origin'];
15626 if (origin) {
15627 getElement(origin).focus();
15628 }
15629 };
15630
15631 self._$q.all([
15632 self._backdropRef ? self._backdropRef.hide() : self,
15633 self._animateClose()
15634 .then(onRemoving)
15635 .then(hidePanel)
15636 .then(removeFromGroupOpen)
15637 .then(focusOnOrigin)
15638 .catch(reject)
15639 ]).then(done, reject);
15640 });
15641};
15642
15643
15644/**
15645 * Add a class to the panel. DO NOT use this to hide/show the panel.
15646 * @deprecated
15647 * This method is in the process of being deprecated in favor of using the panel
15648 * and container JQLite elements that are referenced in the MdPanelRef object.
15649 * Full deprecation is scheduled for material 1.2.
15650 *
15651 * @param {string} newClass Class to be added.
15652 * @param {boolean} toElement Whether or not to add the class to the panel
15653 * element instead of the container.
15654 */
15655MdPanelRef.prototype.addClass = function(newClass, toElement) {
15656 this._$log.warn(
15657 'mdPanel: The addClass method is in the process of being deprecated. ' +
15658 'Full deprecation is scheduled for the Angular Material 1.2 release. ' +
15659 'To achieve the same results, use the panelContainer or panelEl ' +
15660 'JQLite elements that are referenced in MdPanelRef.');
15661
15662 if (!this.panelContainer) {
15663 throw new Error(
15664 'mdPanel: Panel does not exist yet. Call open() or attach().');
15665 }
15666
15667 if (!toElement && !this.panelContainer.hasClass(newClass)) {
15668 this.panelContainer.addClass(newClass);
15669 } else if (toElement && !this.panelEl.hasClass(newClass)) {
15670 this.panelEl.addClass(newClass);
15671 }
15672};
15673
15674
15675/**
15676 * Remove a class from the panel. DO NOT use this to hide/show the panel.
15677 * @deprecated
15678 * This method is in the process of being deprecated in favor of using the panel
15679 * and container JQLite elements that are referenced in the MdPanelRef object.
15680 * Full deprecation is scheduled for material 1.2.
15681 *
15682 * @param {string} oldClass Class to be removed.
15683 * @param {boolean} fromElement Whether or not to remove the class from the
15684 * panel element instead of the container.
15685 */
15686MdPanelRef.prototype.removeClass = function(oldClass, fromElement) {
15687 this._$log.warn(
15688 'mdPanel: The removeClass method is in the process of being deprecated. ' +
15689 'Full deprecation is scheduled for the Angular Material 1.2 release. ' +
15690 'To achieve the same results, use the panelContainer or panelEl ' +
15691 'JQLite elements that are referenced in MdPanelRef.');
15692
15693 if (!this.panelContainer) {
15694 throw new Error(
15695 'mdPanel: Panel does not exist yet. Call open() or attach().');
15696 }
15697
15698 if (!fromElement && this.panelContainer.hasClass(oldClass)) {
15699 this.panelContainer.removeClass(oldClass);
15700 } else if (fromElement && this.panelEl.hasClass(oldClass)) {
15701 this.panelEl.removeClass(oldClass);
15702 }
15703};
15704
15705
15706/**
15707 * Toggle a class on the panel. DO NOT use this to hide/show the panel.
15708 * @deprecated
15709 * This method is in the process of being deprecated in favor of using the panel
15710 * and container JQLite elements that are referenced in the MdPanelRef object.
15711 * Full deprecation is scheduled for material 1.2.
15712 *
15713 * @param {string} toggleClass The class to toggle.
15714 * @param {boolean} onElement Whether or not to toggle the class on the panel
15715 * element instead of the container.
15716 */
15717MdPanelRef.prototype.toggleClass = function(toggleClass, onElement) {
15718 this._$log.warn(
15719 'mdPanel: The toggleClass method is in the process of being deprecated. ' +
15720 'Full deprecation is scheduled for the Angular Material 1.2 release. ' +
15721 'To achieve the same results, use the panelContainer or panelEl ' +
15722 'JQLite elements that are referenced in MdPanelRef.');
15723
15724 if (!this.panelContainer) {
15725 throw new Error(
15726 'mdPanel: Panel does not exist yet. Call open() or attach().');
15727 }
15728
15729 if (!onElement) {
15730 this.panelContainer.toggleClass(toggleClass);
15731 } else {
15732 this.panelEl.toggleClass(toggleClass);
15733 }
15734};
15735
15736
15737/**
15738 * Compiles the panel, according to the passed in config and appends it to
15739 * the DOM. Helps normalize differences in the compilation process between
15740 * using a string template and a content element.
15741 * @returns {!angular.$q.Promise<!MdPanelRef>} Promise that is resolved when
15742 * the element has been compiled and added to the DOM.
15743 * @private
15744 */
15745MdPanelRef.prototype._compile = function() {
15746 var self = this;
15747
15748 // Compile the element via $mdCompiler. Note that when using a
15749 // contentElement, the element isn't actually being compiled, rather the
15750 // compiler saves it's place in the DOM and provides a way of restoring it.
15751 return self._$mdCompiler.compile(self.config).then(function(compileData) {
15752 var config = self.config;
15753
15754 if (config.contentElement) {
15755 var panelEl = compileData.element;
15756
15757 // Since mdPanel modifies the inline styles and CSS classes, we need
15758 // to save them in order to be able to restore on close.
15759 self._restoreCache.styles = panelEl[0].style.cssText;
15760 self._restoreCache.classes = panelEl[0].className;
15761
15762 self.panelContainer = self._$mdPanel._wrapContentElement(panelEl);
15763 self.panelEl = panelEl;
15764 } else {
15765 self.panelContainer = compileData.link(config['scope']);
15766 self.panelEl = angular.element(
15767 self.panelContainer[0].querySelector('.md-panel')
15768 );
15769 }
15770
15771 // Save a reference to the cleanup function from the compiler.
15772 self._compilerCleanup = compileData.cleanup;
15773
15774 // Attach the panel to the proper place in the DOM.
15775 getElement(self.config['attachTo']).append(self.panelContainer);
15776
15777 return self;
15778 });
15779};
15780
15781
15782/**
15783 * Creates a panel and adds it to the dom.
15784 * @returns {!angular.$q.Promise} A promise that is resolved when the panel is
15785 * created.
15786 * @private
15787 */
15788MdPanelRef.prototype._createPanel = function() {
15789 var self = this;
15790
15791 return this._$q(function(resolve, reject) {
15792 if (!self.config.locals) {
15793 self.config.locals = {};
15794 }
15795
15796 self.config.locals.mdPanelRef = self;
15797
15798 self._compile().then(function() {
15799 if (self.config['disableParentScroll']) {
15800 self._restoreScroll = self._$mdUtil.disableScrollAround(
15801 null,
15802 self.panelContainer,
15803 { disableScrollMask: true }
15804 );
15805 }
15806
15807 // Add a custom CSS class to the panel element.
15808 if (self.config['panelClass']) {
15809 self.panelEl.addClass(self.config['panelClass']);
15810 }
15811
15812 // Handle click and touch events for the panel container.
15813 if (self.config['propagateContainerEvents']) {
15814 self.panelContainer.css('pointer-events', 'none');
15815 }
15816
15817 // Panel may be outside the $rootElement, tell ngAnimate to animate
15818 // regardless.
15819 if (self._$animate.pin) {
15820 self._$animate.pin(
15821 self.panelContainer,
15822 getElement(self.config['attachTo'])
15823 );
15824 }
15825
15826 self._configureTrapFocus();
15827 self._addStyles().then(function() {
15828 resolve(self);
15829 }, reject);
15830 }, reject);
15831
15832 });
15833};
15834
15835
15836/**
15837 * Adds the styles for the panel, such as positioning and z-index. Also,
15838 * themes the panel element and panel container using `$mdTheming`.
15839 * @returns {!angular.$q.Promise<!MdPanelRef>}
15840 * @private
15841 */
15842MdPanelRef.prototype._addStyles = function() {
15843 var self = this;
15844 return this._$q(function(resolve) {
15845 self.panelContainer.css('z-index', self.config['zIndex']);
15846 self.panelEl.css('z-index', self.config['zIndex'] + 1);
15847
15848 var hideAndResolve = function() {
15849 // Theme the element and container.
15850 self._setTheming();
15851
15852 // Remove left: -9999px and add hidden class.
15853 self.panelEl.css('left', '');
15854 self.panelContainer.addClass(MD_PANEL_HIDDEN);
15855
15856 resolve(self);
15857 };
15858
15859 if (self.config['fullscreen']) {
15860 self.panelEl.addClass('_md-panel-fullscreen');
15861 hideAndResolve();
15862 return; // Don't setup positioning.
15863 }
15864
15865 var positionConfig = self.config['position'];
15866 if (!positionConfig) {
15867 hideAndResolve();
15868 return; // Don't setup positioning.
15869 }
15870
15871 // Wait for angular to finish processing the template
15872 self._$rootScope['$$postDigest'](function() {
15873 // Position it correctly. This is necessary so that the panel will have a
15874 // defined height and width.
15875 self._updatePosition(true);
15876
15877 // Theme the element and container.
15878 self._setTheming();
15879
15880 resolve(self);
15881 });
15882 });
15883};
15884
15885
15886/**
15887 * Sets the `$mdTheming` classes on the `panelContainer` and `panelEl`.
15888 * @private
15889 */
15890MdPanelRef.prototype._setTheming = function() {
15891 this._$mdTheming(this.panelEl);
15892 this._$mdTheming(this.panelContainer);
15893};
15894
15895
15896/**
15897 * Updates the position configuration of a panel
15898 * @param {!MdPanelPosition} position
15899 */
15900MdPanelRef.prototype.updatePosition = function(position) {
15901 if (!this.panelContainer) {
15902 throw new Error(
15903 'mdPanel: Panel does not exist yet. Call open() or attach().');
15904 }
15905
15906 this.config['position'] = position;
15907 this._updatePosition();
15908};
15909
15910
15911/**
15912 * Calculates and updates the position of the panel.
15913 * @param {boolean=} init
15914 * @private
15915 */
15916MdPanelRef.prototype._updatePosition = function(init) {
15917 var positionConfig = this.config['position'];
15918
15919 if (positionConfig) {
15920 positionConfig._setPanelPosition(this.panelEl);
15921
15922 // Hide the panel now that position is known.
15923 if (init) {
15924 this.panelContainer.addClass(MD_PANEL_HIDDEN);
15925 }
15926
15927 this.panelEl.css(
15928 MdPanelPosition.absPosition.TOP,
15929 positionConfig.getTop()
15930 );
15931 this.panelEl.css(
15932 MdPanelPosition.absPosition.BOTTOM,
15933 positionConfig.getBottom()
15934 );
15935 this.panelEl.css(
15936 MdPanelPosition.absPosition.LEFT,
15937 positionConfig.getLeft()
15938 );
15939 this.panelEl.css(
15940 MdPanelPosition.absPosition.RIGHT,
15941 positionConfig.getRight()
15942 );
15943 }
15944};
15945
15946
15947/**
15948 * Focuses on the panel or the first focus target.
15949 * @private
15950 */
15951MdPanelRef.prototype._focusOnOpen = function() {
15952 if (this.config['focusOnOpen']) {
15953 // Wait for the template to finish rendering to guarantee md-autofocus has
15954 // finished adding the class md-autofocus, otherwise the focusable element
15955 // isn't available to focus.
15956 var self = this;
15957 this._$rootScope['$$postDigest'](function() {
15958 var target = self._$mdUtil.findFocusTarget(self.panelEl) ||
15959 self.panelEl;
15960 target.focus();
15961 });
15962 }
15963};
15964
15965
15966/**
15967 * Shows the backdrop.
15968 * @returns {!angular.$q.Promise} A promise that is resolved when the backdrop
15969 * is created and attached.
15970 * @private
15971 */
15972MdPanelRef.prototype._createBackdrop = function() {
15973 if (this.config.hasBackdrop) {
15974 if (!this._backdropRef) {
15975 var backdropAnimation = this._$mdPanel.newPanelAnimation()
15976 .openFrom(this.config.attachTo)
15977 .withAnimation({
15978 open: '_md-opaque-enter',
15979 close: '_md-opaque-leave'
15980 });
15981
15982 if (this.config.animation) {
15983 backdropAnimation.duration(this.config.animation._rawDuration);
15984 }
15985
15986 var backdropConfig = {
15987 animation: backdropAnimation,
15988 attachTo: this.config.attachTo,
15989 focusOnOpen: false,
15990 panelClass: '_md-panel-backdrop',
15991 zIndex: this.config.zIndex - 1
15992 };
15993
15994 this._backdropRef = this._$mdPanel.create(backdropConfig);
15995 }
15996 if (!this._backdropRef.isAttached) {
15997 return this._backdropRef.attach();
15998 }
15999 }
16000};
16001
16002
16003/**
16004 * Listen for escape keys and outside clicks to auto close.
16005 * @private
16006 */
16007MdPanelRef.prototype._addEventListeners = function() {
16008 this._configureEscapeToClose();
16009 this._configureClickOutsideToClose();
16010 this._configureScrollListener();
16011};
16012
16013
16014/**
16015 * Remove event listeners added in _addEventListeners.
16016 * @private
16017 */
16018MdPanelRef.prototype._removeEventListeners = function() {
16019 this._removeListeners && this._removeListeners.forEach(function(removeFn) {
16020 removeFn();
16021 });
16022 this._removeListeners = [];
16023};
16024
16025
16026/**
16027 * Setup the escapeToClose event listeners.
16028 * @private
16029 */
16030MdPanelRef.prototype._configureEscapeToClose = function() {
16031 if (this.config['escapeToClose']) {
16032 var parentTarget = getElement(this.config['attachTo']);
16033 var self = this;
16034
16035 var keyHandlerFn = function(ev) {
16036 if (ev.keyCode === self._$mdConstant.KEY_CODE.ESCAPE) {
16037 ev.stopPropagation();
16038 ev.preventDefault();
16039
16040 self.close(MdPanelRef.closeReasons.ESCAPE);
16041 }
16042 };
16043
16044 // Add keydown listeners
16045 this.panelContainer.on('keydown', keyHandlerFn);
16046 parentTarget.on('keydown', keyHandlerFn);
16047
16048 // Queue remove listeners function
16049 this._removeListeners.push(function() {
16050 self.panelContainer.off('keydown', keyHandlerFn);
16051 parentTarget.off('keydown', keyHandlerFn);
16052 });
16053 }
16054};
16055
16056
16057/**
16058 * Setup the clickOutsideToClose event listeners.
16059 * @private
16060 */
16061MdPanelRef.prototype._configureClickOutsideToClose = function() {
16062 if (this.config['clickOutsideToClose']) {
16063 var target = this.config['propagateContainerEvents'] ?
16064 angular.element(document.body) :
16065 this.panelContainer;
16066 var sourceEl;
16067
16068 // Keep track of the element on which the mouse originally went down
16069 // so that we can only close the backdrop when the 'click' started on it.
16070 // A simple 'click' handler does not work, it sets the target object as the
16071 // element the mouse went down on.
16072 var mousedownHandler = function(ev) {
16073 sourceEl = ev.target;
16074 };
16075
16076 // We check if our original element and the target is the backdrop
16077 // because if the original was the backdrop and the target was inside the
16078 // panel we don't want to panel to close.
16079 var self = this;
16080 var mouseupHandler = function(ev) {
16081 if (self.config['propagateContainerEvents']) {
16082
16083 // We check if the sourceEl of the event is the panel element or one
16084 // of it's children. If it is not, then close the panel.
16085 if (sourceEl !== self.panelEl[0] && !self.panelEl[0].contains(sourceEl)) {
16086 self.close();
16087 }
16088
16089 } else if (sourceEl === target[0] && ev.target === target[0]) {
16090 ev.stopPropagation();
16091 ev.preventDefault();
16092
16093 self.close(MdPanelRef.closeReasons.CLICK_OUTSIDE);
16094 }
16095 };
16096
16097 // Add listeners
16098 target.on('mousedown', mousedownHandler);
16099 target.on('mouseup', mouseupHandler);
16100
16101 // Queue remove listeners function
16102 this._removeListeners.push(function() {
16103 target.off('mousedown', mousedownHandler);
16104 target.off('mouseup', mouseupHandler);
16105 });
16106 }
16107};
16108
16109
16110/**
16111 * Configures the listeners for updating the panel position on scroll.
16112 * @private
16113*/
16114MdPanelRef.prototype._configureScrollListener = function() {
16115 // No need to bind the event if scrolling is disabled.
16116 if (!this.config['disableParentScroll']) {
16117 var updatePosition = angular.bind(this, this._updatePosition);
16118 var debouncedUpdatePosition = this._$$rAF.throttle(updatePosition);
16119 var self = this;
16120
16121 var onScroll = function() {
16122 debouncedUpdatePosition();
16123 };
16124
16125 // Add listeners.
16126 this._$window.addEventListener('scroll', onScroll, true);
16127
16128 // Queue remove listeners function.
16129 this._removeListeners.push(function() {
16130 self._$window.removeEventListener('scroll', onScroll, true);
16131 });
16132 }
16133};
16134
16135
16136/**
16137 * Setup the focus traps. These traps will wrap focus when tabbing past the
16138 * panel. When shift-tabbing, the focus will stick in place.
16139 * @private
16140 */
16141MdPanelRef.prototype._configureTrapFocus = function() {
16142 // Focus doesn't remain inside of the panel without this.
16143 this.panelEl.attr('tabIndex', '-1');
16144 if (this.config['trapFocus']) {
16145 var element = this.panelEl;
16146 // Set up elements before and after the panel to capture focus and
16147 // redirect back into the panel.
16148 this._topFocusTrap = FOCUS_TRAP_TEMPLATE.clone()[0];
16149 this._bottomFocusTrap = FOCUS_TRAP_TEMPLATE.clone()[0];
16150
16151 // When focus is about to move out of the panel, we want to intercept it
16152 // and redirect it back to the panel element.
16153 var focusHandler = function() {
16154 element.focus();
16155 };
16156 this._topFocusTrap.addEventListener('focus', focusHandler);
16157 this._bottomFocusTrap.addEventListener('focus', focusHandler);
16158
16159 // Queue remove listeners function
16160 this._removeListeners.push(this._simpleBind(function() {
16161 this._topFocusTrap.removeEventListener('focus', focusHandler);
16162 this._bottomFocusTrap.removeEventListener('focus', focusHandler);
16163 }, this));
16164
16165 // The top focus trap inserted immediately before the md-panel element (as
16166 // a sibling). The bottom focus trap inserted immediately after the
16167 // md-panel element (as a sibling).
16168 element[0].parentNode.insertBefore(this._topFocusTrap, element[0]);
16169 element.after(this._bottomFocusTrap);
16170 }
16171};
16172
16173
16174/**
16175 * Updates the animation of a panel.
16176 * @param {!MdPanelAnimation} animation
16177 */
16178MdPanelRef.prototype.updateAnimation = function(animation) {
16179 this.config['animation'] = animation;
16180
16181 if (this._backdropRef) {
16182 this._backdropRef.config.animation.duration(animation._rawDuration);
16183 }
16184};
16185
16186
16187/**
16188 * Animate the panel opening.
16189 * @returns {!angular.$q.Promise} A promise that is resolved when the panel has
16190 * animated open.
16191 * @private
16192 */
16193MdPanelRef.prototype._animateOpen = function() {
16194 this.panelContainer.addClass('md-panel-is-showing');
16195 var animationConfig = this.config['animation'];
16196 if (!animationConfig) {
16197 // Promise is in progress, return it.
16198 this.panelContainer.addClass('_md-panel-shown');
16199 return this._$q.when(this);
16200 }
16201
16202 var self = this;
16203 return this._$q(function(resolve) {
16204 var done = self._done(resolve, self);
16205 var warnAndOpen = function() {
16206 self._$log.warn(
16207 'mdPanel: MdPanel Animations failed. ' +
16208 'Showing panel without animating.');
16209 done();
16210 };
16211
16212 animationConfig.animateOpen(self.panelEl)
16213 .then(done, warnAndOpen);
16214 });
16215};
16216
16217
16218/**
16219 * Animate the panel closing.
16220 * @returns {!angular.$q.Promise} A promise that is resolved when the panel has
16221 * animated closed.
16222 * @private
16223 */
16224MdPanelRef.prototype._animateClose = function() {
16225 var animationConfig = this.config['animation'];
16226 if (!animationConfig) {
16227 this.panelContainer.removeClass('md-panel-is-showing');
16228 this.panelContainer.removeClass('_md-panel-shown');
16229 return this._$q.when(this);
16230 }
16231
16232 var self = this;
16233 return this._$q(function(resolve) {
16234 var done = function() {
16235 self.panelContainer.removeClass('md-panel-is-showing');
16236 resolve(self);
16237 };
16238 var warnAndClose = function() {
16239 self._$log.warn(
16240 'mdPanel: MdPanel Animations failed. ' +
16241 'Hiding panel without animating.');
16242 done();
16243 };
16244
16245 animationConfig.animateClose(self.panelEl)
16246 .then(done, warnAndClose);
16247 });
16248};
16249
16250
16251/**
16252 * Registers a interceptor with the panel. The callback should return a promise,
16253 * which will allow the action to continue when it gets resolved, or will
16254 * prevent an action if it is rejected.
16255 * @param {string} type Type of interceptor.
16256 * @param {!angular.$q.Promise<!any>} callback Callback to be registered.
16257 * @returns {!MdPanelRef}
16258 */
16259MdPanelRef.prototype.registerInterceptor = function(type, callback) {
16260 var error = null;
16261
16262 if (!angular.isString(type)) {
16263 error = 'Interceptor type must be a string, instead got ' + typeof type;
16264 } else if (!angular.isFunction(callback)) {
16265 error = 'Interceptor callback must be a function, instead got ' + typeof callback;
16266 }
16267
16268 if (error) {
16269 throw new Error('MdPanel: ' + error);
16270 }
16271
16272 var interceptors = this._interceptors[type] = this._interceptors[type] || [];
16273
16274 if (interceptors.indexOf(callback) === -1) {
16275 interceptors.push(callback);
16276 }
16277
16278 return this;
16279};
16280
16281
16282/**
16283 * Removes a registered interceptor.
16284 * @param {string} type Type of interceptor to be removed.
16285 * @param {Function} callback Interceptor to be removed.
16286 * @returns {!MdPanelRef}
16287 */
16288MdPanelRef.prototype.removeInterceptor = function(type, callback) {
16289 var index = this._interceptors[type] ?
16290 this._interceptors[type].indexOf(callback) : -1;
16291
16292 if (index > -1) {
16293 this._interceptors[type].splice(index, 1);
16294 }
16295
16296 return this;
16297};
16298
16299
16300/**
16301 * Removes all interceptors.
16302 * @param {string=} type Type of interceptors to be removed.
16303 * If ommited, all interceptors types will be removed.
16304 * @returns {!MdPanelRef}
16305 */
16306MdPanelRef.prototype.removeAllInterceptors = function(type) {
16307 if (type) {
16308 this._interceptors[type] = [];
16309 } else {
16310 this._interceptors = Object.create(null);
16311 }
16312
16313 return this;
16314};
16315
16316
16317/**
16318 * Invokes all the interceptors of a certain type sequantially in
16319 * reverse order. Works in a similar way to `$q.all`, except it
16320 * respects the order of the functions.
16321 * @param {string} type Type of interceptors to be invoked.
16322 * @returns {!angular.$q.Promise<!MdPanelRef>}
16323 * @private
16324 */
16325MdPanelRef.prototype._callInterceptors = function(type) {
16326 var self = this;
16327 var $q = self._$q;
16328 var interceptors = self._interceptors && self._interceptors[type] || [];
16329
16330 return interceptors.reduceRight(function(promise, interceptor) {
16331 var isPromiseLike = interceptor && angular.isFunction(interceptor.then);
16332 var response = isPromiseLike ? interceptor : null;
16333
16334 /**
16335 * For interceptors to reject/cancel subsequent portions of the chain, simply
16336 * return a `$q.reject(<value>)`
16337 */
16338 return promise.then(function() {
16339 if (!response) {
16340 try {
16341 response = interceptor(self);
16342 } catch(e) {
16343 response = $q.reject(e);
16344 }
16345 }
16346
16347 return response;
16348 });
16349 }, $q.resolve(self));
16350};
16351
16352
16353/**
16354 * Faster, more basic than angular.bind
16355 * http://jsperf.com/angular-bind-vs-custom-vs-native
16356 * @param {function} callback
16357 * @param {!Object} self
16358 * @return {function} Callback function with a bound self.
16359 */
16360MdPanelRef.prototype._simpleBind = function(callback, self) {
16361 return function(value) {
16362 return callback.apply(self, value);
16363 };
16364};
16365
16366
16367/**
16368 * @param {function} callback
16369 * @param {!Object} self
16370 * @return {function} Callback function with a self param.
16371 */
16372MdPanelRef.prototype._done = function(callback, self) {
16373 return function() {
16374 callback(self);
16375 };
16376};
16377
16378
16379/**
16380 * Adds a panel to a group if the panel does not exist within the group already.
16381 * A panel can only exist within a single group.
16382 * @param {string} groupName The name of the group.
16383 */
16384MdPanelRef.prototype.addToGroup = function(groupName) {
16385 if (!this._$mdPanel._groups[groupName]) {
16386 this._$mdPanel.newPanelGroup(groupName);
16387 }
16388
16389 var group = this._$mdPanel._groups[groupName];
16390 var index = group.panels.indexOf(this);
16391
16392 if (index < 0) {
16393 group.panels.push(this);
16394 }
16395};
16396
16397
16398/**
16399 * Removes a panel from a group if the panel exists within that group. The group
16400 * must be created ahead of time.
16401 * @param {string} groupName The name of the group.
16402 */
16403MdPanelRef.prototype.removeFromGroup = function(groupName) {
16404 if (!this._$mdPanel._groups[groupName]) {
16405 throw new Error('mdPanel: The group ' + groupName + ' does not exist.');
16406 }
16407
16408 var group = this._$mdPanel._groups[groupName];
16409 var index = group.panels.indexOf(this);
16410
16411 if (index > -1) {
16412 group.panels.splice(index, 1);
16413 }
16414};
16415
16416
16417/**
16418 * Possible default closeReasons for the close function.
16419 * @enum {string}
16420 */
16421MdPanelRef.closeReasons = {
16422 CLICK_OUTSIDE: 'clickOutsideToClose',
16423 ESCAPE: 'escapeToClose',
16424};
16425
16426
16427/*****************************************************************************
16428 * MdPanelPosition *
16429 *****************************************************************************/
16430
16431
16432/**
16433 * Position configuration object. To use, create an MdPanelPosition with the
16434 * desired properties, then pass the object as part of $mdPanel creation.
16435 *
16436 * Example:
16437 *
16438 * var panelPosition = new MdPanelPosition()
16439 * .relativeTo(myButtonEl)
16440 * .addPanelPosition(
16441 * $mdPanel.xPosition.CENTER,
16442 * $mdPanel.yPosition.ALIGN_TOPS
16443 * );
16444 *
16445 * $mdPanel.create({
16446 * position: panelPosition
16447 * });
16448 *
16449 * @param {!angular.$injector} $injector
16450 * @final @constructor
16451 */
16452function MdPanelPosition($injector) {
16453 /** @private @const {!angular.$window} */
16454 this._$window = $injector.get('$window');
16455
16456 /** @private {boolean} */
16457 this._isRTL = $injector.get('$mdUtil').bidi() === 'rtl';
16458
16459 /** @private @const {!angular.$mdConstant} */
16460 this._$mdConstant = $injector.get('$mdConstant');
16461
16462 /** @private {boolean} */
16463 this._absolute = false;
16464
16465 /** @private {!angular.JQLite} */
16466 this._relativeToEl;
16467
16468 /** @private {string} */
16469 this._top = '';
16470
16471 /** @private {string} */
16472 this._bottom = '';
16473
16474 /** @private {string} */
16475 this._left = '';
16476
16477 /** @private {string} */
16478 this._right = '';
16479
16480 /** @private {!Array<string>} */
16481 this._translateX = [];
16482
16483 /** @private {!Array<string>} */
16484 this._translateY = [];
16485
16486 /** @private {!Array<{x:string, y:string}>} */
16487 this._positions = [];
16488
16489 /** @private {?{x:string, y:string}} */
16490 this._actualPosition;
16491}
16492
16493
16494/**
16495 * Possible values of xPosition.
16496 * @enum {string}
16497 */
16498MdPanelPosition.xPosition = {
16499 CENTER: 'center',
16500 ALIGN_START: 'align-start',
16501 ALIGN_END: 'align-end',
16502 OFFSET_START: 'offset-start',
16503 OFFSET_END: 'offset-end'
16504};
16505
16506
16507/**
16508 * Possible values of yPosition.
16509 * @enum {string}
16510 */
16511MdPanelPosition.yPosition = {
16512 CENTER: 'center',
16513 ALIGN_TOPS: 'align-tops',
16514 ALIGN_BOTTOMS: 'align-bottoms',
16515 ABOVE: 'above',
16516 BELOW: 'below'
16517};
16518
16519
16520/**
16521 * Possible values of absolute position.
16522 * @enum {string}
16523 */
16524MdPanelPosition.absPosition = {
16525 TOP: 'top',
16526 RIGHT: 'right',
16527 BOTTOM: 'bottom',
16528 LEFT: 'left'
16529};
16530
16531/**
16532 * Margin between the edges of a panel and the viewport.
16533 * @const {number}
16534 */
16535MdPanelPosition.viewportMargin = 8;
16536
16537
16538/**
16539 * Sets absolute positioning for the panel.
16540 * @return {!MdPanelPosition}
16541 */
16542MdPanelPosition.prototype.absolute = function() {
16543 this._absolute = true;
16544 return this;
16545};
16546
16547
16548/**
16549 * Sets the value of a position for the panel. Clears any previously set
16550 * position.
16551 * @param {string} position Position to set
16552 * @param {string=} value Value of the position. Defaults to '0'.
16553 * @returns {!MdPanelPosition}
16554 * @private
16555 */
16556MdPanelPosition.prototype._setPosition = function(position, value) {
16557 if (position === MdPanelPosition.absPosition.RIGHT ||
16558 position === MdPanelPosition.absPosition.LEFT) {
16559 this._left = this._right = '';
16560 } else if (
16561 position === MdPanelPosition.absPosition.BOTTOM ||
16562 position === MdPanelPosition.absPosition.TOP) {
16563 this._top = this._bottom = '';
16564 } else {
16565 var positions = Object.keys(MdPanelPosition.absPosition).join()
16566 .toLowerCase();
16567
16568 throw new Error('mdPanel: Position must be one of ' + positions + '.');
16569 }
16570
16571 this['_' + position] = angular.isString(value) ? value : '0';
16572
16573 return this;
16574};
16575
16576
16577/**
16578 * Sets the value of `top` for the panel. Clears any previously set vertical
16579 * position.
16580 * @param {string=} top Value of `top`. Defaults to '0'.
16581 * @returns {!MdPanelPosition}
16582 */
16583MdPanelPosition.prototype.top = function(top) {
16584 return this._setPosition(MdPanelPosition.absPosition.TOP, top);
16585};
16586
16587
16588/**
16589 * Sets the value of `bottom` for the panel. Clears any previously set vertical
16590 * position.
16591 * @param {string=} bottom Value of `bottom`. Defaults to '0'.
16592 * @returns {!MdPanelPosition}
16593 */
16594MdPanelPosition.prototype.bottom = function(bottom) {
16595 return this._setPosition(MdPanelPosition.absPosition.BOTTOM, bottom);
16596};
16597
16598
16599/**
16600 * Sets the panel to the start of the page - `left` if `ltr` or `right` for
16601 * `rtl`. Clears any previously set horizontal position.
16602 * @param {string=} start Value of position. Defaults to '0'.
16603 * @returns {!MdPanelPosition}
16604 */
16605MdPanelPosition.prototype.start = function(start) {
16606 var position = this._isRTL ? MdPanelPosition.absPosition.RIGHT : MdPanelPosition.absPosition.LEFT;
16607 return this._setPosition(position, start);
16608};
16609
16610
16611/**
16612 * Sets the panel to the end of the page - `right` if `ltr` or `left` for `rtl`.
16613 * Clears any previously set horizontal position.
16614 * @param {string=} end Value of position. Defaults to '0'.
16615 * @returns {!MdPanelPosition}
16616 */
16617MdPanelPosition.prototype.end = function(end) {
16618 var position = this._isRTL ? MdPanelPosition.absPosition.LEFT : MdPanelPosition.absPosition.RIGHT;
16619 return this._setPosition(position, end);
16620};
16621
16622
16623/**
16624 * Sets the value of `left` for the panel. Clears any previously set
16625 * horizontal position.
16626 * @param {string=} left Value of `left`. Defaults to '0'.
16627 * @returns {!MdPanelPosition}
16628 */
16629MdPanelPosition.prototype.left = function(left) {
16630 return this._setPosition(MdPanelPosition.absPosition.LEFT, left);
16631};
16632
16633
16634/**
16635 * Sets the value of `right` for the panel. Clears any previously set
16636 * horizontal position.
16637 * @param {string=} right Value of `right`. Defaults to '0'.
16638 * @returns {!MdPanelPosition}
16639*/
16640MdPanelPosition.prototype.right = function(right) {
16641 return this._setPosition(MdPanelPosition.absPosition.RIGHT, right);
16642};
16643
16644
16645/**
16646 * Centers the panel horizontally in the viewport. Clears any previously set
16647 * horizontal position.
16648 * @returns {!MdPanelPosition}
16649 */
16650MdPanelPosition.prototype.centerHorizontally = function() {
16651 this._left = '50%';
16652 this._right = '';
16653 this._translateX = ['-50%'];
16654 return this;
16655};
16656
16657
16658/**
16659 * Centers the panel vertically in the viewport. Clears any previously set
16660 * vertical position.
16661 * @returns {!MdPanelPosition}
16662 */
16663MdPanelPosition.prototype.centerVertically = function() {
16664 this._top = '50%';
16665 this._bottom = '';
16666 this._translateY = ['-50%'];
16667 return this;
16668};
16669
16670
16671/**
16672 * Centers the panel horizontally and vertically in the viewport. This is
16673 * equivalent to calling both `centerHorizontally` and `centerVertically`.
16674 * Clears any previously set horizontal and vertical positions.
16675 * @returns {!MdPanelPosition}
16676 */
16677MdPanelPosition.prototype.center = function() {
16678 return this.centerHorizontally().centerVertically();
16679};
16680
16681
16682/**
16683 * Sets element for relative positioning.
16684 * @param {string|!Element|!angular.JQLite} element Query selector, DOM element,
16685 * or angular element to set the panel relative to.
16686 * @returns {!MdPanelPosition}
16687 */
16688MdPanelPosition.prototype.relativeTo = function(element) {
16689 this._absolute = false;
16690 this._relativeToEl = getElement(element);
16691 return this;
16692};
16693
16694
16695/**
16696 * Sets the x and y positions for the panel relative to another element.
16697 * @param {string} xPosition must be one of the MdPanelPosition.xPosition
16698 * values.
16699 * @param {string} yPosition must be one of the MdPanelPosition.yPosition
16700 * values.
16701 * @returns {!MdPanelPosition}
16702 */
16703MdPanelPosition.prototype.addPanelPosition = function(xPosition, yPosition) {
16704 if (!this._relativeToEl) {
16705 throw new Error('mdPanel: addPanelPosition can only be used with ' +
16706 'relative positioning. Set relativeTo first.');
16707 }
16708
16709 this._validateXPosition(xPosition);
16710 this._validateYPosition(yPosition);
16711
16712 this._positions.push({
16713 x: xPosition,
16714 y: yPosition,
16715 });
16716 return this;
16717};
16718
16719
16720/**
16721 * Ensures that yPosition is a valid position name. Throw an exception if not.
16722 * @param {string} yPosition
16723 */
16724MdPanelPosition.prototype._validateYPosition = function(yPosition) {
16725 // empty is ok
16726 if (yPosition == null) {
16727 return;
16728 }
16729
16730 var positionKeys = Object.keys(MdPanelPosition.yPosition);
16731 var positionValues = [];
16732 for (var key, i = 0; key = positionKeys[i]; i++) {
16733 var position = MdPanelPosition.yPosition[key];
16734 positionValues.push(position);
16735
16736 if (position === yPosition) {
16737 return;
16738 }
16739 }
16740
16741 throw new Error('mdPanel: Panel y position only accepts the following ' +
16742 'values:\n' + positionValues.join(' | '));
16743};
16744
16745
16746/**
16747 * Ensures that xPosition is a valid position name. Throw an exception if not.
16748 * @param {string} xPosition
16749 */
16750MdPanelPosition.prototype._validateXPosition = function(xPosition) {
16751 // empty is ok
16752 if (xPosition == null) {
16753 return;
16754 }
16755
16756 var positionKeys = Object.keys(MdPanelPosition.xPosition);
16757 var positionValues = [];
16758 for (var key, i = 0; key = positionKeys[i]; i++) {
16759 var position = MdPanelPosition.xPosition[key];
16760 positionValues.push(position);
16761 if (position === xPosition) {
16762 return;
16763 }
16764 }
16765
16766 throw new Error('mdPanel: Panel x Position only accepts the following ' +
16767 'values:\n' + positionValues.join(' | '));
16768};
16769
16770
16771/**
16772 * Sets the value of the offset in the x-direction. This will add to any
16773 * previously set offsets.
16774 * @param {string|function(MdPanelPosition): string} offsetX
16775 * @returns {!MdPanelPosition}
16776 */
16777MdPanelPosition.prototype.withOffsetX = function(offsetX) {
16778 this._translateX.push(offsetX);
16779 return this;
16780};
16781
16782
16783/**
16784 * Sets the value of the offset in the y-direction. This will add to any
16785 * previously set offsets.
16786 * @param {string|function(MdPanelPosition): string} offsetY
16787 * @returns {!MdPanelPosition}
16788 */
16789MdPanelPosition.prototype.withOffsetY = function(offsetY) {
16790 this._translateY.push(offsetY);
16791 return this;
16792};
16793
16794
16795/**
16796 * Gets the value of `top` for the panel.
16797 * @returns {string}
16798 */
16799MdPanelPosition.prototype.getTop = function() {
16800 return this._top;
16801};
16802
16803
16804/**
16805 * Gets the value of `bottom` for the panel.
16806 * @returns {string}
16807 */
16808MdPanelPosition.prototype.getBottom = function() {
16809 return this._bottom;
16810};
16811
16812
16813/**
16814 * Gets the value of `left` for the panel.
16815 * @returns {string}
16816 */
16817MdPanelPosition.prototype.getLeft = function() {
16818 return this._left;
16819};
16820
16821
16822/**
16823 * Gets the value of `right` for the panel.
16824 * @returns {string}
16825 */
16826MdPanelPosition.prototype.getRight = function() {
16827 return this._right;
16828};
16829
16830
16831/**
16832 * Gets the value of `transform` for the panel.
16833 * @returns {string}
16834 */
16835MdPanelPosition.prototype.getTransform = function() {
16836 var translateX = this._reduceTranslateValues('translateX', this._translateX);
16837 var translateY = this._reduceTranslateValues('translateY', this._translateY);
16838
16839 // It's important to trim the result, because the browser will ignore the set
16840 // operation if the string contains only whitespace.
16841 return (translateX + ' ' + translateY).trim();
16842};
16843
16844
16845/**
16846 * Sets the `transform` value for a panel element.
16847 * @param {!angular.JQLite} panelEl
16848 * @returns {!angular.JQLite}
16849 * @private
16850 */
16851MdPanelPosition.prototype._setTransform = function(panelEl) {
16852 return panelEl.css(this._$mdConstant.CSS.TRANSFORM, this.getTransform());
16853};
16854
16855
16856/**
16857 * True if the panel is completely on-screen with this positioning; false
16858 * otherwise.
16859 * @param {!angular.JQLite} panelEl
16860 * @return {boolean}
16861 * @private
16862 */
16863MdPanelPosition.prototype._isOnscreen = function(panelEl) {
16864 // this works because we always use fixed positioning for the panel,
16865 // which is relative to the viewport.
16866 var left = parseInt(this.getLeft());
16867 var top = parseInt(this.getTop());
16868
16869 if (this._translateX.length || this._translateY.length) {
16870 var prefixedTransform = this._$mdConstant.CSS.TRANSFORM;
16871 var offsets = getComputedTranslations(panelEl, prefixedTransform);
16872 left += offsets.x;
16873 top += offsets.y;
16874 }
16875
16876 var right = left + panelEl[0].offsetWidth;
16877 var bottom = top + panelEl[0].offsetHeight;
16878
16879 return (left >= 0) &&
16880 (top >= 0) &&
16881 (bottom <= this._$window.innerHeight) &&
16882 (right <= this._$window.innerWidth);
16883};
16884
16885
16886/**
16887 * Gets the first x/y position that can fit on-screen.
16888 * @returns {{x: string, y: string}}
16889 */
16890MdPanelPosition.prototype.getActualPosition = function() {
16891 return this._actualPosition;
16892};
16893
16894
16895/**
16896 * Reduces a list of translate values to a string that can be used within
16897 * transform.
16898 * @param {string} translateFn
16899 * @param {!Array<string>} values
16900 * @returns {string}
16901 * @private
16902 */
16903MdPanelPosition.prototype._reduceTranslateValues =
16904 function(translateFn, values) {
16905 return values.map(function(translation) {
16906 // TODO(crisbeto): this should add the units after #9609 is merged.
16907 var translationValue = angular.isFunction(translation) ?
16908 translation(this) : translation;
16909 return translateFn + '(' + translationValue + ')';
16910 }, this).join(' ');
16911 };
16912
16913
16914/**
16915 * Sets the panel position based on the created panel element and best x/y
16916 * positioning.
16917 * @param {!angular.JQLite} panelEl
16918 * @private
16919 */
16920MdPanelPosition.prototype._setPanelPosition = function(panelEl) {
16921 // Remove the "position adjusted" class in case it has been added before.
16922 panelEl.removeClass('_md-panel-position-adjusted');
16923
16924 // Only calculate the position if necessary.
16925 if (this._absolute) {
16926 this._setTransform(panelEl);
16927 return;
16928 }
16929
16930 if (this._actualPosition) {
16931 this._calculatePanelPosition(panelEl, this._actualPosition);
16932 this._setTransform(panelEl);
16933 this._constrainToViewport(panelEl);
16934 return;
16935 }
16936
16937 for (var i = 0; i < this._positions.length; i++) {
16938 this._actualPosition = this._positions[i];
16939 this._calculatePanelPosition(panelEl, this._actualPosition);
16940 this._setTransform(panelEl);
16941
16942 if (this._isOnscreen(panelEl)) {
16943 return;
16944 }
16945 }
16946
16947 this._constrainToViewport(panelEl);
16948};
16949
16950
16951/**
16952 * Constrains a panel's position to the viewport.
16953 * @param {!angular.JQLite} panelEl
16954 * @private
16955 */
16956MdPanelPosition.prototype._constrainToViewport = function(panelEl) {
16957 var margin = MdPanelPosition.viewportMargin;
16958 var initialTop = this._top;
16959 var initialLeft = this._left;
16960
16961 if (this.getTop()) {
16962 var top = parseInt(this.getTop());
16963 var bottom = panelEl[0].offsetHeight + top;
16964 var viewportHeight = this._$window.innerHeight;
16965
16966 if (top < margin) {
16967 this._top = margin + 'px';
16968 } else if (bottom > viewportHeight) {
16969 this._top = top - (bottom - viewportHeight + margin) + 'px';
16970 }
16971 }
16972
16973 if (this.getLeft()) {
16974 var left = parseInt(this.getLeft());
16975 var right = panelEl[0].offsetWidth + left;
16976 var viewportWidth = this._$window.innerWidth;
16977
16978 if (left < margin) {
16979 this._left = margin + 'px';
16980 } else if (right > viewportWidth) {
16981 this._left = left - (right - viewportWidth + margin) + 'px';
16982 }
16983 }
16984
16985 // Class that can be used to re-style the panel if it was repositioned.
16986 panelEl.toggleClass(
16987 '_md-panel-position-adjusted',
16988 this._top !== initialTop || this._left !== initialLeft
16989 );
16990};
16991
16992
16993/**
16994 * Switches between 'start' and 'end'.
16995 * @param {string} position Horizontal position of the panel
16996 * @returns {string} Reversed position
16997 * @private
16998 */
16999MdPanelPosition.prototype._reverseXPosition = function(position) {
17000 if (position === MdPanelPosition.xPosition.CENTER) {
17001 return;
17002 }
17003
17004 var start = 'start';
17005 var end = 'end';
17006
17007 return position.indexOf(start) > -1 ? position.replace(start, end) : position.replace(end, start);
17008};
17009
17010
17011/**
17012 * Handles horizontal positioning in rtl or ltr environments.
17013 * @param {string} position Horizontal position of the panel
17014 * @returns {string} The correct position according the page direction
17015 * @private
17016 */
17017MdPanelPosition.prototype._bidi = function(position) {
17018 return this._isRTL ? this._reverseXPosition(position) : position;
17019};
17020
17021
17022/**
17023 * Calculates the panel position based on the created panel element and the
17024 * provided positioning.
17025 * @param {!angular.JQLite} panelEl
17026 * @param {!{x:string, y:string}} position
17027 * @private
17028 */
17029MdPanelPosition.prototype._calculatePanelPosition = function(panelEl, position) {
17030
17031 var panelBounds = panelEl[0].getBoundingClientRect();
17032 var panelWidth = panelBounds.width;
17033 var panelHeight = panelBounds.height;
17034
17035 var targetBounds = this._relativeToEl[0].getBoundingClientRect();
17036
17037 var targetLeft = targetBounds.left;
17038 var targetRight = targetBounds.right;
17039 var targetWidth = targetBounds.width;
17040
17041 switch (this._bidi(position.x)) {
17042 case MdPanelPosition.xPosition.OFFSET_START:
17043 this._left = targetLeft - panelWidth + 'px';
17044 break;
17045 case MdPanelPosition.xPosition.ALIGN_END:
17046 this._left = targetRight - panelWidth + 'px';
17047 break;
17048 case MdPanelPosition.xPosition.CENTER:
17049 var left = targetLeft + (0.5 * targetWidth) - (0.5 * panelWidth);
17050 this._left = left + 'px';
17051 break;
17052 case MdPanelPosition.xPosition.ALIGN_START:
17053 this._left = targetLeft + 'px';
17054 break;
17055 case MdPanelPosition.xPosition.OFFSET_END:
17056 this._left = targetRight + 'px';
17057 break;
17058 }
17059
17060 var targetTop = targetBounds.top;
17061 var targetBottom = targetBounds.bottom;
17062 var targetHeight = targetBounds.height;
17063
17064 switch (position.y) {
17065 case MdPanelPosition.yPosition.ABOVE:
17066 this._top = targetTop - panelHeight + 'px';
17067 break;
17068 case MdPanelPosition.yPosition.ALIGN_BOTTOMS:
17069 this._top = targetBottom - panelHeight + 'px';
17070 break;
17071 case MdPanelPosition.yPosition.CENTER:
17072 var top = targetTop + (0.5 * targetHeight) - (0.5 * panelHeight);
17073 this._top = top + 'px';
17074 break;
17075 case MdPanelPosition.yPosition.ALIGN_TOPS:
17076 this._top = targetTop + 'px';
17077 break;
17078 case MdPanelPosition.yPosition.BELOW:
17079 this._top = targetBottom + 'px';
17080 break;
17081 }
17082};
17083
17084
17085/*****************************************************************************
17086 * MdPanelAnimation *
17087 *****************************************************************************/
17088
17089
17090/**
17091 * Animation configuration object. To use, create an MdPanelAnimation with the
17092 * desired properties, then pass the object as part of $mdPanel creation.
17093 *
17094 * Example:
17095 *
17096 * var panelAnimation = new MdPanelAnimation()
17097 * .openFrom(myButtonEl)
17098 * .closeTo('.my-button')
17099 * .withAnimation($mdPanel.animation.SCALE);
17100 *
17101 * $mdPanel.create({
17102 * animation: panelAnimation
17103 * });
17104 *
17105 * @param {!angular.$injector} $injector
17106 * @final @constructor
17107 */
17108function MdPanelAnimation($injector) {
17109 /** @private @const {!angular.$mdUtil} */
17110 this._$mdUtil = $injector.get('$mdUtil');
17111
17112 /**
17113 * @private {{element: !angular.JQLite|undefined, bounds: !DOMRect}|
17114 * undefined}
17115 */
17116 this._openFrom;
17117
17118 /**
17119 * @private {{element: !angular.JQLite|undefined, bounds: !DOMRect}|
17120 * undefined}
17121 */
17122 this._closeTo;
17123
17124 /** @private {string|{open: string, close: string}} */
17125 this._animationClass = '';
17126
17127 /** @private {number} */
17128 this._openDuration;
17129
17130 /** @private {number} */
17131 this._closeDuration;
17132
17133 /** @private {number|{open: number, close: number}} */
17134 this._rawDuration;
17135}
17136
17137
17138/**
17139 * Possible default animations.
17140 * @enum {string}
17141 */
17142MdPanelAnimation.animation = {
17143 SLIDE: 'md-panel-animate-slide',
17144 SCALE: 'md-panel-animate-scale',
17145 FADE: 'md-panel-animate-fade'
17146};
17147
17148
17149/**
17150 * Specifies where to start the open animation. `openFrom` accepts a
17151 * click event object, query selector, DOM element, or a Rect object that
17152 * is used to determine the bounds. When passed a click event, the location
17153 * of the click will be used as the position to start the animation.
17154 * @param {string|!Element|!Event|{top: number, left: number}} openFrom
17155 * @returns {!MdPanelAnimation}
17156 */
17157MdPanelAnimation.prototype.openFrom = function(openFrom) {
17158 // Check if 'openFrom' is an Event.
17159 openFrom = openFrom.target ? openFrom.target : openFrom;
17160
17161 this._openFrom = this._getPanelAnimationTarget(openFrom);
17162
17163 if (!this._closeTo) {
17164 this._closeTo = this._openFrom;
17165 }
17166 return this;
17167};
17168
17169
17170/**
17171 * Specifies where to animate the panel close. `closeTo` accepts a
17172 * query selector, DOM element, or a Rect object that is used to determine
17173 * the bounds.
17174 * @param {string|!Element|{top: number, left: number}} closeTo
17175 * @returns {!MdPanelAnimation}
17176 */
17177MdPanelAnimation.prototype.closeTo = function(closeTo) {
17178 this._closeTo = this._getPanelAnimationTarget(closeTo);
17179 return this;
17180};
17181
17182
17183/**
17184 * Specifies the duration of the animation in milliseconds.
17185 * @param {number|{open: number, close: number}} duration
17186 * @returns {!MdPanelAnimation}
17187 */
17188MdPanelAnimation.prototype.duration = function(duration) {
17189 if (duration) {
17190 if (angular.isNumber(duration)) {
17191 this._openDuration = this._closeDuration = toSeconds(duration);
17192 } else if (angular.isObject(duration)) {
17193 this._openDuration = toSeconds(duration.open);
17194 this._closeDuration = toSeconds(duration.close);
17195 }
17196 }
17197
17198 // Save the original value so it can be passed to the backdrop.
17199 this._rawDuration = duration;
17200
17201 return this;
17202
17203 function toSeconds(value) {
17204 if (angular.isNumber(value)) return value / 1000;
17205 }
17206};
17207
17208
17209/**
17210 * Returns the element and bounds for the animation target.
17211 * @param {string|!Element|{top: number, left: number}} location
17212 * @returns {{element: !angular.JQLite|undefined, bounds: !DOMRect}}
17213 * @private
17214 */
17215MdPanelAnimation.prototype._getPanelAnimationTarget = function(location) {
17216 if (angular.isDefined(location.top) || angular.isDefined(location.left)) {
17217 return {
17218 element: undefined,
17219 bounds: {
17220 top: location.top || 0,
17221 left: location.left || 0
17222 }
17223 };
17224 } else {
17225 return this._getBoundingClientRect(getElement(location));
17226 }
17227};
17228
17229
17230/**
17231 * Specifies the animation class.
17232 *
17233 * There are several default animations that can be used:
17234 * (MdPanelAnimation.animation)
17235 * SLIDE: The panel slides in and out from the specified
17236 * elements.
17237 * SCALE: The panel scales in and out.
17238 * FADE: The panel fades in and out.
17239 *
17240 * @param {string|{open: string, close: string}} cssClass
17241 * @returns {!MdPanelAnimation}
17242 */
17243MdPanelAnimation.prototype.withAnimation = function(cssClass) {
17244 this._animationClass = cssClass;
17245 return this;
17246};
17247
17248
17249/**
17250 * Animate the panel open.
17251 * @param {!angular.JQLite} panelEl
17252 * @returns {!angular.$q.Promise} A promise that is resolved when the open
17253 * animation is complete.
17254 */
17255MdPanelAnimation.prototype.animateOpen = function(panelEl) {
17256 var animator = this._$mdUtil.dom.animator;
17257
17258 this._fixBounds(panelEl);
17259 var animationOptions = {};
17260
17261 // Include the panel transformations when calculating the animations.
17262 var panelTransform = panelEl[0].style.transform || '';
17263
17264 var openFrom = animator.toTransformCss(panelTransform);
17265 var openTo = animator.toTransformCss(panelTransform);
17266
17267 switch (this._animationClass) {
17268 case MdPanelAnimation.animation.SLIDE:
17269 // Slide should start with opacity: 1.
17270 panelEl.css('opacity', '1');
17271
17272 animationOptions = {
17273 transitionInClass: '_md-panel-animate-enter'
17274 };
17275
17276 var openSlide = animator.calculateSlideToOrigin(
17277 panelEl, this._openFrom) || '';
17278 openFrom = animator.toTransformCss(openSlide + ' ' + panelTransform);
17279 break;
17280
17281 case MdPanelAnimation.animation.SCALE:
17282 animationOptions = {
17283 transitionInClass: '_md-panel-animate-enter'
17284 };
17285
17286 var openScale = animator.calculateZoomToOrigin(
17287 panelEl, this._openFrom) || '';
17288 openFrom = animator.toTransformCss(openScale + ' ' + panelTransform);
17289 break;
17290
17291 case MdPanelAnimation.animation.FADE:
17292 animationOptions = {
17293 transitionInClass: '_md-panel-animate-enter'
17294 };
17295 break;
17296
17297 default:
17298 if (angular.isString(this._animationClass)) {
17299 animationOptions = {
17300 transitionInClass: this._animationClass
17301 };
17302 } else {
17303 animationOptions = {
17304 transitionInClass: this._animationClass['open'],
17305 transitionOutClass: this._animationClass['close'],
17306 };
17307 }
17308 }
17309
17310 animationOptions.duration = this._openDuration;
17311
17312 return animator
17313 .translate3d(panelEl, openFrom, openTo, animationOptions);
17314};
17315
17316
17317/**
17318 * Animate the panel close.
17319 * @param {!angular.JQLite} panelEl
17320 * @returns {!angular.$q.Promise} A promise that resolves when the close
17321 * animation is complete.
17322 */
17323MdPanelAnimation.prototype.animateClose = function(panelEl) {
17324 var animator = this._$mdUtil.dom.animator;
17325 var reverseAnimationOptions = {};
17326
17327 // Include the panel transformations when calculating the animations.
17328 var panelTransform = panelEl[0].style.transform || '';
17329
17330 var closeFrom = animator.toTransformCss(panelTransform);
17331 var closeTo = animator.toTransformCss(panelTransform);
17332
17333 switch (this._animationClass) {
17334 case MdPanelAnimation.animation.SLIDE:
17335 // Slide should start with opacity: 1.
17336 panelEl.css('opacity', '1');
17337 reverseAnimationOptions = {
17338 transitionInClass: '_md-panel-animate-leave'
17339 };
17340
17341 var closeSlide = animator.calculateSlideToOrigin(
17342 panelEl, this._closeTo) || '';
17343 closeTo = animator.toTransformCss(closeSlide + ' ' + panelTransform);
17344 break;
17345
17346 case MdPanelAnimation.animation.SCALE:
17347 reverseAnimationOptions = {
17348 transitionInClass: '_md-panel-animate-scale-out _md-panel-animate-leave'
17349 };
17350
17351 var closeScale = animator.calculateZoomToOrigin(
17352 panelEl, this._closeTo) || '';
17353 closeTo = animator.toTransformCss(closeScale + ' ' + panelTransform);
17354 break;
17355
17356 case MdPanelAnimation.animation.FADE:
17357 reverseAnimationOptions = {
17358 transitionInClass: '_md-panel-animate-fade-out _md-panel-animate-leave'
17359 };
17360 break;
17361
17362 default:
17363 if (angular.isString(this._animationClass)) {
17364 reverseAnimationOptions = {
17365 transitionOutClass: this._animationClass
17366 };
17367 } else {
17368 reverseAnimationOptions = {
17369 transitionInClass: this._animationClass['close'],
17370 transitionOutClass: this._animationClass['open']
17371 };
17372 }
17373 }
17374
17375 reverseAnimationOptions.duration = this._closeDuration;
17376
17377 return animator
17378 .translate3d(panelEl, closeFrom, closeTo, reverseAnimationOptions);
17379};
17380
17381
17382/**
17383 * Set the height and width to match the panel if not provided.
17384 * @param {!angular.JQLite} panelEl
17385 * @private
17386 */
17387MdPanelAnimation.prototype._fixBounds = function(panelEl) {
17388 var panelWidth = panelEl[0].offsetWidth;
17389 var panelHeight = panelEl[0].offsetHeight;
17390
17391 if (this._openFrom && this._openFrom.bounds.height == null) {
17392 this._openFrom.bounds.height = panelHeight;
17393 }
17394 if (this._openFrom && this._openFrom.bounds.width == null) {
17395 this._openFrom.bounds.width = panelWidth;
17396 }
17397 if (this._closeTo && this._closeTo.bounds.height == null) {
17398 this._closeTo.bounds.height = panelHeight;
17399 }
17400 if (this._closeTo && this._closeTo.bounds.width == null) {
17401 this._closeTo.bounds.width = panelWidth;
17402 }
17403};
17404
17405
17406/**
17407 * Identify the bounding RECT for the target element.
17408 * @param {!angular.JQLite} element
17409 * @returns {{element: !angular.JQLite|undefined, bounds: !DOMRect}}
17410 * @private
17411 */
17412MdPanelAnimation.prototype._getBoundingClientRect = function(element) {
17413 if (element instanceof angular.element) {
17414 return {
17415 element: element,
17416 bounds: element[0].getBoundingClientRect()
17417 };
17418 }
17419};
17420
17421
17422/*****************************************************************************
17423 * Util Methods *
17424 *****************************************************************************/
17425
17426
17427/**
17428 * Returns the angular element associated with a css selector or element.
17429 * @param el {string|!angular.JQLite|!Element}
17430 * @returns {!angular.JQLite}
17431 */
17432function getElement(el) {
17433 var queryResult = angular.isString(el) ?
17434 document.querySelector(el) : el;
17435 return angular.element(queryResult);
17436}
17437
17438
17439/**
17440 * Gets the computed values for an element's translateX and translateY in px.
17441 * @param {!angular.JQLite|!Element} el
17442 * @param {string} property
17443 * @return {{x: number, y: number}}
17444 */
17445function getComputedTranslations(el, property) {
17446 // The transform being returned by `getComputedStyle` is in the format:
17447 // `matrix(a, b, c, d, translateX, translateY)` if defined and `none`
17448 // if the element doesn't have a transform.
17449 var transform = getComputedStyle(el[0] || el)[property];
17450 var openIndex = transform.indexOf('(');
17451 var closeIndex = transform.lastIndexOf(')');
17452 var output = { x: 0, y: 0 };
17453
17454 if (openIndex > -1 && closeIndex > -1) {
17455 var parsedValues = transform
17456 .substring(openIndex + 1, closeIndex)
17457 .split(', ')
17458 .slice(-2);
17459
17460 output.x = parseInt(parsedValues[0]);
17461 output.y = parseInt(parsedValues[1]);
17462 }
17463
17464 return output;
17465}
17466
17467})();
17468(function(){
17469"use strict";
17470
17471/**
17472 * @ngdoc module
17473 * @name material.components.navBar
17474 */
17475
17476
17477MdNavBarController.$inject = ["$element", "$scope", "$timeout", "$mdConstant"];
17478MdNavItem.$inject = ["$mdAria", "$$rAF"];
17479MdNavItemController.$inject = ["$element"];
17480MdNavBar.$inject = ["$mdAria", "$mdTheming"];
17481angular.module('material.components.navBar', ['material.core'])
17482 .controller('MdNavBarController', MdNavBarController)
17483 .directive('mdNavBar', MdNavBar)
17484 .controller('MdNavItemController', MdNavItemController)
17485 .directive('mdNavItem', MdNavItem);
17486
17487
17488/*****************************************************************************
17489 * PUBLIC DOCUMENTATION *
17490 *****************************************************************************/
17491/**
17492 * @ngdoc directive
17493 * @name mdNavBar
17494 * @module material.components.navBar
17495 *
17496 * @restrict E
17497 *
17498 * @description
17499 * The `<md-nav-bar>` directive renders a list of material tabs that can be used
17500 * for top-level page navigation. Unlike `<md-tabs>`, it has no concept of a tab
17501 * body and no bar pagination.
17502 *
17503 * Because it deals with page navigation, certain routing concepts are built-in.
17504 * Route changes via via ng-href, ui-sref, or ng-click events are supported.
17505 * Alternatively, the user could simply watch currentNavItem for changes.
17506 *
17507 * Accessibility functionality is implemented as a site navigator with a
17508 * listbox, according to
17509 * https://www.w3.org/TR/wai-aria-practices/#Site_Navigator_Tabbed_Style
17510 *
17511 * @param {string=} mdSelectedNavItem The name of the current tab; this must
17512 * match the name attribute of `<md-nav-item>`
17513 * @param {boolean=} mdNoInkBar If set to true, the ink bar will be hidden.
17514 * @param {string=} navBarAriaLabel An aria-label for the nav-bar
17515 *
17516 * @usage
17517 * <hljs lang="html">
17518 * <md-nav-bar md-selected-nav-item="currentNavItem">
17519 * <md-nav-item md-nav-click="goto('page1')" name="page1">
17520 * Page One
17521 * </md-nav-item>
17522 * <md-nav-item md-nav-href="#page2" name="page3">Page Two</md-nav-item>
17523 * <md-nav-item md-nav-sref="page3" name="page2">Page Three</md-nav-item>
17524 * <md-nav-item
17525 * md-nav-sref="app.page4"
17526 * sref-opts="{reload: true, notify: true}"
17527 * name="page4">
17528 * Page Four
17529 * </md-nav-item>
17530 * </md-nav-bar>
17531 *</hljs>
17532 * <hljs lang="js">
17533 * (function() {
17534 * 'use strict';
17535 *
17536 * $rootScope.$on('$routeChangeSuccess', function(event, current) {
17537 * $scope.currentLink = getCurrentLinkFromRoute(current);
17538 * });
17539 * });
17540 * </hljs>
17541 */
17542
17543/*****************************************************************************
17544 * mdNavItem
17545 *****************************************************************************/
17546/**
17547 * @ngdoc directive
17548 * @name mdNavItem
17549 * @module material.components.navBar
17550 *
17551 * @restrict E
17552 *
17553 * @description
17554 * `<md-nav-item>` describes a page navigation link within the `<md-nav-bar>`
17555 * component. It renders an md-button as the actual link.
17556 *
17557 * Exactly one of the mdNavClick, mdNavHref, mdNavSref attributes are required
17558 * to be specified.
17559 *
17560 * @param {Function=} mdNavClick Function which will be called when the
17561 * link is clicked to change the page. Renders as an `ng-click`.
17562 * @param {string=} mdNavHref url to transition to when this link is clicked.
17563 * Renders as an `ng-href`.
17564 * @param {string=} mdNavSref Ui-router state to transition to when this link is
17565 * clicked. Renders as a `ui-sref`.
17566 * @param {!Object=} srefOpts Ui-router options that are passed to the
17567 * `$state.go()` function. See the [Ui-router documentation for details]
17568 * (https://ui-router.github.io/docs/latest/interfaces/transition.transitionoptions.html).
17569 * @param {string=} name The name of this link. Used by the nav bar to know
17570 * which link is currently selected.
17571 * @param {string=} aria-label Adds alternative text for accessibility
17572 *
17573 * @usage
17574 * See `<md-nav-bar>` for usage.
17575 */
17576
17577
17578/*****************************************************************************
17579 * IMPLEMENTATION *
17580 *****************************************************************************/
17581
17582function MdNavBar($mdAria, $mdTheming) {
17583 return {
17584 restrict: 'E',
17585 transclude: true,
17586 controller: MdNavBarController,
17587 controllerAs: 'ctrl',
17588 bindToController: true,
17589 scope: {
17590 'mdSelectedNavItem': '=?',
17591 'mdNoInkBar': '=?',
17592 'navBarAriaLabel': '@?',
17593 },
17594 template:
17595 '<div class="md-nav-bar">' +
17596 '<nav role="navigation">' +
17597 '<ul class="_md-nav-bar-list" ng-transclude role="listbox"' +
17598 'tabindex="0"' +
17599 'ng-focus="ctrl.onFocus()"' +
17600 'ng-keydown="ctrl.onKeydown($event)"' +
17601 'aria-label="{{ctrl.navBarAriaLabel}}">' +
17602 '</ul>' +
17603 '</nav>' +
17604 '<md-nav-ink-bar ng-hide="ctrl.mdNoInkBar"></md-nav-ink-bar>' +
17605 '</div>',
17606 link: function(scope, element, attrs, ctrl) {
17607 $mdTheming(element);
17608 if (!ctrl.navBarAriaLabel) {
17609 $mdAria.expectAsync(element, 'aria-label', angular.noop);
17610 }
17611 },
17612 };
17613}
17614
17615/**
17616 * Controller for the nav-bar component.
17617 *
17618 * Accessibility functionality is implemented as a site navigator with a
17619 * listbox, according to
17620 * https://www.w3.org/TR/wai-aria-practices/#Site_Navigator_Tabbed_Style
17621 * @param {!angular.JQLite} $element
17622 * @param {!angular.Scope} $scope
17623 * @param {!angular.Timeout} $timeout
17624 * @param {!Object} $mdConstant
17625 * @constructor
17626 * @final
17627 * @ngInject
17628 */
17629function MdNavBarController($element, $scope, $timeout, $mdConstant) {
17630 // Injected variables
17631 /** @private @const {!angular.Timeout} */
17632 this._$timeout = $timeout;
17633
17634 /** @private @const {!angular.Scope} */
17635 this._$scope = $scope;
17636
17637 /** @private @const {!Object} */
17638 this._$mdConstant = $mdConstant;
17639
17640 // Data-bound variables.
17641 /** @type {string} */
17642 this.mdSelectedNavItem;
17643
17644 /** @type {string} */
17645 this.navBarAriaLabel;
17646
17647 // State variables.
17648
17649 /** @type {?angular.JQLite} */
17650 this._navBarEl = $element[0];
17651
17652 /** @type {?angular.JQLite} */
17653 this._inkbar;
17654
17655 var self = this;
17656 // need to wait for transcluded content to be available
17657 var deregisterTabWatch = this._$scope.$watch(function() {
17658 return self._navBarEl.querySelectorAll('._md-nav-button').length;
17659 },
17660 function(newLength) {
17661 if (newLength > 0) {
17662 self._initTabs();
17663 deregisterTabWatch();
17664 }
17665 });
17666}
17667
17668
17669
17670/**
17671 * Initializes the tab components once they exist.
17672 * @private
17673 */
17674MdNavBarController.prototype._initTabs = function() {
17675 this._inkbar = angular.element(this._navBarEl.querySelector('md-nav-ink-bar'));
17676
17677 var self = this;
17678 this._$timeout(function() {
17679 self._updateTabs(self.mdSelectedNavItem, undefined);
17680 });
17681
17682 this._$scope.$watch('ctrl.mdSelectedNavItem', function(newValue, oldValue) {
17683 // Wait a digest before update tabs for products doing
17684 // anything dynamic in the template.
17685 self._$timeout(function() {
17686 self._updateTabs(newValue, oldValue);
17687 });
17688 });
17689};
17690
17691/**
17692 * Set the current tab to be selected.
17693 * @param {string|undefined} newValue New current tab name.
17694 * @param {string|undefined} oldValue Previous tab name.
17695 * @private
17696 */
17697MdNavBarController.prototype._updateTabs = function(newValue, oldValue) {
17698 var self = this;
17699 var tabs = this._getTabs();
17700
17701 // this._getTabs can return null if nav-bar has not yet been initialized
17702 if(!tabs)
17703 return;
17704
17705 var oldIndex = -1;
17706 var newIndex = -1;
17707 var newTab = this._getTabByName(newValue);
17708 var oldTab = this._getTabByName(oldValue);
17709
17710 if (oldTab) {
17711 oldTab.setSelected(false);
17712 oldIndex = tabs.indexOf(oldTab);
17713 }
17714
17715 if (newTab) {
17716 newTab.setSelected(true);
17717 newIndex = tabs.indexOf(newTab);
17718 }
17719
17720 this._$timeout(function() {
17721 self._updateInkBarStyles(newTab, newIndex, oldIndex);
17722 });
17723};
17724
17725/**
17726 * Repositions the ink bar to the selected tab.
17727 * @private
17728 */
17729MdNavBarController.prototype._updateInkBarStyles = function(tab, newIndex, oldIndex) {
17730 this._inkbar.toggleClass('_md-left', newIndex < oldIndex)
17731 .toggleClass('_md-right', newIndex > oldIndex);
17732
17733 this._inkbar.css({display: newIndex < 0 ? 'none' : ''});
17734
17735 if (tab) {
17736 var tabEl = tab.getButtonEl();
17737 var left = tabEl.offsetLeft;
17738
17739 this._inkbar.css({left: left + 'px', width: tabEl.offsetWidth + 'px'});
17740 }
17741};
17742
17743/**
17744 * Returns an array of the current tabs.
17745 * @return {!Array<!NavItemController>}
17746 * @private
17747 */
17748MdNavBarController.prototype._getTabs = function() {
17749 var controllers = Array.prototype.slice.call(
17750 this._navBarEl.querySelectorAll('.md-nav-item'))
17751 .map(function(el) {
17752 return angular.element(el).controller('mdNavItem')
17753 });
17754 return controllers.indexOf(undefined) ? controllers : null;
17755};
17756
17757/**
17758 * Returns the tab with the specified name.
17759 * @param {string} name The name of the tab, found in its name attribute.
17760 * @return {!NavItemController|undefined}
17761 * @private
17762 */
17763MdNavBarController.prototype._getTabByName = function(name) {
17764 return this._findTab(function(tab) {
17765 return tab.getName() == name;
17766 });
17767};
17768
17769/**
17770 * Returns the selected tab.
17771 * @return {!NavItemController|undefined}
17772 * @private
17773 */
17774MdNavBarController.prototype._getSelectedTab = function() {
17775 return this._findTab(function(tab) {
17776 return tab.isSelected();
17777 });
17778};
17779
17780/**
17781 * Returns the focused tab.
17782 * @return {!NavItemController|undefined}
17783 */
17784MdNavBarController.prototype.getFocusedTab = function() {
17785 return this._findTab(function(tab) {
17786 return tab.hasFocus();
17787 });
17788};
17789
17790/**
17791 * Find a tab that matches the specified function.
17792 * @private
17793 */
17794MdNavBarController.prototype._findTab = function(fn) {
17795 var tabs = this._getTabs();
17796 for (var i = 0; i < tabs.length; i++) {
17797 if (fn(tabs[i])) {
17798 return tabs[i];
17799 }
17800 }
17801
17802 return null;
17803};
17804
17805/**
17806 * Direct focus to the selected tab when focus enters the nav bar.
17807 */
17808MdNavBarController.prototype.onFocus = function() {
17809 var tab = this._getSelectedTab();
17810 if (tab) {
17811 tab.setFocused(true);
17812 }
17813};
17814
17815/**
17816 * Move focus from oldTab to newTab.
17817 * @param {!NavItemController} oldTab
17818 * @param {!NavItemController} newTab
17819 * @private
17820 */
17821MdNavBarController.prototype._moveFocus = function(oldTab, newTab) {
17822 oldTab.setFocused(false);
17823 newTab.setFocused(true);
17824};
17825
17826/**
17827 * Responds to keypress events.
17828 * @param {!Event} e
17829 */
17830MdNavBarController.prototype.onKeydown = function(e) {
17831 var keyCodes = this._$mdConstant.KEY_CODE;
17832 var tabs = this._getTabs();
17833 var focusedTab = this.getFocusedTab();
17834 if (!focusedTab) return;
17835
17836 var focusedTabIndex = tabs.indexOf(focusedTab);
17837
17838 // use arrow keys to navigate between tabs
17839 switch (e.keyCode) {
17840 case keyCodes.UP_ARROW:
17841 case keyCodes.LEFT_ARROW:
17842 if (focusedTabIndex > 0) {
17843 this._moveFocus(focusedTab, tabs[focusedTabIndex - 1]);
17844 }
17845 break;
17846 case keyCodes.DOWN_ARROW:
17847 case keyCodes.RIGHT_ARROW:
17848 if (focusedTabIndex < tabs.length - 1) {
17849 this._moveFocus(focusedTab, tabs[focusedTabIndex + 1]);
17850 }
17851 break;
17852 case keyCodes.SPACE:
17853 case keyCodes.ENTER:
17854 // timeout to avoid a "digest already in progress" console error
17855 this._$timeout(function() {
17856 focusedTab.getButtonEl().click();
17857 });
17858 break;
17859 }
17860};
17861
17862/**
17863 * @ngInject
17864 */
17865function MdNavItem($mdAria, $$rAF) {
17866 return {
17867 restrict: 'E',
17868 require: ['mdNavItem', '^mdNavBar'],
17869 controller: MdNavItemController,
17870 bindToController: true,
17871 controllerAs: 'ctrl',
17872 replace: true,
17873 transclude: true,
17874 template: function(tElement, tAttrs) {
17875 var hasNavClick = tAttrs.mdNavClick;
17876 var hasNavHref = tAttrs.mdNavHref;
17877 var hasNavSref = tAttrs.mdNavSref;
17878 var hasSrefOpts = tAttrs.srefOpts;
17879 var navigationAttribute;
17880 var navigationOptions;
17881 var buttonTemplate;
17882
17883 // Cannot specify more than one nav attribute
17884 if ((hasNavClick ? 1:0) + (hasNavHref ? 1:0) + (hasNavSref ? 1:0) > 1) {
17885 throw Error(
17886 'Must not specify more than one of the md-nav-click, md-nav-href, ' +
17887 'or md-nav-sref attributes per nav-item directive.'
17888 );
17889 }
17890
17891 if (hasNavClick) {
17892 navigationAttribute = 'ng-click="ctrl.mdNavClick()"';
17893 } else if (hasNavHref) {
17894 navigationAttribute = 'ng-href="{{ctrl.mdNavHref}}"';
17895 } else if (hasNavSref) {
17896 navigationAttribute = 'ui-sref="{{ctrl.mdNavSref}}"';
17897 }
17898
17899 navigationOptions = hasSrefOpts ? 'ui-sref-opts="{{ctrl.srefOpts}}" ' : '';
17900
17901 if (navigationAttribute) {
17902 buttonTemplate = '' +
17903 '<md-button class="_md-nav-button md-accent" ' +
17904 'ng-class="ctrl.getNgClassMap()" ' +
17905 'ng-blur="ctrl.setFocused(false)" ' +
17906 'tabindex="-1" ' +
17907 navigationOptions +
17908 navigationAttribute + '>' +
17909 '<span ng-transclude class="_md-nav-button-text"></span>' +
17910 '</md-button>';
17911 }
17912
17913 return '' +
17914 '<li class="md-nav-item" ' +
17915 'role="option" ' +
17916 'aria-selected="{{ctrl.isSelected()}}">' +
17917 (buttonTemplate || '') +
17918 '</li>';
17919 },
17920 scope: {
17921 'mdNavClick': '&?',
17922 'mdNavHref': '@?',
17923 'mdNavSref': '@?',
17924 'srefOpts': '=?',
17925 'name': '@',
17926 },
17927 link: function(scope, element, attrs, controllers) {
17928 // When accessing the element's contents synchronously, they
17929 // may not be defined yet because of transclusion. There is a higher
17930 // chance that it will be accessible if we wait one frame.
17931 $$rAF(function() {
17932 var mdNavItem = controllers[0];
17933 var mdNavBar = controllers[1];
17934 var navButton = angular.element(element[0].querySelector('._md-nav-button'));
17935
17936 if (!mdNavItem.name) {
17937 mdNavItem.name = angular.element(element[0]
17938 .querySelector('._md-nav-button-text')).text().trim();
17939 }
17940
17941 navButton.on('click', function() {
17942 mdNavBar.mdSelectedNavItem = mdNavItem.name;
17943 scope.$apply();
17944 });
17945
17946 $mdAria.expectWithText(element, 'aria-label');
17947 });
17948 }
17949 };
17950}
17951
17952/**
17953 * Controller for the nav-item component.
17954 * @param {!angular.JQLite} $element
17955 * @constructor
17956 * @final
17957 * @ngInject
17958 */
17959function MdNavItemController($element) {
17960
17961 /** @private @const {!angular.JQLite} */
17962 this._$element = $element;
17963
17964 // Data-bound variables
17965
17966 /** @const {?Function} */
17967 this.mdNavClick;
17968
17969 /** @const {?string} */
17970 this.mdNavHref;
17971
17972 /** @const {?string} */
17973 this.mdNavSref;
17974 /** @const {?Object} */
17975 this.srefOpts;
17976 /** @const {?string} */
17977 this.name;
17978
17979 // State variables
17980 /** @private {boolean} */
17981 this._selected = false;
17982
17983 /** @private {boolean} */
17984 this._focused = false;
17985}
17986
17987/**
17988 * Returns a map of class names and values for use by ng-class.
17989 * @return {!Object<string,boolean>}
17990 */
17991MdNavItemController.prototype.getNgClassMap = function() {
17992 return {
17993 'md-active': this._selected,
17994 'md-primary': this._selected,
17995 'md-unselected': !this._selected,
17996 'md-focused': this._focused,
17997 };
17998};
17999
18000/**
18001 * Get the name attribute of the tab.
18002 * @return {string}
18003 */
18004MdNavItemController.prototype.getName = function() {
18005 return this.name;
18006};
18007
18008/**
18009 * Get the button element associated with the tab.
18010 * @return {!Element}
18011 */
18012MdNavItemController.prototype.getButtonEl = function() {
18013 return this._$element[0].querySelector('._md-nav-button');
18014};
18015
18016/**
18017 * Set the selected state of the tab.
18018 * @param {boolean} isSelected
18019 */
18020MdNavItemController.prototype.setSelected = function(isSelected) {
18021 this._selected = isSelected;
18022};
18023
18024/**
18025 * @return {boolean}
18026 */
18027MdNavItemController.prototype.isSelected = function() {
18028 return this._selected;
18029};
18030
18031/**
18032 * Set the focused state of the tab.
18033 * @param {boolean} isFocused
18034 */
18035MdNavItemController.prototype.setFocused = function(isFocused) {
18036 this._focused = isFocused;
18037
18038 if (isFocused) {
18039 this.getButtonEl().focus();
18040 }
18041};
18042
18043/**
18044 * @return {boolean}
18045 */
18046MdNavItemController.prototype.hasFocus = function() {
18047 return this._focused;
18048};
18049
18050})();
18051(function(){
18052"use strict";
18053
18054/**
18055 * @ngdoc module
18056 * @name material.components.progressCircular
18057 * @description Module for a circular progressbar
18058 */
18059
18060angular.module('material.components.progressCircular', ['material.core']);
18061
18062})();
18063(function(){
18064"use strict";
18065
18066/**
18067 * @ngdoc module
18068 * @name material.components.progressLinear
18069 * @description Linear Progress module!
18070 */
18071MdProgressLinearDirective.$inject = ["$mdTheming", "$mdUtil", "$log"];
18072angular.module('material.components.progressLinear', [
18073 'material.core'
18074])
18075 .directive('mdProgressLinear', MdProgressLinearDirective);
18076
18077/**
18078 * @ngdoc directive
18079 * @name mdProgressLinear
18080 * @module material.components.progressLinear
18081 * @restrict E
18082 *
18083 * @description
18084 * The linear progress directive is used to make loading content
18085 * in your app as delightful and painless as possible by minimizing
18086 * the amount of visual change a user sees before they can view
18087 * and interact with content.
18088 *
18089 * Each operation should only be represented by one activity indicator
18090 * For example: one refresh operation should not display both a
18091 * refresh bar and an activity circle.
18092 *
18093 * For operations where the percentage of the operation completed
18094 * can be determined, use a determinate indicator. They give users
18095 * a quick sense of how long an operation will take.
18096 *
18097 * For operations where the user is asked to wait a moment while
18098 * something finishes up, and it’s not necessary to expose what's
18099 * happening behind the scenes and how long it will take, use an
18100 * indeterminate indicator.
18101 *
18102 * @param {string} md-mode Select from one of four modes: determinate, indeterminate, buffer or query.
18103 *
18104 * Note: if the `md-mode` value is set as undefined or specified as 1 of the four (4) valid modes, then `indeterminate`
18105 * will be auto-applied as the mode.
18106 *
18107 * Note: if not configured, the `md-mode="indeterminate"` will be auto injected as an attribute. If `value=""` is also specified, however,
18108 * then `md-mode="determinate"` would be auto-injected instead.
18109 * @param {number=} value In determinate and buffer modes, this number represents the percentage of the primary progress bar. Default: 0
18110 * @param {number=} md-buffer-value In the buffer mode, this number represents the percentage of the secondary progress bar. Default: 0
18111 * @param {boolean=} ng-disabled Determines whether to disable the progress element.
18112 *
18113 * @usage
18114 * <hljs lang="html">
18115 * <md-progress-linear md-mode="determinate" value="..."></md-progress-linear>
18116 *
18117 * <md-progress-linear md-mode="determinate" ng-value="..."></md-progress-linear>
18118 *
18119 * <md-progress-linear md-mode="indeterminate"></md-progress-linear>
18120 *
18121 * <md-progress-linear md-mode="buffer" value="..." md-buffer-value="..."></md-progress-linear>
18122 *
18123 * <md-progress-linear md-mode="query"></md-progress-linear>
18124 * </hljs>
18125 */
18126function MdProgressLinearDirective($mdTheming, $mdUtil, $log) {
18127 var MODE_DETERMINATE = "determinate";
18128 var MODE_INDETERMINATE = "indeterminate";
18129 var MODE_BUFFER = "buffer";
18130 var MODE_QUERY = "query";
18131 var DISABLED_CLASS = "_md-progress-linear-disabled";
18132
18133 return {
18134 restrict: 'E',
18135 template: '<div class="md-container">' +
18136 '<div class="md-dashed"></div>' +
18137 '<div class="md-bar md-bar1"></div>' +
18138 '<div class="md-bar md-bar2"></div>' +
18139 '</div>',
18140 compile: compile
18141 };
18142
18143 function compile(tElement, tAttrs, transclude) {
18144 tElement.attr('aria-valuemin', 0);
18145 tElement.attr('aria-valuemax', 100);
18146 tElement.attr('role', 'progressbar');
18147
18148 return postLink;
18149 }
18150 function postLink(scope, element, attr) {
18151 $mdTheming(element);
18152
18153 var lastMode;
18154 var isDisabled = attr.hasOwnProperty('disabled');
18155 var toVendorCSS = $mdUtil.dom.animator.toCss;
18156 var bar1 = angular.element(element[0].querySelector('.md-bar1'));
18157 var bar2 = angular.element(element[0].querySelector('.md-bar2'));
18158 var container = angular.element(element[0].querySelector('.md-container'));
18159
18160 element
18161 .attr('md-mode', mode())
18162 .toggleClass(DISABLED_CLASS, isDisabled);
18163
18164 validateMode();
18165 watchAttributes();
18166
18167 /**
18168 * Watch the value, md-buffer-value, and md-mode attributes
18169 */
18170 function watchAttributes() {
18171 attr.$observe('value', function(value) {
18172 var percentValue = clamp(value);
18173 element.attr('aria-valuenow', percentValue);
18174
18175 if (mode() != MODE_QUERY) animateIndicator(bar2, percentValue);
18176 });
18177
18178 attr.$observe('mdBufferValue', function(value) {
18179 animateIndicator(bar1, clamp(value));
18180 });
18181
18182 attr.$observe('disabled', function(value) {
18183 if (value === true || value === false) {
18184 isDisabled = !!value;
18185 } else {
18186 isDisabled = angular.isDefined(value);
18187 }
18188
18189 element.toggleClass(DISABLED_CLASS, isDisabled);
18190 container.toggleClass(lastMode, !isDisabled);
18191 });
18192
18193 attr.$observe('mdMode', function(mode) {
18194 if (lastMode) container.removeClass( lastMode );
18195
18196 switch( mode ) {
18197 case MODE_QUERY:
18198 case MODE_BUFFER:
18199 case MODE_DETERMINATE:
18200 case MODE_INDETERMINATE:
18201 container.addClass( lastMode = "md-mode-" + mode );
18202 break;
18203 default:
18204 container.addClass( lastMode = "md-mode-" + MODE_INDETERMINATE );
18205 break;
18206 }
18207 });
18208 }
18209
18210 /**
18211 * Auto-defaults the mode to either `determinate` or `indeterminate` mode; if not specified
18212 */
18213 function validateMode() {
18214 if ( angular.isUndefined(attr.mdMode) ) {
18215 var hasValue = angular.isDefined(attr.value);
18216 var mode = hasValue ? MODE_DETERMINATE : MODE_INDETERMINATE;
18217 var info = "Auto-adding the missing md-mode='{0}' to the ProgressLinear element";
18218
18219 //$log.debug( $mdUtil.supplant(info, [mode]) );
18220
18221 element.attr("md-mode", mode);
18222 attr.mdMode = mode;
18223 }
18224 }
18225
18226 /**
18227 * Is the md-mode a valid option?
18228 */
18229 function mode() {
18230 var value = (attr.mdMode || "").trim();
18231 if ( value ) {
18232 switch(value) {
18233 case MODE_DETERMINATE:
18234 case MODE_INDETERMINATE:
18235 case MODE_BUFFER:
18236 case MODE_QUERY:
18237 break;
18238 default:
18239 value = MODE_INDETERMINATE;
18240 break;
18241 }
18242 }
18243 return value;
18244 }
18245
18246 /**
18247 * Manually set CSS to animate the Determinate indicator based on the specified
18248 * percentage value (0-100).
18249 */
18250 function animateIndicator(target, value) {
18251 if ( isDisabled || !mode() ) return;
18252
18253 var to = $mdUtil.supplant("translateX({0}%) scale({1},1)", [ (value-100)/2, value/100 ]);
18254 var styles = toVendorCSS({ transform : to });
18255 angular.element(target).css( styles );
18256 }
18257 }
18258
18259 /**
18260 * Clamps the value to be between 0 and 100.
18261 * @param {number} value The value to clamp.
18262 * @returns {number}
18263 */
18264 function clamp(value) {
18265 return Math.max(0, Math.min(value || 0, 100));
18266 }
18267}
18268
18269
18270})();
18271(function(){
18272"use strict";
18273
18274/**
18275 * @ngdoc module
18276 * @name material.components.radioButton
18277 * @description radioButton module!
18278 */
18279mdRadioGroupDirective.$inject = ["$mdUtil", "$mdConstant", "$mdTheming", "$timeout"];
18280mdRadioButtonDirective.$inject = ["$mdAria", "$mdUtil", "$mdTheming"];
18281angular.module('material.components.radioButton', [
18282 'material.core'
18283])
18284 .directive('mdRadioGroup', mdRadioGroupDirective)
18285 .directive('mdRadioButton', mdRadioButtonDirective);
18286
18287/**
18288 * @ngdoc directive
18289 * @module material.components.radioButton
18290 * @name mdRadioGroup
18291 *
18292 * @restrict E
18293 *
18294 * @description
18295 * The `<md-radio-group>` directive identifies a grouping
18296 * container for the 1..n grouped radio buttons; specified using nested
18297 * `<md-radio-button>` tags.
18298 *
18299 * As per the [material design spec](http://www.google.com/design/spec/style/color.html#color-ui-color-application)
18300 * the radio button is in the accent color by default. The primary color palette may be used with
18301 * the `md-primary` class.
18302 *
18303 * Note: `<md-radio-group>` and `<md-radio-button>` handle tabindex differently
18304 * than the native `<input type='radio'>` controls. Whereas the native controls
18305 * force the user to tab through all the radio buttons, `<md-radio-group>`
18306 * is focusable, and by default the `<md-radio-button>`s are not.
18307 *
18308 * @param {string} ng-model Assignable angular expression to data-bind to.
18309 * @param {boolean=} md-no-ink Use of attribute indicates flag to disable ink ripple effects.
18310 *
18311 * @usage
18312 * <hljs lang="html">
18313 * <md-radio-group ng-model="selected">
18314 *
18315 * <md-radio-button
18316 * ng-repeat="d in colorOptions"
18317 * ng-value="d.value" aria-label="{{ d.label }}">
18318 *
18319 * {{ d.label }}
18320 *
18321 * </md-radio-button>
18322 *
18323 * </md-radio-group>
18324 * </hljs>
18325 *
18326 */
18327function mdRadioGroupDirective($mdUtil, $mdConstant, $mdTheming, $timeout) {
18328 RadioGroupController.prototype = createRadioGroupControllerProto();
18329
18330 return {
18331 restrict: 'E',
18332 controller: ['$element', RadioGroupController],
18333 require: ['mdRadioGroup', '?ngModel'],
18334 link: { pre: linkRadioGroup }
18335 };
18336
18337 function linkRadioGroup(scope, element, attr, ctrls) {
18338 element.addClass('_md'); // private md component indicator for styling
18339 $mdTheming(element);
18340
18341 var rgCtrl = ctrls[0];
18342 var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel();
18343
18344 rgCtrl.init(ngModelCtrl);
18345
18346 scope.mouseActive = false;
18347
18348 element
18349 .attr({
18350 'role': 'radiogroup',
18351 'tabIndex': element.attr('tabindex') || '0'
18352 })
18353 .on('keydown', keydownListener)
18354 .on('mousedown', function(event) {
18355 scope.mouseActive = true;
18356 $timeout(function() {
18357 scope.mouseActive = false;
18358 }, 100);
18359 })
18360 .on('focus', function() {
18361 if(scope.mouseActive === false) {
18362 rgCtrl.$element.addClass('md-focused');
18363 }
18364 })
18365 .on('blur', function() {
18366 rgCtrl.$element.removeClass('md-focused');
18367 });
18368
18369 /**
18370 *
18371 */
18372 function setFocus() {
18373 if (!element.hasClass('md-focused')) { element.addClass('md-focused'); }
18374 }
18375
18376 /**
18377 *
18378 */
18379 function keydownListener(ev) {
18380 var keyCode = ev.which || ev.keyCode;
18381
18382 // Only listen to events that we originated ourselves
18383 // so that we don't trigger on things like arrow keys in
18384 // inputs.
18385
18386 if (keyCode != $mdConstant.KEY_CODE.ENTER &&
18387 ev.currentTarget != ev.target) {
18388 return;
18389 }
18390
18391 switch (keyCode) {
18392 case $mdConstant.KEY_CODE.LEFT_ARROW:
18393 case $mdConstant.KEY_CODE.UP_ARROW:
18394 ev.preventDefault();
18395 rgCtrl.selectPrevious();
18396 setFocus();
18397 break;
18398
18399 case $mdConstant.KEY_CODE.RIGHT_ARROW:
18400 case $mdConstant.KEY_CODE.DOWN_ARROW:
18401 ev.preventDefault();
18402 rgCtrl.selectNext();
18403 setFocus();
18404 break;
18405
18406 case $mdConstant.KEY_CODE.ENTER:
18407 var form = angular.element($mdUtil.getClosest(element[0], 'form'));
18408 if (form.length > 0) {
18409 form.triggerHandler('submit');
18410 }
18411 break;
18412 }
18413
18414 }
18415 }
18416
18417 function RadioGroupController($element) {
18418 this._radioButtonRenderFns = [];
18419 this.$element = $element;
18420 }
18421
18422 function createRadioGroupControllerProto() {
18423 return {
18424 init: function(ngModelCtrl) {
18425 this._ngModelCtrl = ngModelCtrl;
18426 this._ngModelCtrl.$render = angular.bind(this, this.render);
18427 },
18428 add: function(rbRender) {
18429 this._radioButtonRenderFns.push(rbRender);
18430 },
18431 remove: function(rbRender) {
18432 var index = this._radioButtonRenderFns.indexOf(rbRender);
18433 if (index !== -1) {
18434 this._radioButtonRenderFns.splice(index, 1);
18435 }
18436 },
18437 render: function() {
18438 this._radioButtonRenderFns.forEach(function(rbRender) {
18439 rbRender();
18440 });
18441 },
18442 setViewValue: function(value, eventType) {
18443 this._ngModelCtrl.$setViewValue(value, eventType);
18444 // update the other radio buttons as well
18445 this.render();
18446 },
18447 getViewValue: function() {
18448 return this._ngModelCtrl.$viewValue;
18449 },
18450 selectNext: function() {
18451 return changeSelectedButton(this.$element, 1);
18452 },
18453 selectPrevious: function() {
18454 return changeSelectedButton(this.$element, -1);
18455 },
18456 setActiveDescendant: function (radioId) {
18457 this.$element.attr('aria-activedescendant', radioId);
18458 },
18459 isDisabled: function() {
18460 return this.$element[0].hasAttribute('disabled');
18461 }
18462 };
18463 }
18464 /**
18465 * Change the radio group's selected button by a given increment.
18466 * If no button is selected, select the first button.
18467 */
18468 function changeSelectedButton(parent, increment) {
18469 // Coerce all child radio buttons into an array, then wrap then in an iterator
18470 var buttons = $mdUtil.iterator(parent[0].querySelectorAll('md-radio-button'), true);
18471
18472 if (buttons.count()) {
18473 var validate = function (button) {
18474 // If disabled, then NOT valid
18475 return !angular.element(button).attr("disabled");
18476 };
18477
18478 var selected = parent[0].querySelector('md-radio-button.md-checked');
18479 var target = buttons[increment < 0 ? 'previous' : 'next'](selected, validate) || buttons.first();
18480
18481 // Activate radioButton's click listener (triggerHandler won't create a real click event)
18482 angular.element(target).triggerHandler('click');
18483 }
18484 }
18485
18486}
18487
18488/**
18489 * @ngdoc directive
18490 * @module material.components.radioButton
18491 * @name mdRadioButton
18492 *
18493 * @restrict E
18494 *
18495 * @description
18496 * The `<md-radio-button>`directive is the child directive required to be used within `<md-radio-group>` elements.
18497 *
18498 * While similar to the `<input type="radio" ng-model="" value="">` directive,
18499 * the `<md-radio-button>` directive provides ink effects, ARIA support, and
18500 * supports use within named radio groups.
18501 *
18502 * @param {string} ngModel Assignable angular expression to data-bind to.
18503 * @param {string=} ngChange Angular expression to be executed when input changes due to user
18504 * interaction with the input element.
18505 * @param {string} ngValue Angular expression which sets the value to which the expression should
18506 * be set when selected.
18507 * @param {string} value The value to which the expression should be set when selected.
18508 * @param {string=} name Property name of the form under which the control is published.
18509 * @param {string=} aria-label Adds label to radio button for accessibility.
18510 * Defaults to radio button's text. If no text content is available, a warning will be logged.
18511 *
18512 * @usage
18513 * <hljs lang="html">
18514 *
18515 * <md-radio-button value="1" aria-label="Label 1">
18516 * Label 1
18517 * </md-radio-button>
18518 *
18519 * <md-radio-button ng-model="color" ng-value="specialValue" aria-label="Green">
18520 * Green
18521 * </md-radio-button>
18522 *
18523 * </hljs>
18524 *
18525 */
18526function mdRadioButtonDirective($mdAria, $mdUtil, $mdTheming) {
18527
18528 var CHECKED_CSS = 'md-checked';
18529
18530 return {
18531 restrict: 'E',
18532 require: '^mdRadioGroup',
18533 transclude: true,
18534 template: '<div class="md-container" md-ink-ripple md-ink-ripple-checkbox>' +
18535 '<div class="md-off"></div>' +
18536 '<div class="md-on"></div>' +
18537 '</div>' +
18538 '<div ng-transclude class="md-label"></div>',
18539 link: link
18540 };
18541
18542 function link(scope, element, attr, rgCtrl) {
18543 var lastChecked;
18544
18545 $mdTheming(element);
18546 configureAria(element, scope);
18547
18548 // ngAria overwrites the aria-checked inside a $watch for ngValue.
18549 // We should defer the initialization until all the watches have fired.
18550 // This can also be fixed by removing the `lastChecked` check, but that'll
18551 // cause more DOM manipulation on each digest.
18552 if (attr.ngValue) {
18553 $mdUtil.nextTick(initialize, false);
18554 } else {
18555 initialize();
18556 }
18557
18558 /**
18559 * Initializes the component.
18560 */
18561 function initialize() {
18562 if (!rgCtrl) {
18563 throw 'RadioButton: No RadioGroupController could be found.';
18564 }
18565
18566 rgCtrl.add(render);
18567 attr.$observe('value', render);
18568
18569 element
18570 .on('click', listener)
18571 .on('$destroy', function() {
18572 rgCtrl.remove(render);
18573 });
18574 }
18575
18576 /**
18577 * On click functionality.
18578 */
18579 function listener(ev) {
18580 if (element[0].hasAttribute('disabled') || rgCtrl.isDisabled()) return;
18581
18582 scope.$apply(function() {
18583 rgCtrl.setViewValue(attr.value, ev && ev.type);
18584 });
18585 }
18586
18587 /**
18588 * Add or remove the `.md-checked` class from the RadioButton (and conditionally its parent).
18589 * Update the `aria-activedescendant` attribute.
18590 */
18591 function render() {
18592 var checked = rgCtrl.getViewValue() == attr.value;
18593
18594 if (checked === lastChecked) return;
18595
18596 if (element[0].parentNode.nodeName.toLowerCase() !== 'md-radio-group') {
18597 // If the radioButton is inside a div, then add class so highlighting will work
18598 element.parent().toggleClass(CHECKED_CSS, checked);
18599 }
18600
18601 if (checked) {
18602 rgCtrl.setActiveDescendant(element.attr('id'));
18603 }
18604
18605 lastChecked = checked;
18606
18607 element
18608 .attr('aria-checked', checked)
18609 .toggleClass(CHECKED_CSS, checked);
18610 }
18611
18612 /**
18613 * Inject ARIA-specific attributes appropriate for each radio button
18614 */
18615 function configureAria(element, scope){
18616 element.attr({
18617 id: attr.id || 'radio_' + $mdUtil.nextUid(),
18618 role: 'radio',
18619 'aria-checked': 'false'
18620 });
18621
18622 $mdAria.expectWithText(element, 'aria-label');
18623 }
18624 }
18625}
18626
18627})();
18628(function(){
18629"use strict";
18630
18631/**
18632 * @ngdoc module
18633 * @name material.components.select
18634 */
18635
18636/***************************************************
18637
18638 ### TODO - POST RC1 ###
18639 - [ ] Abstract placement logic in $mdSelect service to $mdMenu service
18640
18641 ***************************************************/
18642
18643SelectDirective.$inject = ["$mdSelect", "$mdUtil", "$mdConstant", "$mdTheming", "$mdAria", "$parse", "$sce", "$injector"];
18644SelectMenuDirective.$inject = ["$parse", "$mdUtil", "$mdConstant", "$mdTheming"];
18645OptionDirective.$inject = ["$mdButtonInkRipple", "$mdUtil"];
18646SelectProvider.$inject = ["$$interimElementProvider"];
18647var SELECT_EDGE_MARGIN = 8;
18648var selectNextId = 0;
18649var CHECKBOX_SELECTION_INDICATOR =
18650 angular.element('<div class="md-container"><div class="md-icon"></div></div>');
18651
18652angular.module('material.components.select', [
18653 'material.core',
18654 'material.components.backdrop'
18655 ])
18656 .directive('mdSelect', SelectDirective)
18657 .directive('mdSelectMenu', SelectMenuDirective)
18658 .directive('mdOption', OptionDirective)
18659 .directive('mdOptgroup', OptgroupDirective)
18660 .directive('mdSelectHeader', SelectHeaderDirective)
18661 .provider('$mdSelect', SelectProvider);
18662
18663/**
18664 * @ngdoc directive
18665 * @name mdSelect
18666 * @restrict E
18667 * @module material.components.select
18668 *
18669 * @description Displays a select box, bound to an ng-model.
18670 *
18671 * When the select is required and uses a floating label, then the label will automatically contain
18672 * an asterisk (`*`). This behavior can be disabled by using the `md-no-asterisk` attribute.
18673 *
18674 * By default, the select will display with an underline to match other form elements. This can be
18675 * disabled by applying the `md-no-underline` CSS class.
18676 *
18677 * ### Option Params
18678 *
18679 * When applied, `md-option-empty` will mark the option as "empty" allowing the option to clear the
18680 * select and put it back in it's default state. You may supply this attribute on any option you
18681 * wish, however, it is automatically applied to an option whose `value` or `ng-value` are not
18682 * defined.
18683 *
18684 * **Automatically Applied**
18685 *
18686 * - `<md-option>`
18687 * - `<md-option value>`
18688 * - `<md-option value="">`
18689 * - `<md-option ng-value>`
18690 * - `<md-option ng-value="">`
18691 *
18692 * **NOT Automatically Applied**
18693 *
18694 * - `<md-option ng-value="1">`
18695 * - `<md-option ng-value="''">`
18696 * - `<md-option ng-value="undefined">`
18697 * - `<md-option value="undefined">` (this evaluates to the string `"undefined"`)
18698 * - <code ng-non-bindable><md-option ng-value="{{someValueThatMightBeUndefined}}"></code>
18699 *
18700 * **Note:** A value of `undefined` ***is considered a valid value*** (and does not auto-apply this
18701 * attribute) since you may wish this to be your "Not Available" or "None" option.
18702 *
18703 * **Note:** Using the `value` attribute (as opposed to `ng-value`) always evaluates to a string, so
18704 * `value="null"` will require the test `ng-if="myValue != 'null'"` rather than `ng-if="!myValue"`.
18705 *
18706 * @param {expression} ng-model The model!
18707 * @param {boolean=} multiple When set to true, allows for more than one option to be selected. The model is an array with the selected choices.
18708 * @param {expression=} md-on-close Expression to be evaluated when the select is closed.
18709 * @param {expression=} md-on-open Expression to be evaluated when opening the select.
18710 * Will hide the select options and show a spinner until the evaluated promise resolves.
18711 * @param {expression=} md-selected-text Expression to be evaluated that will return a string
18712 * to be displayed as a placeholder in the select input box when it is closed. The value
18713 * will be treated as *text* (not html).
18714 * @param {expression=} md-selected-html Expression to be evaluated that will return a string
18715 * to be displayed as a placeholder in the select input box when it is closed. The value
18716 * will be treated as *html*. The value must either be explicitly marked as trustedHtml or
18717 * the ngSanitize module must be loaded.
18718 * @param {string=} placeholder Placeholder hint text.
18719 * @param md-no-asterisk {boolean=} When set to true, an asterisk will not be appended to the
18720 * floating label. **Note:** This attribute is only evaluated once; it is not watched.
18721 * @param {string=} aria-label Optional label for accessibility. Only necessary if no placeholder or
18722 * explicit label is present.
18723 * @param {string=} md-container-class Class list to get applied to the `.md-select-menu-container`
18724 * element (for custom styling).
18725 *
18726 * @usage
18727 * With a placeholder (label and aria-label are added dynamically)
18728 * <hljs lang="html">
18729 * <md-input-container>
18730 * <md-select
18731 * ng-model="someModel"
18732 * placeholder="Select a state">
18733 * <md-option ng-value="opt" ng-repeat="opt in neighborhoods2">{{ opt }}</md-option>
18734 * </md-select>
18735 * </md-input-container>
18736 * </hljs>
18737 *
18738 * With an explicit label
18739 * <hljs lang="html">
18740 * <md-input-container>
18741 * <label>State</label>
18742 * <md-select
18743 * ng-model="someModel">
18744 * <md-option ng-value="opt" ng-repeat="opt in neighborhoods2">{{ opt }}</md-option>
18745 * </md-select>
18746 * </md-input-container>
18747 * </hljs>
18748 *
18749 * With a select-header
18750 *
18751 * When a developer needs to put more than just a text label in the
18752 * md-select-menu, they should use the md-select-header.
18753 * The user can put custom HTML inside of the header and style it to their liking.
18754 * One common use case of this would be a sticky search bar.
18755 *
18756 * When using the md-select-header the labels that would previously be added to the
18757 * OptGroupDirective are ignored.
18758 *
18759 * <hljs lang="html">
18760 * <md-input-container>
18761 * <md-select ng-model="someModel">
18762 * <md-select-header>
18763 * <span> Neighborhoods - </span>
18764 * </md-select-header>
18765 * <md-option ng-value="opt" ng-repeat="opt in neighborhoods2">{{ opt }}</md-option>
18766 * </md-select>
18767 * </md-input-container>
18768 * </hljs>
18769 *
18770 * ## Selects and object equality
18771 * When using a `md-select` to pick from a list of objects, it is important to realize how javascript handles
18772 * equality. Consider the following example:
18773 * <hljs lang="js">
18774 * angular.controller('MyCtrl', function($scope) {
18775 * $scope.users = [
18776 * { id: 1, name: 'Bob' },
18777 * { id: 2, name: 'Alice' },
18778 * { id: 3, name: 'Steve' }
18779 * ];
18780 * $scope.selectedUser = { id: 1, name: 'Bob' };
18781 * });
18782 * </hljs>
18783 * <hljs lang="html">
18784 * <div ng-controller="MyCtrl">
18785 * <md-select ng-model="selectedUser">
18786 * <md-option ng-value="user" ng-repeat="user in users">{{ user.name }}</md-option>
18787 * </md-select>
18788 * </div>
18789 * </hljs>
18790 *
18791 * At first one might expect that the select should be populated with "Bob" as the selected user. However,
18792 * this is not true. To determine whether something is selected,
18793 * `ngModelController` is looking at whether `$scope.selectedUser == (any user in $scope.users);`;
18794 *
18795 * Javascript's `==` operator does not check for deep equality (ie. that all properties
18796 * on the object are the same), but instead whether the objects are *the same object in memory*.
18797 * In this case, we have two instances of identical objects, but they exist in memory as unique
18798 * entities. Because of this, the select will have no value populated for a selected user.
18799 *
18800 * To get around this, `ngModelController` provides a `track by` option that allows us to specify a different
18801 * expression which will be used for the equality operator. As such, we can update our `html` to
18802 * make use of this by specifying the `ng-model-options="{trackBy: '$value.id'}"` on the `md-select`
18803 * element. This converts our equality expression to be
18804 * `$scope.selectedUser.id == (any id in $scope.users.map(function(u) { return u.id; }));`
18805 * which results in Bob being selected as desired.
18806 *
18807 * Working HTML:
18808 * <hljs lang="html">
18809 * <div ng-controller="MyCtrl">
18810 * <md-select ng-model="selectedUser" ng-model-options="{trackBy: '$value.id'}">
18811 * <md-option ng-value="user" ng-repeat="user in users">{{ user.name }}</md-option>
18812 * </md-select>
18813 * </div>
18814 * </hljs>
18815 */
18816function SelectDirective($mdSelect, $mdUtil, $mdConstant, $mdTheming, $mdAria, $parse, $sce,
18817 $injector) {
18818 var keyCodes = $mdConstant.KEY_CODE;
18819 var NAVIGATION_KEYS = [keyCodes.SPACE, keyCodes.ENTER, keyCodes.UP_ARROW, keyCodes.DOWN_ARROW];
18820
18821 return {
18822 restrict: 'E',
18823 require: ['^?mdInputContainer', 'mdSelect', 'ngModel', '?^form'],
18824 compile: compile,
18825 controller: function() {
18826 } // empty placeholder controller to be initialized in link
18827 };
18828
18829 function compile(element, attr) {
18830 // add the select value that will hold our placeholder or selected option value
18831 var valueEl = angular.element('<md-select-value><span></span></md-select-value>');
18832 valueEl.append('<span class="md-select-icon" aria-hidden="true"></span>');
18833 valueEl.addClass('md-select-value');
18834 if (!valueEl[0].hasAttribute('id')) {
18835 valueEl.attr('id', 'select_value_label_' + $mdUtil.nextUid());
18836 }
18837
18838 // There's got to be an md-content inside. If there's not one, let's add it.
18839 if (!element.find('md-content').length) {
18840 element.append(angular.element('<md-content>').append(element.contents()));
18841 }
18842
18843
18844 // Add progress spinner for md-options-loading
18845 if (attr.mdOnOpen) {
18846
18847 // Show progress indicator while loading async
18848 // Use ng-hide for `display:none` so the indicator does not interfere with the options list
18849 element
18850 .find('md-content')
18851 .prepend(angular.element(
18852 '<div>' +
18853 ' <md-progress-circular md-mode="indeterminate" ng-if="$$loadingAsyncDone === false" md-diameter="25px"></md-progress-circular>' +
18854 '</div>'
18855 ));
18856
18857 // Hide list [of item options] while loading async
18858 element
18859 .find('md-option')
18860 .attr('ng-show', '$$loadingAsyncDone');
18861 }
18862
18863 if (attr.name) {
18864 var autofillClone = angular.element('<select class="md-visually-hidden">');
18865 autofillClone.attr({
18866 'name': attr.name,
18867 'aria-hidden': 'true',
18868 'tabindex': '-1'
18869 });
18870 var opts = element.find('md-option');
18871 angular.forEach(opts, function(el) {
18872 var newEl = angular.element('<option>' + el.innerHTML + '</option>');
18873 if (el.hasAttribute('ng-value')) newEl.attr('ng-value', el.getAttribute('ng-value'));
18874 else if (el.hasAttribute('value')) newEl.attr('value', el.getAttribute('value'));
18875 autofillClone.append(newEl);
18876 });
18877
18878 // Adds an extra option that will hold the selected value for the
18879 // cases where the select is a part of a non-angular form. This can be done with a ng-model,
18880 // however if the `md-option` is being `ng-repeat`-ed, Angular seems to insert a similar
18881 // `option` node, but with a value of `? string: <value> ?` which would then get submitted.
18882 // This also goes around having to prepend a dot to the name attribute.
18883 autofillClone.append(
18884 '<option ng-value="' + attr.ngModel + '" selected></option>'
18885 );
18886
18887 element.parent().append(autofillClone);
18888 }
18889
18890 var isMultiple = $mdUtil.parseAttributeBoolean(attr.multiple);
18891
18892 // Use everything that's left inside element.contents() as the contents of the menu
18893 var multipleContent = isMultiple ? 'multiple' : '';
18894 var selectTemplate = '' +
18895 '<div class="md-select-menu-container" aria-hidden="true">' +
18896 '<md-select-menu {0}>{1}</md-select-menu>' +
18897 '</div>';
18898
18899 selectTemplate = $mdUtil.supplant(selectTemplate, [multipleContent, element.html()]);
18900 element.empty().append(valueEl);
18901 element.append(selectTemplate);
18902
18903 if(!attr.tabindex){
18904 attr.$set('tabindex', 0);
18905 }
18906
18907 return function postLink(scope, element, attr, ctrls) {
18908 var untouched = true;
18909 var isDisabled, ariaLabelBase;
18910
18911 var containerCtrl = ctrls[0];
18912 var mdSelectCtrl = ctrls[1];
18913 var ngModelCtrl = ctrls[2];
18914 var formCtrl = ctrls[3];
18915 // grab a reference to the select menu value label
18916 var valueEl = element.find('md-select-value');
18917 var isReadonly = angular.isDefined(attr.readonly);
18918 var disableAsterisk = $mdUtil.parseAttributeBoolean(attr.mdNoAsterisk);
18919
18920 if (disableAsterisk) {
18921 element.addClass('md-no-asterisk');
18922 }
18923
18924 if (containerCtrl) {
18925 var isErrorGetter = containerCtrl.isErrorGetter || function() {
18926 return ngModelCtrl.$invalid && (ngModelCtrl.$touched || (formCtrl && formCtrl.$submitted));
18927 };
18928
18929 if (containerCtrl.input) {
18930 // We ignore inputs that are in the md-select-header (one
18931 // case where this might be useful would be adding as searchbox)
18932 if (element.find('md-select-header').find('input')[0] !== containerCtrl.input[0]) {
18933 throw new Error("<md-input-container> can only have *one* child <input>, <textarea> or <select> element!");
18934 }
18935 }
18936
18937 containerCtrl.input = element;
18938 if (!containerCtrl.label) {
18939 $mdAria.expect(element, 'aria-label', element.attr('placeholder'));
18940 }
18941
18942 scope.$watch(isErrorGetter, containerCtrl.setInvalid);
18943 }
18944
18945 var selectContainer, selectScope, selectMenuCtrl;
18946
18947 findSelectContainer();
18948 $mdTheming(element);
18949
18950 if (formCtrl && angular.isDefined(attr.multiple)) {
18951 $mdUtil.nextTick(function() {
18952 var hasModelValue = ngModelCtrl.$modelValue || ngModelCtrl.$viewValue;
18953 if (hasModelValue) {
18954 formCtrl.$setPristine();
18955 }
18956 });
18957 }
18958
18959 var originalRender = ngModelCtrl.$render;
18960 ngModelCtrl.$render = function() {
18961 originalRender();
18962 syncLabelText();
18963 syncAriaLabel();
18964 inputCheckValue();
18965 };
18966
18967 attr.$observe('placeholder', ngModelCtrl.$render);
18968
18969 if (containerCtrl && containerCtrl.label) {
18970 attr.$observe('required', function (value) {
18971 // Toggle the md-required class on the input containers label, because the input container is automatically
18972 // applying the asterisk indicator on the label.
18973 containerCtrl.label.toggleClass('md-required', value && !disableAsterisk);
18974 });
18975 }
18976
18977 mdSelectCtrl.setLabelText = function(text) {
18978 mdSelectCtrl.setIsPlaceholder(!text);
18979
18980 // Whether the select label has been given via user content rather than the internal
18981 // template of <md-option>
18982 var isSelectLabelFromUser = false;
18983
18984 if (attr.mdSelectedText && attr.mdSelectedHtml) {
18985 throw Error('md-select cannot have both `md-selected-text` and `md-selected-html`');
18986 }
18987
18988 if (attr.mdSelectedText || attr.mdSelectedHtml) {
18989 text = $parse(attr.mdSelectedText || attr.mdSelectedHtml)(scope);
18990 isSelectLabelFromUser = true;
18991 } else if (!text) {
18992 // Use placeholder attribute, otherwise fallback to the md-input-container label
18993 var tmpPlaceholder = attr.placeholder ||
18994 (containerCtrl && containerCtrl.label ? containerCtrl.label.text() : '');
18995
18996 text = tmpPlaceholder || '';
18997 isSelectLabelFromUser = true;
18998 }
18999
19000 var target = valueEl.children().eq(0);
19001
19002 if (attr.mdSelectedHtml) {
19003 // Using getTrustedHtml will run the content through $sanitize if it is not already
19004 // explicitly trusted. If the ngSanitize module is not loaded, this will
19005 // *correctly* throw an sce error.
19006 target.html($sce.getTrustedHtml(text));
19007 } else if (isSelectLabelFromUser) {
19008 target.text(text);
19009 } else {
19010 // If we've reached this point, the text is not user-provided.
19011 target.html(text);
19012 }
19013 };
19014
19015 mdSelectCtrl.setIsPlaceholder = function(isPlaceholder) {
19016 if (isPlaceholder) {
19017 valueEl.addClass('md-select-placeholder');
19018 if (containerCtrl && containerCtrl.label) {
19019 containerCtrl.label.addClass('md-placeholder');
19020 }
19021 } else {
19022 valueEl.removeClass('md-select-placeholder');
19023 if (containerCtrl && containerCtrl.label) {
19024 containerCtrl.label.removeClass('md-placeholder');
19025 }
19026 }
19027 };
19028
19029 if (!isReadonly) {
19030 element
19031 .on('focus', function(ev) {
19032 // Always focus the container (if we have one) so floating labels and other styles are
19033 // applied properly
19034 containerCtrl && containerCtrl.setFocused(true);
19035 });
19036
19037 // Attach before ngModel's blur listener to stop propagation of blur event
19038 // to prevent from setting $touched.
19039 element.on('blur', function(event) {
19040 if (untouched) {
19041 untouched = false;
19042 if (selectScope._mdSelectIsOpen) {
19043 event.stopImmediatePropagation();
19044 }
19045 }
19046
19047 if (selectScope._mdSelectIsOpen) return;
19048 containerCtrl && containerCtrl.setFocused(false);
19049 inputCheckValue();
19050 });
19051 }
19052
19053 mdSelectCtrl.triggerClose = function() {
19054 $parse(attr.mdOnClose)(scope);
19055 };
19056
19057 scope.$$postDigest(function() {
19058 initAriaLabel();
19059 syncLabelText();
19060 syncAriaLabel();
19061 });
19062
19063 function initAriaLabel() {
19064 var labelText = element.attr('aria-label') || element.attr('placeholder');
19065 if (!labelText && containerCtrl && containerCtrl.label) {
19066 labelText = containerCtrl.label.text();
19067 }
19068 ariaLabelBase = labelText;
19069 $mdAria.expect(element, 'aria-label', labelText);
19070 }
19071
19072 scope.$watch(function() {
19073 return selectMenuCtrl.selectedLabels();
19074 }, syncLabelText);
19075
19076 function syncLabelText() {
19077 if (selectContainer) {
19078 selectMenuCtrl = selectMenuCtrl || selectContainer.find('md-select-menu').controller('mdSelectMenu');
19079 mdSelectCtrl.setLabelText(selectMenuCtrl.selectedLabels());
19080 }
19081 }
19082
19083 function syncAriaLabel() {
19084 if (!ariaLabelBase) return;
19085 var ariaLabels = selectMenuCtrl.selectedLabels({mode: 'aria'});
19086 element.attr('aria-label', ariaLabels.length ? ariaLabelBase + ': ' + ariaLabels : ariaLabelBase);
19087 }
19088
19089 var deregisterWatcher;
19090 attr.$observe('ngMultiple', function(val) {
19091 if (deregisterWatcher) deregisterWatcher();
19092 var parser = $parse(val);
19093 deregisterWatcher = scope.$watch(function() {
19094 return parser(scope);
19095 }, function(multiple, prevVal) {
19096 if (multiple === undefined && prevVal === undefined) return; // assume compiler did a good job
19097 if (multiple) {
19098 element.attr('multiple', 'multiple');
19099 } else {
19100 element.removeAttr('multiple');
19101 }
19102 element.attr('aria-multiselectable', multiple ? 'true' : 'false');
19103 if (selectContainer) {
19104 selectMenuCtrl.setMultiple(multiple);
19105 originalRender = ngModelCtrl.$render;
19106 ngModelCtrl.$render = function() {
19107 originalRender();
19108 syncLabelText();
19109 syncAriaLabel();
19110 inputCheckValue();
19111 };
19112 ngModelCtrl.$render();
19113 }
19114 });
19115 });
19116
19117 attr.$observe('disabled', function(disabled) {
19118 if (angular.isString(disabled)) {
19119 disabled = true;
19120 }
19121 // Prevent click event being registered twice
19122 if (isDisabled !== undefined && isDisabled === disabled) {
19123 return;
19124 }
19125 isDisabled = disabled;
19126 if (disabled) {
19127 element
19128 .attr({'aria-disabled': 'true'})
19129 .removeAttr('tabindex')
19130 .off('click', openSelect)
19131 .off('keydown', handleKeypress);
19132 } else {
19133 element
19134 .attr({'tabindex': attr.tabindex, 'aria-disabled': 'false'})
19135 .on('click', openSelect)
19136 .on('keydown', handleKeypress);
19137 }
19138 });
19139
19140 if (!attr.hasOwnProperty('disabled') && !attr.hasOwnProperty('ngDisabled')) {
19141 element.attr({'aria-disabled': 'false'});
19142 element.on('click', openSelect);
19143 element.on('keydown', handleKeypress);
19144 }
19145
19146 var ariaAttrs = {
19147 role: 'listbox',
19148 'aria-expanded': 'false',
19149 'aria-multiselectable': isMultiple && !attr.ngMultiple ? 'true' : 'false'
19150 };
19151
19152 if (!element[0].hasAttribute('id')) {
19153 ariaAttrs.id = 'select_' + $mdUtil.nextUid();
19154 }
19155
19156 var containerId = 'select_container_' + $mdUtil.nextUid();
19157 selectContainer.attr('id', containerId);
19158 ariaAttrs['aria-owns'] = containerId;
19159 element.attr(ariaAttrs);
19160
19161 scope.$on('$destroy', function() {
19162 $mdSelect
19163 .destroy()
19164 .finally(function() {
19165 if (containerCtrl) {
19166 containerCtrl.setFocused(false);
19167 containerCtrl.setHasValue(false);
19168 containerCtrl.input = null;
19169 }
19170 ngModelCtrl.$setTouched();
19171 });
19172 });
19173
19174
19175
19176 function inputCheckValue() {
19177 // The select counts as having a value if one or more options are selected,
19178 // or if the input's validity state says it has bad input (eg string in a number input)
19179 containerCtrl && containerCtrl.setHasValue(selectMenuCtrl.selectedLabels().length > 0 || (element[0].validity || {}).badInput);
19180 }
19181
19182 function findSelectContainer() {
19183 selectContainer = angular.element(
19184 element[0].querySelector('.md-select-menu-container')
19185 );
19186 selectScope = scope;
19187 if (attr.mdContainerClass) {
19188 var value = selectContainer[0].getAttribute('class') + ' ' + attr.mdContainerClass;
19189 selectContainer[0].setAttribute('class', value);
19190 }
19191 selectMenuCtrl = selectContainer.find('md-select-menu').controller('mdSelectMenu');
19192 selectMenuCtrl.init(ngModelCtrl, attr.ngModel);
19193 element.on('$destroy', function() {
19194 selectContainer.remove();
19195 });
19196 }
19197
19198 function handleKeypress(e) {
19199 if ($mdConstant.isNavigationKey(e)) {
19200 // prevent page scrolling on interaction
19201 e.preventDefault();
19202 openSelect(e);
19203 } else {
19204 if ($mdConstant.isInputKey(e) || $mdConstant.isNumPadKey(e)) {
19205 e.preventDefault();
19206
19207 var node = selectMenuCtrl.optNodeForKeyboardSearch(e);
19208 if (!node || node.hasAttribute('disabled')) return;
19209 var optionCtrl = angular.element(node).controller('mdOption');
19210 if (!selectMenuCtrl.isMultiple) {
19211 selectMenuCtrl.deselect(Object.keys(selectMenuCtrl.selected)[0]);
19212 }
19213 selectMenuCtrl.select(optionCtrl.hashKey, optionCtrl.value);
19214 selectMenuCtrl.refreshViewValue();
19215 }
19216 }
19217 }
19218
19219 function openSelect() {
19220 selectScope._mdSelectIsOpen = true;
19221 element.attr('aria-expanded', 'true');
19222
19223 $mdSelect.show({
19224 scope: selectScope,
19225 preserveScope: true,
19226 skipCompile: true,
19227 element: selectContainer,
19228 target: element[0],
19229 selectCtrl: mdSelectCtrl,
19230 preserveElement: true,
19231 hasBackdrop: true,
19232 loadingAsync: attr.mdOnOpen ? scope.$eval(attr.mdOnOpen) || true : false
19233 }).finally(function() {
19234 selectScope._mdSelectIsOpen = false;
19235 element.focus();
19236 element.attr('aria-expanded', 'false');
19237 ngModelCtrl.$setTouched();
19238 });
19239 }
19240
19241 };
19242 }
19243}
19244
19245function SelectMenuDirective($parse, $mdUtil, $mdConstant, $mdTheming) {
19246 // We want the scope to be set to 'false' so an isolated scope is not created
19247 // which would interfere with the md-select-header's access to the
19248 // parent scope.
19249 SelectMenuController.$inject = ["$scope", "$attrs", "$element"];
19250 return {
19251 restrict: 'E',
19252 require: ['mdSelectMenu'],
19253 scope: false,
19254 controller: SelectMenuController,
19255 link: {pre: preLink}
19256 };
19257
19258 // We use preLink instead of postLink to ensure that the select is initialized before
19259 // its child options run postLink.
19260 function preLink(scope, element, attr, ctrls) {
19261 var selectCtrl = ctrls[0];
19262
19263 element.addClass('_md'); // private md component indicator for styling
19264
19265 $mdTheming(element);
19266 element.on('click', clickListener);
19267 element.on('keypress', keyListener);
19268
19269 function keyListener(e) {
19270 if (e.keyCode == 13 || e.keyCode == 32) {
19271 clickListener(e);
19272 }
19273 }
19274
19275 function clickListener(ev) {
19276 var option = $mdUtil.getClosest(ev.target, 'md-option');
19277 var optionCtrl = option && angular.element(option).data('$mdOptionController');
19278 if (!option || !optionCtrl) return;
19279 if (option.hasAttribute('disabled')) {
19280 ev.stopImmediatePropagation();
19281 return false;
19282 }
19283
19284 var optionHashKey = selectCtrl.hashGetter(optionCtrl.value);
19285 var isSelected = angular.isDefined(selectCtrl.selected[optionHashKey]);
19286
19287 scope.$apply(function() {
19288 if (selectCtrl.isMultiple) {
19289 if (isSelected) {
19290 selectCtrl.deselect(optionHashKey);
19291 } else {
19292 selectCtrl.select(optionHashKey, optionCtrl.value);
19293 }
19294 } else {
19295 if (!isSelected) {
19296 selectCtrl.deselect(Object.keys(selectCtrl.selected)[0]);
19297 selectCtrl.select(optionHashKey, optionCtrl.value);
19298 }
19299 }
19300 selectCtrl.refreshViewValue();
19301 });
19302 }
19303 }
19304
19305 function SelectMenuController($scope, $attrs, $element) {
19306 var self = this;
19307 self.isMultiple = angular.isDefined($attrs.multiple);
19308 // selected is an object with keys matching all of the selected options' hashed values
19309 self.selected = {};
19310 // options is an object with keys matching every option's hash value,
19311 // and values matching every option's controller.
19312 self.options = {};
19313
19314 $scope.$watchCollection(function() {
19315 return self.options;
19316 }, function() {
19317 self.ngModel.$render();
19318 });
19319
19320 var deregisterCollectionWatch;
19321 var defaultIsEmpty;
19322 self.setMultiple = function(isMultiple) {
19323 var ngModel = self.ngModel;
19324 defaultIsEmpty = defaultIsEmpty || ngModel.$isEmpty;
19325
19326 self.isMultiple = isMultiple;
19327 if (deregisterCollectionWatch) deregisterCollectionWatch();
19328
19329 if (self.isMultiple) {
19330 ngModel.$validators['md-multiple'] = validateArray;
19331 ngModel.$render = renderMultiple;
19332
19333 // watchCollection on the model because by default ngModel only watches the model's
19334 // reference. This allowed the developer to also push and pop from their array.
19335 $scope.$watchCollection(self.modelBinding, function(value) {
19336 if (validateArray(value)) renderMultiple(value);
19337 self.ngModel.$setPristine();
19338 });
19339
19340 ngModel.$isEmpty = function(value) {
19341 return !value || value.length === 0;
19342 };
19343 } else {
19344 delete ngModel.$validators['md-multiple'];
19345 ngModel.$render = renderSingular;
19346 }
19347
19348 function validateArray(modelValue, viewValue) {
19349 // If a value is truthy but not an array, reject it.
19350 // If value is undefined/falsy, accept that it's an empty array.
19351 return angular.isArray(modelValue || viewValue || []);
19352 }
19353 };
19354
19355 var searchStr = '';
19356 var clearSearchTimeout, optNodes, optText;
19357 var CLEAR_SEARCH_AFTER = 300;
19358
19359 self.optNodeForKeyboardSearch = function(e) {
19360 clearSearchTimeout && clearTimeout(clearSearchTimeout);
19361 clearSearchTimeout = setTimeout(function() {
19362 clearSearchTimeout = undefined;
19363 searchStr = '';
19364 optText = undefined;
19365 optNodes = undefined;
19366 }, CLEAR_SEARCH_AFTER);
19367
19368 // Support 1-9 on numpad
19369 var keyCode = e.keyCode - ($mdConstant.isNumPadKey(e) ? 48 : 0);
19370
19371 searchStr += String.fromCharCode(keyCode);
19372 var search = new RegExp('^' + searchStr, 'i');
19373 if (!optNodes) {
19374 optNodes = $element.find('md-option');
19375 optText = new Array(optNodes.length);
19376 angular.forEach(optNodes, function(el, i) {
19377 optText[i] = el.textContent.trim();
19378 });
19379 }
19380 for (var i = 0; i < optText.length; ++i) {
19381 if (search.test(optText[i])) {
19382 return optNodes[i];
19383 }
19384 }
19385 };
19386
19387 self.init = function(ngModel, binding) {
19388 self.ngModel = ngModel;
19389 self.modelBinding = binding;
19390
19391 // Setup a more robust version of isEmpty to ensure value is a valid option
19392 self.ngModel.$isEmpty = function($viewValue) {
19393 // We have to transform the viewValue into the hashKey, because otherwise the
19394 // OptionCtrl may not exist. Developers may have specified a trackBy function.
19395 return !self.options[self.hashGetter($viewValue)];
19396 };
19397
19398 // Allow users to provide `ng-model="foo" ng-model-options="{trackBy: 'foo.id'}"` so
19399 // that we can properly compare objects set on the model to the available options
19400 var trackByOption = $mdUtil.getModelOption(ngModel, 'trackBy');
19401
19402 if (trackByOption) {
19403 var trackByLocals = {};
19404 var trackByParsed = $parse(trackByOption);
19405 self.hashGetter = function(value, valueScope) {
19406 trackByLocals.$value = value;
19407 return trackByParsed(valueScope || $scope, trackByLocals);
19408 };
19409 // If the user doesn't provide a trackBy, we automatically generate an id for every
19410 // value passed in
19411 } else {
19412 self.hashGetter = function getHashValue(value) {
19413 if (angular.isObject(value)) {
19414 return 'object_' + (value.$$mdSelectId || (value.$$mdSelectId = ++selectNextId));
19415 }
19416 return value;
19417 };
19418 }
19419 self.setMultiple(self.isMultiple);
19420 };
19421
19422 self.selectedLabels = function(opts) {
19423 opts = opts || {};
19424 var mode = opts.mode || 'html';
19425 var selectedOptionEls = $mdUtil.nodesToArray($element[0].querySelectorAll('md-option[selected]'));
19426 if (selectedOptionEls.length) {
19427 var mapFn;
19428
19429 if (mode == 'html') {
19430 // Map the given element to its innerHTML string. If the element has a child ripple
19431 // container remove it from the HTML string, before returning the string.
19432 mapFn = function(el) {
19433 // If we do not have a `value` or `ng-value`, assume it is an empty option which clears the select
19434 if (el.hasAttribute('md-option-empty')) {
19435 return '';
19436 }
19437
19438 var html = el.innerHTML;
19439
19440 // Remove the ripple container from the selected option, copying it would cause a CSP violation.
19441 var rippleContainer = el.querySelector('.md-ripple-container');
19442 if (rippleContainer) {
19443 html = html.replace(rippleContainer.outerHTML, '');
19444 }
19445
19446 // Remove the checkbox container, because it will cause the label to wrap inside of the placeholder.
19447 // It should be not displayed inside of the label element.
19448 var checkboxContainer = el.querySelector('.md-container');
19449 if (checkboxContainer) {
19450 html = html.replace(checkboxContainer.outerHTML, '');
19451 }
19452
19453 return html;
19454 };
19455 } else if (mode == 'aria') {
19456 mapFn = function(el) { return el.hasAttribute('aria-label') ? el.getAttribute('aria-label') : el.textContent; };
19457 }
19458
19459 // Ensure there are no duplicates; see https://github.com/angular/material/issues/9442
19460 return $mdUtil.uniq(selectedOptionEls.map(mapFn)).join(', ');
19461 } else {
19462 return '';
19463 }
19464 };
19465
19466 self.select = function(hashKey, hashedValue) {
19467 var option = self.options[hashKey];
19468 option && option.setSelected(true);
19469 self.selected[hashKey] = hashedValue;
19470 };
19471 self.deselect = function(hashKey) {
19472 var option = self.options[hashKey];
19473 option && option.setSelected(false);
19474 delete self.selected[hashKey];
19475 };
19476
19477 self.addOption = function(hashKey, optionCtrl) {
19478 if (angular.isDefined(self.options[hashKey])) {
19479 throw new Error('Duplicate md-option values are not allowed in a select. ' +
19480 'Duplicate value "' + optionCtrl.value + '" found.');
19481 }
19482
19483 self.options[hashKey] = optionCtrl;
19484
19485 // If this option's value was already in our ngModel, go ahead and select it.
19486 if (angular.isDefined(self.selected[hashKey])) {
19487 self.select(hashKey, optionCtrl.value);
19488
19489 // When the current $modelValue of the ngModel Controller is using the same hash as
19490 // the current option, which will be added, then we can be sure, that the validation
19491 // of the option has occurred before the option was added properly.
19492 // This means, that we have to manually trigger a new validation of the current option.
19493 if (angular.isDefined(self.ngModel.$modelValue) && self.hashGetter(self.ngModel.$modelValue) === hashKey) {
19494 self.ngModel.$validate();
19495 }
19496
19497 self.refreshViewValue();
19498 }
19499 };
19500 self.removeOption = function(hashKey) {
19501 delete self.options[hashKey];
19502 // Don't deselect an option when it's removed - the user's ngModel should be allowed
19503 // to have values that do not match a currently available option.
19504 };
19505
19506 self.refreshViewValue = function() {
19507 var values = [];
19508 var option;
19509 for (var hashKey in self.selected) {
19510 // If this hashKey has an associated option, push that option's value to the model.
19511 if ((option = self.options[hashKey])) {
19512 values.push(option.value);
19513 } else {
19514 // Otherwise, the given hashKey has no associated option, and we got it
19515 // from an ngModel value at an earlier time. Push the unhashed value of
19516 // this hashKey to the model.
19517 // This allows the developer to put a value in the model that doesn't yet have
19518 // an associated option.
19519 values.push(self.selected[hashKey]);
19520 }
19521 }
19522 var usingTrackBy = $mdUtil.getModelOption(self.ngModel, 'trackBy');
19523
19524 var newVal = self.isMultiple ? values : values[0];
19525 var prevVal = self.ngModel.$modelValue;
19526
19527 if (usingTrackBy ? !angular.equals(prevVal, newVal) : (prevVal + '') !== newVal) {
19528 self.ngModel.$setViewValue(newVal);
19529 self.ngModel.$render();
19530 }
19531 };
19532
19533 function renderMultiple() {
19534 var newSelectedValues = self.ngModel.$modelValue || self.ngModel.$viewValue || [];
19535 if (!angular.isArray(newSelectedValues)) return;
19536
19537 var oldSelected = Object.keys(self.selected);
19538
19539 var newSelectedHashes = newSelectedValues.map(self.hashGetter);
19540 var deselected = oldSelected.filter(function(hash) {
19541 return newSelectedHashes.indexOf(hash) === -1;
19542 });
19543
19544 deselected.forEach(self.deselect);
19545 newSelectedHashes.forEach(function(hashKey, i) {
19546 self.select(hashKey, newSelectedValues[i]);
19547 });
19548 }
19549
19550 function renderSingular() {
19551 var value = self.ngModel.$viewValue || self.ngModel.$modelValue;
19552 Object.keys(self.selected).forEach(self.deselect);
19553 self.select(self.hashGetter(value), value);
19554 }
19555 }
19556
19557}
19558
19559function OptionDirective($mdButtonInkRipple, $mdUtil) {
19560
19561 OptionController.$inject = ["$element"];
19562 return {
19563 restrict: 'E',
19564 require: ['mdOption', '^^mdSelectMenu'],
19565 controller: OptionController,
19566 compile: compile
19567 };
19568
19569 function compile(element, attr) {
19570 // Manual transclusion to avoid the extra inner <span> that ng-transclude generates
19571 element.append(angular.element('<div class="md-text">').append(element.contents()));
19572
19573 element.attr('tabindex', attr.tabindex || '0');
19574
19575 if (!hasDefinedValue(attr)) {
19576 element.attr('md-option-empty', '');
19577 }
19578
19579 return postLink;
19580 }
19581
19582 function hasDefinedValue(attr) {
19583 var value = attr.value;
19584 var ngValue = attr.ngValue;
19585
19586 return value || ngValue;
19587 }
19588
19589 function postLink(scope, element, attr, ctrls) {
19590 var optionCtrl = ctrls[0];
19591 var selectCtrl = ctrls[1];
19592
19593 if (selectCtrl.isMultiple) {
19594 element.addClass('md-checkbox-enabled');
19595 element.prepend(CHECKBOX_SELECTION_INDICATOR.clone());
19596 }
19597
19598 if (angular.isDefined(attr.ngValue)) {
19599 scope.$watch(attr.ngValue, setOptionValue);
19600 } else if (angular.isDefined(attr.value)) {
19601 setOptionValue(attr.value);
19602 } else {
19603 scope.$watch(function() {
19604 return element.text().trim();
19605 }, setOptionValue);
19606 }
19607
19608 attr.$observe('disabled', function(disabled) {
19609 if (disabled) {
19610 element.attr('tabindex', '-1');
19611 } else {
19612 element.attr('tabindex', '0');
19613 }
19614 });
19615
19616 scope.$$postDigest(function() {
19617 attr.$observe('selected', function(selected) {
19618 if (!angular.isDefined(selected)) return;
19619 if (typeof selected == 'string') selected = true;
19620 if (selected) {
19621 if (!selectCtrl.isMultiple) {
19622 selectCtrl.deselect(Object.keys(selectCtrl.selected)[0]);
19623 }
19624 selectCtrl.select(optionCtrl.hashKey, optionCtrl.value);
19625 } else {
19626 selectCtrl.deselect(optionCtrl.hashKey);
19627 }
19628 selectCtrl.refreshViewValue();
19629 });
19630 });
19631
19632 $mdButtonInkRipple.attach(scope, element);
19633 configureAria();
19634
19635 function setOptionValue(newValue, oldValue, prevAttempt) {
19636 if (!selectCtrl.hashGetter) {
19637 if (!prevAttempt) {
19638 scope.$$postDigest(function() {
19639 setOptionValue(newValue, oldValue, true);
19640 });
19641 }
19642 return;
19643 }
19644 var oldHashKey = selectCtrl.hashGetter(oldValue, scope);
19645 var newHashKey = selectCtrl.hashGetter(newValue, scope);
19646
19647 optionCtrl.hashKey = newHashKey;
19648 optionCtrl.value = newValue;
19649
19650 selectCtrl.removeOption(oldHashKey, optionCtrl);
19651 selectCtrl.addOption(newHashKey, optionCtrl);
19652 }
19653
19654 scope.$on('$destroy', function() {
19655 selectCtrl.removeOption(optionCtrl.hashKey, optionCtrl);
19656 });
19657
19658 function configureAria() {
19659 var ariaAttrs = {
19660 'role': 'option',
19661 'aria-selected': 'false'
19662 };
19663
19664 if (!element[0].hasAttribute('id')) {
19665 ariaAttrs.id = 'select_option_' + $mdUtil.nextUid();
19666 }
19667 element.attr(ariaAttrs);
19668 }
19669 }
19670
19671 function OptionController($element) {
19672 this.selected = false;
19673 this.setSelected = function(isSelected) {
19674 if (isSelected && !this.selected) {
19675 $element.attr({
19676 'selected': 'selected',
19677 'aria-selected': 'true'
19678 });
19679 } else if (!isSelected && this.selected) {
19680 $element.removeAttr('selected');
19681 $element.attr('aria-selected', 'false');
19682 }
19683 this.selected = isSelected;
19684 };
19685 }
19686
19687}
19688
19689function OptgroupDirective() {
19690 return {
19691 restrict: 'E',
19692 compile: compile
19693 };
19694 function compile(el, attrs) {
19695 // If we have a select header element, we don't want to add the normal label
19696 // header.
19697 if (!hasSelectHeader()) {
19698 setupLabelElement();
19699 }
19700
19701 function hasSelectHeader() {
19702 return el.parent().find('md-select-header').length;
19703 }
19704
19705 function setupLabelElement() {
19706 var labelElement = el.find('label');
19707 if (!labelElement.length) {
19708 labelElement = angular.element('<label>');
19709 el.prepend(labelElement);
19710 }
19711 labelElement.addClass('md-container-ignore');
19712 if (attrs.label) labelElement.text(attrs.label);
19713 }
19714 }
19715}
19716
19717function SelectHeaderDirective() {
19718 return {
19719 restrict: 'E',
19720 };
19721}
19722
19723function SelectProvider($$interimElementProvider) {
19724 selectDefaultOptions.$inject = ["$mdSelect", "$mdConstant", "$mdUtil", "$window", "$q", "$$rAF", "$animateCss", "$animate", "$document"];
19725 return $$interimElementProvider('$mdSelect')
19726 .setDefaults({
19727 methods: ['target'],
19728 options: selectDefaultOptions
19729 });
19730
19731 /* @ngInject */
19732 function selectDefaultOptions($mdSelect, $mdConstant, $mdUtil, $window, $q, $$rAF, $animateCss, $animate, $document) {
19733 var ERROR_TARGET_EXPECTED = "$mdSelect.show() expected a target element in options.target but got '{0}'!";
19734 var animator = $mdUtil.dom.animator;
19735 var keyCodes = $mdConstant.KEY_CODE;
19736
19737 return {
19738 parent: 'body',
19739 themable: true,
19740 onShow: onShow,
19741 onRemove: onRemove,
19742 hasBackdrop: true,
19743 disableParentScroll: true
19744 };
19745
19746 /**
19747 * Interim-element onRemove logic....
19748 */
19749 function onRemove(scope, element, opts) {
19750 opts = opts || { };
19751 opts.cleanupInteraction();
19752 opts.cleanupResizing();
19753 opts.hideBackdrop();
19754
19755 // For navigation $destroy events, do a quick, non-animated removal,
19756 // but for normal closes (from clicks, etc) animate the removal
19757
19758 return (opts.$destroy === true) ? cleanElement() : animateRemoval().then( cleanElement );
19759
19760 /**
19761 * For normal closes (eg clicks), animate the removal.
19762 * For forced closes (like $destroy events from navigation),
19763 * skip the animations
19764 */
19765 function animateRemoval() {
19766 return $animateCss(element, {addClass: 'md-leave'}).start();
19767 }
19768
19769 /**
19770 * Restore the element to a closed state
19771 */
19772 function cleanElement() {
19773
19774 element.removeClass('md-active');
19775 element.attr('aria-hidden', 'true');
19776 element[0].style.display = 'none';
19777
19778 announceClosed(opts);
19779
19780 if (!opts.$destroy && opts.restoreFocus) {
19781 opts.target.focus();
19782 }
19783 }
19784
19785 }
19786
19787 /**
19788 * Interim-element onShow logic....
19789 */
19790 function onShow(scope, element, opts) {
19791
19792 watchAsyncLoad();
19793 sanitizeAndConfigure(scope, opts);
19794
19795 opts.hideBackdrop = showBackdrop(scope, element, opts);
19796
19797 return showDropDown(scope, element, opts)
19798 .then(function(response) {
19799 element.attr('aria-hidden', 'false');
19800 opts.alreadyOpen = true;
19801 opts.cleanupInteraction = activateInteraction();
19802 opts.cleanupResizing = activateResizing();
19803
19804 return response;
19805 }, opts.hideBackdrop);
19806
19807 // ************************************
19808 // Closure Functions
19809 // ************************************
19810
19811 /**
19812 * Attach the select DOM element(s) and animate to the correct positions
19813 * and scalings...
19814 */
19815 function showDropDown(scope, element, opts) {
19816 opts.parent.append(element);
19817
19818 return $q(function(resolve, reject) {
19819
19820 try {
19821
19822 $animateCss(element, {removeClass: 'md-leave', duration: 0})
19823 .start()
19824 .then(positionAndFocusMenu)
19825 .then(resolve);
19826
19827 } catch (e) {
19828 reject(e);
19829 }
19830
19831 });
19832 }
19833
19834 /**
19835 * Initialize container and dropDown menu positions/scale, then animate
19836 * to show... and autoFocus.
19837 */
19838 function positionAndFocusMenu() {
19839 return $q(function(resolve) {
19840 if (opts.isRemoved) return $q.reject(false);
19841
19842 var info = calculateMenuPositions(scope, element, opts);
19843
19844 info.container.element.css(animator.toCss(info.container.styles));
19845 info.dropDown.element.css(animator.toCss(info.dropDown.styles));
19846
19847 $$rAF(function() {
19848 element.addClass('md-active');
19849 info.dropDown.element.css(animator.toCss({transform: ''}));
19850
19851 autoFocus(opts.focusedNode);
19852 resolve();
19853 });
19854
19855 });
19856 }
19857
19858 /**
19859 * Show modal backdrop element...
19860 */
19861 function showBackdrop(scope, element, options) {
19862
19863 // If we are not within a dialog...
19864 if (options.disableParentScroll && !$mdUtil.getClosest(options.target, 'MD-DIALOG')) {
19865 // !! DO this before creating the backdrop; since disableScrollAround()
19866 // configures the scroll offset; which is used by mdBackDrop postLink()
19867 options.restoreScroll = $mdUtil.disableScrollAround(options.element, options.parent);
19868 } else {
19869 options.disableParentScroll = false;
19870 }
19871
19872 if (options.hasBackdrop) {
19873 // Override duration to immediately show invisible backdrop
19874 options.backdrop = $mdUtil.createBackdrop(scope, "md-select-backdrop md-click-catcher");
19875 $animate.enter(options.backdrop, $document[0].body, null, {duration: 0});
19876 }
19877
19878 /**
19879 * Hide modal backdrop element...
19880 */
19881 return function hideBackdrop() {
19882 if (options.backdrop) options.backdrop.remove();
19883 if (options.disableParentScroll) options.restoreScroll();
19884
19885 delete options.restoreScroll;
19886 };
19887 }
19888
19889 /**
19890 *
19891 */
19892 function autoFocus(focusedNode) {
19893 if (focusedNode && !focusedNode.hasAttribute('disabled')) {
19894 focusedNode.focus();
19895 }
19896 }
19897
19898 /**
19899 * Check for valid opts and set some sane defaults
19900 */
19901 function sanitizeAndConfigure(scope, options) {
19902 var selectEl = element.find('md-select-menu');
19903
19904 if (!options.target) {
19905 throw new Error($mdUtil.supplant(ERROR_TARGET_EXPECTED, [options.target]));
19906 }
19907
19908 angular.extend(options, {
19909 isRemoved: false,
19910 target: angular.element(options.target), //make sure it's not a naked dom node
19911 parent: angular.element(options.parent),
19912 selectEl: selectEl,
19913 contentEl: element.find('md-content'),
19914 optionNodes: selectEl[0].getElementsByTagName('md-option')
19915 });
19916 }
19917
19918 /**
19919 * Configure various resize listeners for screen changes
19920 */
19921 function activateResizing() {
19922 var debouncedOnResize = (function(scope, target, options) {
19923
19924 return function() {
19925 if (options.isRemoved) return;
19926
19927 var updates = calculateMenuPositions(scope, target, options);
19928 var container = updates.container;
19929 var dropDown = updates.dropDown;
19930
19931 container.element.css(animator.toCss(container.styles));
19932 dropDown.element.css(animator.toCss(dropDown.styles));
19933 };
19934
19935 })(scope, element, opts);
19936
19937 var window = angular.element($window);
19938 window.on('resize', debouncedOnResize);
19939 window.on('orientationchange', debouncedOnResize);
19940
19941 // Publish deactivation closure...
19942 return function deactivateResizing() {
19943
19944 // Disable resizing handlers
19945 window.off('resize', debouncedOnResize);
19946 window.off('orientationchange', debouncedOnResize);
19947 };
19948 }
19949
19950 /**
19951 * If asynchronously loading, watch and update internal
19952 * '$$loadingAsyncDone' flag
19953 */
19954 function watchAsyncLoad() {
19955 if (opts.loadingAsync && !opts.isRemoved) {
19956 scope.$$loadingAsyncDone = false;
19957
19958 $q.when(opts.loadingAsync)
19959 .then(function() {
19960 scope.$$loadingAsyncDone = true;
19961 delete opts.loadingAsync;
19962 }).then(function() {
19963 $$rAF(positionAndFocusMenu);
19964 });
19965 }
19966 }
19967
19968 /**
19969 *
19970 */
19971 function activateInteraction() {
19972 if (opts.isRemoved) return;
19973
19974 var dropDown = opts.selectEl;
19975 var selectCtrl = dropDown.controller('mdSelectMenu') || {};
19976
19977 element.addClass('md-clickable');
19978
19979 // Close on backdrop click
19980 opts.backdrop && opts.backdrop.on('click', onBackdropClick);
19981
19982 // Escape to close
19983 // Cycling of options, and closing on enter
19984 dropDown.on('keydown', onMenuKeyDown);
19985 dropDown.on('click', checkCloseMenu);
19986
19987 return function cleanupInteraction() {
19988 opts.backdrop && opts.backdrop.off('click', onBackdropClick);
19989 dropDown.off('keydown', onMenuKeyDown);
19990 dropDown.off('click', checkCloseMenu);
19991
19992 element.removeClass('md-clickable');
19993 opts.isRemoved = true;
19994 };
19995
19996 // ************************************
19997 // Closure Functions
19998 // ************************************
19999
20000 function onBackdropClick(e) {
20001 e.preventDefault();
20002 e.stopPropagation();
20003 opts.restoreFocus = false;
20004 $mdUtil.nextTick($mdSelect.hide, true);
20005 }
20006
20007 function onMenuKeyDown(ev) {
20008 ev.preventDefault();
20009 ev.stopPropagation();
20010
20011 switch (ev.keyCode) {
20012 case keyCodes.UP_ARROW:
20013 return focusPrevOption();
20014 case keyCodes.DOWN_ARROW:
20015 return focusNextOption();
20016 case keyCodes.SPACE:
20017 case keyCodes.ENTER:
20018 var option = $mdUtil.getClosest(ev.target, 'md-option');
20019 if (option) {
20020 dropDown.triggerHandler({
20021 type: 'click',
20022 target: option
20023 });
20024 ev.preventDefault();
20025 }
20026 checkCloseMenu(ev);
20027 break;
20028 case keyCodes.TAB:
20029 case keyCodes.ESCAPE:
20030 ev.stopPropagation();
20031 ev.preventDefault();
20032 opts.restoreFocus = true;
20033 $mdUtil.nextTick($mdSelect.hide, true);
20034 break;
20035 default:
20036 if ($mdConstant.isInputKey(ev) || $mdConstant.isNumPadKey(ev)) {
20037 var optNode = dropDown.controller('mdSelectMenu').optNodeForKeyboardSearch(ev);
20038 opts.focusedNode = optNode || opts.focusedNode;
20039 optNode && optNode.focus();
20040 }
20041 }
20042 }
20043
20044 function focusOption(direction) {
20045 var optionsArray = $mdUtil.nodesToArray(opts.optionNodes);
20046 var index = optionsArray.indexOf(opts.focusedNode);
20047
20048 var newOption;
20049
20050 do {
20051 if (index === -1) {
20052 // We lost the previously focused element, reset to first option
20053 index = 0;
20054 } else if (direction === 'next' && index < optionsArray.length - 1) {
20055 index++;
20056 } else if (direction === 'prev' && index > 0) {
20057 index--;
20058 }
20059 newOption = optionsArray[index];
20060 if (newOption.hasAttribute('disabled')) newOption = undefined;
20061 } while (!newOption && index < optionsArray.length - 1 && index > 0);
20062
20063 newOption && newOption.focus();
20064 opts.focusedNode = newOption;
20065 }
20066
20067 function focusNextOption() {
20068 focusOption('next');
20069 }
20070
20071 function focusPrevOption() {
20072 focusOption('prev');
20073 }
20074
20075 function checkCloseMenu(ev) {
20076 if (ev && ( ev.type == 'click') && (ev.currentTarget != dropDown[0])) return;
20077 if ( mouseOnScrollbar() ) return;
20078
20079 var option = $mdUtil.getClosest(ev.target, 'md-option');
20080 if (option && option.hasAttribute && !option.hasAttribute('disabled')) {
20081 ev.preventDefault();
20082 ev.stopPropagation();
20083 if (!selectCtrl.isMultiple) {
20084 opts.restoreFocus = true;
20085
20086 $mdUtil.nextTick(function () {
20087 $mdSelect.hide(selectCtrl.ngModel.$viewValue);
20088 }, true);
20089 }
20090 }
20091 /**
20092 * check if the mouseup event was on a scrollbar
20093 */
20094 function mouseOnScrollbar() {
20095 var clickOnScrollbar = false;
20096 if (ev && (ev.currentTarget.children.length > 0)) {
20097 var child = ev.currentTarget.children[0];
20098 var hasScrollbar = child.scrollHeight > child.clientHeight;
20099 if (hasScrollbar && child.children.length > 0) {
20100 var relPosX = ev.pageX - ev.currentTarget.getBoundingClientRect().left;
20101 if (relPosX > child.querySelector('md-option').offsetWidth)
20102 clickOnScrollbar = true;
20103 }
20104 }
20105 return clickOnScrollbar;
20106 }
20107 }
20108 }
20109
20110 }
20111
20112 /**
20113 * To notify listeners that the Select menu has closed,
20114 * trigger the [optional] user-defined expression
20115 */
20116 function announceClosed(opts) {
20117 var mdSelect = opts.selectCtrl;
20118 if (mdSelect) {
20119 var menuController = opts.selectEl.controller('mdSelectMenu');
20120 mdSelect.setLabelText(menuController ? menuController.selectedLabels() : '');
20121 mdSelect.triggerClose();
20122 }
20123 }
20124
20125
20126 /**
20127 * Calculate the
20128 */
20129 function calculateMenuPositions(scope, element, opts) {
20130 var
20131 containerNode = element[0],
20132 targetNode = opts.target[0].children[0], // target the label
20133 parentNode = $document[0].body,
20134 selectNode = opts.selectEl[0],
20135 contentNode = opts.contentEl[0],
20136 parentRect = parentNode.getBoundingClientRect(),
20137 targetRect = targetNode.getBoundingClientRect(),
20138 shouldOpenAroundTarget = false,
20139 bounds = {
20140 left: parentRect.left + SELECT_EDGE_MARGIN,
20141 top: SELECT_EDGE_MARGIN,
20142 bottom: parentRect.height - SELECT_EDGE_MARGIN,
20143 right: parentRect.width - SELECT_EDGE_MARGIN - ($mdUtil.floatingScrollbars() ? 16 : 0)
20144 },
20145 spaceAvailable = {
20146 top: targetRect.top - bounds.top,
20147 left: targetRect.left - bounds.left,
20148 right: bounds.right - (targetRect.left + targetRect.width),
20149 bottom: bounds.bottom - (targetRect.top + targetRect.height)
20150 },
20151 maxWidth = parentRect.width - SELECT_EDGE_MARGIN * 2,
20152 selectedNode = selectNode.querySelector('md-option[selected]'),
20153 optionNodes = selectNode.getElementsByTagName('md-option'),
20154 optgroupNodes = selectNode.getElementsByTagName('md-optgroup'),
20155 isScrollable = calculateScrollable(element, contentNode),
20156 centeredNode;
20157
20158 var loading = isPromiseLike(opts.loadingAsync);
20159 if (!loading) {
20160 // If a selected node, center around that
20161 if (selectedNode) {
20162 centeredNode = selectedNode;
20163 // If there are option groups, center around the first option group
20164 } else if (optgroupNodes.length) {
20165 centeredNode = optgroupNodes[0];
20166 // Otherwise - if we are not loading async - center around the first optionNode
20167 } else if (optionNodes.length) {
20168 centeredNode = optionNodes[0];
20169 // In case there are no options, center on whatever's in there... (eg progress indicator)
20170 } else {
20171 centeredNode = contentNode.firstElementChild || contentNode;
20172 }
20173 } else {
20174 // If loading, center on progress indicator
20175 centeredNode = contentNode.firstElementChild || contentNode;
20176 }
20177
20178 if (contentNode.offsetWidth > maxWidth) {
20179 contentNode.style['max-width'] = maxWidth + 'px';
20180 } else {
20181 contentNode.style.maxWidth = null;
20182 }
20183 if (shouldOpenAroundTarget) {
20184 contentNode.style['min-width'] = targetRect.width + 'px';
20185 }
20186
20187 // Remove padding before we compute the position of the menu
20188 if (isScrollable) {
20189 selectNode.classList.add('md-overflow');
20190 }
20191
20192 var focusedNode = centeredNode;
20193 if ((focusedNode.tagName || '').toUpperCase() === 'MD-OPTGROUP') {
20194 focusedNode = optionNodes[0] || contentNode.firstElementChild || contentNode;
20195 centeredNode = focusedNode;
20196 }
20197 // Cache for autoFocus()
20198 opts.focusedNode = focusedNode;
20199
20200 // Get the selectMenuRect *after* max-width is possibly set above
20201 containerNode.style.display = 'block';
20202 var selectMenuRect = selectNode.getBoundingClientRect();
20203 var centeredRect = getOffsetRect(centeredNode);
20204
20205 if (centeredNode) {
20206 var centeredStyle = $window.getComputedStyle(centeredNode);
20207 centeredRect.paddingLeft = parseInt(centeredStyle.paddingLeft, 10) || 0;
20208 centeredRect.paddingRight = parseInt(centeredStyle.paddingRight, 10) || 0;
20209 }
20210
20211 if (isScrollable) {
20212 var scrollBuffer = contentNode.offsetHeight / 2;
20213 contentNode.scrollTop = centeredRect.top + centeredRect.height / 2 - scrollBuffer;
20214
20215 if (spaceAvailable.top < scrollBuffer) {
20216 contentNode.scrollTop = Math.min(
20217 centeredRect.top,
20218 contentNode.scrollTop + scrollBuffer - spaceAvailable.top
20219 );
20220 } else if (spaceAvailable.bottom < scrollBuffer) {
20221 contentNode.scrollTop = Math.max(
20222 centeredRect.top + centeredRect.height - selectMenuRect.height,
20223 contentNode.scrollTop - scrollBuffer + spaceAvailable.bottom
20224 );
20225 }
20226 }
20227
20228 var left, top, transformOrigin, minWidth, fontSize;
20229 if (shouldOpenAroundTarget) {
20230 left = targetRect.left;
20231 top = targetRect.top + targetRect.height;
20232 transformOrigin = '50% 0';
20233 if (top + selectMenuRect.height > bounds.bottom) {
20234 top = targetRect.top - selectMenuRect.height;
20235 transformOrigin = '50% 100%';
20236 }
20237 } else {
20238 left = (targetRect.left + centeredRect.left - centeredRect.paddingLeft) + 2;
20239 top = Math.floor(targetRect.top + targetRect.height / 2 - centeredRect.height / 2 -
20240 centeredRect.top + contentNode.scrollTop) + 2;
20241
20242 transformOrigin = (centeredRect.left + targetRect.width / 2) + 'px ' +
20243 (centeredRect.top + centeredRect.height / 2 - contentNode.scrollTop) + 'px 0px';
20244
20245 minWidth = Math.min(targetRect.width + centeredRect.paddingLeft + centeredRect.paddingRight, maxWidth);
20246
20247 fontSize = window.getComputedStyle(targetNode)['font-size'];
20248 }
20249
20250 // Keep left and top within the window
20251 var containerRect = containerNode.getBoundingClientRect();
20252 var scaleX = Math.round(100 * Math.min(targetRect.width / selectMenuRect.width, 1.0)) / 100;
20253 var scaleY = Math.round(100 * Math.min(targetRect.height / selectMenuRect.height, 1.0)) / 100;
20254
20255 return {
20256 container: {
20257 element: angular.element(containerNode),
20258 styles: {
20259 left: Math.floor(clamp(bounds.left, left, bounds.right - containerRect.width)),
20260 top: Math.floor(clamp(bounds.top, top, bounds.bottom - containerRect.height)),
20261 'min-width': minWidth,
20262 'font-size': fontSize
20263 }
20264 },
20265 dropDown: {
20266 element: angular.element(selectNode),
20267 styles: {
20268 transformOrigin: transformOrigin,
20269 transform: !opts.alreadyOpen ? $mdUtil.supplant('scale({0},{1})', [scaleX, scaleY]) : ""
20270 }
20271 }
20272 };
20273
20274 }
20275
20276 }
20277
20278 function isPromiseLike(obj) {
20279 return obj && angular.isFunction(obj.then);
20280 }
20281
20282 function clamp(min, n, max) {
20283 return Math.max(min, Math.min(n, max));
20284 }
20285
20286 function getOffsetRect(node) {
20287 return node ? {
20288 left: node.offsetLeft,
20289 top: node.offsetTop,
20290 width: node.offsetWidth,
20291 height: node.offsetHeight
20292 } : {left: 0, top: 0, width: 0, height: 0};
20293 }
20294
20295 function calculateScrollable(element, contentNode) {
20296 var isScrollable = false;
20297
20298 try {
20299 var oldDisplay = element[0].style.display;
20300
20301 // Set the element's display to block so that this calculation is correct
20302 element[0].style.display = 'block';
20303
20304 isScrollable = contentNode.scrollHeight > contentNode.offsetHeight;
20305
20306 // Reset it back afterwards
20307 element[0].style.display = oldDisplay;
20308 } finally {
20309 // Nothing to do
20310 }
20311 return isScrollable;
20312 }
20313}
20314
20315})();
20316(function(){
20317"use strict";
20318
20319/**
20320 * @ngdoc module
20321 * @name material.components.showHide
20322 */
20323
20324// Add additional handlers to ng-show and ng-hide that notify directives
20325// contained within that they should recompute their size.
20326// These run in addition to Angular's built-in ng-hide and ng-show directives.
20327angular.module('material.components.showHide', [
20328 'material.core'
20329])
20330 .directive('ngShow', createDirective('ngShow', true))
20331 .directive('ngHide', createDirective('ngHide', false));
20332
20333
20334function createDirective(name, targetValue) {
20335 return ['$mdUtil', '$window', function($mdUtil, $window) {
20336 return {
20337 restrict: 'A',
20338 multiElement: true,
20339 link: function($scope, $element, $attr) {
20340 var unregister = $scope.$on('$md-resize-enable', function() {
20341 unregister();
20342
20343 var node = $element[0];
20344 var cachedTransitionStyles = node.nodeType === $window.Node.ELEMENT_NODE ?
20345 $window.getComputedStyle(node) : {};
20346
20347 $scope.$watch($attr[name], function(value) {
20348 if (!!value === targetValue) {
20349 $mdUtil.nextTick(function() {
20350 $scope.$broadcast('$md-resize');
20351 });
20352
20353 var opts = {
20354 cachedTransitionStyles: cachedTransitionStyles
20355 };
20356
20357 $mdUtil.dom.animator.waitTransitionEnd($element, opts).then(function() {
20358 $scope.$broadcast('$md-resize');
20359 });
20360 }
20361 });
20362 });
20363 }
20364 };
20365 }];
20366}
20367
20368})();
20369(function(){
20370"use strict";
20371
20372/**
20373 * @ngdoc module
20374 * @name material.components.sidenav
20375 *
20376 * @description
20377 * A Sidenav QP component.
20378 */
20379SidenavService.$inject = ["$mdComponentRegistry", "$mdUtil", "$q", "$log"];
20380SidenavDirective.$inject = ["$mdMedia", "$mdUtil", "$mdConstant", "$mdTheming", "$mdInteraction", "$animate", "$compile", "$parse", "$log", "$q", "$document", "$window", "$$rAF"];
20381SidenavController.$inject = ["$scope", "$attrs", "$mdComponentRegistry", "$q", "$interpolate"];
20382angular
20383 .module('material.components.sidenav', [
20384 'material.core',
20385 'material.components.backdrop'
20386 ])
20387 .factory('$mdSidenav', SidenavService )
20388 .directive('mdSidenav', SidenavDirective)
20389 .directive('mdSidenavFocus', SidenavFocusDirective)
20390 .controller('$mdSidenavController', SidenavController);
20391
20392
20393/**
20394 * @ngdoc service
20395 * @name $mdSidenav
20396 * @module material.components.sidenav
20397 *
20398 * @description
20399 * `$mdSidenav` makes it easy to interact with multiple sidenavs
20400 * in an app. When looking up a sidenav instance, you can either look
20401 * it up synchronously or wait for it to be initializied asynchronously.
20402 * This is done by passing the second argument to `$mdSidenav`.
20403 *
20404 * @usage
20405 * <hljs lang="js">
20406 * // Async lookup for sidenav instance; will resolve when the instance is available
20407 * $mdSidenav(componentId, true).then(function(instance) {
20408 * $log.debug( componentId + "is now ready" );
20409 * });
20410 * // Sync lookup for sidenav instance; this will resolve immediately.
20411 * $mdSidenav(componentId).then(function(instance) {
20412 * $log.debug( componentId + "is now ready" );
20413 * });
20414 * // Async toggle the given sidenav;
20415 * // when instance is known ready and lazy lookup is not needed.
20416 * $mdSidenav(componentId)
20417 * .toggle()
20418 * .then(function(){
20419 * $log.debug('toggled');
20420 * });
20421 * // Async open the given sidenav
20422 * $mdSidenav(componentId)
20423 * .open()
20424 * .then(function(){
20425 * $log.debug('opened');
20426 * });
20427 * // Async close the given sidenav
20428 * $mdSidenav(componentId)
20429 * .close()
20430 * .then(function(){
20431 * $log.debug('closed');
20432 * });
20433 * // Sync check to see if the specified sidenav is set to be open
20434 * $mdSidenav(componentId).isOpen();
20435 * // Sync check to whether given sidenav is locked open
20436 * // If this is true, the sidenav will be open regardless of close()
20437 * $mdSidenav(componentId).isLockedOpen();
20438 * // On close callback to handle close, backdrop click or escape key pressed
20439 * // Callback happens BEFORE the close action occurs.
20440 * $mdSidenav(componentId).onClose(function () {
20441 * $log.debug('closing');
20442 * });
20443 * </hljs>
20444 */
20445function SidenavService($mdComponentRegistry, $mdUtil, $q, $log) {
20446 var errorMsg = "SideNav '{0}' is not available! Did you use md-component-id='{0}'?";
20447 var service = {
20448 find : findInstance, // sync - returns proxy API
20449 waitFor : waitForInstance // async - returns promise
20450 };
20451
20452 /**
20453 * Service API that supports three (3) usages:
20454 * $mdSidenav().find("left") // sync (must already exist) or returns undefined
20455 * $mdSidenav("left").toggle(); // sync (must already exist) or returns reject promise;
20456 * $mdSidenav("left",true).then( function(left){ // async returns instance when available
20457 * left.toggle();
20458 * });
20459 */
20460 return function(handle, enableWait) {
20461 if ( angular.isUndefined(handle) ) return service;
20462
20463 var shouldWait = enableWait === true;
20464 var instance = service.find(handle, shouldWait);
20465 return !instance && shouldWait ? service.waitFor(handle) :
20466 !instance && angular.isUndefined(enableWait) ? addLegacyAPI(service, handle) : instance;
20467 };
20468
20469 /**
20470 * For failed instance/handle lookups, older-clients expect an response object with noops
20471 * that include `rejected promise APIs`
20472 */
20473 function addLegacyAPI(service, handle) {
20474 var falseFn = function() { return false; };
20475 var rejectFn = function() {
20476 return $q.when($mdUtil.supplant(errorMsg, [handle || ""]));
20477 };
20478
20479 return angular.extend({
20480 isLockedOpen : falseFn,
20481 isOpen : falseFn,
20482 toggle : rejectFn,
20483 open : rejectFn,
20484 close : rejectFn,
20485 onClose : angular.noop,
20486 then : function(callback) {
20487 return waitForInstance(handle)
20488 .then(callback || angular.noop);
20489 }
20490 }, service);
20491 }
20492 /**
20493 * Synchronously lookup the controller instance for the specified sidNav instance which has been
20494 * registered with the markup `md-component-id`
20495 */
20496 function findInstance(handle, shouldWait) {
20497 var instance = $mdComponentRegistry.get(handle);
20498
20499 if (!instance && !shouldWait) {
20500
20501 // Report missing instance
20502 $log.error( $mdUtil.supplant(errorMsg, [handle || ""]) );
20503
20504 // The component has not registered itself... most like NOT yet created
20505 // return null to indicate that the Sidenav is not in the DOM
20506 return undefined;
20507 }
20508 return instance;
20509 }
20510
20511 /**
20512 * Asynchronously wait for the component instantiation,
20513 * Deferred lookup of component instance using $component registry
20514 */
20515 function waitForInstance(handle) {
20516 return $mdComponentRegistry.when(handle).catch($log.error);
20517 }
20518}
20519/**
20520 * @ngdoc directive
20521 * @name mdSidenavFocus
20522 * @module material.components.sidenav
20523 *
20524 * @restrict A
20525 *
20526 * @description
20527 * `mdSidenavFocus` provides a way to specify the focused element when a sidenav opens.
20528 * This is completely optional, as the sidenav itself is focused by default.
20529 *
20530 * @usage
20531 * <hljs lang="html">
20532 * <md-sidenav>
20533 * <form>
20534 * <md-input-container>
20535 * <label for="testInput">Label</label>
20536 * <input id="testInput" type="text" md-sidenav-focus>
20537 * </md-input-container>
20538 * </form>
20539 * </md-sidenav>
20540 * </hljs>
20541 **/
20542function SidenavFocusDirective() {
20543 return {
20544 restrict: 'A',
20545 require: '^mdSidenav',
20546 link: function(scope, element, attr, sidenavCtrl) {
20547 // @see $mdUtil.findFocusTarget(...)
20548 }
20549 };
20550}
20551/**
20552 * @ngdoc directive
20553 * @name mdSidenav
20554 * @module material.components.sidenav
20555 * @restrict E
20556 *
20557 * @description
20558 *
20559 * A Sidenav component that can be opened and closed programatically.
20560 *
20561 * By default, upon opening it will slide out on top of the main content area.
20562 *
20563 * For keyboard and screen reader accessibility, focus is sent to the sidenav wrapper by default.
20564 * It can be overridden with the `md-autofocus` directive on the child element you want focused.
20565 *
20566 * @usage
20567 * <hljs lang="html">
20568 * <div layout="row" ng-controller="MyController">
20569 * <md-sidenav md-component-id="left" class="md-sidenav-left">
20570 * Left Nav!
20571 * </md-sidenav>
20572 *
20573 * <md-content>
20574 * Center Content
20575 * <md-button ng-click="openLeftMenu()">
20576 * Open Left Menu
20577 * </md-button>
20578 * </md-content>
20579 *
20580 * <md-sidenav md-component-id="right"
20581 * md-is-locked-open="$mdMedia('min-width: 333px')"
20582 * class="md-sidenav-right">
20583 * <form>
20584 * <md-input-container>
20585 * <label for="testInput">Test input</label>
20586 * <input id="testInput" type="text"
20587 * ng-model="data" md-autofocus>
20588 * </md-input-container>
20589 * </form>
20590 * </md-sidenav>
20591 * </div>
20592 * </hljs>
20593 *
20594 * <hljs lang="js">
20595 * var app = angular.module('myApp', ['ngMaterial']);
20596 * app.controller('MyController', function($scope, $mdSidenav) {
20597 * $scope.openLeftMenu = function() {
20598 * $mdSidenav('left').toggle();
20599 * };
20600 * });
20601 * </hljs>
20602 *
20603 * @param {expression=} md-is-open A model bound to whether the sidenav is opened.
20604 * @param {boolean=} md-disable-backdrop When present in the markup, the sidenav will not show a backdrop.
20605 * @param {string=} md-component-id componentId to use with $mdSidenav service.
20606 * @param {expression=} md-is-locked-open When this expression evaluates to true,
20607 * the sidenav 'locks open': it falls into the content's flow instead
20608 * of appearing over it. This overrides the `md-is-open` attribute.
20609 * @param {string=} md-disable-scroll-target Selector, pointing to an element, whose scrolling will
20610 * be disabled when the sidenav is opened. By default this is the sidenav's direct parent.
20611 *
20612* The $mdMedia() service is exposed to the is-locked-open attribute, which
20613 * can be given a media query or one of the `sm`, `gt-sm`, `md`, `gt-md`, `lg` or `gt-lg` presets.
20614 * Examples:
20615 *
20616 * - `<md-sidenav md-is-locked-open="shouldLockOpen"></md-sidenav>`
20617 * - `<md-sidenav md-is-locked-open="$mdMedia('min-width: 1000px')"></md-sidenav>`
20618 * - `<md-sidenav md-is-locked-open="$mdMedia('sm')"></md-sidenav>` (locks open on small screens)
20619 */
20620function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $mdInteraction, $animate,
20621 $compile, $parse, $log, $q, $document, $window, $$rAF) {
20622 return {
20623 restrict: 'E',
20624 scope: {
20625 isOpen: '=?mdIsOpen'
20626 },
20627 controller: '$mdSidenavController',
20628 compile: function(element) {
20629 element.addClass('md-closed').attr('tabIndex', '-1');
20630 return postLink;
20631 }
20632 };
20633
20634 /**
20635 * Directive Post Link function...
20636 */
20637 function postLink(scope, element, attr, sidenavCtrl) {
20638 var lastParentOverFlow;
20639 var backdrop;
20640 var disableScrollTarget = null;
20641 var triggeringInteractionType;
20642 var triggeringElement = null;
20643 var previousContainerStyles;
20644 var promise = $q.when(true);
20645 var isLockedOpenParsed = $parse(attr.mdIsLockedOpen);
20646 var ngWindow = angular.element($window);
20647 var isLocked = function() {
20648 return isLockedOpenParsed(scope.$parent, {
20649 $media: function(arg) {
20650 $log.warn("$media is deprecated for is-locked-open. Use $mdMedia instead.");
20651 return $mdMedia(arg);
20652 },
20653 $mdMedia: $mdMedia
20654 });
20655 };
20656
20657 if (attr.mdDisableScrollTarget) {
20658 disableScrollTarget = $document[0].querySelector(attr.mdDisableScrollTarget);
20659
20660 if (disableScrollTarget) {
20661 disableScrollTarget = angular.element(disableScrollTarget);
20662 } else {
20663 $log.warn($mdUtil.supplant('mdSidenav: couldn\'t find element matching ' +
20664 'selector "{selector}". Falling back to parent.', { selector: attr.mdDisableScrollTarget }));
20665 }
20666 }
20667
20668 if (!disableScrollTarget) {
20669 disableScrollTarget = element.parent();
20670 }
20671
20672 // Only create the backdrop if the backdrop isn't disabled.
20673 if (!attr.hasOwnProperty('mdDisableBackdrop')) {
20674 backdrop = $mdUtil.createBackdrop(scope, "md-sidenav-backdrop md-opaque ng-enter");
20675 }
20676
20677 element.addClass('_md'); // private md component indicator for styling
20678 $mdTheming(element);
20679
20680 // The backdrop should inherit the sidenavs theme,
20681 // because the backdrop will take its parent theme by default.
20682 if ( backdrop ) $mdTheming.inherit(backdrop, element);
20683
20684 element.on('$destroy', function() {
20685 backdrop && backdrop.remove();
20686 sidenavCtrl.destroy();
20687 });
20688
20689 scope.$on('$destroy', function(){
20690 backdrop && backdrop.remove();
20691 });
20692
20693 scope.$watch(isLocked, updateIsLocked);
20694 scope.$watch('isOpen', updateIsOpen);
20695
20696
20697 // Publish special accessor for the Controller instance
20698 sidenavCtrl.$toggleOpen = toggleOpen;
20699
20700 /**
20701 * Toggle the DOM classes to indicate `locked`
20702 * @param isLocked
20703 */
20704 function updateIsLocked(isLocked, oldValue) {
20705 scope.isLockedOpen = isLocked;
20706 if (isLocked === oldValue) {
20707 element.toggleClass('md-locked-open', !!isLocked);
20708 } else {
20709 $animate[isLocked ? 'addClass' : 'removeClass'](element, 'md-locked-open');
20710 }
20711 if (backdrop) {
20712 backdrop.toggleClass('md-locked-open', !!isLocked);
20713 }
20714 }
20715
20716 /**
20717 * Toggle the SideNav view and attach/detach listeners
20718 * @param isOpen
20719 */
20720 function updateIsOpen(isOpen) {
20721 // Support deprecated md-sidenav-focus attribute as fallback
20722 var focusEl = $mdUtil.findFocusTarget(element) || $mdUtil.findFocusTarget(element,'[md-sidenav-focus]') || element;
20723 var parent = element.parent();
20724
20725 parent[isOpen ? 'on' : 'off']('keydown', onKeyDown);
20726 if (backdrop) backdrop[isOpen ? 'on' : 'off']('click', close);
20727
20728 var restorePositioning = updateContainerPositions(parent, isOpen);
20729
20730 if ( isOpen ) {
20731 // Capture upon opening..
20732 triggeringElement = $document[0].activeElement;
20733 triggeringInteractionType = $mdInteraction.getLastInteractionType();
20734 }
20735
20736 disableParentScroll(isOpen);
20737
20738 return promise = $q.all([
20739 isOpen && backdrop ? $animate.enter(backdrop, parent) : backdrop ?
20740 $animate.leave(backdrop) : $q.when(true),
20741 $animate[isOpen ? 'removeClass' : 'addClass'](element, 'md-closed')
20742 ]).then(function() {
20743 // Perform focus when animations are ALL done...
20744 if (scope.isOpen) {
20745 $$rAF(function() {
20746 // Notifies child components that the sidenav was opened. Should wait
20747 // a frame in order to allow for the element height to be computed.
20748 ngWindow.triggerHandler('resize');
20749 });
20750
20751 focusEl && focusEl.focus();
20752 }
20753
20754 // Restores the positioning on the sidenav and backdrop.
20755 restorePositioning && restorePositioning();
20756 });
20757 }
20758
20759 function updateContainerPositions(parent, willOpen) {
20760 var drawerEl = element[0];
20761 var scrollTop = parent[0].scrollTop;
20762
20763 if (willOpen && scrollTop) {
20764 previousContainerStyles = {
20765 top: drawerEl.style.top,
20766 bottom: drawerEl.style.bottom,
20767 height: drawerEl.style.height
20768 };
20769
20770 // When the parent is scrolled down, then we want to be able to show the sidenav at the current scroll
20771 // position. We're moving the sidenav down to the correct scroll position and apply the height of the
20772 // parent, to increase the performance. Using 100% as height, will impact the performance heavily.
20773 var positionStyle = {
20774 top: scrollTop + 'px',
20775 bottom: 'auto',
20776 height: parent[0].clientHeight + 'px'
20777 };
20778
20779 // Apply the new position styles to the sidenav and backdrop.
20780 element.css(positionStyle);
20781 backdrop.css(positionStyle);
20782 }
20783
20784 // When the sidenav is closing and we have previous defined container styles,
20785 // then we return a restore function, which resets the sidenav and backdrop.
20786 if (!willOpen && previousContainerStyles) {
20787 return function() {
20788 drawerEl.style.top = previousContainerStyles.top;
20789 drawerEl.style.bottom = previousContainerStyles.bottom;
20790 drawerEl.style.height = previousContainerStyles.height;
20791
20792 backdrop[0].style.top = null;
20793 backdrop[0].style.bottom = null;
20794 backdrop[0].style.height = null;
20795
20796 previousContainerStyles = null;
20797 };
20798 }
20799 }
20800
20801 /**
20802 * Prevent parent scrolling (when the SideNav is open)
20803 */
20804 function disableParentScroll(disabled) {
20805 if ( disabled && !lastParentOverFlow ) {
20806 lastParentOverFlow = disableScrollTarget.css('overflow');
20807 disableScrollTarget.css('overflow', 'hidden');
20808 } else if (angular.isDefined(lastParentOverFlow)) {
20809 disableScrollTarget.css('overflow', lastParentOverFlow);
20810 lastParentOverFlow = undefined;
20811 }
20812 }
20813
20814 /**
20815 * Toggle the sideNav view and publish a promise to be resolved when
20816 * the view animation finishes.
20817 *
20818 * @param isOpen
20819 * @returns {*}
20820 */
20821 function toggleOpen( isOpen ) {
20822 if (scope.isOpen == isOpen ) {
20823
20824 return $q.when(true);
20825
20826 } else {
20827 if (scope.isOpen && sidenavCtrl.onCloseCb) sidenavCtrl.onCloseCb();
20828
20829 return $q(function(resolve){
20830 // Toggle value to force an async `updateIsOpen()` to run
20831 scope.isOpen = isOpen;
20832
20833 $mdUtil.nextTick(function() {
20834 // When the current `updateIsOpen()` animation finishes
20835 promise.then(function(result) {
20836
20837 if ( !scope.isOpen && triggeringElement && triggeringInteractionType === 'keyboard') {
20838 // reset focus to originating element (if available) upon close
20839 triggeringElement.focus();
20840 triggeringElement = null;
20841 }
20842
20843 resolve(result);
20844 });
20845 });
20846
20847 });
20848
20849 }
20850 }
20851
20852 /**
20853 * Auto-close sideNav when the `escape` key is pressed.
20854 * @param evt
20855 */
20856 function onKeyDown(ev) {
20857 var isEscape = (ev.keyCode === $mdConstant.KEY_CODE.ESCAPE);
20858 return isEscape ? close(ev) : $q.when(true);
20859 }
20860
20861 /**
20862 * With backdrop `clicks` or `escape` key-press, immediately
20863 * apply the CSS close transition... Then notify the controller
20864 * to close() and perform its own actions.
20865 */
20866 function close(ev) {
20867 ev.preventDefault();
20868
20869 return sidenavCtrl.close();
20870 }
20871
20872 }
20873}
20874
20875/*
20876 * @private
20877 * @ngdoc controller
20878 * @name SidenavController
20879 * @module material.components.sidenav
20880 */
20881function SidenavController($scope, $attrs, $mdComponentRegistry, $q, $interpolate) {
20882
20883 var self = this;
20884
20885 // Use Default internal method until overridden by directive postLink
20886
20887 // Synchronous getters
20888 self.isOpen = function() { return !!$scope.isOpen; };
20889 self.isLockedOpen = function() { return !!$scope.isLockedOpen; };
20890
20891 // Synchronous setters
20892 self.onClose = function (callback) {
20893 self.onCloseCb = callback;
20894 return self;
20895 };
20896
20897 // Async actions
20898 self.open = function() { return self.$toggleOpen( true ); };
20899 self.close = function() { return self.$toggleOpen( false ); };
20900 self.toggle = function() { return self.$toggleOpen( !$scope.isOpen ); };
20901 self.$toggleOpen = function(value) { return $q.when($scope.isOpen = value); };
20902
20903 // Evaluate the component id.
20904 var rawId = $attrs.mdComponentId;
20905 var hasDataBinding = rawId && rawId.indexOf($interpolate.startSymbol()) > -1;
20906 var componentId = hasDataBinding ? $interpolate(rawId)($scope.$parent) : rawId;
20907
20908 // Register the component.
20909 self.destroy = $mdComponentRegistry.register(self, componentId);
20910
20911 // Watch and update the component, if the id has changed.
20912 if (hasDataBinding) {
20913 $attrs.$observe('mdComponentId', function(id) {
20914 if (id && id !== self.$$mdHandle) {
20915 self.destroy(); // `destroy` only deregisters the old component id so we can add the new one.
20916 self.destroy = $mdComponentRegistry.register(self, id);
20917 }
20918 });
20919 }
20920}
20921
20922})();
20923(function(){
20924"use strict";
20925
20926 /**
20927 * @ngdoc module
20928 * @name material.components.slider
20929 */
20930SliderDirective.$inject = ["$$rAF", "$window", "$mdAria", "$mdUtil", "$mdConstant", "$mdTheming", "$mdGesture", "$parse", "$log", "$timeout"];
20931 angular.module('material.components.slider', [
20932 'material.core'
20933 ])
20934 .directive('mdSlider', SliderDirective)
20935 .directive('mdSliderContainer', SliderContainerDirective);
20936
20937/**
20938 * @ngdoc directive
20939 * @name mdSliderContainer
20940 * @module material.components.slider
20941 * @restrict E
20942 * @description
20943 * The `<md-slider-container>` contains slider with two other elements.
20944 *
20945 *
20946 * @usage
20947 * <h4>Normal Mode</h4>
20948 * <hljs lang="html">
20949 * </hljs>
20950 */
20951function SliderContainerDirective() {
20952 return {
20953 controller: function () {},
20954 compile: function (elem) {
20955 var slider = elem.find('md-slider');
20956
20957 if (!slider) {
20958 return;
20959 }
20960
20961 var vertical = slider.attr('md-vertical');
20962
20963 if (vertical !== undefined) {
20964 elem.attr('md-vertical', '');
20965 }
20966
20967 if(!slider.attr('flex')) {
20968 slider.attr('flex', '');
20969 }
20970
20971 return function postLink(scope, element, attr, ctrl) {
20972 element.addClass('_md'); // private md component indicator for styling
20973
20974 // We have to manually stop the $watch on ngDisabled because it exists
20975 // on the parent scope, and won't be automatically destroyed when
20976 // the component is destroyed.
20977 function setDisable(value) {
20978 element.children().attr('disabled', value);
20979 element.find('input').attr('disabled', value);
20980 }
20981
20982 var stopDisabledWatch = angular.noop;
20983
20984 if (attr.disabled) {
20985 setDisable(true);
20986 }
20987 else if (attr.ngDisabled) {
20988 stopDisabledWatch = scope.$watch(attr.ngDisabled, function (value) {
20989 setDisable(value);
20990 });
20991 }
20992
20993 scope.$on('$destroy', function () {
20994 stopDisabledWatch();
20995 });
20996
20997 var initialMaxWidth;
20998
20999 ctrl.fitInputWidthToTextLength = function (length) {
21000 var input = element[0].querySelector('md-input-container');
21001
21002 if (input) {
21003 var computedStyle = getComputedStyle(input);
21004 var minWidth = parseInt(computedStyle.minWidth);
21005 var padding = parseInt(computedStyle.padding) * 2;
21006
21007 initialMaxWidth = initialMaxWidth || parseInt(computedStyle.maxWidth);
21008 var newMaxWidth = Math.max(initialMaxWidth, minWidth + padding + (minWidth / 2 * length));
21009
21010 input.style.maxWidth = newMaxWidth + 'px';
21011 }
21012 };
21013 };
21014 }
21015 };
21016}
21017
21018/**
21019 * @ngdoc directive
21020 * @name mdSlider
21021 * @module material.components.slider
21022 * @restrict E
21023 * @description
21024 * The `<md-slider>` component allows the user to choose from a range of
21025 * values.
21026 *
21027 * As per the [material design spec](http://www.google.com/design/spec/style/color.html#color-ui-color-application)
21028 * the slider is in the accent color by default. The primary color palette may be used with
21029 * the `md-primary` class.
21030 *
21031 * It has two modes: 'normal' mode, where the user slides between a wide range
21032 * of values, and 'discrete' mode, where the user slides between only a few
21033 * select values.
21034 *
21035 * To enable discrete mode, add the `md-discrete` attribute to a slider,
21036 * and use the `step` attribute to change the distance between
21037 * values the user is allowed to pick.
21038 *
21039 * @usage
21040 * <h4>Normal Mode</h4>
21041 * <hljs lang="html">
21042 * <md-slider ng-model="myValue" min="5" max="500">
21043 * </md-slider>
21044 * </hljs>
21045 * <h4>Discrete Mode</h4>
21046 * <hljs lang="html">
21047 * <md-slider md-discrete ng-model="myDiscreteValue" step="10" min="10" max="130">
21048 * </md-slider>
21049 * </hljs>
21050 * <h4>Invert Mode</h4>
21051 * <hljs lang="html">
21052 * <md-slider md-invert ng-model="myValue" step="10" min="10" max="130">
21053 * </md-slider>
21054 * </hljs>
21055 *
21056 * @param {boolean=} md-discrete Whether to enable discrete mode.
21057 * @param {boolean=} md-invert Whether to enable invert mode.
21058 * @param {number=} step The distance between values the user is allowed to pick. Default 1.
21059 * @param {number=} min The minimum value the user is allowed to pick. Default 0.
21060 * @param {number=} max The maximum value the user is allowed to pick. Default 100.
21061 * @param {number=} round The amount of numbers after the decimal point, maximum is 6 to prevent scientific notation. Default 3.
21062 */
21063function SliderDirective($$rAF, $window, $mdAria, $mdUtil, $mdConstant, $mdTheming, $mdGesture, $parse, $log, $timeout) {
21064 return {
21065 scope: {},
21066 require: ['?ngModel', '?^mdSliderContainer'],
21067 template:
21068 '<div class="md-slider-wrapper">' +
21069 '<div class="md-slider-content">' +
21070 '<div class="md-track-container">' +
21071 '<div class="md-track"></div>' +
21072 '<div class="md-track md-track-fill"></div>' +
21073 '<div class="md-track-ticks"></div>' +
21074 '</div>' +
21075 '<div class="md-thumb-container">' +
21076 '<div class="md-thumb"></div>' +
21077 '<div class="md-focus-thumb"></div>' +
21078 '<div class="md-focus-ring"></div>' +
21079 '<div class="md-sign">' +
21080 '<span class="md-thumb-text"></span>' +
21081 '</div>' +
21082 '<div class="md-disabled-thumb"></div>' +
21083 '</div>' +
21084 '</div>' +
21085 '</div>',
21086 compile: compile
21087 };
21088
21089 // **********************************************************
21090 // Private Methods
21091 // **********************************************************
21092
21093 function compile (tElement, tAttrs) {
21094 var wrapper = angular.element(tElement[0].getElementsByClassName('md-slider-wrapper'));
21095
21096 var tabIndex = tAttrs.tabindex || 0;
21097 wrapper.attr('tabindex', tabIndex);
21098
21099 if (tAttrs.disabled || tAttrs.ngDisabled) wrapper.attr('tabindex', -1);
21100
21101 wrapper.attr('role', 'slider');
21102
21103 $mdAria.expect(tElement, 'aria-label');
21104
21105 return postLink;
21106 }
21107
21108 function postLink(scope, element, attr, ctrls) {
21109 $mdTheming(element);
21110 var ngModelCtrl = ctrls[0] || {
21111 // Mock ngModelController if it doesn't exist to give us
21112 // the minimum functionality needed
21113 $setViewValue: function(val) {
21114 this.$viewValue = val;
21115 this.$viewChangeListeners.forEach(function(cb) { cb(); });
21116 },
21117 $parsers: [],
21118 $formatters: [],
21119 $viewChangeListeners: []
21120 };
21121
21122 var containerCtrl = ctrls[1];
21123 var container = angular.element($mdUtil.getClosest(element, '_md-slider-container', true));
21124 var isDisabled = attr.ngDisabled ? angular.bind(null, $parse(attr.ngDisabled), scope.$parent) : function () {
21125 return element[0].hasAttribute('disabled');
21126 };
21127
21128 var thumb = angular.element(element[0].querySelector('.md-thumb'));
21129 var thumbText = angular.element(element[0].querySelector('.md-thumb-text'));
21130 var thumbContainer = thumb.parent();
21131 var trackContainer = angular.element(element[0].querySelector('.md-track-container'));
21132 var activeTrack = angular.element(element[0].querySelector('.md-track-fill'));
21133 var tickContainer = angular.element(element[0].querySelector('.md-track-ticks'));
21134 var wrapper = angular.element(element[0].getElementsByClassName('md-slider-wrapper'));
21135 var content = angular.element(element[0].getElementsByClassName('md-slider-content'));
21136 var throttledRefreshDimensions = $mdUtil.throttle(refreshSliderDimensions, 5000);
21137
21138 // Default values, overridable by attrs
21139 var DEFAULT_ROUND = 3;
21140 var vertical = angular.isDefined(attr.mdVertical);
21141 var discrete = angular.isDefined(attr.mdDiscrete);
21142 var invert = angular.isDefined(attr.mdInvert);
21143 angular.isDefined(attr.min) ? attr.$observe('min', updateMin) : updateMin(0);
21144 angular.isDefined(attr.max) ? attr.$observe('max', updateMax) : updateMax(100);
21145 angular.isDefined(attr.step)? attr.$observe('step', updateStep) : updateStep(1);
21146 angular.isDefined(attr.round)? attr.$observe('round', updateRound) : updateRound(DEFAULT_ROUND);
21147
21148 // We have to manually stop the $watch on ngDisabled because it exists
21149 // on the parent scope, and won't be automatically destroyed when
21150 // the component is destroyed.
21151 var stopDisabledWatch = angular.noop;
21152 if (attr.ngDisabled) {
21153 stopDisabledWatch = scope.$parent.$watch(attr.ngDisabled, updateAriaDisabled);
21154 }
21155
21156 $mdGesture.register(wrapper, 'drag', { horizontal: !vertical });
21157
21158 scope.mouseActive = false;
21159
21160 wrapper
21161 .on('keydown', keydownListener)
21162 .on('mousedown', mouseDownListener)
21163 .on('focus', focusListener)
21164 .on('blur', blurListener)
21165 .on('$md.pressdown', onPressDown)
21166 .on('$md.pressup', onPressUp)
21167 .on('$md.dragstart', onDragStart)
21168 .on('$md.drag', onDrag)
21169 .on('$md.dragend', onDragEnd);
21170
21171 // On resize, recalculate the slider's dimensions and re-render
21172 function updateAll() {
21173 refreshSliderDimensions();
21174 ngModelRender();
21175 }
21176 setTimeout(updateAll, 0);
21177
21178 var debouncedUpdateAll = $$rAF.throttle(updateAll);
21179 angular.element($window).on('resize', debouncedUpdateAll);
21180
21181 scope.$on('$destroy', function() {
21182 angular.element($window).off('resize', debouncedUpdateAll);
21183 });
21184
21185 ngModelCtrl.$render = ngModelRender;
21186 ngModelCtrl.$viewChangeListeners.push(ngModelRender);
21187 ngModelCtrl.$formatters.push(minMaxValidator);
21188 ngModelCtrl.$formatters.push(stepValidator);
21189
21190 /**
21191 * Attributes
21192 */
21193 var min;
21194 var max;
21195 var step;
21196 var round;
21197 function updateMin(value) {
21198 min = parseFloat(value);
21199 element.attr('aria-valuemin', value);
21200 updateAll();
21201 }
21202 function updateMax(value) {
21203 max = parseFloat(value);
21204 element.attr('aria-valuemax', value);
21205 updateAll();
21206 }
21207 function updateStep(value) {
21208 step = parseFloat(value);
21209 }
21210 function updateRound(value) {
21211 // Set max round digits to 6, after 6 the input uses scientific notation
21212 round = minMaxValidator(parseInt(value), 0, 6);
21213 }
21214 function updateAriaDisabled() {
21215 element.attr('aria-disabled', !!isDisabled());
21216 }
21217
21218 // Draw the ticks with canvas.
21219 // The alternative to drawing ticks with canvas is to draw one element for each tick,
21220 // which could quickly become a performance bottleneck.
21221 var tickCanvas, tickCtx;
21222 function redrawTicks() {
21223 if (!discrete || isDisabled()) return;
21224 if ( angular.isUndefined(step) ) return;
21225
21226 if ( step <= 0 ) {
21227 var msg = 'Slider step value must be greater than zero when in discrete mode';
21228 $log.error(msg);
21229 throw new Error(msg);
21230 }
21231
21232 var numSteps = Math.floor( (max - min) / step );
21233 if (!tickCanvas) {
21234 tickCanvas = angular.element('<canvas>').css('position', 'absolute');
21235 tickContainer.append(tickCanvas);
21236
21237 tickCtx = tickCanvas[0].getContext('2d');
21238 }
21239
21240 var dimensions = getSliderDimensions();
21241
21242 // If `dimensions` doesn't have height and width it might be the first attempt so we will refresh dimensions
21243 if (dimensions && !dimensions.height && !dimensions.width) {
21244 refreshSliderDimensions();
21245 dimensions = sliderDimensions;
21246 }
21247
21248 tickCanvas[0].width = dimensions.width;
21249 tickCanvas[0].height = dimensions.height;
21250
21251 var distance;
21252 for (var i = 0; i <= numSteps; i++) {
21253 var trackTicksStyle = $window.getComputedStyle(tickContainer[0]);
21254 tickCtx.fillStyle = trackTicksStyle.color || 'black';
21255
21256 distance = Math.floor((vertical ? dimensions.height : dimensions.width) * (i / numSteps));
21257
21258 tickCtx.fillRect(vertical ? 0 : distance - 1,
21259 vertical ? distance - 1 : 0,
21260 vertical ? dimensions.width : 2,
21261 vertical ? 2 : dimensions.height);
21262 }
21263 }
21264
21265 function clearTicks() {
21266 if(tickCanvas && tickCtx) {
21267 var dimensions = getSliderDimensions();
21268 tickCtx.clearRect(0, 0, dimensions.width, dimensions.height);
21269 }
21270 }
21271
21272 /**
21273 * Refreshing Dimensions
21274 */
21275 var sliderDimensions = {};
21276 refreshSliderDimensions();
21277 function refreshSliderDimensions() {
21278 sliderDimensions = trackContainer[0].getBoundingClientRect();
21279 }
21280 function getSliderDimensions() {
21281 throttledRefreshDimensions();
21282 return sliderDimensions;
21283 }
21284
21285 /**
21286 * left/right/up/down arrow listener
21287 */
21288 function keydownListener(ev) {
21289 if (isDisabled()) return;
21290
21291 var changeAmount;
21292 if (vertical ? ev.keyCode === $mdConstant.KEY_CODE.DOWN_ARROW : ev.keyCode === $mdConstant.KEY_CODE.LEFT_ARROW) {
21293 changeAmount = -step;
21294 } else if (vertical ? ev.keyCode === $mdConstant.KEY_CODE.UP_ARROW : ev.keyCode === $mdConstant.KEY_CODE.RIGHT_ARROW) {
21295 changeAmount = step;
21296 }
21297 changeAmount = invert ? -changeAmount : changeAmount;
21298 if (changeAmount) {
21299 if (ev.metaKey || ev.ctrlKey || ev.altKey) {
21300 changeAmount *= 4;
21301 }
21302 ev.preventDefault();
21303 ev.stopPropagation();
21304 scope.$evalAsync(function() {
21305 setModelValue(ngModelCtrl.$viewValue + changeAmount);
21306 });
21307 }
21308 }
21309
21310 function mouseDownListener() {
21311 redrawTicks();
21312
21313 scope.mouseActive = true;
21314 wrapper.removeClass('md-focused');
21315
21316 $timeout(function() {
21317 scope.mouseActive = false;
21318 }, 100);
21319 }
21320
21321 function focusListener() {
21322 if (scope.mouseActive === false) {
21323 wrapper.addClass('md-focused');
21324 }
21325 }
21326
21327 function blurListener() {
21328 wrapper.removeClass('md-focused');
21329 element.removeClass('md-active');
21330 clearTicks();
21331 }
21332
21333 /**
21334 * ngModel setters and validators
21335 */
21336 function setModelValue(value) {
21337 ngModelCtrl.$setViewValue( minMaxValidator(stepValidator(value)) );
21338 }
21339 function ngModelRender() {
21340 if (isNaN(ngModelCtrl.$viewValue)) {
21341 ngModelCtrl.$viewValue = ngModelCtrl.$modelValue;
21342 }
21343
21344 ngModelCtrl.$viewValue = minMaxValidator(ngModelCtrl.$viewValue);
21345
21346 var percent = valueToPercent(ngModelCtrl.$viewValue);
21347 scope.modelValue = ngModelCtrl.$viewValue;
21348 element.attr('aria-valuenow', ngModelCtrl.$viewValue);
21349 setSliderPercent(percent);
21350 thumbText.text( ngModelCtrl.$viewValue );
21351 }
21352
21353 function minMaxValidator(value, minValue, maxValue) {
21354 if (angular.isNumber(value)) {
21355 minValue = angular.isNumber(minValue) ? minValue : min;
21356 maxValue = angular.isNumber(maxValue) ? maxValue : max;
21357
21358 return Math.max(minValue, Math.min(maxValue, value));
21359 }
21360 }
21361
21362 function stepValidator(value) {
21363 if (angular.isNumber(value)) {
21364 var formattedValue = (Math.round((value - min) / step) * step + min);
21365 formattedValue = (Math.round(formattedValue * Math.pow(10, round)) / Math.pow(10, round));
21366
21367 if (containerCtrl && containerCtrl.fitInputWidthToTextLength){
21368 $mdUtil.debounce(function () {
21369 containerCtrl.fitInputWidthToTextLength(formattedValue.toString().length);
21370 }, 100)();
21371 }
21372
21373 return formattedValue;
21374 }
21375 }
21376
21377 /**
21378 * @param percent 0-1
21379 */
21380 function setSliderPercent(percent) {
21381
21382 percent = clamp(percent);
21383
21384 var thumbPosition = (percent * 100) + '%';
21385 var activeTrackPercent = invert ? (1 - percent) * 100 + '%' : thumbPosition;
21386
21387 if (vertical) {
21388 thumbContainer.css('bottom', thumbPosition);
21389 }
21390 else {
21391 $mdUtil.bidiProperty(thumbContainer, 'left', 'right', thumbPosition);
21392 }
21393
21394
21395 activeTrack.css(vertical ? 'height' : 'width', activeTrackPercent);
21396
21397 element.toggleClass((invert ? 'md-max' : 'md-min'), percent === 0);
21398 element.toggleClass((invert ? 'md-min' : 'md-max'), percent === 1);
21399 }
21400
21401 /**
21402 * Slide listeners
21403 */
21404 var isDragging = false;
21405
21406 function onPressDown(ev) {
21407 if (isDisabled()) return;
21408
21409 element.addClass('md-active');
21410 element[0].focus();
21411 refreshSliderDimensions();
21412
21413 var exactVal = percentToValue( positionToPercent( vertical ? ev.pointer.y : ev.pointer.x ));
21414 var closestVal = minMaxValidator( stepValidator(exactVal) );
21415 scope.$apply(function() {
21416 setModelValue( closestVal );
21417 setSliderPercent( valueToPercent(closestVal));
21418 });
21419 }
21420 function onPressUp(ev) {
21421 if (isDisabled()) return;
21422
21423 element.removeClass('md-dragging');
21424
21425 var exactVal = percentToValue( positionToPercent( vertical ? ev.pointer.y : ev.pointer.x ));
21426 var closestVal = minMaxValidator( stepValidator(exactVal) );
21427 scope.$apply(function() {
21428 setModelValue(closestVal);
21429 ngModelRender();
21430 });
21431 }
21432 function onDragStart(ev) {
21433 if (isDisabled()) return;
21434 isDragging = true;
21435
21436 ev.stopPropagation();
21437
21438 element.addClass('md-dragging');
21439 setSliderFromEvent(ev);
21440 }
21441 function onDrag(ev) {
21442 if (!isDragging) return;
21443 ev.stopPropagation();
21444 setSliderFromEvent(ev);
21445 }
21446 function onDragEnd(ev) {
21447 if (!isDragging) return;
21448 ev.stopPropagation();
21449 isDragging = false;
21450 }
21451
21452 function setSliderFromEvent(ev) {
21453 // While panning discrete, update only the
21454 // visual positioning but not the model value.
21455 if ( discrete ) adjustThumbPosition( vertical ? ev.pointer.y : ev.pointer.x );
21456 else doSlide( vertical ? ev.pointer.y : ev.pointer.x );
21457 }
21458
21459 /**
21460 * Slide the UI by changing the model value
21461 * @param x
21462 */
21463 function doSlide( x ) {
21464 scope.$evalAsync( function() {
21465 setModelValue( percentToValue( positionToPercent(x) ));
21466 });
21467 }
21468
21469 /**
21470 * Slide the UI without changing the model (while dragging/panning)
21471 * @param x
21472 */
21473 function adjustThumbPosition( x ) {
21474 var exactVal = percentToValue( positionToPercent( x ));
21475 var closestVal = minMaxValidator( stepValidator(exactVal) );
21476 setSliderPercent( positionToPercent(x) );
21477 thumbText.text( closestVal );
21478 }
21479
21480 /**
21481 * Clamps the value to be between 0 and 1.
21482 * @param {number} value The value to clamp.
21483 * @returns {number}
21484 */
21485 function clamp(value) {
21486 return Math.max(0, Math.min(value || 0, 1));
21487 }
21488
21489 /**
21490 * Convert position on slider to percentage value of offset from beginning...
21491 * @param position
21492 * @returns {number}
21493 */
21494 function positionToPercent( position ) {
21495 var offset = vertical ? sliderDimensions.top : sliderDimensions.left;
21496 var size = vertical ? sliderDimensions.height : sliderDimensions.width;
21497 var calc = (position - offset) / size;
21498
21499 if (!vertical && $mdUtil.bidi() === 'rtl') {
21500 calc = 1 - calc;
21501 }
21502
21503 return Math.max(0, Math.min(1, vertical ? 1 - calc : calc));
21504 }
21505
21506 /**
21507 * Convert percentage offset on slide to equivalent model value
21508 * @param percent
21509 * @returns {*}
21510 */
21511 function percentToValue( percent ) {
21512 var adjustedPercent = invert ? (1 - percent) : percent;
21513 return (min + adjustedPercent * (max - min));
21514 }
21515
21516 function valueToPercent( val ) {
21517 var percent = (val - min) / (max - min);
21518 return invert ? (1 - percent) : percent;
21519 }
21520 }
21521}
21522
21523})();
21524(function(){
21525"use strict";
21526
21527/**
21528 * @ngdoc module
21529 * @name material.components.sticky
21530 * @description
21531 * Sticky effects for md
21532 *
21533 */
21534MdSticky.$inject = ["$mdConstant", "$$rAF", "$mdUtil", "$compile"];
21535angular
21536 .module('material.components.sticky', [
21537 'material.core',
21538 'material.components.content'
21539 ])
21540 .factory('$mdSticky', MdSticky);
21541
21542/**
21543 * @ngdoc service
21544 * @name $mdSticky
21545 * @module material.components.sticky
21546 *
21547 * @description
21548 * The `$mdSticky`service provides a mixin to make elements sticky.
21549 *
21550 * Whenever the current browser supports stickiness natively, the `$mdSticky` service will just
21551 * use the native browser stickiness.
21552 *
21553 * By default the `$mdSticky` service compiles the cloned element, when not specified through the `elementClone`
21554 * parameter, in the same scope as the actual element lives.
21555 *
21556 *
21557 * <h3>Notes</h3>
21558 * When using an element which is containing a compiled directive, which changed its DOM structure during compilation,
21559 * you should compile the clone yourself using the plain template.<br/><br/>
21560 * See the right usage below:
21561 * <hljs lang="js">
21562 * angular.module('myModule')
21563 * .directive('stickySelect', function($mdSticky, $compile) {
21564 * var SELECT_TEMPLATE =
21565 * '<md-select ng-model="selected">' +
21566 * '<md-option>Option 1</md-option>' +
21567 * '</md-select>';
21568 *
21569 * return {
21570 * restrict: 'E',
21571 * replace: true,
21572 * template: SELECT_TEMPLATE,
21573 * link: function(scope,element) {
21574 * $mdSticky(scope, element, $compile(SELECT_TEMPLATE)(scope));
21575 * }
21576 * };
21577 * });
21578 * </hljs>
21579 *
21580 * @usage
21581 * <hljs lang="js">
21582 * angular.module('myModule')
21583 * .directive('stickyText', function($mdSticky, $compile) {
21584 * return {
21585 * restrict: 'E',
21586 * template: '<span>Sticky Text</span>',
21587 * link: function(scope,element) {
21588 * $mdSticky(scope, element);
21589 * }
21590 * };
21591 * });
21592 * </hljs>
21593 *
21594 * @returns A `$mdSticky` function that takes three arguments:
21595 * - `scope`
21596 * - `element`: The element that will be 'sticky'
21597 * - `elementClone`: A clone of the element, that will be shown
21598 * when the user starts scrolling past the original element.
21599 * If not provided, it will use the result of `element.clone()` and compiles it in the given scope.
21600 */
21601function MdSticky($mdConstant, $$rAF, $mdUtil, $compile) {
21602
21603 var browserStickySupport = $mdUtil.checkStickySupport();
21604
21605 /**
21606 * Registers an element as sticky, used internally by directives to register themselves
21607 */
21608 return function registerStickyElement(scope, element, stickyClone) {
21609 var contentCtrl = element.controller('mdContent');
21610 if (!contentCtrl) return;
21611
21612 if (browserStickySupport) {
21613 element.css({
21614 position: browserStickySupport,
21615 top: 0,
21616 'z-index': 2
21617 });
21618 } else {
21619 var $$sticky = contentCtrl.$element.data('$$sticky');
21620 if (!$$sticky) {
21621 $$sticky = setupSticky(contentCtrl);
21622 contentCtrl.$element.data('$$sticky', $$sticky);
21623 }
21624
21625 // Compile our cloned element, when cloned in this service, into the given scope.
21626 var cloneElement = stickyClone || $compile(element.clone())(scope);
21627
21628 var deregister = $$sticky.add(element, cloneElement);
21629 scope.$on('$destroy', deregister);
21630 }
21631 };
21632
21633 function setupSticky(contentCtrl) {
21634 var contentEl = contentCtrl.$element;
21635
21636 // Refresh elements is very expensive, so we use the debounced
21637 // version when possible.
21638 var debouncedRefreshElements = $$rAF.throttle(refreshElements);
21639
21640 // setupAugmentedScrollEvents gives us `$scrollstart` and `$scroll`,
21641 // more reliable than `scroll` on android.
21642 setupAugmentedScrollEvents(contentEl);
21643 contentEl.on('$scrollstart', debouncedRefreshElements);
21644 contentEl.on('$scroll', onScroll);
21645
21646 var self;
21647 return self = {
21648 prev: null,
21649 current: null, //the currently stickied item
21650 next: null,
21651 items: [],
21652 add: add,
21653 refreshElements: refreshElements
21654 };
21655
21656 /***************
21657 * Public
21658 ***************/
21659 // Add an element and its sticky clone to this content's sticky collection
21660 function add(element, stickyClone) {
21661 stickyClone.addClass('md-sticky-clone');
21662
21663 var item = {
21664 element: element,
21665 clone: stickyClone
21666 };
21667 self.items.push(item);
21668
21669 $mdUtil.nextTick(function() {
21670 contentEl.prepend(item.clone);
21671 });
21672
21673 debouncedRefreshElements();
21674
21675 return function remove() {
21676 self.items.forEach(function(item, index) {
21677 if (item.element[0] === element[0]) {
21678 self.items.splice(index, 1);
21679 item.clone.remove();
21680 }
21681 });
21682 debouncedRefreshElements();
21683 };
21684 }
21685
21686 function refreshElements() {
21687 // Sort our collection of elements by their current position in the DOM.
21688 // We need to do this because our elements' order of being added may not
21689 // be the same as their order of display.
21690 self.items.forEach(refreshPosition);
21691 self.items = self.items.sort(function(a, b) {
21692 return a.top < b.top ? -1 : 1;
21693 });
21694
21695 // Find which item in the list should be active,
21696 // based upon the content's current scroll position
21697 var item;
21698 var currentScrollTop = contentEl.prop('scrollTop');
21699 for (var i = self.items.length - 1; i >= 0; i--) {
21700 if (currentScrollTop > self.items[i].top) {
21701 item = self.items[i];
21702 break;
21703 }
21704 }
21705 setCurrentItem(item);
21706 }
21707
21708 /***************
21709 * Private
21710 ***************/
21711
21712 // Find the `top` of an item relative to the content element,
21713 // and also the height.
21714 function refreshPosition(item) {
21715 // Find the top of an item by adding to the offsetHeight until we reach the
21716 // content element.
21717 var current = item.element[0];
21718 item.top = 0;
21719 item.left = 0;
21720 item.right = 0;
21721 while (current && current !== contentEl[0]) {
21722 item.top += current.offsetTop;
21723 item.left += current.offsetLeft;
21724 if ( current.offsetParent ){
21725 item.right += current.offsetParent.offsetWidth - current.offsetWidth - current.offsetLeft; //Compute offsetRight
21726 }
21727 current = current.offsetParent;
21728 }
21729 item.height = item.element.prop('offsetHeight');
21730
21731 var defaultVal = $mdUtil.floatingScrollbars() ? '0' : undefined;
21732 $mdUtil.bidi(item.clone, 'margin-left', item.left, defaultVal);
21733 $mdUtil.bidi(item.clone, 'margin-right', defaultVal, item.right);
21734 }
21735
21736 // As we scroll, push in and select the correct sticky element.
21737 function onScroll() {
21738 var scrollTop = contentEl.prop('scrollTop');
21739 var isScrollingDown = scrollTop > (onScroll.prevScrollTop || 0);
21740
21741 // Store the previous scroll so we know which direction we are scrolling
21742 onScroll.prevScrollTop = scrollTop;
21743
21744 //
21745 // AT TOP (not scrolling)
21746 //
21747 if (scrollTop === 0) {
21748 // If we're at the top, just clear the current item and return
21749 setCurrentItem(null);
21750 return;
21751 }
21752
21753 //
21754 // SCROLLING DOWN (going towards the next item)
21755 //
21756 if (isScrollingDown) {
21757
21758 // If we've scrolled down past the next item's position, sticky it and return
21759 if (self.next && self.next.top <= scrollTop) {
21760 setCurrentItem(self.next);
21761 return;
21762 }
21763
21764 // If the next item is close to the current one, push the current one up out of the way
21765 if (self.current && self.next && self.next.top - scrollTop <= self.next.height) {
21766 translate(self.current, scrollTop + (self.next.top - self.next.height - scrollTop));
21767 return;
21768 }
21769 }
21770
21771 //
21772 // SCROLLING UP (not at the top & not scrolling down; must be scrolling up)
21773 //
21774 if (!isScrollingDown) {
21775
21776 // If we've scrolled up past the previous item's position, sticky it and return
21777 if (self.current && self.prev && scrollTop < self.current.top) {
21778 setCurrentItem(self.prev);
21779 return;
21780 }
21781
21782 // If the next item is close to the current one, pull the current one down into view
21783 if (self.next && self.current && (scrollTop >= (self.next.top - self.current.height))) {
21784 translate(self.current, scrollTop + (self.next.top - scrollTop - self.current.height));
21785 return;
21786 }
21787 }
21788
21789 //
21790 // Otherwise, just move the current item to the proper place (scrolling up or down)
21791 //
21792 if (self.current) {
21793 translate(self.current, scrollTop);
21794 }
21795 }
21796
21797 function setCurrentItem(item) {
21798 if (self.current === item) return;
21799 // Deactivate currently active item
21800 if (self.current) {
21801 translate(self.current, null);
21802 setStickyState(self.current, null);
21803 }
21804
21805 // Activate new item if given
21806 if (item) {
21807 setStickyState(item, 'active');
21808 }
21809
21810 self.current = item;
21811 var index = self.items.indexOf(item);
21812 // If index === -1, index + 1 = 0. It works out.
21813 self.next = self.items[index + 1];
21814 self.prev = self.items[index - 1];
21815 setStickyState(self.next, 'next');
21816 setStickyState(self.prev, 'prev');
21817 }
21818
21819 function setStickyState(item, state) {
21820 if (!item || item.state === state) return;
21821 if (item.state) {
21822 item.clone.attr('sticky-prev-state', item.state);
21823 item.element.attr('sticky-prev-state', item.state);
21824 }
21825 item.clone.attr('sticky-state', state);
21826 item.element.attr('sticky-state', state);
21827 item.state = state;
21828 }
21829
21830 function translate(item, amount) {
21831 if (!item) return;
21832 if (amount === null || amount === undefined) {
21833 if (item.translateY) {
21834 item.translateY = null;
21835 item.clone.css($mdConstant.CSS.TRANSFORM, '');
21836 }
21837 } else {
21838 item.translateY = amount;
21839
21840 $mdUtil.bidi( item.clone, $mdConstant.CSS.TRANSFORM,
21841 'translate3d(' + item.left + 'px,' + amount + 'px,0)',
21842 'translateY(' + amount + 'px)'
21843 );
21844 }
21845 }
21846 }
21847
21848
21849 // Android 4.4 don't accurately give scroll events.
21850 // To fix this problem, we setup a fake scroll event. We say:
21851 // > If a scroll or touchmove event has happened in the last DELAY milliseconds,
21852 // then send a `$scroll` event every animationFrame.
21853 // Additionally, we add $scrollstart and $scrollend events.
21854 function setupAugmentedScrollEvents(element) {
21855 var SCROLL_END_DELAY = 200;
21856 var isScrolling;
21857 var lastScrollTime;
21858 element.on('scroll touchmove', function() {
21859 if (!isScrolling) {
21860 isScrolling = true;
21861 $$rAF.throttle(loopScrollEvent);
21862 element.triggerHandler('$scrollstart');
21863 }
21864 element.triggerHandler('$scroll');
21865 lastScrollTime = +$mdUtil.now();
21866 });
21867
21868 function loopScrollEvent() {
21869 if (+$mdUtil.now() - lastScrollTime > SCROLL_END_DELAY) {
21870 isScrolling = false;
21871 element.triggerHandler('$scrollend');
21872 } else {
21873 element.triggerHandler('$scroll');
21874 $$rAF.throttle(loopScrollEvent);
21875 }
21876 }
21877 }
21878
21879}
21880
21881})();
21882(function(){
21883"use strict";
21884
21885/**
21886 * @ngdoc module
21887 * @name material.components.subheader
21888 * @description
21889 * SubHeader module
21890 *
21891 * Subheaders are special list tiles that delineate distinct sections of a
21892 * list or grid list and are typically related to the current filtering or
21893 * sorting criteria. Subheader tiles are either displayed inline with tiles or
21894 * can be associated with content, for example, in an adjacent column.
21895 *
21896 * Upon scrolling, subheaders remain pinned to the top of the screen and remain
21897 * pinned until pushed on or off screen by the next subheader. @see [Material
21898 * Design Specifications](https://www.google.com/design/spec/components/subheaders.html)
21899 *
21900 * > To improve the visual grouping of content, use the system color for your subheaders.
21901 *
21902 */
21903MdSubheaderDirective.$inject = ["$mdSticky", "$compile", "$mdTheming", "$mdUtil", "$mdAria"];
21904angular
21905 .module('material.components.subheader', [
21906 'material.core',
21907 'material.components.sticky'
21908 ])
21909 .directive('mdSubheader', MdSubheaderDirective);
21910
21911/**
21912 * @ngdoc directive
21913 * @name mdSubheader
21914 * @module material.components.subheader
21915 *
21916 * @restrict E
21917 *
21918 * @description
21919 * The `md-subheader` directive creates a sticky subheader for a section.
21920 *
21921 * Developers are able to disable the stickiness of the subheader by using the following markup
21922 *
21923 * <hljs lang="html">
21924 * <md-subheader class="md-no-sticky">Not Sticky</md-subheader>
21925 * </hljs>
21926 *
21927 * ### Notes
21928 * - The `md-subheader` directive uses the <a ng-href="api/service/$mdSticky">$mdSticky</a> service
21929 * to make the subheader sticky.
21930 *
21931 * > Whenever the current browser doesn't support stickiness natively, the subheader
21932 * will be compiled twice to create a sticky clone of the subheader.
21933 *
21934 * @usage
21935 * <hljs lang="html">
21936 * <md-subheader>Online Friends</md-subheader>
21937 * </hljs>
21938 */
21939
21940function MdSubheaderDirective($mdSticky, $compile, $mdTheming, $mdUtil, $mdAria) {
21941 return {
21942 restrict: 'E',
21943 replace: true,
21944 transclude: true,
21945 template: (
21946 '<div class="md-subheader _md">' +
21947 ' <div class="md-subheader-inner">' +
21948 ' <div class="md-subheader-content"></div>' +
21949 ' </div>' +
21950 '</div>'
21951 ),
21952 link: function postLink(scope, element, attr, controllers, transclude) {
21953 $mdTheming(element);
21954 element.addClass('_md');
21955
21956 // Remove the ngRepeat attribute from the root element, because we don't want to compile
21957 // the ngRepeat for the sticky clone again.
21958 $mdUtil.prefixer().removeAttribute(element, 'ng-repeat');
21959
21960 var outerHTML = element[0].outerHTML;
21961
21962 function getContent(el) {
21963 return angular.element(el[0].querySelector('.md-subheader-content'));
21964 }
21965
21966 // Set the ARIA attributes on the original element since it keeps it's original place in
21967 // the DOM, whereas the clones are in reverse order. Should be done after the outerHTML,
21968 // in order to avoid having multiple element be marked as headers.
21969 attr.$set('role', 'heading');
21970 $mdAria.expect(element, 'aria-level', '2');
21971
21972 // Transclude the user-given contents of the subheader
21973 // the conventional way.
21974 transclude(scope, function(clone) {
21975 getContent(element).append(clone);
21976 });
21977
21978 // Create another clone, that uses the outer and inner contents
21979 // of the element, that will be 'stickied' as the user scrolls.
21980 if (!element.hasClass('md-no-sticky')) {
21981 transclude(scope, function(clone) {
21982 // If the user adds an ng-if or ng-repeat directly to the md-subheader element, the
21983 // compiled clone below will only be a comment tag (since they replace their elements with
21984 // a comment) which cannot be properly passed to the $mdSticky; so we wrap it in our own
21985 // DIV to ensure we have something $mdSticky can use
21986 var wrapper = $compile('<div class="md-subheader-wrapper" aria-hidden="true">' + outerHTML + '</div>')(scope);
21987
21988 // Delay initialization until after any `ng-if`/`ng-repeat`/etc has finished before
21989 // attempting to create the clone
21990 $mdUtil.nextTick(function() {
21991 // Append our transcluded clone into the wrapper.
21992 // We don't have to recompile the element again, because the clone is already
21993 // compiled in it's transclusion scope. If we recompile the outerHTML of the new clone, we would lose
21994 // our ngIf's and other previous registered bindings / properties.
21995 getContent(wrapper).append(clone);
21996 });
21997
21998 // Make the element sticky and provide the stickyClone our self, to avoid recompilation of the subheader
21999 // element.
22000 $mdSticky(scope, element, wrapper);
22001 });
22002 }
22003 }
22004 };
22005}
22006
22007})();
22008(function(){
22009"use strict";
22010
22011/**
22012 * @ngdoc module
22013 * @name material.components.swipe
22014 * @description Swipe module!
22015 */
22016/**
22017 * @ngdoc directive
22018 * @module material.components.swipe
22019 * @name mdSwipeLeft
22020 *
22021 * @restrict A
22022 *
22023 * @description
22024 * The md-swipe-left directive allows you to specify custom behavior when an element is swiped
22025 * left.
22026 *
22027 * @usage
22028 * <hljs lang="html">
22029 * <div md-swipe-left="onSwipeLeft()">Swipe me left!</div>
22030 * </hljs>
22031 */
22032/**
22033 * @ngdoc directive
22034 * @module material.components.swipe
22035 * @name mdSwipeRight
22036 *
22037 * @restrict A
22038 *
22039 * @description
22040 * The md-swipe-right directive allows you to specify custom behavior when an element is swiped
22041 * right.
22042 *
22043 * @usage
22044 * <hljs lang="html">
22045 * <div md-swipe-right="onSwipeRight()">Swipe me right!</div>
22046 * </hljs>
22047 */
22048/**
22049 * @ngdoc directive
22050 * @module material.components.swipe
22051 * @name mdSwipeUp
22052 *
22053 * @restrict A
22054 *
22055 * @description
22056 * The md-swipe-up directive allows you to specify custom behavior when an element is swiped
22057 * up.
22058 *
22059 * @usage
22060 * <hljs lang="html">
22061 * <div md-swipe-up="onSwipeUp()">Swipe me up!</div>
22062 * </hljs>
22063 */
22064/**
22065 * @ngdoc directive
22066 * @module material.components.swipe
22067 * @name mdSwipeDown
22068 *
22069 * @restrict A
22070 *
22071 * @description
22072 * The md-swipe-down directive allows you to specify custom behavior when an element is swiped
22073 * down.
22074 *
22075 * @usage
22076 * <hljs lang="html">
22077 * <div md-swipe-down="onSwipDown()">Swipe me down!</div>
22078 * </hljs>
22079 */
22080
22081angular.module('material.components.swipe', ['material.core'])
22082 .directive('mdSwipeLeft', getDirective('SwipeLeft'))
22083 .directive('mdSwipeRight', getDirective('SwipeRight'))
22084 .directive('mdSwipeUp', getDirective('SwipeUp'))
22085 .directive('mdSwipeDown', getDirective('SwipeDown'));
22086
22087function getDirective(name) {
22088 DirectiveFactory.$inject = ["$parse"];
22089 var directiveName = 'md' + name;
22090 var eventName = '$md.' + name.toLowerCase();
22091
22092 return DirectiveFactory;
22093
22094 /* @ngInject */
22095 function DirectiveFactory($parse) {
22096 return { restrict: 'A', link: postLink };
22097 function postLink(scope, element, attr) {
22098 element.css('touch-action', 'none');
22099
22100 var fn = $parse(attr[directiveName]);
22101 element.on(eventName, function(ev) {
22102 scope.$applyAsync(function() { fn(scope, { $event: ev }); });
22103 });
22104 }
22105 }
22106}
22107
22108
22109
22110})();
22111(function(){
22112"use strict";
22113
22114/**
22115 * @ngdoc module
22116 * @name material.components.switch
22117 */
22118
22119MdSwitch.$inject = ["mdCheckboxDirective", "$mdUtil", "$mdConstant", "$parse", "$$rAF", "$mdGesture", "$timeout"];
22120angular.module('material.components.switch', [
22121 'material.core',
22122 'material.components.checkbox'
22123])
22124 .directive('mdSwitch', MdSwitch);
22125
22126/**
22127 * @ngdoc directive
22128 * @module material.components.switch
22129 * @name mdSwitch
22130 * @restrict E
22131 *
22132 * The switch directive is used very much like the normal [angular checkbox](https://docs.angularjs.org/api/ng/input/input%5Bcheckbox%5D).
22133 *
22134 * As per the [material design spec](http://www.google.com/design/spec/style/color.html#color-ui-color-application)
22135 * the switch is in the accent color by default. The primary color palette may be used with
22136 * the `md-primary` class.
22137 *
22138 * @param {string} ng-model Assignable angular expression to data-bind to.
22139 * @param {string=} name Property name of the form under which the control is published.
22140 * @param {expression=} ng-true-value The value to which the expression should be set when selected.
22141 * @param {expression=} ng-false-value The value to which the expression should be set when not selected.
22142 * @param {string=} ng-change Angular expression to be executed when input changes due to user interaction with the input element.
22143 * @param {expression=} ng-disabled En/Disable based on the expression.
22144 * @param {boolean=} md-no-ink Use of attribute indicates use of ripple ink effects.
22145 * @param {string=} aria-label Publish the button label used by screen-readers for accessibility. Defaults to the switch's text.
22146 * @param {boolean=} md-invert When set to true, the switch will be inverted.
22147 *
22148 * @usage
22149 * <hljs lang="html">
22150 * <md-switch ng-model="isActive" aria-label="Finished?">
22151 * Finished ?
22152 * </md-switch>
22153 *
22154 * <md-switch md-no-ink ng-model="hasInk" aria-label="No Ink Effects">
22155 * No Ink Effects
22156 * </md-switch>
22157 *
22158 * <md-switch ng-disabled="true" ng-model="isDisabled" aria-label="Disabled">
22159 * Disabled
22160 * </md-switch>
22161 *
22162 * </hljs>
22163 */
22164function MdSwitch(mdCheckboxDirective, $mdUtil, $mdConstant, $parse, $$rAF, $mdGesture, $timeout) {
22165 var checkboxDirective = mdCheckboxDirective[0];
22166
22167 return {
22168 restrict: 'E',
22169 priority: $mdConstant.BEFORE_NG_ARIA,
22170 transclude: true,
22171 template:
22172 '<div class="md-container">' +
22173 '<div class="md-bar"></div>' +
22174 '<div class="md-thumb-container">' +
22175 '<div class="md-thumb" md-ink-ripple md-ink-ripple-checkbox></div>' +
22176 '</div>'+
22177 '</div>' +
22178 '<div ng-transclude class="md-label"></div>',
22179 require: ['^?mdInputContainer', '?ngModel', '?^form'],
22180 compile: mdSwitchCompile
22181 };
22182
22183 function mdSwitchCompile(element, attr) {
22184 var checkboxLink = checkboxDirective.compile(element, attr).post;
22185 // No transition on initial load.
22186 element.addClass('md-dragging');
22187
22188 return function (scope, element, attr, ctrls) {
22189 var containerCtrl = ctrls[0];
22190 var ngModel = ctrls[1] || $mdUtil.fakeNgModel();
22191 var formCtrl = ctrls[2];
22192
22193 var disabledGetter = null;
22194 if (attr.disabled != null) {
22195 disabledGetter = function() { return true; };
22196 } else if (attr.ngDisabled) {
22197 disabledGetter = $parse(attr.ngDisabled);
22198 }
22199
22200 var thumbContainer = angular.element(element[0].querySelector('.md-thumb-container'));
22201 var switchContainer = angular.element(element[0].querySelector('.md-container'));
22202 var labelContainer = angular.element(element[0].querySelector('.md-label'));
22203
22204 // no transition on initial load
22205 $$rAF(function() {
22206 element.removeClass('md-dragging');
22207 });
22208
22209 checkboxLink(scope, element, attr, ctrls);
22210
22211 if (disabledGetter) {
22212 scope.$watch(disabledGetter, function(isDisabled) {
22213 element.attr('tabindex', isDisabled ? -1 : 0);
22214 });
22215 }
22216
22217 attr.$observe('mdInvert', function(newValue) {
22218 var isInverted = $mdUtil.parseAttributeBoolean(newValue);
22219
22220 isInverted ? element.prepend(labelContainer) : element.prepend(switchContainer);
22221
22222 // Toggle a CSS class to update the margin.
22223 element.toggleClass('md-inverted', isInverted);
22224 });
22225
22226 // These events are triggered by setup drag
22227 $mdGesture.register(switchContainer, 'drag');
22228 switchContainer
22229 .on('$md.dragstart', onDragStart)
22230 .on('$md.drag', onDrag)
22231 .on('$md.dragend', onDragEnd);
22232
22233 var drag;
22234 function onDragStart(ev) {
22235 // Don't go if the switch is disabled.
22236 if (disabledGetter && disabledGetter(scope)) return;
22237 ev.stopPropagation();
22238
22239 element.addClass('md-dragging');
22240 drag = {width: thumbContainer.prop('offsetWidth')};
22241 }
22242
22243 function onDrag(ev) {
22244 if (!drag) return;
22245 ev.stopPropagation();
22246 ev.srcEvent && ev.srcEvent.preventDefault();
22247
22248 var percent = ev.pointer.distanceX / drag.width;
22249
22250 //if checked, start from right. else, start from left
22251 var translate = ngModel.$viewValue ? 1 + percent : percent;
22252 // Make sure the switch stays inside its bounds, 0-1%
22253 translate = Math.max(0, Math.min(1, translate));
22254
22255 thumbContainer.css($mdConstant.CSS.TRANSFORM, 'translate3d(' + (100*translate) + '%,0,0)');
22256 drag.translate = translate;
22257 }
22258
22259 function onDragEnd(ev) {
22260 if (!drag) return;
22261 ev.stopPropagation();
22262
22263 element.removeClass('md-dragging');
22264 thumbContainer.css($mdConstant.CSS.TRANSFORM, '');
22265
22266 // We changed if there is no distance (this is a click a click),
22267 // or if the drag distance is >50% of the total.
22268 var isChanged = ngModel.$viewValue ? drag.translate < 0.5 : drag.translate > 0.5;
22269 if (isChanged) {
22270 applyModelValue(!ngModel.$viewValue);
22271 }
22272 drag = null;
22273
22274 // Wait for incoming mouse click
22275 scope.skipToggle = true;
22276 $timeout(function() {
22277 scope.skipToggle = false;
22278 }, 1);
22279 }
22280
22281 function applyModelValue(newValue) {
22282 scope.$apply(function() {
22283 ngModel.$setViewValue(newValue);
22284 ngModel.$render();
22285 });
22286 }
22287
22288 };
22289 }
22290
22291
22292}
22293
22294})();
22295(function(){
22296"use strict";
22297
22298/**
22299 * @ngdoc module
22300 * @name material.components.toast
22301 * @description
22302 * Toast
22303 */
22304MdToastDirective.$inject = ["$mdToast"];
22305MdToastProvider.$inject = ["$$interimElementProvider"];
22306angular.module('material.components.toast', [
22307 'material.core',
22308 'material.components.button'
22309])
22310 .directive('mdToast', MdToastDirective)
22311 .provider('$mdToast', MdToastProvider);
22312
22313/* @ngInject */
22314function MdToastDirective($mdToast) {
22315 return {
22316 restrict: 'E',
22317 link: function postLink(scope, element) {
22318 element.addClass('_md'); // private md component indicator for styling
22319
22320 // When navigation force destroys an interimElement, then
22321 // listen and $destroy() that interim instance...
22322 scope.$on('$destroy', function() {
22323 $mdToast.destroy();
22324 });
22325 }
22326 };
22327}
22328
22329/**
22330 * @ngdoc service
22331 * @name $mdToast
22332 * @module material.components.toast
22333 *
22334 * @description
22335 * `$mdToast` is a service to build a toast notification on any position
22336 * on the screen with an optional duration, and provides a simple promise API.
22337 *
22338 * The toast will be always positioned at the `bottom`, when the screen size is
22339 * between `600px` and `959px` (`sm` breakpoint)
22340 *
22341 * ## Restrictions on custom toasts
22342 * - The toast's template must have an outer `<md-toast>` element.
22343 * - For a toast action, use element with class `md-action`.
22344 * - Add the class `md-capsule` for curved corners.
22345 *
22346 * ### Custom Presets
22347 * Developers are also able to create their own preset, which can be easily used without repeating
22348 * their options each time.
22349 *
22350 * <hljs lang="js">
22351 * $mdToastProvider.addPreset('testPreset', {
22352 * options: function() {
22353 * return {
22354 * template:
22355 * '<md-toast>' +
22356 * '<div class="md-toast-content">' +
22357 * 'This is a custom preset' +
22358 * '</div>' +
22359 * '</md-toast>',
22360 * controllerAs: 'toast',
22361 * bindToController: true
22362 * };
22363 * }
22364 * });
22365 * </hljs>
22366 *
22367 * After you created your preset at config phase, you can easily access it.
22368 *
22369 * <hljs lang="js">
22370 * $mdToast.show(
22371 * $mdToast.testPreset()
22372 * );
22373 * </hljs>
22374 *
22375 * ## Parent container notes
22376 *
22377 * The toast is positioned using absolute positioning relative to its first non-static parent
22378 * container. Thus, if the requested parent container uses static positioning, we will temporarily
22379 * set its positioning to `relative` while the toast is visible and reset it when the toast is
22380 * hidden.
22381 *
22382 * Because of this, it is usually best to ensure that the parent container has a fixed height and
22383 * prevents scrolling by setting the `overflow: hidden;` style. Since the position is based off of
22384 * the parent's height, the toast may be mispositioned if you allow the parent to scroll.
22385 *
22386 * You can, however, have a scrollable element inside of the container; just make sure the
22387 * container itself does not scroll.
22388 *
22389 * <hljs lang="html">
22390 * <div layout-fill id="toast-container">
22391 * <md-content>
22392 * I can have lots of content and scroll!
22393 * </md-content>
22394 * </div>
22395 * </hljs>
22396 *
22397 * @usage
22398 * <hljs lang="html">
22399 * <div ng-controller="MyController">
22400 * <md-button ng-click="openToast()">
22401 * Open a Toast!
22402 * </md-button>
22403 * </div>
22404 * </hljs>
22405 *
22406 * <hljs lang="js">
22407 * var app = angular.module('app', ['ngMaterial']);
22408 * app.controller('MyController', function($scope, $mdToast) {
22409 * $scope.openToast = function($event) {
22410 * $mdToast.show($mdToast.simple().textContent('Hello!'));
22411 * // Could also do $mdToast.showSimple('Hello');
22412 * };
22413 * });
22414 * </hljs>
22415 */
22416
22417/**
22418 * @ngdoc method
22419 * @name $mdToast#showSimple
22420 *
22421 * @param {string} message The message to display inside the toast
22422 * @description
22423 * Convenience method which builds and shows a simple toast.
22424 *
22425 * @returns {promise} A promise that can be resolved with `$mdToast.hide()` or
22426 * rejected with `$mdToast.cancel()`.
22427 *
22428 */
22429
22430 /**
22431 * @ngdoc method
22432 * @name $mdToast#simple
22433 *
22434 * @description
22435 * Builds a preconfigured toast.
22436 *
22437 * @returns {obj} a `$mdToastPreset` with the following chainable configuration methods.
22438 *
22439 * _**Note:** These configuration methods are provided in addition to the methods provided by
22440 * the `build()` and `show()` methods below._
22441 *
22442 * <table class="md-api-table methods">
22443 * <thead>
22444 * <tr>
22445 * <th>Method</th>
22446 * <th>Description</th>
22447 * </tr>
22448 * </thead>
22449 * <tbody>
22450 * <tr>
22451 * <td>`.textContent(string)`</td>
22452 * <td>Sets the toast content to the specified string</td>
22453 * </tr>
22454 * <tr>
22455 * <td>`.action(string)`</td>
22456 * <td>
22457 * Adds an action button. <br/>
22458 * If clicked, the promise (returned from `show()`)
22459 * will resolve with the value `'ok'`; otherwise, it is resolved with `true` after a `hideDelay`
22460 * timeout
22461 * </td>
22462 * </tr>
22463 * <tr>
22464 * <td>`.highlightAction(boolean)`</td>
22465 * <td>
22466 * Whether or not the action button will have an additional highlight class.<br/>
22467 * By default the `accent` color will be applied to the action button.
22468 * </td>
22469 * </tr>
22470 * <tr>
22471 * <td>`.highlightClass(string)`</td>
22472 * <td>
22473 * If set, the given class will be applied to the highlighted action button.<br/>
22474 * This allows you to specify the highlight color easily. Highlight classes are `md-primary`, `md-warn`
22475 * and `md-accent`
22476 * </td>
22477 * </tr>
22478 * <tr>
22479 * <td>`.capsule(boolean)`</td>
22480 * <td>Whether or not to add the `md-capsule` class to the toast to provide rounded corners</td>
22481 * </tr>
22482 * <tr>
22483 * <td>`.theme(string)`</td>
22484 * <td>Sets the theme on the toast to the requested theme. Default is `$mdThemingProvider`'s default.</td>
22485 * </tr>
22486 * <tr>
22487 * <td>`.toastClass(string)`</td>
22488 * <td>Sets a class on the toast element</td>
22489 * </tr>
22490 * </tbody>
22491 * </table>
22492 *
22493 */
22494
22495/**
22496 * @ngdoc method
22497 * @name $mdToast#updateTextContent
22498 *
22499 * @description
22500 * Updates the content of an existing toast. Useful for updating things like counts, etc.
22501 *
22502 */
22503
22504 /**
22505 * @ngdoc method
22506 * @name $mdToast#build
22507 *
22508 * @description
22509 * Creates a custom `$mdToastPreset` that you can configure.
22510 *
22511 * @returns {obj} a `$mdToastPreset` with the chainable configuration methods for shows' options (see below).
22512 */
22513
22514 /**
22515 * @ngdoc method
22516 * @name $mdToast#show
22517 *
22518 * @description Shows the toast.
22519 *
22520 * @param {object} optionsOrPreset Either provide an `$mdToastPreset` returned from `simple()`
22521 * and `build()`, or an options object with the following properties:
22522 *
22523 * - `templateUrl` - `{string=}`: The url of an html template file that will
22524 * be used as the content of the toast. Restrictions: the template must
22525 * have an outer `md-toast` element.
22526 * - `template` - `{string=}`: Same as templateUrl, except this is an actual
22527 * template string.
22528 * - `autoWrap` - `{boolean=}`: Whether or not to automatically wrap the template content with a
22529 * `<div class="md-toast-content">` if one is not provided. Defaults to true. Can be disabled if you provide a
22530 * custom toast directive.
22531 * - `scope` - `{object=}`: the scope to link the template / controller to. If none is specified, it will create a new child scope.
22532 * This scope will be destroyed when the toast is removed unless `preserveScope` is set to true.
22533 * - `preserveScope` - `{boolean=}`: whether to preserve the scope when the element is removed. Default is false
22534 * - `hideDelay` - `{number=}`: How many milliseconds the toast should stay
22535 * active before automatically closing. Set to 0 or false to have the toast stay open until
22536 * closed manually. Default: 3000.
22537 * - `position` - `{string=}`: Sets the position of the toast. <br/>
22538 * Available: any combination of `'bottom'`, `'left'`, `'top'`, `'right'`, `'end'` and `'start'`.
22539 * The properties `'end'` and `'start'` are dynamic and can be used for RTL support.<br/>
22540 * Default combination: `'bottom left'`.
22541 * - `toastClass` - `{string=}`: A class to set on the toast element.
22542 * - `controller` - `{string=}`: The controller to associate with this toast.
22543 * The controller will be injected the local `$mdToast.hide( )`, which is a function
22544 * used to hide the toast.
22545 * - `locals` - `{string=}`: An object containing key/value pairs. The keys will
22546 * be used as names of values to inject into the controller. For example,
22547 * `locals: {three: 3}` would inject `three` into the controller with the value
22548 * of 3.
22549 * - `bindToController` - `bool`: bind the locals to the controller, instead of passing them in.
22550 * - `resolve` - `{object=}`: Similar to locals, except it takes promises as values
22551 * and the toast will not open until the promises resolve.
22552 * - `controllerAs` - `{string=}`: An alias to assign the controller to on the scope.
22553 * - `parent` - `{element=}`: The element to append the toast to. Defaults to appending
22554 * to the root element of the application.
22555 *
22556 * @returns {promise} A promise that can be resolved with `$mdToast.hide()` or
22557 * rejected with `$mdToast.cancel()`. `$mdToast.hide()` will resolve either with a Boolean
22558 * value == 'true' or the value passed as an argument to `$mdToast.hide()`.
22559 * And `$mdToast.cancel()` will resolve the promise with a Boolean value == 'false'
22560 */
22561
22562/**
22563 * @ngdoc method
22564 * @name $mdToast#hide
22565 *
22566 * @description
22567 * Hide an existing toast and resolve the promise returned from `$mdToast.show()`.
22568 *
22569 * @param {*=} response An argument for the resolved promise.
22570 *
22571 * @returns {promise} a promise that is called when the existing element is removed from the DOM.
22572 * The promise is resolved with either a Boolean value == 'true' or the value passed as the
22573 * argument to `.hide()`.
22574 *
22575 */
22576
22577/**
22578 * @ngdoc method
22579 * @name $mdToast#cancel
22580 *
22581 * @description
22582 * `DEPRECATED` - The promise returned from opening a toast is used only to notify about the closing of the toast.
22583 * As such, there isn't any reason to also allow that promise to be rejected,
22584 * since it's not clear what the difference between resolve and reject would be.
22585 *
22586 * Hide the existing toast and reject the promise returned from
22587 * `$mdToast.show()`.
22588 *
22589 * @param {*=} response An argument for the rejected promise.
22590 *
22591 * @returns {promise} a promise that is called when the existing element is removed from the DOM
22592 * The promise is resolved with a Boolean value == 'false'.
22593 *
22594 */
22595
22596function MdToastProvider($$interimElementProvider) {
22597 // Differentiate promise resolves: hide timeout (value == true) and hide action clicks (value == ok).
22598 toastDefaultOptions.$inject = ["$animate", "$mdToast", "$mdUtil", "$mdMedia"];
22599 var ACTION_RESOLVE = 'ok';
22600
22601 var activeToastContent;
22602 var $mdToast = $$interimElementProvider('$mdToast')
22603 .setDefaults({
22604 methods: ['position', 'hideDelay', 'capsule', 'parent', 'position', 'toastClass'],
22605 options: toastDefaultOptions
22606 })
22607 .addPreset('simple', {
22608 argOption: 'textContent',
22609 methods: ['textContent', 'content', 'action', 'highlightAction', 'highlightClass', 'theme', 'parent' ],
22610 options: /* @ngInject */ ["$mdToast", "$mdTheming", function($mdToast, $mdTheming) {
22611 return {
22612 template:
22613 '<md-toast md-theme="{{ toast.theme }}" ng-class="{\'md-capsule\': toast.capsule}">' +
22614 ' <div class="md-toast-content">' +
22615 ' <span class="md-toast-text" role="alert" aria-relevant="all" aria-atomic="true">' +
22616 ' {{ toast.content }}' +
22617 ' </span>' +
22618 ' <md-button class="md-action" ng-if="toast.action" ng-click="toast.resolve()" ' +
22619 ' ng-class="highlightClasses">' +
22620 ' {{ toast.action }}' +
22621 ' </md-button>' +
22622 ' </div>' +
22623 '</md-toast>',
22624 controller: /* @ngInject */ ["$scope", function mdToastCtrl($scope) {
22625 var self = this;
22626
22627 if (self.highlightAction) {
22628 $scope.highlightClasses = [
22629 'md-highlight',
22630 self.highlightClass
22631 ]
22632 }
22633
22634 $scope.$watch(function() { return activeToastContent; }, function() {
22635 self.content = activeToastContent;
22636 });
22637
22638 this.resolve = function() {
22639 $mdToast.hide( ACTION_RESOLVE );
22640 };
22641 }],
22642 theme: $mdTheming.defaultTheme(),
22643 controllerAs: 'toast',
22644 bindToController: true
22645 };
22646 }]
22647 })
22648 .addMethod('updateTextContent', updateTextContent)
22649 .addMethod('updateContent', updateTextContent);
22650
22651 function updateTextContent(newContent) {
22652 activeToastContent = newContent;
22653 }
22654
22655 return $mdToast;
22656
22657 /* @ngInject */
22658 function toastDefaultOptions($animate, $mdToast, $mdUtil, $mdMedia) {
22659 var SWIPE_EVENTS = '$md.swipeleft $md.swiperight $md.swipeup $md.swipedown';
22660 return {
22661 onShow: onShow,
22662 onRemove: onRemove,
22663 toastClass: '',
22664 position: 'bottom left',
22665 themable: true,
22666 hideDelay: 3000,
22667 autoWrap: true,
22668 transformTemplate: function(template, options) {
22669 var shouldAddWrapper = options.autoWrap && template && !/md-toast-content/g.test(template);
22670
22671 if (shouldAddWrapper) {
22672 // Root element of template will be <md-toast>. We need to wrap all of its content inside of
22673 // of <div class="md-toast-content">. All templates provided here should be static, developer-controlled
22674 // content (meaning we're not attempting to guard against XSS).
22675 var templateRoot = document.createElement('md-template');
22676 templateRoot.innerHTML = template;
22677
22678 // Iterate through all root children, to detect possible md-toast directives.
22679 for (var i = 0; i < templateRoot.children.length; i++) {
22680 if (templateRoot.children[i].nodeName === 'MD-TOAST') {
22681 var wrapper = angular.element('<div class="md-toast-content">');
22682
22683 // Wrap the children of the `md-toast` directive in jqLite, to be able to append multiple
22684 // nodes with the same execution.
22685 wrapper.append(angular.element(templateRoot.children[i].childNodes));
22686
22687 // Append the new wrapped element to the `md-toast` directive.
22688 templateRoot.children[i].appendChild(wrapper[0]);
22689 }
22690 }
22691
22692 // We have to return the innerHTMl, because we do not want to have the `md-template` element to be
22693 // the root element of our interimElement.
22694 return templateRoot.innerHTML;
22695 }
22696
22697 return template || '';
22698 }
22699 };
22700
22701 function onShow(scope, element, options) {
22702 activeToastContent = options.textContent || options.content; // support deprecated #content method
22703
22704 var isSmScreen = !$mdMedia('gt-sm');
22705
22706 element = $mdUtil.extractElementByName(element, 'md-toast', true);
22707 options.element = element;
22708
22709 options.onSwipe = function(ev, gesture) {
22710 //Add the relevant swipe class to the element so it can animate correctly
22711 var swipe = ev.type.replace('$md.','');
22712 var direction = swipe.replace('swipe', '');
22713
22714 // If the swipe direction is down/up but the toast came from top/bottom don't fade away
22715 // Unless the screen is small, then the toast always on bottom
22716 if ((direction === 'down' && options.position.indexOf('top') != -1 && !isSmScreen) ||
22717 (direction === 'up' && (options.position.indexOf('bottom') != -1 || isSmScreen))) {
22718 return;
22719 }
22720
22721 if ((direction === 'left' || direction === 'right') && isSmScreen) {
22722 return;
22723 }
22724
22725 element.addClass('md-' + swipe);
22726 $mdUtil.nextTick($mdToast.cancel);
22727 };
22728 options.openClass = toastOpenClass(options.position);
22729
22730 element.addClass(options.toastClass);
22731
22732 // 'top left' -> 'md-top md-left'
22733 options.parent.addClass(options.openClass);
22734
22735 // static is the default position
22736 if ($mdUtil.hasComputedStyle(options.parent, 'position', 'static')) {
22737 options.parent.css('position', 'relative');
22738 }
22739
22740 element.on(SWIPE_EVENTS, options.onSwipe);
22741 element.addClass(isSmScreen ? 'md-bottom' : options.position.split(' ').map(function(pos) {
22742 return 'md-' + pos;
22743 }).join(' '));
22744
22745 if (options.parent) options.parent.addClass('md-toast-animating');
22746 return $animate.enter(element, options.parent).then(function() {
22747 if (options.parent) options.parent.removeClass('md-toast-animating');
22748 });
22749 }
22750
22751 function onRemove(scope, element, options) {
22752 element.off(SWIPE_EVENTS, options.onSwipe);
22753 if (options.parent) options.parent.addClass('md-toast-animating');
22754 if (options.openClass) options.parent.removeClass(options.openClass);
22755
22756 return ((options.$destroy == true) ? element.remove() : $animate.leave(element))
22757 .then(function () {
22758 if (options.parent) options.parent.removeClass('md-toast-animating');
22759 if ($mdUtil.hasComputedStyle(options.parent, 'position', 'static')) {
22760 options.parent.css('position', '');
22761 }
22762 });
22763 }
22764
22765 function toastOpenClass(position) {
22766 // For mobile, always open full-width on bottom
22767 if (!$mdMedia('gt-xs')) {
22768 return 'md-toast-open-bottom';
22769 }
22770
22771 return 'md-toast-open-' +
22772 (position.indexOf('top') > -1 ? 'top' : 'bottom');
22773 }
22774 }
22775
22776}
22777
22778})();
22779(function(){
22780"use strict";
22781
22782/**
22783 * @ngdoc module
22784 * @name material.components.tabs
22785 * @description
22786 *
22787 * Tabs, created with the `<md-tabs>` directive provide *tabbed* navigation with different styles.
22788 * The Tabs component consists of clickable tabs that are aligned horizontally side-by-side.
22789 *
22790 * Features include support for:
22791 *
22792 * - static or dynamic tabs,
22793 * - responsive designs,
22794 * - accessibility support (ARIA),
22795 * - tab pagination,
22796 * - external or internal tab content,
22797 * - focus indicators and arrow-key navigations,
22798 * - programmatic lookup and access to tab controllers, and
22799 * - dynamic transitions through different tab contents.
22800 *
22801 */
22802/*
22803 * @see js folder for tabs implementation
22804 */
22805angular.module('material.components.tabs', [
22806 'material.core',
22807 'material.components.icon'
22808]);
22809
22810})();
22811(function(){
22812"use strict";
22813
22814/**
22815 * @ngdoc module
22816 * @name material.components.toolbar
22817 */
22818mdToolbarDirective.$inject = ["$$rAF", "$mdConstant", "$mdUtil", "$mdTheming", "$animate"];
22819angular.module('material.components.toolbar', [
22820 'material.core',
22821 'material.components.content'
22822])
22823 .directive('mdToolbar', mdToolbarDirective);
22824
22825/**
22826 * @ngdoc directive
22827 * @name mdToolbar
22828 * @module material.components.toolbar
22829 * @restrict E
22830 * @description
22831 * `md-toolbar` is used to place a toolbar in your app.
22832 *
22833 * Toolbars are usually used above a content area to display the title of the
22834 * current page, and show relevant action buttons for that page.
22835 *
22836 * You can change the height of the toolbar by adding either the
22837 * `md-medium-tall` or `md-tall` class to the toolbar.
22838 *
22839 * @usage
22840 * <hljs lang="html">
22841 * <div layout="column" layout-fill>
22842 * <md-toolbar>
22843 *
22844 * <div class="md-toolbar-tools">
22845 * <h2 md-truncate flex>My App's Title</h2>
22846 *
22847 * <md-button>
22848 * Right Bar Button
22849 * </md-button>
22850 * </div>
22851 *
22852 * </md-toolbar>
22853 * <md-content>
22854 * Hello!
22855 * </md-content>
22856 * </div>
22857 * </hljs>
22858 *
22859 * <i><b>Note:</b> The code above shows usage with the `md-truncate` component which provides an
22860 * ellipsis if the title is longer than the width of the Toolbar.</i>
22861 *
22862 * ## CSS & Styles
22863 *
22864 * The `<md-toolbar>` provides a few custom CSS classes that you may use to enhance the
22865 * functionality of your toolbar.
22866 *
22867 * <div>
22868 * <docs-css-api-table>
22869 *
22870 * <docs-css-selector code="md-toolbar .md-toolbar-tools">
22871 * The `md-toolbar-tools` class provides quite a bit of automatic styling for your toolbar
22872 * buttons and text. When applied, it will center the buttons and text vertically for you.
22873 * </docs-css-selector>
22874 *
22875 * </docs-css-api-table>
22876 * </div>
22877 *
22878 * ### Private Classes
22879 *
22880 * Currently, the only private class is the `md-toolbar-transitions` class. All other classes are
22881 * considered public.
22882 *
22883 * @param {boolean=} md-scroll-shrink Whether the header should shrink away as
22884 * the user scrolls down, and reveal itself as the user scrolls up.
22885 *
22886 * _**Note (1):** for scrollShrink to work, the toolbar must be a sibling of a
22887 * `md-content` element, placed before it. See the scroll shrink demo._
22888 *
22889 * _**Note (2):** The `md-scroll-shrink` attribute is only parsed on component
22890 * initialization, it does not watch for scope changes._
22891 *
22892 *
22893 * @param {number=} md-shrink-speed-factor How much to change the speed of the toolbar's
22894 * shrinking by. For example, if 0.25 is given then the toolbar will shrink
22895 * at one fourth the rate at which the user scrolls down. Default 0.5.
22896 *
22897 */
22898
22899function mdToolbarDirective($$rAF, $mdConstant, $mdUtil, $mdTheming, $animate) {
22900 var translateY = angular.bind(null, $mdUtil.supplant, 'translate3d(0,{0}px,0)');
22901
22902 return {
22903 template: '',
22904 restrict: 'E',
22905
22906 link: function(scope, element, attr) {
22907
22908 element.addClass('_md'); // private md component indicator for styling
22909 $mdTheming(element);
22910
22911 $mdUtil.nextTick(function () {
22912 element.addClass('_md-toolbar-transitions'); // adding toolbar transitions after digest
22913 }, false);
22914
22915 if (angular.isDefined(attr.mdScrollShrink)) {
22916 setupScrollShrink();
22917 }
22918
22919 function setupScrollShrink() {
22920
22921 var toolbarHeight;
22922 var contentElement;
22923 var disableScrollShrink = angular.noop;
22924
22925 // Current "y" position of scroll
22926 // Store the last scroll top position
22927 var y = 0;
22928 var prevScrollTop = 0;
22929 var shrinkSpeedFactor = attr.mdShrinkSpeedFactor || 0.5;
22930
22931 var debouncedContentScroll = $$rAF.throttle(onContentScroll);
22932 var debouncedUpdateHeight = $mdUtil.debounce(updateToolbarHeight, 5 * 1000);
22933
22934 // Wait for $mdContentLoaded event from mdContent directive.
22935 // If the mdContent element is a sibling of our toolbar, hook it up
22936 // to scroll events.
22937
22938 scope.$on('$mdContentLoaded', onMdContentLoad);
22939
22940 // If the toolbar is used inside an ng-if statement, we may miss the
22941 // $mdContentLoaded event, so we attempt to fake it if we have a
22942 // md-content close enough.
22943
22944 attr.$observe('mdScrollShrink', onChangeScrollShrink);
22945
22946 // If the toolbar has ngShow or ngHide we need to update height immediately as it changed
22947 // and not wait for $mdUtil.debounce to happen
22948
22949 if (attr.ngShow) { scope.$watch(attr.ngShow, updateToolbarHeight); }
22950 if (attr.ngHide) { scope.$watch(attr.ngHide, updateToolbarHeight); }
22951
22952 // If the scope is destroyed (which could happen with ng-if), make sure
22953 // to disable scroll shrinking again
22954
22955 scope.$on('$destroy', disableScrollShrink);
22956
22957 /**
22958 *
22959 */
22960 function onChangeScrollShrink(shrinkWithScroll) {
22961 var closestContent = element.parent().find('md-content');
22962
22963 // If we have a content element, fake the call; this might still fail
22964 // if the content element isn't a sibling of the toolbar
22965
22966 if (!contentElement && closestContent.length) {
22967 onMdContentLoad(null, closestContent);
22968 }
22969
22970 // Evaluate the expression
22971 shrinkWithScroll = scope.$eval(shrinkWithScroll);
22972
22973 // Disable only if the attribute's expression evaluates to false
22974 if (shrinkWithScroll === false) {
22975 disableScrollShrink();
22976 } else {
22977 disableScrollShrink = enableScrollShrink();
22978 }
22979 }
22980
22981 /**
22982 *
22983 */
22984 function onMdContentLoad($event, newContentEl) {
22985 // Toolbar and content must be siblings
22986 if (newContentEl && element.parent()[0] === newContentEl.parent()[0]) {
22987 // unhook old content event listener if exists
22988 if (contentElement) {
22989 contentElement.off('scroll', debouncedContentScroll);
22990 }
22991
22992 contentElement = newContentEl;
22993 disableScrollShrink = enableScrollShrink();
22994 }
22995 }
22996
22997 /**
22998 *
22999 */
23000 function onContentScroll(e) {
23001 var scrollTop = e ? e.target.scrollTop : prevScrollTop;
23002
23003 debouncedUpdateHeight();
23004
23005 y = Math.min(
23006 toolbarHeight / shrinkSpeedFactor,
23007 Math.max(0, y + scrollTop - prevScrollTop)
23008 );
23009
23010 element.css($mdConstant.CSS.TRANSFORM, translateY([-y * shrinkSpeedFactor]));
23011 contentElement.css($mdConstant.CSS.TRANSFORM, translateY([(toolbarHeight - y) * shrinkSpeedFactor]));
23012
23013 prevScrollTop = scrollTop;
23014
23015 $mdUtil.nextTick(function() {
23016 var hasWhiteFrame = element.hasClass('md-whiteframe-z1');
23017
23018 if (hasWhiteFrame && !y) {
23019 $animate.removeClass(element, 'md-whiteframe-z1');
23020 } else if (!hasWhiteFrame && y) {
23021 $animate.addClass(element, 'md-whiteframe-z1');
23022 }
23023 });
23024
23025 }
23026
23027 /**
23028 *
23029 */
23030 function enableScrollShrink() {
23031 if (!contentElement) return angular.noop; // no md-content
23032
23033 contentElement.on('scroll', debouncedContentScroll);
23034 contentElement.attr('scroll-shrink', 'true');
23035
23036 $mdUtil.nextTick(updateToolbarHeight, false);
23037
23038 return function disableScrollShrink() {
23039 contentElement.off('scroll', debouncedContentScroll);
23040 contentElement.attr('scroll-shrink', 'false');
23041
23042 updateToolbarHeight();
23043 };
23044 }
23045
23046 /**
23047 *
23048 */
23049 function updateToolbarHeight() {
23050 toolbarHeight = element.prop('offsetHeight');
23051 // Add a negative margin-top the size of the toolbar to the content el.
23052 // The content will start transformed down the toolbarHeight amount,
23053 // so everything looks normal.
23054 //
23055 // As the user scrolls down, the content will be transformed up slowly
23056 // to put the content underneath where the toolbar was.
23057 var margin = (-toolbarHeight * shrinkSpeedFactor) + 'px';
23058
23059 contentElement.css({
23060 "margin-top": margin,
23061 "margin-bottom": margin
23062 });
23063
23064 onContentScroll();
23065 }
23066
23067 }
23068
23069 }
23070 };
23071
23072}
23073
23074})();
23075(function(){
23076"use strict";
23077
23078/**
23079 * @ngdoc module
23080 * @name material.components.tooltip
23081 */
23082MdTooltipDirective.$inject = ["$timeout", "$window", "$$rAF", "$document", "$interpolate", "$mdUtil", "$mdPanel", "$$mdTooltipRegistry"];
23083angular
23084 .module('material.components.tooltip', [
23085 'material.core',
23086 'material.components.panel'
23087 ])
23088 .directive('mdTooltip', MdTooltipDirective)
23089 .service('$$mdTooltipRegistry', MdTooltipRegistry);
23090
23091
23092/**
23093 * @ngdoc directive
23094 * @name mdTooltip
23095 * @module material.components.tooltip
23096 * @description
23097 * Tooltips are used to describe elements that are interactive and primarily
23098 * graphical (not textual).
23099 *
23100 * Place a `<md-tooltip>` as a child of the element it describes.
23101 *
23102 * A tooltip will activate when the user hovers over, focuses, or touches the
23103 * parent element.
23104 *
23105 * @usage
23106 * <hljs lang="html">
23107 * <md-button class="md-fab md-accent" aria-label="Play">
23108 * <md-tooltip>Play Music</md-tooltip>
23109 * <md-icon md-svg-src="img/icons/ic_play_arrow_24px.svg"></md-icon>
23110 * </md-button>
23111 * </hljs>
23112 *
23113 * @param {number=} md-z-index The visual level that the tooltip will appear
23114 * in comparison with the rest of the elements of the application.
23115 * @param {expression=} md-visible Boolean bound to whether the tooltip is
23116 * currently visible.
23117 * @param {number=} md-delay How many milliseconds to wait to show the tooltip
23118 * after the user hovers over, focuses, or touches the parent element.
23119 * Defaults to 0ms on non-touch devices and 75ms on touch.
23120 * @param {boolean=} md-autohide If present or provided with a boolean value,
23121 * the tooltip will hide on mouse leave, regardless of focus.
23122 * @param {string=} md-direction The direction that the tooltip is shown,
23123 * relative to the parent element. Supports top, right, bottom, and left.
23124 * Defaults to bottom.
23125 */
23126function MdTooltipDirective($timeout, $window, $$rAF, $document, $interpolate,
23127 $mdUtil, $mdPanel, $$mdTooltipRegistry) {
23128
23129 var ENTER_EVENTS = 'focus touchstart mouseenter';
23130 var LEAVE_EVENTS = 'blur touchcancel mouseleave';
23131 var TOOLTIP_DEFAULT_Z_INDEX = 100;
23132 var TOOLTIP_DEFAULT_SHOW_DELAY = 0;
23133 var TOOLTIP_DEFAULT_DIRECTION = 'bottom';
23134 var TOOLTIP_DIRECTIONS = {
23135 top: { x: $mdPanel.xPosition.CENTER, y: $mdPanel.yPosition.ABOVE },
23136 right: { x: $mdPanel.xPosition.OFFSET_END, y: $mdPanel.yPosition.CENTER },
23137 bottom: { x: $mdPanel.xPosition.CENTER, y: $mdPanel.yPosition.BELOW },
23138 left: { x: $mdPanel.xPosition.OFFSET_START, y: $mdPanel.yPosition.CENTER }
23139 };
23140
23141 return {
23142 restrict: 'E',
23143 priority: 210, // Before ngAria
23144 scope: {
23145 mdZIndex: '=?mdZIndex',
23146 mdDelay: '=?mdDelay',
23147 mdVisible: '=?mdVisible',
23148 mdAutohide: '=?mdAutohide',
23149 mdDirection: '@?mdDirection' // Do not expect expressions.
23150 },
23151 link: linkFunc
23152 };
23153
23154 function linkFunc(scope, element, attr) {
23155 // Set constants.
23156 var parent = $mdUtil.getParentWithPointerEvents(element);
23157 var debouncedOnResize = $$rAF.throttle(updatePosition);
23158 var mouseActive = false;
23159 var origin, position, panelPosition, panelRef, autohide, showTimeout,
23160 elementFocusedOnWindowBlur = null;
23161
23162 // Set defaults
23163 setDefaults();
23164
23165 // Set parent aria-label.
23166 addAriaLabel();
23167
23168 // Remove the element from its current DOM position.
23169 element.detach();
23170
23171 updatePosition();
23172 bindEvents();
23173 configureWatchers();
23174
23175 function setDefaults() {
23176 scope.mdZIndex = scope.mdZIndex || TOOLTIP_DEFAULT_Z_INDEX;
23177 scope.mdDelay = scope.mdDelay || TOOLTIP_DEFAULT_SHOW_DELAY;
23178 if (!TOOLTIP_DIRECTIONS[scope.mdDirection]) {
23179 scope.mdDirection = TOOLTIP_DEFAULT_DIRECTION;
23180 }
23181 }
23182
23183 function addAriaLabel(override) {
23184 if (override || !parent.attr('aria-label')) {
23185 // Only interpolate the text from the HTML element because otherwise the custom text
23186 // could be interpolated twice and cause XSS violations.
23187 var interpolatedText = override || $interpolate(element.text().trim())(scope.$parent);
23188 parent.attr('aria-label', interpolatedText);
23189 }
23190 }
23191
23192 function updatePosition() {
23193 setDefaults();
23194
23195 // If the panel has already been created, remove the current origin
23196 // class from the panel element.
23197 if (panelRef && panelRef.panelEl) {
23198 panelRef.panelEl.removeClass(origin);
23199 }
23200
23201 // Set the panel element origin class based off of the current
23202 // mdDirection.
23203 origin = 'md-origin-' + scope.mdDirection;
23204
23205 // Create the position of the panel based off of the mdDirection.
23206 position = TOOLTIP_DIRECTIONS[scope.mdDirection];
23207
23208 // Using the newly created position object, use the MdPanel
23209 // panelPosition API to build the panel's position.
23210 panelPosition = $mdPanel.newPanelPosition()
23211 .relativeTo(parent)
23212 .addPanelPosition(position.x, position.y);
23213
23214 // If the panel has already been created, add the new origin class to
23215 // the panel element and update it's position with the panelPosition.
23216 if (panelRef && panelRef.panelEl) {
23217 panelRef.panelEl.addClass(origin);
23218 panelRef.updatePosition(panelPosition);
23219 }
23220 }
23221
23222 function bindEvents() {
23223 // Add a mutationObserver where there is support for it and the need
23224 // for it in the form of viable host(parent[0]).
23225 if (parent[0] && 'MutationObserver' in $window) {
23226 // Use a mutationObserver to tackle #2602.
23227 var attributeObserver = new MutationObserver(function(mutations) {
23228 if (isDisabledMutation(mutations)) {
23229 $mdUtil.nextTick(function() {
23230 setVisible(false);
23231 });
23232 }
23233 });
23234
23235 attributeObserver.observe(parent[0], {
23236 attributes: true
23237 });
23238 }
23239
23240 elementFocusedOnWindowBlur = false;
23241
23242 $$mdTooltipRegistry.register('scroll', windowScrollEventHandler, true);
23243 $$mdTooltipRegistry.register('blur', windowBlurEventHandler);
23244 $$mdTooltipRegistry.register('resize', debouncedOnResize);
23245
23246 scope.$on('$destroy', onDestroy);
23247
23248 // To avoid 'synthetic clicks', we listen to mousedown instead of
23249 // 'click'.
23250 parent.on('mousedown', mousedownEventHandler);
23251 parent.on(ENTER_EVENTS, enterEventHandler);
23252
23253 function isDisabledMutation(mutations) {
23254 mutations.some(function(mutation) {
23255 return mutation.attributeName === 'disabled' && parent[0].disabled;
23256 });
23257 return false;
23258 }
23259
23260 function windowScrollEventHandler() {
23261 setVisible(false);
23262 }
23263
23264 function windowBlurEventHandler() {
23265 elementFocusedOnWindowBlur = document.activeElement === parent[0];
23266 }
23267
23268 function enterEventHandler($event) {
23269 // Prevent the tooltip from showing when the window is receiving
23270 // focus.
23271 if ($event.type === 'focus' && elementFocusedOnWindowBlur) {
23272 elementFocusedOnWindowBlur = false;
23273 } else if (!scope.mdVisible) {
23274 parent.on(LEAVE_EVENTS, leaveEventHandler);
23275 setVisible(true);
23276
23277 // If the user is on a touch device, we should bind the tap away
23278 // after the 'touched' in order to prevent the tooltip being
23279 // removed immediately.
23280 if ($event.type === 'touchstart') {
23281 parent.one('touchend', function() {
23282 $mdUtil.nextTick(function() {
23283 $document.one('touchend', leaveEventHandler);
23284 }, false);
23285 });
23286 }
23287 }
23288 }
23289
23290 function leaveEventHandler() {
23291 autohide = scope.hasOwnProperty('mdAutohide') ?
23292 scope.mdAutohide :
23293 attr.hasOwnProperty('mdAutohide');
23294
23295 if (autohide || mouseActive ||
23296 $document[0].activeElement !== parent[0]) {
23297 // When a show timeout is currently in progress, then we have
23298 // to cancel it, otherwise the tooltip will remain showing
23299 // without focus or hover.
23300 if (showTimeout) {
23301 $timeout.cancel(showTimeout);
23302 setVisible.queued = false;
23303 showTimeout = null;
23304 }
23305
23306 parent.off(LEAVE_EVENTS, leaveEventHandler);
23307 parent.triggerHandler('blur');
23308 setVisible(false);
23309 }
23310 mouseActive = false;
23311 }
23312
23313 function mousedownEventHandler() {
23314 mouseActive = true;
23315 }
23316
23317 function onDestroy() {
23318 $$mdTooltipRegistry.deregister('scroll', windowScrollEventHandler, true);
23319 $$mdTooltipRegistry.deregister('blur', windowBlurEventHandler);
23320 $$mdTooltipRegistry.deregister('resize', debouncedOnResize);
23321
23322 parent
23323 .off(ENTER_EVENTS, enterEventHandler)
23324 .off(LEAVE_EVENTS, leaveEventHandler)
23325 .off('mousedown', mousedownEventHandler);
23326
23327 // Trigger the handler in case any of the tooltips are
23328 // still visible.
23329 leaveEventHandler();
23330 attributeObserver && attributeObserver.disconnect();
23331 }
23332 }
23333
23334 function configureWatchers() {
23335 if (element[0] && 'MutationObserver' in $window) {
23336 var attributeObserver = new MutationObserver(function(mutations) {
23337 mutations.forEach(function(mutation) {
23338 if (mutation.attributeName === 'md-visible' &&
23339 !scope.visibleWatcher ) {
23340 scope.visibleWatcher = scope.$watch('mdVisible',
23341 onVisibleChanged);
23342 }
23343 });
23344 });
23345
23346 attributeObserver.observe(element[0], {
23347 attributes: true
23348 });
23349
23350 // Build watcher only if mdVisible is being used.
23351 if (attr.hasOwnProperty('mdVisible')) {
23352 scope.visibleWatcher = scope.$watch('mdVisible',
23353 onVisibleChanged);
23354 }
23355 } else {
23356 // MutationObserver not supported
23357 scope.visibleWatcher = scope.$watch('mdVisible', onVisibleChanged);
23358 }
23359
23360 // Direction watcher
23361 scope.$watch('mdDirection', updatePosition);
23362
23363 // Clean up if the element or parent was removed via jqLite's .remove.
23364 // A couple of notes:
23365 // - In these cases the scope might not have been destroyed, which
23366 // is why we destroy it manually. An example of this can be having
23367 // `md-visible="false"` and adding tooltips while they're
23368 // invisible. If `md-visible` becomes true, at some point, you'd
23369 // usually get a lot of tooltips.
23370 // - We use `.one`, not `.on`, because this only needs to fire once.
23371 // If we were using `.on`, it would get thrown into an infinite
23372 // loop.
23373 // - This kicks off the scope's `$destroy` event which finishes the
23374 // cleanup.
23375 element.one('$destroy', onElementDestroy);
23376 parent.one('$destroy', onElementDestroy);
23377 scope.$on('$destroy', function() {
23378 setVisible(false);
23379 panelRef && panelRef.destroy();
23380 attributeObserver && attributeObserver.disconnect();
23381 element.remove();
23382 });
23383
23384 // Updates the aria-label when the element text changes. This watch
23385 // doesn't need to be set up if the element doesn't have any data
23386 // bindings.
23387 if (element.text().indexOf($interpolate.startSymbol()) > -1) {
23388 scope.$watch(function() {
23389 return element.text().trim();
23390 }, addAriaLabel);
23391 }
23392
23393 function onElementDestroy() {
23394 scope.$destroy();
23395 }
23396 }
23397
23398 function setVisible(value) {
23399 // Break if passed value is already in queue or there is no queue and
23400 // passed value is current in the controller.
23401 if (setVisible.queued && setVisible.value === !!value ||
23402 !setVisible.queued && scope.mdVisible === !!value) {
23403 return;
23404 }
23405 setVisible.value = !!value;
23406
23407 if (!setVisible.queued) {
23408 if (value) {
23409 setVisible.queued = true;
23410 showTimeout = $timeout(function() {
23411 scope.mdVisible = setVisible.value;
23412 setVisible.queued = false;
23413 showTimeout = null;
23414 if (!scope.visibleWatcher) {
23415 onVisibleChanged(scope.mdVisible);
23416 }
23417 }, scope.mdDelay);
23418 } else {
23419 $mdUtil.nextTick(function() {
23420 scope.mdVisible = false;
23421 if (!scope.visibleWatcher) {
23422 onVisibleChanged(false);
23423 }
23424 });
23425 }
23426 }
23427 }
23428
23429 function onVisibleChanged(isVisible) {
23430 isVisible ? showTooltip() : hideTooltip();
23431 }
23432
23433 function showTooltip() {
23434 // Do not show the tooltip if the text is empty.
23435 if (!element[0].textContent.trim()) {
23436 throw new Error('Text for the tooltip has not been provided. ' +
23437 'Please include text within the mdTooltip element.');
23438 }
23439
23440 if (!panelRef) {
23441 var id = 'tooltip-' + $mdUtil.nextUid();
23442 var attachTo = angular.element(document.body);
23443 var panelAnimation = $mdPanel.newPanelAnimation()
23444 .openFrom(parent)
23445 .closeTo(parent)
23446 .withAnimation({
23447 open: 'md-show',
23448 close: 'md-hide'
23449 });
23450
23451 var panelConfig = {
23452 id: id,
23453 attachTo: attachTo,
23454 contentElement: element,
23455 propagateContainerEvents: true,
23456 panelClass: 'md-tooltip ' + origin,
23457 animation: panelAnimation,
23458 position: panelPosition,
23459 zIndex: scope.mdZIndex,
23460 focusOnOpen: false
23461 };
23462
23463 panelRef = $mdPanel.create(panelConfig);
23464 }
23465
23466 panelRef.open().then(function() {
23467 panelRef.panelEl.attr('role', 'tooltip');
23468 });
23469 }
23470
23471 function hideTooltip() {
23472 panelRef && panelRef.close();
23473 }
23474 }
23475
23476}
23477
23478
23479/**
23480 * Service that is used to reduce the amount of listeners that are being
23481 * registered on the `window` by the tooltip component. Works by collecting
23482 * the individual event handlers and dispatching them from a global handler.
23483 *
23484 * @ngInject
23485 */
23486function MdTooltipRegistry() {
23487 var listeners = {};
23488 var ngWindow = angular.element(window);
23489
23490 return {
23491 register: register,
23492 deregister: deregister
23493 };
23494
23495 /**
23496 * Global event handler that dispatches the registered handlers in the
23497 * service.
23498 * @param {!Event} event Event object passed in by the browser
23499 */
23500 function globalEventHandler(event) {
23501 if (listeners[event.type]) {
23502 listeners[event.type].forEach(function(currentHandler) {
23503 currentHandler.call(this, event);
23504 }, this);
23505 }
23506 }
23507
23508 /**
23509 * Registers a new handler with the service.
23510 * @param {string} type Type of event to be registered.
23511 * @param {!Function} handler Event handler.
23512 * @param {boolean} useCapture Whether to use event capturing.
23513 */
23514 function register(type, handler, useCapture) {
23515 var handlers = listeners[type] = listeners[type] || [];
23516
23517 if (!handlers.length) {
23518 useCapture ? window.addEventListener(type, globalEventHandler, true) :
23519 ngWindow.on(type, globalEventHandler);
23520 }
23521
23522 if (handlers.indexOf(handler) === -1) {
23523 handlers.push(handler);
23524 }
23525 }
23526
23527 /**
23528 * Removes an event handler from the service.
23529 * @param {string} type Type of event handler.
23530 * @param {!Function} handler The event handler itself.
23531 * @param {boolean} useCapture Whether the event handler used event capturing.
23532 */
23533 function deregister(type, handler, useCapture) {
23534 var handlers = listeners[type];
23535 var index = handlers ? handlers.indexOf(handler) : -1;
23536
23537 if (index > -1) {
23538 handlers.splice(index, 1);
23539
23540 if (handlers.length === 0) {
23541 useCapture ? window.removeEventListener(type, globalEventHandler, true) :
23542 ngWindow.off(type, globalEventHandler);
23543 }
23544 }
23545 }
23546}
23547
23548})();
23549(function(){
23550"use strict";
23551
23552/**
23553 * @ngdoc module
23554 * @name material.components.truncate
23555 */
23556MdTruncateController.$inject = ["$element"];
23557angular.module('material.components.truncate', ['material.core'])
23558 .directive('mdTruncate', MdTruncateDirective);
23559
23560/**
23561 * @ngdoc directive
23562 * @name mdTruncate
23563 * @module material.components.truncate
23564 * @restrict AE
23565 * @description
23566 *
23567 * The `md-truncate` component displays a label that will automatically clip text which is wider
23568 * than the component. By default, it displays an ellipsis, but you may apply the `md-clip` CSS
23569 * class to override this default and use a standard "clipping" approach.
23570 *
23571 * <i><b>Note:</b> The `md-truncate` component does not automatically adjust it's width. You must
23572 * provide the `flex` attribute, or some other CSS-based width management. See the
23573 * <a ng-href="./demo/truncate">demos</a> for examples.</i>
23574 *
23575 * @usage
23576 *
23577 * ### As an Element
23578 *
23579 * <hljs lang="html">
23580 * <div layout="row">
23581 * <md-button>Back</md-button>
23582 *
23583 * <md-truncate flex>Chapter 1 - The Way of the Old West</md-truncate>
23584 *
23585 * <md-button>Forward</md-button>
23586 * </div>
23587 * </hljs>
23588 *
23589 * ### As an Attribute
23590 *
23591 * <hljs lang="html">
23592 * <h2 md-truncate style="max-width: 100px;">Some Title With a Lot of Text</h2>
23593 * </hljs>
23594 *
23595 * ## CSS & Styles
23596 *
23597 * `<md-truncate>` provides two CSS classes that you may use to control the type of clipping.
23598 *
23599 * <i><b>Note:</b> The `md-truncate` also applies a setting of `width: 0;` when used with the `flex`
23600 * attribute to fix an issue with the flex element not shrinking properly.</i>
23601 *
23602 * <div>
23603 * <docs-css-api-table>
23604 *
23605 * <docs-css-selector code=".md-ellipsis">
23606 * Assigns the "ellipsis" behavior (default) which will cut off mid-word and append an ellipsis
23607 * (…) to the end of the text.
23608 * </docs-css-selector>
23609 *
23610 * <docs-css-selector code=".md-clip">
23611 * Assigns the "clipping" behavior which will simply chop off the text. This may happen
23612 * mid-word or even mid-character.
23613 * </docs-css-selector>
23614 *
23615 * </docs-css-api-table>
23616 * </div>
23617 */
23618function MdTruncateDirective() {
23619 return {
23620 restrict: 'AE',
23621
23622 controller: MdTruncateController,
23623 controllerAs: '$ctrl',
23624 bindToController: true
23625 }
23626}
23627
23628/**
23629 * Controller for the <md-truncate> component.
23630 *
23631 * @param $element The md-truncate element.
23632 *
23633 * @constructor
23634 * @ngInject
23635 */
23636function MdTruncateController($element) {
23637 $element.addClass('md-truncate');
23638}
23639
23640})();
23641(function(){
23642"use strict";
23643
23644/**
23645 * @ngdoc module
23646 * @name material.components.virtualRepeat
23647 */
23648VirtualRepeatContainerController.$inject = ["$$rAF", "$mdUtil", "$mdConstant", "$parse", "$rootScope", "$window", "$scope", "$element", "$attrs"];
23649VirtualRepeatController.$inject = ["$scope", "$element", "$attrs", "$browser", "$document", "$rootScope", "$$rAF", "$mdUtil"];
23650VirtualRepeatDirective.$inject = ["$parse"];
23651angular.module('material.components.virtualRepeat', [
23652 'material.core',
23653 'material.components.showHide'
23654])
23655.directive('mdVirtualRepeatContainer', VirtualRepeatContainerDirective)
23656.directive('mdVirtualRepeat', VirtualRepeatDirective);
23657
23658
23659/**
23660 * @ngdoc directive
23661 * @name mdVirtualRepeatContainer
23662 * @module material.components.virtualRepeat
23663 * @restrict E
23664 * @description
23665 * `md-virtual-repeat-container` provides the scroll container for md-virtual-repeat.
23666 *
23667 * VirtualRepeat is a limited substitute for ng-repeat that renders only
23668 * enough DOM nodes to fill the container and recycling them as the user scrolls.
23669 *
23670 * Once an element is not visible anymore, the VirtualRepeat recycles it and will reuse it for
23671 * another visible item by replacing the previous dataset with the new one.
23672 *
23673 * ### Common Issues
23674 *
23675 * - When having one-time bindings inside of the view template, the VirtualRepeat will not properly
23676 * update the bindings for new items, since the view will be recycled.
23677 *
23678 * - Directives inside of a VirtualRepeat will be only compiled (linked) once, because those
23679 * are will be recycled items and used for other items.
23680 * The VirtualRepeat just updates the scope bindings.
23681 *
23682 *
23683 * ### Notes
23684 *
23685 * > The VirtualRepeat is a similar implementation to the Android
23686 * [RecyclerView](https://developer.android.com/reference/android/support/v7/widget/RecyclerView.html)
23687 *
23688 * <!-- This comment forces a break between blockquotes //-->
23689 *
23690 * > Please also review the <a ng-href="api/directive/mdVirtualRepeat">VirtualRepeat</a>
23691 * documentation for more information.
23692 *
23693 *
23694 * @usage
23695 * <hljs lang="html">
23696 *
23697 * <md-virtual-repeat-container md-top-index="topIndex">
23698 * <div md-virtual-repeat="i in items" md-item-size="20">Hello {{i}}!</div>
23699 * </md-virtual-repeat-container>
23700 * </hljs>
23701 *
23702 * @param {number=} md-top-index Binds the index of the item that is at the top of the scroll
23703 * container to $scope. It can both read and set the scroll position.
23704 * @param {boolean=} md-orient-horizontal Whether the container should scroll horizontally
23705 * (defaults to orientation and scrolling vertically).
23706 * @param {boolean=} md-auto-shrink When present, the container will shrink to fit
23707 * the number of items when that number is less than its original size.
23708 * @param {number=} md-auto-shrink-min Minimum number of items that md-auto-shrink
23709 * will shrink to (default: 0).
23710 */
23711function VirtualRepeatContainerDirective() {
23712 return {
23713 controller: VirtualRepeatContainerController,
23714 template: virtualRepeatContainerTemplate,
23715 compile: function virtualRepeatContainerCompile($element, $attrs) {
23716 $element
23717 .addClass('md-virtual-repeat-container')
23718 .addClass($attrs.hasOwnProperty('mdOrientHorizontal')
23719 ? 'md-orient-horizontal'
23720 : 'md-orient-vertical');
23721 }
23722 };
23723}
23724
23725
23726function virtualRepeatContainerTemplate($element) {
23727 return '<div class="md-virtual-repeat-scroller">' +
23728 '<div class="md-virtual-repeat-sizer"></div>' +
23729 '<div class="md-virtual-repeat-offsetter">' +
23730 $element[0].innerHTML +
23731 '</div></div>';
23732}
23733
23734/**
23735 * Number of additional elements to render above and below the visible area inside
23736 * of the virtual repeat container. A higher number results in less flicker when scrolling
23737 * very quickly in Safari, but comes with a higher rendering and dirty-checking cost.
23738 * @const {number}
23739 */
23740var NUM_EXTRA = 3;
23741
23742/** @ngInject */
23743function VirtualRepeatContainerController($$rAF, $mdUtil, $mdConstant, $parse, $rootScope, $window, $scope,
23744 $element, $attrs) {
23745 this.$rootScope = $rootScope;
23746 this.$scope = $scope;
23747 this.$element = $element;
23748 this.$attrs = $attrs;
23749
23750 /** @type {number} The width or height of the container */
23751 this.size = 0;
23752 /** @type {number} The scroll width or height of the scroller */
23753 this.scrollSize = 0;
23754 /** @type {number} The scrollLeft or scrollTop of the scroller */
23755 this.scrollOffset = 0;
23756 /** @type {boolean} Whether the scroller is oriented horizontally */
23757 this.horizontal = this.$attrs.hasOwnProperty('mdOrientHorizontal');
23758 /** @type {!VirtualRepeatController} The repeater inside of this container */
23759 this.repeater = null;
23760 /** @type {boolean} Whether auto-shrink is enabled */
23761 this.autoShrink = this.$attrs.hasOwnProperty('mdAutoShrink');
23762 /** @type {number} Minimum number of items to auto-shrink to */
23763 this.autoShrinkMin = parseInt(this.$attrs.mdAutoShrinkMin, 10) || 0;
23764 /** @type {?number} Original container size when shrank */
23765 this.originalSize = null;
23766 /** @type {number} Amount to offset the total scroll size by. */
23767 this.offsetSize = parseInt(this.$attrs.mdOffsetSize, 10) || 0;
23768 /** @type {?string} height or width element style on the container prior to auto-shrinking. */
23769 this.oldElementSize = null;
23770 /** @type {!number} Maximum amount of pixels allowed for a single DOM element */
23771 this.maxElementPixels = $mdConstant.ELEMENT_MAX_PIXELS;
23772
23773 if (this.$attrs.mdTopIndex) {
23774 /** @type {function(angular.Scope): number} Binds to topIndex on Angular scope */
23775 this.bindTopIndex = $parse(this.$attrs.mdTopIndex);
23776 /** @type {number} The index of the item that is at the top of the scroll container */
23777 this.topIndex = this.bindTopIndex(this.$scope);
23778
23779 if (!angular.isDefined(this.topIndex)) {
23780 this.topIndex = 0;
23781 this.bindTopIndex.assign(this.$scope, 0);
23782 }
23783
23784 this.$scope.$watch(this.bindTopIndex, angular.bind(this, function(newIndex) {
23785 if (newIndex !== this.topIndex) {
23786 this.scrollToIndex(newIndex);
23787 }
23788 }));
23789 } else {
23790 this.topIndex = 0;
23791 }
23792
23793 this.scroller = $element[0].querySelector('.md-virtual-repeat-scroller');
23794 this.sizer = this.scroller.querySelector('.md-virtual-repeat-sizer');
23795 this.offsetter = this.scroller.querySelector('.md-virtual-repeat-offsetter');
23796
23797 // After the dom stablizes, measure the initial size of the container and
23798 // make a best effort at re-measuring as it changes.
23799 var boundUpdateSize = angular.bind(this, this.updateSize);
23800
23801 $$rAF(angular.bind(this, function() {
23802 boundUpdateSize();
23803
23804 var debouncedUpdateSize = $mdUtil.debounce(boundUpdateSize, 10, null, false);
23805 var jWindow = angular.element($window);
23806
23807 // Make one more attempt to get the size if it is 0.
23808 // This is not by any means a perfect approach, but there's really no
23809 // silver bullet here.
23810 if (!this.size) {
23811 debouncedUpdateSize();
23812 }
23813
23814 jWindow.on('resize', debouncedUpdateSize);
23815 $scope.$on('$destroy', function() {
23816 jWindow.off('resize', debouncedUpdateSize);
23817 });
23818
23819 $scope.$emit('$md-resize-enable');
23820 $scope.$on('$md-resize', boundUpdateSize);
23821 }));
23822}
23823
23824
23825/** Called by the md-virtual-repeat inside of the container at startup. */
23826VirtualRepeatContainerController.prototype.register = function(repeaterCtrl) {
23827 this.repeater = repeaterCtrl;
23828
23829 angular.element(this.scroller)
23830 .on('scroll wheel touchmove touchend', angular.bind(this, this.handleScroll_));
23831};
23832
23833
23834/** @return {boolean} Whether the container is configured for horizontal scrolling. */
23835VirtualRepeatContainerController.prototype.isHorizontal = function() {
23836 return this.horizontal;
23837};
23838
23839
23840/** @return {number} The size (width or height) of the container. */
23841VirtualRepeatContainerController.prototype.getSize = function() {
23842 return this.size;
23843};
23844
23845
23846/**
23847 * Resizes the container.
23848 * @private
23849 * @param {number} size The new size to set.
23850 */
23851VirtualRepeatContainerController.prototype.setSize_ = function(size) {
23852 var dimension = this.getDimensionName_();
23853
23854 this.size = size;
23855 this.$element[0].style[dimension] = size + 'px';
23856};
23857
23858
23859VirtualRepeatContainerController.prototype.unsetSize_ = function() {
23860 this.$element[0].style[this.getDimensionName_()] = this.oldElementSize;
23861 this.oldElementSize = null;
23862};
23863
23864
23865/** Instructs the container to re-measure its size. */
23866VirtualRepeatContainerController.prototype.updateSize = function() {
23867 // If the original size is already determined, we can skip the update.
23868 if (this.originalSize) return;
23869
23870 this.size = this.isHorizontal()
23871 ? this.$element[0].clientWidth
23872 : this.$element[0].clientHeight;
23873
23874 // Recheck the scroll position after updating the size. This resolves
23875 // problems that can result if the scroll position was measured while the
23876 // element was display: none or detached from the document.
23877 this.handleScroll_();
23878
23879 this.repeater && this.repeater.containerUpdated();
23880};
23881
23882
23883/** @return {number} The container's scrollHeight or scrollWidth. */
23884VirtualRepeatContainerController.prototype.getScrollSize = function() {
23885 return this.scrollSize;
23886};
23887
23888
23889VirtualRepeatContainerController.prototype.getDimensionName_ = function() {
23890 return this.isHorizontal() ? 'width' : 'height';
23891};
23892
23893
23894/**
23895 * Sets the scroller element to the specified size.
23896 * @private
23897 * @param {number} size The new size.
23898 */
23899VirtualRepeatContainerController.prototype.sizeScroller_ = function(size) {
23900 var dimension = this.getDimensionName_();
23901 var crossDimension = this.isHorizontal() ? 'height' : 'width';
23902
23903 // Clear any existing dimensions.
23904 this.sizer.innerHTML = '';
23905
23906 // If the size falls within the browser's maximum explicit size for a single element, we can
23907 // set the size and be done. Otherwise, we have to create children that add up the the desired
23908 // size.
23909 if (size < this.maxElementPixels) {
23910 this.sizer.style[dimension] = size + 'px';
23911 } else {
23912 this.sizer.style[dimension] = 'auto';
23913 this.sizer.style[crossDimension] = 'auto';
23914
23915 // Divide the total size we have to render into N max-size pieces.
23916 var numChildren = Math.floor(size / this.maxElementPixels);
23917
23918 // Element template to clone for each max-size piece.
23919 var sizerChild = document.createElement('div');
23920 sizerChild.style[dimension] = this.maxElementPixels + 'px';
23921 sizerChild.style[crossDimension] = '1px';
23922
23923 for (var i = 0; i < numChildren; i++) {
23924 this.sizer.appendChild(sizerChild.cloneNode(false));
23925 }
23926
23927 // Re-use the element template for the remainder.
23928 sizerChild.style[dimension] = (size - (numChildren * this.maxElementPixels)) + 'px';
23929 this.sizer.appendChild(sizerChild);
23930 }
23931};
23932
23933
23934/**
23935 * If auto-shrinking is enabled, shrinks or unshrinks as appropriate.
23936 * @private
23937 * @param {number} size The new size.
23938 */
23939VirtualRepeatContainerController.prototype.autoShrink_ = function(size) {
23940 var shrinkSize = Math.max(size, this.autoShrinkMin * this.repeater.getItemSize());
23941
23942 if (this.autoShrink && shrinkSize !== this.size) {
23943 if (this.oldElementSize === null) {
23944 this.oldElementSize = this.$element[0].style[this.getDimensionName_()];
23945 }
23946
23947 var currentSize = this.originalSize || this.size;
23948
23949 if (!currentSize || shrinkSize < currentSize) {
23950 if (!this.originalSize) {
23951 this.originalSize = this.size;
23952 }
23953
23954 // Now we update the containers size, because shrinking is enabled.
23955 this.setSize_(shrinkSize);
23956 } else if (this.originalSize !== null) {
23957 // Set the size back to our initial size.
23958 this.unsetSize_();
23959
23960 var _originalSize = this.originalSize;
23961 this.originalSize = null;
23962
23963 // We determine the repeaters size again, if the original size was zero.
23964 // The originalSize needs to be null, to be able to determine the size.
23965 if (!_originalSize) this.updateSize();
23966
23967 // Apply the original size or the determined size back to the container, because
23968 // it has been overwritten before, in the shrink block.
23969 this.setSize_(_originalSize || this.size);
23970 }
23971
23972 this.repeater.containerUpdated();
23973 }
23974};
23975
23976
23977/**
23978 * Sets the scrollHeight or scrollWidth. Called by the repeater based on
23979 * its item count and item size.
23980 * @param {number} itemsSize The total size of the items.
23981 */
23982VirtualRepeatContainerController.prototype.setScrollSize = function(itemsSize) {
23983 var size = itemsSize + this.offsetSize;
23984 if (this.scrollSize === size) return;
23985
23986 this.sizeScroller_(size);
23987 this.autoShrink_(size);
23988 this.scrollSize = size;
23989};
23990
23991
23992/** @return {number} The container's current scroll offset. */
23993VirtualRepeatContainerController.prototype.getScrollOffset = function() {
23994 return this.scrollOffset;
23995};
23996
23997/**
23998 * Scrolls to a given scrollTop position.
23999 * @param {number} position
24000 */
24001VirtualRepeatContainerController.prototype.scrollTo = function(position) {
24002 this.scroller[this.isHorizontal() ? 'scrollLeft' : 'scrollTop'] = position;
24003 this.handleScroll_();
24004};
24005
24006/**
24007 * Scrolls the item with the given index to the top of the scroll container.
24008 * @param {number} index
24009 */
24010VirtualRepeatContainerController.prototype.scrollToIndex = function(index) {
24011 var itemSize = this.repeater.getItemSize();
24012 var itemsLength = this.repeater.itemsLength;
24013 if(index > itemsLength) {
24014 index = itemsLength - 1;
24015 }
24016 this.scrollTo(itemSize * index);
24017};
24018
24019VirtualRepeatContainerController.prototype.resetScroll = function() {
24020 this.scrollTo(0);
24021};
24022
24023
24024VirtualRepeatContainerController.prototype.handleScroll_ = function() {
24025 var ltr = document.dir != 'rtl' && document.body.dir != 'rtl';
24026 if(!ltr && !this.maxSize) {
24027 this.scroller.scrollLeft = this.scrollSize;
24028 this.maxSize = this.scroller.scrollLeft;
24029 }
24030 var offset = this.isHorizontal() ?
24031 (ltr?this.scroller.scrollLeft : this.maxSize - this.scroller.scrollLeft)
24032 : this.scroller.scrollTop;
24033 if (offset === this.scrollOffset || offset > this.scrollSize - this.size) return;
24034
24035 var itemSize = this.repeater.getItemSize();
24036 if (!itemSize) return;
24037
24038 var numItems = Math.max(0, Math.floor(offset / itemSize) - NUM_EXTRA);
24039
24040 var transform = (this.isHorizontal() ? 'translateX(' : 'translateY(') +
24041 (!this.isHorizontal() || ltr ? (numItems * itemSize) : - (numItems * itemSize)) + 'px)';
24042
24043 this.scrollOffset = offset;
24044 this.offsetter.style.webkitTransform = transform;
24045 this.offsetter.style.transform = transform;
24046
24047 if (this.bindTopIndex) {
24048 var topIndex = Math.floor(offset / itemSize);
24049 if (topIndex !== this.topIndex && topIndex < this.repeater.getItemCount()) {
24050 this.topIndex = topIndex;
24051 this.bindTopIndex.assign(this.$scope, topIndex);
24052 if (!this.$rootScope.$$phase) this.$scope.$digest();
24053 }
24054 }
24055
24056 this.repeater.containerUpdated();
24057};
24058
24059
24060/**
24061 * @ngdoc directive
24062 * @name mdVirtualRepeat
24063 * @module material.components.virtualRepeat
24064 * @restrict A
24065 * @priority 1000
24066 * @description
24067 * `md-virtual-repeat` specifies an element to repeat using virtual scrolling.
24068 *
24069 * Virtual repeat is a limited substitute for ng-repeat that renders only
24070 * enough DOM nodes to fill the container and recycling them as the user scrolls.
24071 *
24072 * Arrays, but not objects are supported for iteration.
24073 * Track by, as alias, and (key, value) syntax are not supported.
24074 *
24075 * ### On-Demand Async Item Loading
24076 *
24077 * When using the `md-on-demand` attribute and loading some asynchronous data, the `getItemAtIndex` function will
24078 * mostly return nothing.
24079 *
24080 * <hljs lang="js">
24081 * DynamicItems.prototype.getItemAtIndex = function(index) {
24082 * if (this.pages[index]) {
24083 * return this.pages[index];
24084 * } else {
24085 * // This is an asynchronous action and does not return any value.
24086 * this.loadPage(index);
24087 * }
24088 * };
24089 * </hljs>
24090 *
24091 * This means that the VirtualRepeat will not have any value for the given index.<br/>
24092 * After the data loading completed, the user expects the VirtualRepeat to recognize the change.
24093 *
24094 * To make sure that the VirtualRepeat properly detects any change, you need to run the operation
24095 * in another digest.
24096 *
24097 * <hljs lang="js">
24098 * DynamicItems.prototype.loadPage = function(index) {
24099 * var self = this;
24100 *
24101 * // Trigger a new digest by using $timeout
24102 * $timeout(function() {
24103 * self.pages[index] = Data;
24104 * });
24105 * };
24106 * </hljs>
24107 *
24108 * > <b>Note:</b> Please also review the
24109 * <a ng-href="api/directive/mdVirtualRepeatContainer">VirtualRepeatContainer</a> documentation
24110 * for more information.
24111 *
24112 * @usage
24113 * <hljs lang="html">
24114 * <md-virtual-repeat-container>
24115 * <div md-virtual-repeat="i in items">Hello {{i}}!</div>
24116 * </md-virtual-repeat-container>
24117 *
24118 * <md-virtual-repeat-container md-orient-horizontal>
24119 * <div md-virtual-repeat="i in items" md-item-size="20">Hello {{i}}!</div>
24120 * </md-virtual-repeat-container>
24121 * </hljs>
24122 *
24123 * @param {number=} md-item-size The height or width of the repeated elements (which must be
24124 * identical for each element). Optional. Will attempt to read the size from the dom if missing,
24125 * but still assumes that all repeated nodes have same height or width.
24126 * @param {string=} md-extra-name Evaluates to an additional name to which the current iterated item
24127 * can be assigned on the repeated scope (needed for use in `md-autocomplete`).
24128 * @param {boolean=} md-on-demand When present, treats the md-virtual-repeat argument as an object
24129 * that can fetch rows rather than an array.
24130 *
24131 * **NOTE:** This object must implement the following interface with two (2) methods:
24132 *
24133 * - `getItemAtIndex: function(index) [object]` The item at that index or null if it is not yet
24134 * loaded (it should start downloading the item in that case).
24135 * - `getLength: function() [number]` The data length to which the repeater container
24136 * should be sized. Ideally, when the count is known, this method should return it.
24137 * Otherwise, return a higher number than the currently loaded items to produce an
24138 * infinite-scroll behavior.
24139 */
24140function VirtualRepeatDirective($parse) {
24141 return {
24142 controller: VirtualRepeatController,
24143 priority: 1000,
24144 require: ['mdVirtualRepeat', '^^mdVirtualRepeatContainer'],
24145 restrict: 'A',
24146 terminal: true,
24147 transclude: 'element',
24148 compile: function VirtualRepeatCompile($element, $attrs) {
24149 var expression = $attrs.mdVirtualRepeat;
24150 var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)\s*$/);
24151 var repeatName = match[1];
24152 var repeatListExpression = $parse(match[2]);
24153 var extraName = $attrs.mdExtraName && $parse($attrs.mdExtraName);
24154
24155 return function VirtualRepeatLink($scope, $element, $attrs, ctrl, $transclude) {
24156 ctrl[0].link_(ctrl[1], $transclude, repeatName, repeatListExpression, extraName);
24157 };
24158 }
24159 };
24160}
24161
24162
24163/** @ngInject */
24164function VirtualRepeatController($scope, $element, $attrs, $browser, $document, $rootScope,
24165 $$rAF, $mdUtil) {
24166 this.$scope = $scope;
24167 this.$element = $element;
24168 this.$attrs = $attrs;
24169 this.$browser = $browser;
24170 this.$document = $document;
24171 this.$rootScope = $rootScope;
24172 this.$$rAF = $$rAF;
24173
24174 /** @type {boolean} Whether we are in on-demand mode. */
24175 this.onDemand = $mdUtil.parseAttributeBoolean($attrs.mdOnDemand);
24176 /** @type {!Function} Backup reference to $browser.$$checkUrlChange */
24177 this.browserCheckUrlChange = $browser.$$checkUrlChange;
24178 /** @type {number} Most recent starting repeat index (based on scroll offset) */
24179 this.newStartIndex = 0;
24180 /** @type {number} Most recent ending repeat index (based on scroll offset) */
24181 this.newEndIndex = 0;
24182 /** @type {number} Most recent end visible index (based on scroll offset) */
24183 this.newVisibleEnd = 0;
24184 /** @type {number} Previous starting repeat index (based on scroll offset) */
24185 this.startIndex = 0;
24186 /** @type {number} Previous ending repeat index (based on scroll offset) */
24187 this.endIndex = 0;
24188 // TODO: measure width/height of first element from dom if not provided.
24189 // getComputedStyle?
24190 /** @type {?number} Height/width of repeated elements. */
24191 this.itemSize = $scope.$eval($attrs.mdItemSize) || null;
24192
24193 /** @type {boolean} Whether this is the first time that items are rendered. */
24194 this.isFirstRender = true;
24195
24196 /**
24197 * @private {boolean} Whether the items in the list are already being updated. Used to prevent
24198 * nested calls to virtualRepeatUpdate_.
24199 */
24200 this.isVirtualRepeatUpdating_ = false;
24201
24202 /** @type {number} Most recently seen length of items. */
24203 this.itemsLength = 0;
24204
24205 /**
24206 * @type {!Function} Unwatch callback for item size (when md-items-size is
24207 * not specified), or angular.noop otherwise.
24208 */
24209 this.unwatchItemSize_ = angular.noop;
24210
24211 /**
24212 * Presently rendered blocks by repeat index.
24213 * @type {Object<number, !VirtualRepeatController.Block}
24214 */
24215 this.blocks = {};
24216 /** @type {Array<!VirtualRepeatController.Block>} A pool of presently unused blocks. */
24217 this.pooledBlocks = [];
24218
24219 $scope.$on('$destroy', angular.bind(this, this.cleanupBlocks_));
24220}
24221
24222
24223/**
24224 * An object representing a repeated item.
24225 * @typedef {{element: !jqLite, new: boolean, scope: !angular.Scope}}
24226 */
24227VirtualRepeatController.Block;
24228
24229
24230/**
24231 * Called at startup by the md-virtual-repeat postLink function.
24232 * @param {!VirtualRepeatContainerController} container The container's controller.
24233 * @param {!Function} transclude The repeated element's bound transclude function.
24234 * @param {string} repeatName The left hand side of the repeat expression, indicating
24235 * the name for each item in the array.
24236 * @param {!Function} repeatListExpression A compiled expression based on the right hand side
24237 * of the repeat expression. Points to the array to repeat over.
24238 * @param {string|undefined} extraName The optional extra repeatName.
24239 */
24240VirtualRepeatController.prototype.link_ =
24241 function(container, transclude, repeatName, repeatListExpression, extraName) {
24242 this.container = container;
24243 this.transclude = transclude;
24244 this.repeatName = repeatName;
24245 this.rawRepeatListExpression = repeatListExpression;
24246 this.extraName = extraName;
24247 this.sized = false;
24248
24249 this.repeatListExpression = angular.bind(this, this.repeatListExpression_);
24250
24251 this.container.register(this);
24252};
24253
24254
24255/** @private Cleans up unused blocks. */
24256VirtualRepeatController.prototype.cleanupBlocks_ = function() {
24257 angular.forEach(this.pooledBlocks, function cleanupBlock(block) {
24258 block.element.remove();
24259 });
24260};
24261
24262
24263/** @private Attempts to set itemSize by measuring a repeated element in the dom */
24264VirtualRepeatController.prototype.readItemSize_ = function() {
24265 if (this.itemSize) {
24266 // itemSize was successfully read in a different asynchronous call.
24267 return;
24268 }
24269
24270 this.items = this.repeatListExpression(this.$scope);
24271 this.parentNode = this.$element[0].parentNode;
24272 var block = this.getBlock_(0);
24273 if (!block.element[0].parentNode) {
24274 this.parentNode.appendChild(block.element[0]);
24275 }
24276
24277 this.itemSize = block.element[0][
24278 this.container.isHorizontal() ? 'offsetWidth' : 'offsetHeight'] || null;
24279
24280 this.blocks[0] = block;
24281 this.poolBlock_(0);
24282
24283 if (this.itemSize) {
24284 this.containerUpdated();
24285 }
24286};
24287
24288
24289/**
24290 * Returns the user-specified repeat list, transforming it into an array-like
24291 * object in the case of infinite scroll/dynamic load mode.
24292 * @param {!angular.Scope} The scope.
24293 * @return {!Array|!Object} An array or array-like object for iteration.
24294 */
24295VirtualRepeatController.prototype.repeatListExpression_ = function(scope) {
24296 var repeatList = this.rawRepeatListExpression(scope);
24297
24298 if (this.onDemand && repeatList) {
24299 var virtualList = new VirtualRepeatModelArrayLike(repeatList);
24300 virtualList.$$includeIndexes(this.newStartIndex, this.newVisibleEnd);
24301 return virtualList;
24302 } else {
24303 return repeatList;
24304 }
24305};
24306
24307
24308/**
24309 * Called by the container. Informs us that the containers scroll or size has
24310 * changed.
24311 */
24312VirtualRepeatController.prototype.containerUpdated = function() {
24313 // If itemSize is unknown, attempt to measure it.
24314 if (!this.itemSize) {
24315 // Make sure to clean up watchers if we can (see #8178)
24316 if(this.unwatchItemSize_ && this.unwatchItemSize_ !== angular.noop){
24317 this.unwatchItemSize_();
24318 }
24319 this.unwatchItemSize_ = this.$scope.$watchCollection(
24320 this.repeatListExpression,
24321 angular.bind(this, function(items) {
24322 if (items && items.length) {
24323 this.readItemSize_();
24324 }
24325 }));
24326 if (!this.$rootScope.$$phase) this.$scope.$digest();
24327
24328 return;
24329 } else if (!this.sized) {
24330 this.items = this.repeatListExpression(this.$scope);
24331 }
24332
24333 if (!this.sized) {
24334 this.unwatchItemSize_();
24335 this.sized = true;
24336 this.$scope.$watchCollection(this.repeatListExpression,
24337 angular.bind(this, function(items, oldItems) {
24338 if (!this.isVirtualRepeatUpdating_) {
24339 this.virtualRepeatUpdate_(items, oldItems);
24340 }
24341 }));
24342 }
24343
24344 this.updateIndexes_();
24345
24346 if (this.newStartIndex !== this.startIndex ||
24347 this.newEndIndex !== this.endIndex ||
24348 this.container.getScrollOffset() > this.container.getScrollSize()) {
24349 if (this.items instanceof VirtualRepeatModelArrayLike) {
24350 this.items.$$includeIndexes(this.newStartIndex, this.newEndIndex);
24351 }
24352 this.virtualRepeatUpdate_(this.items, this.items);
24353 }
24354};
24355
24356
24357/**
24358 * Called by the container. Returns the size of a single repeated item.
24359 * @return {?number} Size of a repeated item.
24360 */
24361VirtualRepeatController.prototype.getItemSize = function() {
24362 return this.itemSize;
24363};
24364
24365
24366/**
24367 * Called by the container. Returns the size of a single repeated item.
24368 * @return {?number} Size of a repeated item.
24369 */
24370VirtualRepeatController.prototype.getItemCount = function() {
24371 return this.itemsLength;
24372};
24373
24374
24375/**
24376 * Updates the order and visible offset of repeated blocks in response to scrolling
24377 * or items updates.
24378 * @private
24379 */
24380VirtualRepeatController.prototype.virtualRepeatUpdate_ = function(items, oldItems) {
24381 this.isVirtualRepeatUpdating_ = true;
24382
24383 var itemsLength = items && items.length || 0;
24384 var lengthChanged = false;
24385
24386 // If the number of items shrank, keep the scroll position.
24387 if (this.items && itemsLength < this.items.length && this.container.getScrollOffset() !== 0) {
24388 this.items = items;
24389 var previousScrollOffset = this.container.getScrollOffset();
24390 this.container.resetScroll();
24391 this.container.scrollTo(previousScrollOffset);
24392 }
24393
24394 if (itemsLength !== this.itemsLength) {
24395 lengthChanged = true;
24396 this.itemsLength = itemsLength;
24397 }
24398
24399 this.items = items;
24400 if (items !== oldItems || lengthChanged) {
24401 this.updateIndexes_();
24402 }
24403
24404 this.parentNode = this.$element[0].parentNode;
24405
24406 if (lengthChanged) {
24407 this.container.setScrollSize(itemsLength * this.itemSize);
24408 }
24409
24410 if (this.isFirstRender) {
24411 this.isFirstRender = false;
24412 var startIndex = this.$attrs.mdStartIndex ?
24413 this.$scope.$eval(this.$attrs.mdStartIndex) :
24414 this.container.topIndex;
24415 this.container.scrollToIndex(startIndex);
24416 }
24417
24418 // Detach and pool any blocks that are no longer in the viewport.
24419 Object.keys(this.blocks).forEach(function(blockIndex) {
24420 var index = parseInt(blockIndex, 10);
24421 if (index < this.newStartIndex || index >= this.newEndIndex) {
24422 this.poolBlock_(index);
24423 }
24424 }, this);
24425
24426 // Add needed blocks.
24427 // For performance reasons, temporarily block browser url checks as we digest
24428 // the restored block scopes ($$checkUrlChange reads window.location to
24429 // check for changes and trigger route change, etc, which we don't need when
24430 // trying to scroll at 60fps).
24431 this.$browser.$$checkUrlChange = angular.noop;
24432
24433 var i, block,
24434 newStartBlocks = [],
24435 newEndBlocks = [];
24436
24437 // Collect blocks at the top.
24438 for (i = this.newStartIndex; i < this.newEndIndex && this.blocks[i] == null; i++) {
24439 block = this.getBlock_(i);
24440 this.updateBlock_(block, i);
24441 newStartBlocks.push(block);
24442 }
24443
24444 // Update blocks that are already rendered.
24445 for (; this.blocks[i] != null; i++) {
24446 this.updateBlock_(this.blocks[i], i);
24447 }
24448 var maxIndex = i - 1;
24449
24450 // Collect blocks at the end.
24451 for (; i < this.newEndIndex; i++) {
24452 block = this.getBlock_(i);
24453 this.updateBlock_(block, i);
24454 newEndBlocks.push(block);
24455 }
24456
24457 // Attach collected blocks to the document.
24458 if (newStartBlocks.length) {
24459 this.parentNode.insertBefore(
24460 this.domFragmentFromBlocks_(newStartBlocks),
24461 this.$element[0].nextSibling);
24462 }
24463 if (newEndBlocks.length) {
24464 this.parentNode.insertBefore(
24465 this.domFragmentFromBlocks_(newEndBlocks),
24466 this.blocks[maxIndex] && this.blocks[maxIndex].element[0].nextSibling);
24467 }
24468
24469 // Restore $$checkUrlChange.
24470 this.$browser.$$checkUrlChange = this.browserCheckUrlChange;
24471
24472 this.startIndex = this.newStartIndex;
24473 this.endIndex = this.newEndIndex;
24474
24475 this.isVirtualRepeatUpdating_ = false;
24476};
24477
24478
24479/**
24480 * @param {number} index Where the block is to be in the repeated list.
24481 * @return {!VirtualRepeatController.Block} A new or pooled block to place at the specified index.
24482 * @private
24483 */
24484VirtualRepeatController.prototype.getBlock_ = function(index) {
24485 if (this.pooledBlocks.length) {
24486 return this.pooledBlocks.pop();
24487 }
24488
24489 var block;
24490 this.transclude(angular.bind(this, function(clone, scope) {
24491 block = {
24492 element: clone,
24493 new: true,
24494 scope: scope
24495 };
24496
24497 this.updateScope_(scope, index);
24498 this.parentNode.appendChild(clone[0]);
24499 }));
24500
24501 return block;
24502};
24503
24504
24505/**
24506 * Updates and if not in a digest cycle, digests the specified block's scope to the data
24507 * at the specified index.
24508 * @param {!VirtualRepeatController.Block} block The block whose scope should be updated.
24509 * @param {number} index The index to set.
24510 * @private
24511 */
24512VirtualRepeatController.prototype.updateBlock_ = function(block, index) {
24513 this.blocks[index] = block;
24514
24515 if (!block.new &&
24516 (block.scope.$index === index && block.scope[this.repeatName] === this.items[index])) {
24517 return;
24518 }
24519 block.new = false;
24520
24521 // Update and digest the block's scope.
24522 this.updateScope_(block.scope, index);
24523
24524 // Perform digest before reattaching the block.
24525 // Any resulting synchronous dom mutations should be much faster as a result.
24526 // This might break some directives, but I'm going to try it for now.
24527 if (!this.$rootScope.$$phase) {
24528 block.scope.$digest();
24529 }
24530};
24531
24532
24533/**
24534 * Updates scope to the data at the specified index.
24535 * @param {!angular.Scope} scope The scope which should be updated.
24536 * @param {number} index The index to set.
24537 * @private
24538 */
24539VirtualRepeatController.prototype.updateScope_ = function(scope, index) {
24540 scope.$index = index;
24541 scope[this.repeatName] = this.items && this.items[index];
24542 if (this.extraName) scope[this.extraName(this.$scope)] = this.items[index];
24543};
24544
24545
24546/**
24547 * Pools the block at the specified index (Pulls its element out of the dom and stores it).
24548 * @param {number} index The index at which the block to pool is stored.
24549 * @private
24550 */
24551VirtualRepeatController.prototype.poolBlock_ = function(index) {
24552 this.pooledBlocks.push(this.blocks[index]);
24553 this.parentNode.removeChild(this.blocks[index].element[0]);
24554 delete this.blocks[index];
24555};
24556
24557
24558/**
24559 * Produces a dom fragment containing the elements from the list of blocks.
24560 * @param {!Array<!VirtualRepeatController.Block>} blocks The blocks whose elements
24561 * should be added to the document fragment.
24562 * @return {DocumentFragment}
24563 * @private
24564 */
24565VirtualRepeatController.prototype.domFragmentFromBlocks_ = function(blocks) {
24566 var fragment = this.$document[0].createDocumentFragment();
24567 blocks.forEach(function(block) {
24568 fragment.appendChild(block.element[0]);
24569 });
24570 return fragment;
24571};
24572
24573
24574/**
24575 * Updates start and end indexes based on length of repeated items and container size.
24576 * @private
24577 */
24578VirtualRepeatController.prototype.updateIndexes_ = function() {
24579 var itemsLength = this.items ? this.items.length : 0;
24580 var containerLength = Math.ceil(this.container.getSize() / this.itemSize);
24581
24582 this.newStartIndex = Math.max(0, Math.min(
24583 itemsLength - containerLength,
24584 Math.floor(this.container.getScrollOffset() / this.itemSize)));
24585 this.newVisibleEnd = this.newStartIndex + containerLength + NUM_EXTRA;
24586 this.newEndIndex = Math.min(itemsLength, this.newVisibleEnd);
24587 this.newStartIndex = Math.max(0, this.newStartIndex - NUM_EXTRA);
24588};
24589
24590/**
24591 * This VirtualRepeatModelArrayLike class enforces the interface requirements
24592 * for infinite scrolling within a mdVirtualRepeatContainer. An object with this
24593 * interface must implement the following interface with two (2) methods:
24594 *
24595 * getItemAtIndex: function(index) -> item at that index or null if it is not yet
24596 * loaded (It should start downloading the item in that case).
24597 *
24598 * getLength: function() -> number The data legnth to which the repeater container
24599 * should be sized. Ideally, when the count is known, this method should return it.
24600 * Otherwise, return a higher number than the currently loaded items to produce an
24601 * infinite-scroll behavior.
24602 *
24603 * @usage
24604 * <hljs lang="html">
24605 * <md-virtual-repeat-container md-orient-horizontal>
24606 * <div md-virtual-repeat="i in items" md-on-demand>
24607 * Hello {{i}}!
24608 * </div>
24609 * </md-virtual-repeat-container>
24610 * </hljs>
24611 *
24612 */
24613function VirtualRepeatModelArrayLike(model) {
24614 if (!angular.isFunction(model.getItemAtIndex) ||
24615 !angular.isFunction(model.getLength)) {
24616 throw Error('When md-on-demand is enabled, the Object passed to md-virtual-repeat must implement ' +
24617 'functions getItemAtIndex() and getLength() ');
24618 }
24619
24620 this.model = model;
24621}
24622
24623
24624VirtualRepeatModelArrayLike.prototype.$$includeIndexes = function(start, end) {
24625 for (var i = start; i < end; i++) {
24626 if (!this.hasOwnProperty(i)) {
24627 this[i] = this.model.getItemAtIndex(i);
24628 }
24629 }
24630 this.length = this.model.getLength();
24631};
24632
24633})();
24634(function(){
24635"use strict";
24636
24637/**
24638 * @ngdoc module
24639 * @name material.components.whiteframe
24640 */
24641MdWhiteframeDirective.$inject = ["$log"];
24642angular
24643 .module('material.components.whiteframe', ['material.core'])
24644 .directive('mdWhiteframe', MdWhiteframeDirective);
24645
24646/**
24647 * @ngdoc directive
24648 * @module material.components.whiteframe
24649 * @name mdWhiteframe
24650 *
24651 * @description
24652 * The md-whiteframe directive allows you to apply an elevation shadow to an element.
24653 *
24654 * The attribute values needs to be a number between 1 and 24 or -1.
24655 * When set to -1 no style is applied.
24656 *
24657 * ### Notes
24658 * - If there is no value specified it defaults to 4dp.
24659 * - If the value is not valid it defaults to 4dp.
24660
24661 * @usage
24662 * <hljs lang="html">
24663 * <div md-whiteframe="3">
24664 * <span>Elevation of 3dp</span>
24665 * </div>
24666 * </hljs>
24667 *
24668 * <hljs lang="html">
24669 * <div md-whiteframe="-1">
24670 * <span>No elevation shadow applied</span>
24671 * </div>
24672 * </hljs>
24673 *
24674 * <hljs lang="html">
24675 * <div ng-init="elevation = 5" md-whiteframe="{{elevation}}">
24676 * <span>Elevation of 5dp with an interpolated value</span>
24677 * </div>
24678 * </hljs>
24679 */
24680function MdWhiteframeDirective($log) {
24681 var DISABLE_DP = -1;
24682 var MIN_DP = 1;
24683 var MAX_DP = 24;
24684 var DEFAULT_DP = 4;
24685
24686 return {
24687 link: postLink
24688 };
24689
24690 function postLink(scope, element, attr) {
24691 var oldClass = '';
24692
24693 attr.$observe('mdWhiteframe', function(elevation) {
24694 elevation = parseInt(elevation, 10) || DEFAULT_DP;
24695
24696 if (elevation != DISABLE_DP && (elevation > MAX_DP || elevation < MIN_DP)) {
24697 $log.warn('md-whiteframe attribute value is invalid. It should be a number between ' + MIN_DP + ' and ' + MAX_DP, element[0]);
24698 elevation = DEFAULT_DP;
24699 }
24700
24701 var newClass = elevation == DISABLE_DP ? '' : 'md-whiteframe-' + elevation + 'dp';
24702 attr.$updateClass(newClass, oldClass);
24703 oldClass = newClass;
24704 });
24705 }
24706}
24707
24708
24709})();
24710(function(){
24711"use strict";
24712
24713
24714MdAutocompleteCtrl.$inject = ["$scope", "$element", "$mdUtil", "$mdConstant", "$mdTheming", "$window", "$animate", "$rootElement", "$attrs", "$q", "$log", "$mdLiveAnnouncer"];angular
24715 .module('material.components.autocomplete')
24716 .controller('MdAutocompleteCtrl', MdAutocompleteCtrl);
24717
24718var ITEM_HEIGHT = 48,
24719 MAX_ITEMS = 5,
24720 MENU_PADDING = 8,
24721 INPUT_PADDING = 2; // Padding provided by `md-input-container`
24722
24723function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, $window,
24724 $animate, $rootElement, $attrs, $q, $log, $mdLiveAnnouncer) {
24725
24726 // Internal Variables.
24727 var ctrl = this,
24728 itemParts = $scope.itemsExpr.split(/ in /i),
24729 itemExpr = itemParts[ 1 ],
24730 elements = null,
24731 cache = {},
24732 noBlur = false,
24733 selectedItemWatchers = [],
24734 hasFocus = false,
24735 fetchesInProgress = 0,
24736 enableWrapScroll = null,
24737 inputModelCtrl = null,
24738 debouncedOnResize = $mdUtil.debounce(onWindowResize);
24739
24740 // Public Exported Variables with handlers
24741 defineProperty('hidden', handleHiddenChange, true);
24742
24743 // Public Exported Variables
24744 ctrl.scope = $scope;
24745 ctrl.parent = $scope.$parent;
24746 ctrl.itemName = itemParts[ 0 ];
24747 ctrl.matches = [];
24748 ctrl.loading = false;
24749 ctrl.hidden = true;
24750 ctrl.index = null;
24751 ctrl.id = $mdUtil.nextUid();
24752 ctrl.isDisabled = null;
24753 ctrl.isRequired = null;
24754 ctrl.isReadonly = null;
24755 ctrl.hasNotFound = false;
24756
24757 // Public Exported Methods
24758 ctrl.keydown = keydown;
24759 ctrl.blur = blur;
24760 ctrl.focus = focus;
24761 ctrl.clear = clearValue;
24762 ctrl.select = select;
24763 ctrl.listEnter = onListEnter;
24764 ctrl.listLeave = onListLeave;
24765 ctrl.mouseUp = onMouseup;
24766 ctrl.getCurrentDisplayValue = getCurrentDisplayValue;
24767 ctrl.registerSelectedItemWatcher = registerSelectedItemWatcher;
24768 ctrl.unregisterSelectedItemWatcher = unregisterSelectedItemWatcher;
24769 ctrl.notFoundVisible = notFoundVisible;
24770 ctrl.loadingIsVisible = loadingIsVisible;
24771 ctrl.positionDropdown = positionDropdown;
24772
24773 /**
24774 * Report types to be used for the $mdLiveAnnouncer
24775 * @enum {number} Unique flag id.
24776 */
24777 var ReportType = {
24778 Count: 1,
24779 Selected: 2
24780 };
24781
24782 return init();
24783
24784 //-- initialization methods
24785
24786 /**
24787 * Initialize the controller, setup watchers, gather elements
24788 */
24789 function init () {
24790
24791 $mdUtil.initOptionalProperties($scope, $attrs, {
24792 searchText: '',
24793 selectedItem: null,
24794 clearButton: false
24795 });
24796
24797 $mdTheming($element);
24798 configureWatchers();
24799 $mdUtil.nextTick(function () {
24800
24801 gatherElements();
24802 moveDropdown();
24803
24804 // Forward all focus events to the input element when autofocus is enabled
24805 if ($scope.autofocus) {
24806 $element.on('focus', focusInputElement);
24807 }
24808 });
24809 }
24810
24811 function updateModelValidators() {
24812 if (!$scope.requireMatch || !inputModelCtrl) return;
24813
24814 inputModelCtrl.$setValidity('md-require-match', !!$scope.selectedItem || !$scope.searchText);
24815 }
24816
24817 /**
24818 * Calculates the dropdown's position and applies the new styles to the menu element
24819 * @returns {*}
24820 */
24821 function positionDropdown () {
24822 if (!elements) {
24823 return $mdUtil.nextTick(positionDropdown, false, $scope);
24824 }
24825
24826 var dropdownHeight = ($scope.dropdownItems || MAX_ITEMS) * ITEM_HEIGHT;
24827
24828 var hrect = elements.wrap.getBoundingClientRect(),
24829 vrect = elements.snap.getBoundingClientRect(),
24830 root = elements.root.getBoundingClientRect(),
24831 top = vrect.bottom - root.top,
24832 bot = root.bottom - vrect.top,
24833 left = hrect.left - root.left,
24834 width = hrect.width,
24835 offset = getVerticalOffset(),
24836 position = $scope.dropdownPosition,
24837 styles;
24838
24839 // Automatically determine dropdown placement based on available space in viewport.
24840 if (!position) {
24841 position = (top > bot && root.height - hrect.bottom - MENU_PADDING < dropdownHeight) ? 'top' : 'bottom';
24842 }
24843 // Adjust the width to account for the padding provided by `md-input-container`
24844 if ($attrs.mdFloatingLabel) {
24845 left += INPUT_PADDING;
24846 width -= INPUT_PADDING * 2;
24847 }
24848 styles = {
24849 left: left + 'px',
24850 minWidth: width + 'px',
24851 maxWidth: Math.max(hrect.right - root.left, root.right - hrect.left) - MENU_PADDING + 'px'
24852 };
24853
24854 if (position === 'top') {
24855 styles.top = 'auto';
24856 styles.bottom = bot + 'px';
24857 styles.maxHeight = Math.min(dropdownHeight, hrect.top - root.top - MENU_PADDING) + 'px';
24858 } else {
24859 var bottomSpace = root.bottom - hrect.bottom - MENU_PADDING + $mdUtil.getViewportTop();
24860
24861 styles.top = (top - offset) + 'px';
24862 styles.bottom = 'auto';
24863 styles.maxHeight = Math.min(dropdownHeight, bottomSpace) + 'px';
24864 }
24865
24866 elements.$.scrollContainer.css(styles);
24867 $mdUtil.nextTick(correctHorizontalAlignment, false);
24868
24869 /**
24870 * Calculates the vertical offset for floating label examples to account for ngMessages
24871 * @returns {number}
24872 */
24873 function getVerticalOffset () {
24874 var offset = 0;
24875 var inputContainer = $element.find('md-input-container');
24876 if (inputContainer.length) {
24877 var input = inputContainer.find('input');
24878 offset = inputContainer.prop('offsetHeight');
24879 offset -= input.prop('offsetTop');
24880 offset -= input.prop('offsetHeight');
24881 // add in the height left up top for the floating label text
24882 offset += inputContainer.prop('offsetTop');
24883 }
24884 return offset;
24885 }
24886
24887 /**
24888 * Makes sure that the menu doesn't go off of the screen on either side.
24889 */
24890 function correctHorizontalAlignment () {
24891 var dropdown = elements.scrollContainer.getBoundingClientRect(),
24892 styles = {};
24893 if (dropdown.right > root.right - MENU_PADDING) {
24894 styles.left = (hrect.right - dropdown.width) + 'px';
24895 }
24896 elements.$.scrollContainer.css(styles);
24897 }
24898 }
24899
24900 /**
24901 * Moves the dropdown menu to the body tag in order to avoid z-index and overflow issues.
24902 */
24903 function moveDropdown () {
24904 if (!elements.$.root.length) return;
24905 $mdTheming(elements.$.scrollContainer);
24906 elements.$.scrollContainer.detach();
24907 elements.$.root.append(elements.$.scrollContainer);
24908 if ($animate.pin) $animate.pin(elements.$.scrollContainer, $rootElement);
24909 }
24910
24911 /**
24912 * Sends focus to the input element.
24913 */
24914 function focusInputElement () {
24915 elements.input.focus();
24916 }
24917
24918 /**
24919 * Sets up any watchers used by autocomplete
24920 */
24921 function configureWatchers () {
24922 var wait = parseInt($scope.delay, 10) || 0;
24923
24924 $attrs.$observe('disabled', function (value) { ctrl.isDisabled = $mdUtil.parseAttributeBoolean(value, false); });
24925 $attrs.$observe('required', function (value) { ctrl.isRequired = $mdUtil.parseAttributeBoolean(value, false); });
24926 $attrs.$observe('readonly', function (value) { ctrl.isReadonly = $mdUtil.parseAttributeBoolean(value, false); });
24927
24928 $scope.$watch('searchText', wait ? $mdUtil.debounce(handleSearchText, wait) : handleSearchText);
24929 $scope.$watch('selectedItem', selectedItemChange);
24930
24931 angular.element($window).on('resize', debouncedOnResize);
24932
24933 $scope.$on('$destroy', cleanup);
24934 }
24935
24936 /**
24937 * Removes any events or leftover elements created by this controller
24938 */
24939 function cleanup () {
24940 if (!ctrl.hidden) {
24941 $mdUtil.enableScrolling();
24942 }
24943
24944 angular.element($window).off('resize', debouncedOnResize);
24945
24946 if ( elements ){
24947 var items = ['ul', 'scroller', 'scrollContainer', 'input'];
24948 angular.forEach(items, function(key){
24949 elements.$[key].remove();
24950 });
24951 }
24952 }
24953
24954 /**
24955 * Event handler to be called whenever the window resizes.
24956 */
24957 function onWindowResize() {
24958 if (!ctrl.hidden) {
24959 positionDropdown();
24960 }
24961 }
24962
24963 /**
24964 * Gathers all of the elements needed for this controller
24965 */
24966 function gatherElements () {
24967
24968 var snapWrap = gatherSnapWrap();
24969
24970 elements = {
24971 main: $element[0],
24972 scrollContainer: $element[0].querySelector('.md-virtual-repeat-container'),
24973 scroller: $element[0].querySelector('.md-virtual-repeat-scroller'),
24974 ul: $element.find('ul')[0],
24975 input: $element.find('input')[0],
24976 wrap: snapWrap.wrap,
24977 snap: snapWrap.snap,
24978 root: document.body
24979 };
24980
24981 elements.li = elements.ul.getElementsByTagName('li');
24982 elements.$ = getAngularElements(elements);
24983
24984 inputModelCtrl = elements.$.input.controller('ngModel');
24985 }
24986
24987 /**
24988 * Gathers the snap and wrap elements
24989 *
24990 */
24991 function gatherSnapWrap() {
24992 var element;
24993 var value;
24994 for (element = $element; element.length; element = element.parent()) {
24995 value = element.attr('md-autocomplete-snap');
24996 if (angular.isDefined(value)) break;
24997 }
24998
24999 if (element.length) {
25000 return {
25001 snap: element[0],
25002 wrap: (value.toLowerCase() === 'width') ? element[0] : $element.find('md-autocomplete-wrap')[0]
25003 };
25004 }
25005
25006 var wrap = $element.find('md-autocomplete-wrap')[0];
25007 return {
25008 snap: wrap,
25009 wrap: wrap
25010 };
25011 }
25012
25013 /**
25014 * Gathers angular-wrapped versions of each element
25015 * @param elements
25016 * @returns {{}}
25017 */
25018 function getAngularElements (elements) {
25019 var obj = {};
25020 for (var key in elements) {
25021 if (elements.hasOwnProperty(key)) obj[ key ] = angular.element(elements[ key ]);
25022 }
25023 return obj;
25024 }
25025
25026 //-- event/change handlers
25027
25028 /**
25029 * Handles changes to the `hidden` property.
25030 * @param hidden
25031 * @param oldHidden
25032 */
25033 function handleHiddenChange (hidden, oldHidden) {
25034 if (!hidden && oldHidden) {
25035 positionDropdown();
25036
25037 // Report in polite mode, because the screenreader should finish the default description of
25038 // the input. element.
25039 reportMessages(true, ReportType.Count | ReportType.Selected);
25040
25041 if (elements) {
25042 $mdUtil.disableScrollAround(elements.ul);
25043 enableWrapScroll = disableElementScrollEvents(angular.element(elements.wrap));
25044 }
25045 } else if (hidden && !oldHidden) {
25046 $mdUtil.enableScrolling();
25047
25048 if (enableWrapScroll) {
25049 enableWrapScroll();
25050 enableWrapScroll = null;
25051 }
25052 }
25053 }
25054
25055 /**
25056 * Disables scrolling for a specific element
25057 */
25058 function disableElementScrollEvents(element) {
25059
25060 function preventDefault(e) {
25061 e.preventDefault();
25062 }
25063
25064 element.on('wheel', preventDefault);
25065 element.on('touchmove', preventDefault);
25066
25067 return function() {
25068 element.off('wheel', preventDefault);
25069 element.off('touchmove', preventDefault);
25070 };
25071 }
25072
25073 /**
25074 * When the user mouses over the dropdown menu, ignore blur events.
25075 */
25076 function onListEnter () {
25077 noBlur = true;
25078 }
25079
25080 /**
25081 * When the user's mouse leaves the menu, blur events may hide the menu again.
25082 */
25083 function onListLeave () {
25084 if (!hasFocus && !ctrl.hidden) elements.input.focus();
25085 noBlur = false;
25086 ctrl.hidden = shouldHide();
25087 }
25088
25089 /**
25090 * When the mouse button is released, send focus back to the input field.
25091 */
25092 function onMouseup () {
25093 elements.input.focus();
25094 }
25095
25096 /**
25097 * Handles changes to the selected item.
25098 * @param selectedItem
25099 * @param previousSelectedItem
25100 */
25101 function selectedItemChange (selectedItem, previousSelectedItem) {
25102
25103 updateModelValidators();
25104
25105 if (selectedItem) {
25106 getDisplayValue(selectedItem).then(function (val) {
25107 $scope.searchText = val;
25108 handleSelectedItemChange(selectedItem, previousSelectedItem);
25109 });
25110 } else if (previousSelectedItem && $scope.searchText) {
25111 getDisplayValue(previousSelectedItem).then(function(displayValue) {
25112 // Clear the searchText, when the selectedItem is set to null.
25113 // Do not clear the searchText, when the searchText isn't matching with the previous
25114 // selected item.
25115 if (angular.isString($scope.searchText)
25116 && displayValue.toString().toLowerCase() === $scope.searchText.toLowerCase()) {
25117 $scope.searchText = '';
25118 }
25119 });
25120 }
25121
25122 if (selectedItem !== previousSelectedItem) announceItemChange();
25123 }
25124
25125 /**
25126 * Use the user-defined expression to announce changes each time a new item is selected
25127 */
25128 function announceItemChange () {
25129 angular.isFunction($scope.itemChange) && $scope.itemChange(getItemAsNameVal($scope.selectedItem));
25130 }
25131
25132 /**
25133 * Use the user-defined expression to announce changes each time the search text is changed
25134 */
25135 function announceTextChange () {
25136 angular.isFunction($scope.textChange) && $scope.textChange();
25137 }
25138
25139 /**
25140 * Calls any external watchers listening for the selected item. Used in conjunction with
25141 * `registerSelectedItemWatcher`.
25142 * @param selectedItem
25143 * @param previousSelectedItem
25144 */
25145 function handleSelectedItemChange (selectedItem, previousSelectedItem) {
25146 selectedItemWatchers.forEach(function (watcher) { watcher(selectedItem, previousSelectedItem); });
25147 }
25148
25149 /**
25150 * Register a function to be called when the selected item changes.
25151 * @param cb
25152 */
25153 function registerSelectedItemWatcher (cb) {
25154 if (selectedItemWatchers.indexOf(cb) == -1) {
25155 selectedItemWatchers.push(cb);
25156 }
25157 }
25158
25159 /**
25160 * Unregister a function previously registered for selected item changes.
25161 * @param cb
25162 */
25163 function unregisterSelectedItemWatcher (cb) {
25164 var i = selectedItemWatchers.indexOf(cb);
25165 if (i != -1) {
25166 selectedItemWatchers.splice(i, 1);
25167 }
25168 }
25169
25170 /**
25171 * Handles changes to the searchText property.
25172 * @param searchText
25173 * @param previousSearchText
25174 */
25175 function handleSearchText (searchText, previousSearchText) {
25176 ctrl.index = getDefaultIndex();
25177
25178 // do nothing on init
25179 if (searchText === previousSearchText) return;
25180
25181 updateModelValidators();
25182
25183 getDisplayValue($scope.selectedItem).then(function (val) {
25184 // clear selected item if search text no longer matches it
25185 if (searchText !== val) {
25186 $scope.selectedItem = null;
25187
25188
25189 // trigger change event if available
25190 if (searchText !== previousSearchText) announceTextChange();
25191
25192 // cancel results if search text is not long enough
25193 if (!isMinLengthMet()) {
25194 ctrl.matches = [];
25195
25196 setLoading(false);
25197 reportMessages(false, ReportType.Count);
25198
25199 } else {
25200 handleQuery();
25201 }
25202 }
25203 });
25204
25205 }
25206
25207 /**
25208 * Handles input blur event, determines if the dropdown should hide.
25209 */
25210 function blur($event) {
25211 hasFocus = false;
25212
25213 if (!noBlur) {
25214 ctrl.hidden = shouldHide();
25215 evalAttr('ngBlur', { $event: $event });
25216 }
25217 }
25218
25219 /**
25220 * Force blur on input element
25221 * @param forceBlur
25222 */
25223 function doBlur(forceBlur) {
25224 if (forceBlur) {
25225 noBlur = false;
25226 hasFocus = false;
25227 }
25228 elements.input.blur();
25229 }
25230
25231 /**
25232 * Handles input focus event, determines if the dropdown should show.
25233 */
25234 function focus($event) {
25235 hasFocus = true;
25236
25237 if (isSearchable() && isMinLengthMet()) {
25238 handleQuery();
25239 }
25240
25241 ctrl.hidden = shouldHide();
25242
25243 evalAttr('ngFocus', { $event: $event });
25244 }
25245
25246 /**
25247 * Handles keyboard input.
25248 * @param event
25249 */
25250 function keydown (event) {
25251 switch (event.keyCode) {
25252 case $mdConstant.KEY_CODE.DOWN_ARROW:
25253 if (ctrl.loading) return;
25254 event.stopPropagation();
25255 event.preventDefault();
25256 ctrl.index = Math.min(ctrl.index + 1, ctrl.matches.length - 1);
25257 updateScroll();
25258 reportMessages(false, ReportType.Selected);
25259 break;
25260 case $mdConstant.KEY_CODE.UP_ARROW:
25261 if (ctrl.loading) return;
25262 event.stopPropagation();
25263 event.preventDefault();
25264 ctrl.index = ctrl.index < 0 ? ctrl.matches.length - 1 : Math.max(0, ctrl.index - 1);
25265 updateScroll();
25266 reportMessages(false, ReportType.Selected);
25267 break;
25268 case $mdConstant.KEY_CODE.TAB:
25269 // If we hit tab, assume that we've left the list so it will close
25270 onListLeave();
25271
25272 if (ctrl.hidden || ctrl.loading || ctrl.index < 0 || ctrl.matches.length < 1) return;
25273 select(ctrl.index);
25274 break;
25275 case $mdConstant.KEY_CODE.ENTER:
25276 if (ctrl.hidden || ctrl.loading || ctrl.index < 0 || ctrl.matches.length < 1) return;
25277 if (hasSelection()) return;
25278 event.stopPropagation();
25279 event.preventDefault();
25280 select(ctrl.index);
25281 break;
25282 case $mdConstant.KEY_CODE.ESCAPE:
25283 event.preventDefault(); // Prevent browser from always clearing input
25284 if (!shouldProcessEscape()) return;
25285 event.stopPropagation();
25286
25287 clearSelectedItem();
25288 if ($scope.searchText && hasEscapeOption('clear')) {
25289 clearSearchText();
25290 }
25291
25292 // Manually hide (needed for mdNotFound support)
25293 ctrl.hidden = true;
25294
25295 if (hasEscapeOption('blur')) {
25296 // Force the component to blur if they hit escape
25297 doBlur(true);
25298 }
25299
25300 break;
25301 default:
25302 }
25303 }
25304
25305 //-- getters
25306
25307 /**
25308 * Returns the minimum length needed to display the dropdown.
25309 * @returns {*}
25310 */
25311 function getMinLength () {
25312 return angular.isNumber($scope.minLength) ? $scope.minLength : 1;
25313 }
25314
25315 /**
25316 * Returns the display value for an item.
25317 * @param item
25318 * @returns {*}
25319 */
25320 function getDisplayValue (item) {
25321 return $q.when(getItemText(item) || item).then(function(itemText) {
25322 if (itemText && !angular.isString(itemText)) {
25323 $log.warn('md-autocomplete: Could not resolve display value to a string. ' +
25324 'Please check the `md-item-text` attribute.');
25325 }
25326
25327 return itemText;
25328 });
25329
25330 /**
25331 * Getter function to invoke user-defined expression (in the directive)
25332 * to convert your object to a single string.
25333 */
25334 function getItemText (item) {
25335 return (item && $scope.itemText) ? $scope.itemText(getItemAsNameVal(item)) : null;
25336 }
25337 }
25338
25339 /**
25340 * Returns the locals object for compiling item templates.
25341 * @param item
25342 * @returns {{}}
25343 */
25344 function getItemAsNameVal (item) {
25345 if (!item) return undefined;
25346
25347 var locals = {};
25348 if (ctrl.itemName) locals[ ctrl.itemName ] = item;
25349
25350 return locals;
25351 }
25352
25353 /**
25354 * Returns the default index based on whether or not autoselect is enabled.
25355 * @returns {number}
25356 */
25357 function getDefaultIndex () {
25358 return $scope.autoselect ? 0 : -1;
25359 }
25360
25361 /**
25362 * Sets the loading parameter and updates the hidden state.
25363 * @param value {boolean} Whether or not the component is currently loading.
25364 */
25365 function setLoading(value) {
25366 if (ctrl.loading != value) {
25367 ctrl.loading = value;
25368 }
25369
25370 // Always refresh the hidden variable as something else might have changed
25371 ctrl.hidden = shouldHide();
25372 }
25373
25374 /**
25375 * Determines if the menu should be hidden.
25376 * @returns {boolean}
25377 */
25378 function shouldHide () {
25379 if (!isSearchable()) return true; // Hide when not able to query
25380 else return !shouldShow(); // Hide when the dropdown is not able to show.
25381 }
25382
25383 /**
25384 * Determines whether the autocomplete is able to query within the current state.
25385 * @returns {boolean}
25386 */
25387 function isSearchable() {
25388 if (ctrl.loading && !hasMatches()) return false; // No query when query is in progress.
25389 else if (hasSelection()) return false; // No query if there is already a selection
25390 else if (!hasFocus) return false; // No query if the input does not have focus
25391 return true;
25392 }
25393
25394 /**
25395 * Determines if the escape keydown should be processed
25396 * @returns {boolean}
25397 */
25398 function shouldProcessEscape() {
25399 return hasEscapeOption('blur') || !ctrl.hidden || ctrl.loading || hasEscapeOption('clear') && $scope.searchText;
25400 }
25401
25402 /**
25403 * Determines if an escape option is set
25404 * @returns {boolean}
25405 */
25406 function hasEscapeOption(option) {
25407 return !$scope.escapeOptions || $scope.escapeOptions.toLowerCase().indexOf(option) !== -1;
25408 }
25409
25410 /**
25411 * Determines if the menu should be shown.
25412 * @returns {boolean}
25413 */
25414 function shouldShow() {
25415 return (isMinLengthMet() && hasMatches()) || notFoundVisible();
25416 }
25417
25418 /**
25419 * Returns true if the search text has matches.
25420 * @returns {boolean}
25421 */
25422 function hasMatches() {
25423 return ctrl.matches.length ? true : false;
25424 }
25425
25426 /**
25427 * Returns true if the autocomplete has a valid selection.
25428 * @returns {boolean}
25429 */
25430 function hasSelection() {
25431 return ctrl.scope.selectedItem ? true : false;
25432 }
25433
25434 /**
25435 * Returns true if the loading indicator is, or should be, visible.
25436 * @returns {boolean}
25437 */
25438 function loadingIsVisible() {
25439 return ctrl.loading && !hasSelection();
25440 }
25441
25442 /**
25443 * Returns the display value of the current item.
25444 * @returns {*}
25445 */
25446 function getCurrentDisplayValue () {
25447 return getDisplayValue(ctrl.matches[ ctrl.index ]);
25448 }
25449
25450 /**
25451 * Determines if the minimum length is met by the search text.
25452 * @returns {*}
25453 */
25454 function isMinLengthMet () {
25455 return ($scope.searchText || '').length >= getMinLength();
25456 }
25457
25458 //-- actions
25459
25460 /**
25461 * Defines a public property with a handler and a default value.
25462 * @param key
25463 * @param handler
25464 * @param value
25465 */
25466 function defineProperty (key, handler, value) {
25467 Object.defineProperty(ctrl, key, {
25468 get: function () { return value; },
25469 set: function (newValue) {
25470 var oldValue = value;
25471 value = newValue;
25472 handler(newValue, oldValue);
25473 }
25474 });
25475 }
25476
25477 /**
25478 * Selects the item at the given index.
25479 * @param index
25480 */
25481 function select (index) {
25482 //-- force form to update state for validation
25483 $mdUtil.nextTick(function () {
25484 getDisplayValue(ctrl.matches[ index ]).then(function (val) {
25485 var ngModel = elements.$.input.controller('ngModel');
25486 ngModel.$setViewValue(val);
25487 ngModel.$render();
25488 }).finally(function () {
25489 $scope.selectedItem = ctrl.matches[ index ];
25490 setLoading(false);
25491 });
25492 }, false);
25493 }
25494
25495 /**
25496 * Clears the searchText value and selected item.
25497 */
25498 function clearValue () {
25499 clearSelectedItem();
25500 clearSearchText();
25501 }
25502
25503 /**
25504 * Clears the selected item
25505 */
25506 function clearSelectedItem () {
25507 // Reset our variables
25508 ctrl.index = 0;
25509 ctrl.matches = [];
25510 }
25511
25512 /**
25513 * Clears the searchText value
25514 */
25515 function clearSearchText () {
25516 // Set the loading to true so we don't see flashes of content.
25517 // The flashing will only occur when an async request is running.
25518 // So the loading process will stop when the results had been retrieved.
25519 setLoading(true);
25520
25521 $scope.searchText = '';
25522
25523 // Normally, triggering the change / input event is unnecessary, because the browser detects it properly.
25524 // But some browsers are not detecting it properly, which means that we have to trigger the event.
25525 // Using the `input` is not working properly, because for example IE11 is not supporting the `input` event.
25526 // The `change` event is a good alternative and is supported by all supported browsers.
25527 var eventObj = document.createEvent('CustomEvent');
25528 eventObj.initCustomEvent('change', true, true, { value: '' });
25529 elements.input.dispatchEvent(eventObj);
25530
25531 // For some reason, firing the above event resets the value of $scope.searchText if
25532 // $scope.searchText has a space character at the end, so we blank it one more time and then
25533 // focus.
25534 elements.input.blur();
25535 $scope.searchText = '';
25536 elements.input.focus();
25537 }
25538
25539 /**
25540 * Fetches the results for the provided search text.
25541 * @param searchText
25542 */
25543 function fetchResults (searchText) {
25544 var items = $scope.$parent.$eval(itemExpr),
25545 term = searchText.toLowerCase(),
25546 isList = angular.isArray(items),
25547 isPromise = !!items.then; // Every promise should contain a `then` property
25548
25549 if (isList) onResultsRetrieved(items);
25550 else if (isPromise) handleAsyncResults(items);
25551
25552 function handleAsyncResults(items) {
25553 if ( !items ) return;
25554
25555 items = $q.when(items);
25556 fetchesInProgress++;
25557 setLoading(true);
25558
25559 $mdUtil.nextTick(function () {
25560 items
25561 .then(onResultsRetrieved)
25562 .finally(function(){
25563 if (--fetchesInProgress === 0) {
25564 setLoading(false);
25565 }
25566 });
25567 },true, $scope);
25568 }
25569
25570 function onResultsRetrieved(matches) {
25571 cache[term] = matches;
25572
25573 // Just cache the results if the request is now outdated.
25574 // The request becomes outdated, when the new searchText has changed during the result fetching.
25575 if ((searchText || '') !== ($scope.searchText || '')) {
25576 return;
25577 }
25578
25579 handleResults(matches);
25580 }
25581 }
25582
25583
25584 /**
25585 * Reports given message types to supported screenreaders.
25586 * @param {boolean} isPolite Whether the announcement should be polite.
25587 * @param {!number} types Message flags to be reported to the screenreader.
25588 */
25589 function reportMessages(isPolite, types) {
25590
25591 var politeness = isPolite ? 'polite' : 'assertive';
25592 var messages = [];
25593
25594 if (types & ReportType.Selected && ctrl.index !== -1) {
25595 messages.push(getCurrentDisplayValue());
25596 }
25597
25598 if (types & ReportType.Count) {
25599 messages.push($q.resolve(getCountMessage()));
25600 }
25601
25602 $q.all(messages).then(function(data) {
25603 $mdLiveAnnouncer.announce(data.join(' '), politeness);
25604 });
25605
25606 }
25607
25608 /**
25609 * Returns the ARIA message for how many results match the current query.
25610 * @returns {*}
25611 */
25612 function getCountMessage () {
25613 switch (ctrl.matches.length) {
25614 case 0:
25615 return 'There are no matches available.';
25616 case 1:
25617 return 'There is 1 match available.';
25618 default:
25619 return 'There are ' + ctrl.matches.length + ' matches available.';
25620 }
25621 }
25622
25623 /**
25624 * Makes sure that the focused element is within view.
25625 */
25626 function updateScroll () {
25627 if (!elements.li[0]) return;
25628 var height = elements.li[0].offsetHeight,
25629 top = height * ctrl.index,
25630 bot = top + height,
25631 hgt = elements.scroller.clientHeight,
25632 scrollTop = elements.scroller.scrollTop;
25633 if (top < scrollTop) {
25634 scrollTo(top);
25635 } else if (bot > scrollTop + hgt) {
25636 scrollTo(bot - hgt);
25637 }
25638 }
25639
25640 function isPromiseFetching() {
25641 return fetchesInProgress !== 0;
25642 }
25643
25644 function scrollTo (offset) {
25645 elements.$.scrollContainer.controller('mdVirtualRepeatContainer').scrollTo(offset);
25646 }
25647
25648 function notFoundVisible () {
25649 var textLength = (ctrl.scope.searchText || '').length;
25650
25651 return ctrl.hasNotFound && !hasMatches() && (!ctrl.loading || isPromiseFetching()) && textLength >= getMinLength() && (hasFocus || noBlur) && !hasSelection();
25652 }
25653
25654 /**
25655 * Starts the query to gather the results for the current searchText. Attempts to return cached
25656 * results first, then forwards the process to `fetchResults` if necessary.
25657 */
25658 function handleQuery () {
25659 var searchText = $scope.searchText || '';
25660 var term = searchText.toLowerCase();
25661
25662 // If caching is enabled and the current searchText is stored in the cache
25663 if (!$scope.noCache && cache[term]) {
25664 // The results should be handled as same as a normal un-cached request does.
25665 handleResults(cache[term]);
25666 } else {
25667 fetchResults(searchText);
25668 }
25669
25670 ctrl.hidden = shouldHide();
25671 }
25672
25673 /**
25674 * Handles the retrieved results by showing them in the autocompletes dropdown.
25675 * @param results Retrieved results
25676 */
25677 function handleResults(results) {
25678 ctrl.matches = results;
25679 ctrl.hidden = shouldHide();
25680
25681 // If loading is in progress, then we'll end the progress. This is needed for example,
25682 // when the `clear` button was clicked, because there we always show the loading process, to prevent flashing.
25683 if (ctrl.loading) setLoading(false);
25684
25685 if ($scope.selectOnMatch) selectItemOnMatch();
25686
25687 positionDropdown();
25688 reportMessages(true, ReportType.Count);
25689 }
25690
25691 /**
25692 * If there is only one matching item and the search text matches its display value exactly,
25693 * automatically select that item. Note: This function is only called if the user uses the
25694 * `md-select-on-match` flag.
25695 */
25696 function selectItemOnMatch () {
25697 var searchText = $scope.searchText,
25698 matches = ctrl.matches,
25699 item = matches[ 0 ];
25700 if (matches.length === 1) getDisplayValue(item).then(function (displayValue) {
25701 var isMatching = searchText == displayValue;
25702 if ($scope.matchInsensitive && !isMatching) {
25703 isMatching = searchText.toLowerCase() == displayValue.toLowerCase();
25704 }
25705
25706 if (isMatching) select(0);
25707 });
25708 }
25709
25710 /**
25711 * Evaluates an attribute expression against the parent scope.
25712 * @param {String} attr Name of the attribute to be evaluated.
25713 * @param {Object?} locals Properties to be injected into the evaluation context.
25714 */
25715 function evalAttr(attr, locals) {
25716 if ($attrs[attr]) {
25717 $scope.$parent.$eval($attrs[attr], locals || {});
25718 }
25719 }
25720
25721}
25722
25723})();
25724(function(){
25725"use strict";
25726
25727
25728MdAutocomplete.$inject = ["$$mdSvgRegistry"];angular
25729 .module('material.components.autocomplete')
25730 .directive('mdAutocomplete', MdAutocomplete);
25731
25732/**
25733 * @ngdoc directive
25734 * @name mdAutocomplete
25735 * @module material.components.autocomplete
25736 *
25737 * @description
25738 * `<md-autocomplete>` is a special input component with a drop-down of all possible matches to a
25739 * custom query. This component allows you to provide real-time suggestions as the user types
25740 * in the input area.
25741 *
25742 * To start, you will need to specify the required parameters and provide a template for your
25743 * results. The content inside `md-autocomplete` will be treated as a template.
25744 *
25745 * In more complex cases, you may want to include other content such as a message to display when
25746 * no matches were found. You can do this by wrapping your template in `md-item-template` and
25747 * adding a tag for `md-not-found`. An example of this is shown below.
25748 *
25749 * To reset the displayed value you must clear both values for `md-search-text` and `md-selected-item`.
25750 *
25751 * ### Validation
25752 *
25753 * You can use `ng-messages` to include validation the same way that you would normally validate;
25754 * however, if you want to replicate a standard input with a floating label, you will have to
25755 * do the following:
25756 *
25757 * - Make sure that your template is wrapped in `md-item-template`
25758 * - Add your `ng-messages` code inside of `md-autocomplete`
25759 * - Add your validation properties to `md-autocomplete` (ie. `required`)
25760 * - Add a `name` to `md-autocomplete` (to be used on the generated `input`)
25761 *
25762 * There is an example below of how this should look.
25763 *
25764 * ### Snapping Drop-Down
25765 *
25766 * You can cause the autocomplete drop-down to snap to an ancestor element by applying the
25767 * `md-autocomplete-snap` attribute to that element. You can also snap to the width of
25768 * the `md-autocomplete-snap` element by setting the attribute's value to `width`
25769 * (ie. `md-autocomplete-snap="width"`).
25770 *
25771 * ### Notes
25772 *
25773 * **Autocomplete Dropdown Items Rendering**
25774 *
25775 * The `md-autocomplete` uses the the <a ng-href="api/directive/mdVirtualRepeatContainer">VirtualRepeat</a>
25776 * directive for displaying the results inside of the dropdown.<br/>
25777 *
25778 * > When encountering issues regarding the item template please take a look at the
25779 * <a ng-href="api/directive/mdVirtualRepeatContainer">VirtualRepeatContainer</a> documentation.
25780 *
25781 * **Autocomplete inside of a Virtual Repeat**
25782 *
25783 * When using the `md-autocomplete` directive inside of a
25784 * <a ng-href="api/directive/mdVirtualRepeatContainer">VirtualRepeatContainer</a> the dropdown items might
25785 * not update properly, because caching of the results is enabled by default.
25786 *
25787 * The autocomplete will then show invalid dropdown items, because the VirtualRepeat only updates the
25788 * scope bindings, rather than re-creating the `md-autocomplete` and the previous cached results will be used.
25789 *
25790 * > To avoid such problems ensure that the autocomplete does not cache any results.
25791 *
25792 * <hljs lang="html">
25793 * <md-autocomplete
25794 * md-no-cache="true"
25795 * md-selected-item="selectedItem"
25796 * md-items="item in items"
25797 * md-search-text="searchText"
25798 * md-item-text="item.display">
25799 * <span>{{ item.display }}</span>
25800 * </md-autocomplete>
25801 * </hljs>
25802 *
25803 *
25804 *
25805 * @param {expression} md-items An expression in the format of `item in results` to iterate over
25806 * matches for your search.<br/><br/>
25807 * The `results` expression can be also a function, which returns the results synchronously
25808 * or asynchronously (per Promise)
25809 * @param {expression=} md-selected-item-change An expression to be run each time a new item is
25810 * selected
25811 * @param {expression=} md-search-text-change An expression to be run each time the search text
25812 * updates
25813 * @param {expression=} md-search-text A model to bind the search query text to
25814 * @param {object=} md-selected-item A model to bind the selected item to
25815 * @param {expression=} md-item-text An expression that will convert your object to a single string.
25816 * @param {string=} placeholder Placeholder text that will be forwarded to the input.
25817 * @param {boolean=} md-no-cache Disables the internal caching that happens in autocomplete
25818 * @param {boolean=} ng-disabled Determines whether or not to disable the input field
25819 * @param {boolean=} md-require-match When set to true, the autocomplete will add a validator,
25820 * which will evaluate to false, when no item is currently selected.
25821 * @param {number=} md-min-length Specifies the minimum length of text before autocomplete will
25822 * make suggestions
25823 * @param {number=} md-delay Specifies the amount of time (in milliseconds) to wait before looking
25824 * for results
25825 * @param {boolean=} md-clear-button Whether the clear button for the autocomplete input should show up or not.
25826 * @param {boolean=} md-autofocus If true, the autocomplete will be automatically focused when a `$mdDialog`,
25827 * `$mdBottomsheet` or `$mdSidenav`, which contains the autocomplete, is opening. <br/><br/>
25828 * Also the autocomplete will immediately focus the input element.
25829 * @param {boolean=} md-no-asterisk When present, asterisk will not be appended to the floating label
25830 * @param {boolean=} md-autoselect If set to true, the first item will be automatically selected
25831 * in the dropdown upon open.
25832 * @param {string=} md-menu-class This will be applied to the dropdown menu for styling
25833 * @param {string=} md-floating-label This will add a floating label to autocomplete and wrap it in
25834 * `md-input-container`
25835 * @param {string=} md-input-name The name attribute given to the input element to be used with
25836 * FormController
25837 * @param {string=} md-select-on-focus When present the inputs text will be automatically selected
25838 * on focus.
25839 * @param {string=} md-input-id An ID to be added to the input element
25840 * @param {number=} md-input-minlength The minimum length for the input's value for validation
25841 * @param {number=} md-input-maxlength The maximum length for the input's value for validation
25842 * @param {boolean=} md-select-on-match When set, autocomplete will automatically select exact
25843 * the item if the search text is an exact match. <br/><br/>
25844 * Exact match means that there is only one match showing up.
25845 * @param {boolean=} md-match-case-insensitive When set and using `md-select-on-match`, autocomplete
25846 * will select on case-insensitive match
25847 * @param {string=} md-escape-options Override escape key logic. Default is `blur clear`.<br/>
25848 * Options: `blur | clear`, `none`
25849 * @param {string=} md-dropdown-items Specifies the maximum amount of items to be shown in
25850 * the dropdown.<br/><br/>
25851 * When the dropdown doesn't fit into the viewport, the dropdown will shrink
25852 * as less as possible.
25853 * @param {string=} md-dropdown-position Overrides the default dropdown position. Options: `top`, `bottom`.
25854 * @param {string=} ng-trim If set to false, the search text will be not trimmed automatically.
25855 * Defaults to true.
25856 * @param {string=} ng-pattern Adds the pattern validator to the ngModel of the search text.
25857 * [ngPattern Directive](https://docs.angularjs.org/api/ng/directive/ngPattern)
25858 *
25859 * @usage
25860 * ### Basic Example
25861 * <hljs lang="html">
25862 * <md-autocomplete
25863 * md-selected-item="selectedItem"
25864 * md-search-text="searchText"
25865 * md-items="item in getMatches(searchText)"
25866 * md-item-text="item.display">
25867 * <span md-highlight-text="searchText">{{item.display}}</span>
25868 * </md-autocomplete>
25869 * </hljs>
25870 *
25871 * ### Example with "not found" message
25872 * <hljs lang="html">
25873 * <md-autocomplete
25874 * md-selected-item="selectedItem"
25875 * md-search-text="searchText"
25876 * md-items="item in getMatches(searchText)"
25877 * md-item-text="item.display">
25878 * <md-item-template>
25879 * <span md-highlight-text="searchText">{{item.display}}</span>
25880 * </md-item-template>
25881 * <md-not-found>
25882 * No matches found.
25883 * </md-not-found>
25884 * </md-autocomplete>
25885 * </hljs>
25886 *
25887 * In this example, our code utilizes `md-item-template` and `md-not-found` to specify the
25888 * different parts that make up our component.
25889 *
25890 * ### Clear button for the input
25891 * By default, for floating label autocomplete's the clear button is not showing up
25892 * ([See specs](https://material.google.com/components/text-fields.html#text-fields-auto-complete-text-field))
25893 *
25894 * Nevertheless, developers are able to explicitly toggle the clear button for all types of autocomplete's.
25895 *
25896 * <hljs lang="html">
25897 * <md-autocomplete ... md-clear-button="true"></md-autocomplete>
25898 * <md-autocomplete ... md-clear-button="false"></md-autocomplete>
25899 * </hljs>
25900 *
25901 * ### Example with validation
25902 * <hljs lang="html">
25903 * <form name="autocompleteForm">
25904 * <md-autocomplete
25905 * required
25906 * md-input-name="autocomplete"
25907 * md-selected-item="selectedItem"
25908 * md-search-text="searchText"
25909 * md-items="item in getMatches(searchText)"
25910 * md-item-text="item.display">
25911 * <md-item-template>
25912 * <span md-highlight-text="searchText">{{item.display}}</span>
25913 * </md-item-template>
25914 * <div ng-messages="autocompleteForm.autocomplete.$error">
25915 * <div ng-message="required">This field is required</div>
25916 * </div>
25917 * </md-autocomplete>
25918 * </form>
25919 * </hljs>
25920 *
25921 * In this example, our code utilizes `md-item-template` and `ng-messages` to specify
25922 * input validation for the field.
25923 *
25924 * ### Asynchronous Results
25925 * The autocomplete items expression also supports promises, which will resolve with the query results.
25926 *
25927 * <hljs lang="js">
25928 * function AppController($scope, $http) {
25929 * $scope.query = function(searchText) {
25930 * return $http
25931 * .get(BACKEND_URL + '/items/' + searchText)
25932 * .then(function(data) {
25933 * // Map the response object to the data object.
25934 * return data;
25935 * });
25936 * };
25937 * }
25938 * </hljs>
25939 *
25940 * <hljs lang="html">
25941 * <md-autocomplete
25942 * md-selected-item="selectedItem"
25943 * md-search-text="searchText"
25944 * md-items="item in query(searchText)">
25945 * <md-item-template>
25946 * <span md-highlight-text="searchText">{{item}}</span>
25947 * </md-item-template>
25948 * </md-autocomplete>
25949 * </hljs>
25950 *
25951 */
25952
25953function MdAutocomplete ($$mdSvgRegistry) {
25954
25955 return {
25956 controller: 'MdAutocompleteCtrl',
25957 controllerAs: '$mdAutocompleteCtrl',
25958 scope: {
25959 inputName: '@mdInputName',
25960 inputMinlength: '@mdInputMinlength',
25961 inputMaxlength: '@mdInputMaxlength',
25962 searchText: '=?mdSearchText',
25963 selectedItem: '=?mdSelectedItem',
25964 itemsExpr: '@mdItems',
25965 itemText: '&mdItemText',
25966 placeholder: '@placeholder',
25967 noCache: '=?mdNoCache',
25968 requireMatch: '=?mdRequireMatch',
25969 selectOnMatch: '=?mdSelectOnMatch',
25970 matchInsensitive: '=?mdMatchCaseInsensitive',
25971 itemChange: '&?mdSelectedItemChange',
25972 textChange: '&?mdSearchTextChange',
25973 minLength: '=?mdMinLength',
25974 delay: '=?mdDelay',
25975 autofocus: '=?mdAutofocus',
25976 floatingLabel: '@?mdFloatingLabel',
25977 autoselect: '=?mdAutoselect',
25978 menuClass: '@?mdMenuClass',
25979 inputId: '@?mdInputId',
25980 escapeOptions: '@?mdEscapeOptions',
25981 dropdownItems: '=?mdDropdownItems',
25982 dropdownPosition: '@?mdDropdownPosition',
25983 clearButton: '=?mdClearButton'
25984 },
25985 compile: function(tElement, tAttrs) {
25986 var attributes = ['md-select-on-focus', 'md-no-asterisk', 'ng-trim', 'ng-pattern'];
25987 var input = tElement.find('input');
25988
25989 attributes.forEach(function(attribute) {
25990 var attrValue = tAttrs[tAttrs.$normalize(attribute)];
25991
25992 if (attrValue !== null) {
25993 input.attr(attribute, attrValue);
25994 }
25995 });
25996
25997 return function(scope, element, attrs, ctrl) {
25998 // Retrieve the state of using a md-not-found template by using our attribute, which will
25999 // be added to the element in the template function.
26000 ctrl.hasNotFound = !!element.attr('md-has-not-found');
26001
26002 // By default the inset autocomplete should show the clear button when not explicitly overwritten.
26003 if (!angular.isDefined(attrs.mdClearButton) && !scope.floatingLabel) {
26004 scope.clearButton = true;
26005 }
26006 }
26007 },
26008 template: function (element, attr) {
26009 var noItemsTemplate = getNoItemsTemplate(),
26010 itemTemplate = getItemTemplate(),
26011 leftover = element.html(),
26012 tabindex = attr.tabindex;
26013
26014 // Set our attribute for the link function above which runs later.
26015 // We will set an attribute, because otherwise the stored variables will be trashed when
26016 // removing the element is hidden while retrieving the template. For example when using ngIf.
26017 if (noItemsTemplate) element.attr('md-has-not-found', true);
26018
26019 // Always set our tabindex of the autocomplete directive to -1, because our input
26020 // will hold the actual tabindex.
26021 element.attr('tabindex', '-1');
26022
26023 return '\
26024 <md-autocomplete-wrap\
26025 ng-class="{ \'md-whiteframe-z1\': !floatingLabel, \
26026 \'md-menu-showing\': !$mdAutocompleteCtrl.hidden, \
26027 \'md-show-clear-button\': !!clearButton }">\
26028 ' + getInputElement() + '\
26029 ' + getClearButton() + '\
26030 <md-progress-linear\
26031 class="' + (attr.mdFloatingLabel ? 'md-inline' : '') + '"\
26032 ng-if="$mdAutocompleteCtrl.loadingIsVisible()"\
26033 md-mode="indeterminate"></md-progress-linear>\
26034 <md-virtual-repeat-container\
26035 md-auto-shrink\
26036 md-auto-shrink-min="1"\
26037 ng-mouseenter="$mdAutocompleteCtrl.listEnter()"\
26038 ng-mouseleave="$mdAutocompleteCtrl.listLeave()"\
26039 ng-mouseup="$mdAutocompleteCtrl.mouseUp()"\
26040 ng-hide="$mdAutocompleteCtrl.hidden"\
26041 class="md-autocomplete-suggestions-container md-whiteframe-z1"\
26042 ng-class="{ \'md-not-found\': $mdAutocompleteCtrl.notFoundVisible() }"\
26043 role="presentation">\
26044 <ul class="md-autocomplete-suggestions"\
26045 ng-class="::menuClass"\
26046 id="ul-{{$mdAutocompleteCtrl.id}}">\
26047 <li md-virtual-repeat="item in $mdAutocompleteCtrl.matches"\
26048 ng-class="{ selected: $index === $mdAutocompleteCtrl.index }"\
26049 ng-click="$mdAutocompleteCtrl.select($index)"\
26050 md-extra-name="$mdAutocompleteCtrl.itemName">\
26051 ' + itemTemplate + '\
26052 </li>' + noItemsTemplate + '\
26053 </ul>\
26054 </md-virtual-repeat-container>\
26055 </md-autocomplete-wrap>';
26056
26057 function getItemTemplate() {
26058 var templateTag = element.find('md-item-template').detach(),
26059 html = templateTag.length ? templateTag.html() : element.html();
26060 if (!templateTag.length) element.empty();
26061 return '<md-autocomplete-parent-scope md-autocomplete-replace>' + html + '</md-autocomplete-parent-scope>';
26062 }
26063
26064 function getNoItemsTemplate() {
26065 var templateTag = element.find('md-not-found').detach(),
26066 template = templateTag.length ? templateTag.html() : '';
26067 return template
26068 ? '<li ng-if="$mdAutocompleteCtrl.notFoundVisible()"\
26069 md-autocomplete-parent-scope>' + template + '</li>'
26070 : '';
26071
26072 }
26073
26074 function getInputElement () {
26075 if (attr.mdFloatingLabel) {
26076 return '\
26077 <md-input-container ng-if="floatingLabel">\
26078 <label>{{floatingLabel}}</label>\
26079 <input type="search"\
26080 ' + (tabindex != null ? 'tabindex="' + tabindex + '"' : '') + '\
26081 id="{{ inputId || \'fl-input-\' + $mdAutocompleteCtrl.id }}"\
26082 name="{{inputName}}"\
26083 autocomplete="off"\
26084 ng-required="$mdAutocompleteCtrl.isRequired"\
26085 ng-readonly="$mdAutocompleteCtrl.isReadonly"\
26086 ng-minlength="inputMinlength"\
26087 ng-maxlength="inputMaxlength"\
26088 ng-disabled="$mdAutocompleteCtrl.isDisabled"\
26089 ng-model="$mdAutocompleteCtrl.scope.searchText"\
26090 ng-model-options="{ allowInvalid: true }"\
26091 ng-keydown="$mdAutocompleteCtrl.keydown($event)"\
26092 ng-blur="$mdAutocompleteCtrl.blur($event)"\
26093 ng-focus="$mdAutocompleteCtrl.focus($event)"\
26094 aria-owns="ul-{{$mdAutocompleteCtrl.id}}"\
26095 aria-label="{{floatingLabel}}"\
26096 aria-autocomplete="list"\
26097 role="combobox"\
26098 aria-haspopup="true"\
26099 aria-activedescendant=""\
26100 aria-expanded="{{!$mdAutocompleteCtrl.hidden}}"/>\
26101 <div md-autocomplete-parent-scope md-autocomplete-replace>' + leftover + '</div>\
26102 </md-input-container>';
26103 } else {
26104 return '\
26105 <input type="search"\
26106 ' + (tabindex != null ? 'tabindex="' + tabindex + '"' : '') + '\
26107 id="{{ inputId || \'input-\' + $mdAutocompleteCtrl.id }}"\
26108 name="{{inputName}}"\
26109 ng-if="!floatingLabel"\
26110 autocomplete="off"\
26111 ng-required="$mdAutocompleteCtrl.isRequired"\
26112 ng-disabled="$mdAutocompleteCtrl.isDisabled"\
26113 ng-readonly="$mdAutocompleteCtrl.isReadonly"\
26114 ng-minlength="inputMinlength"\
26115 ng-maxlength="inputMaxlength"\
26116 ng-model="$mdAutocompleteCtrl.scope.searchText"\
26117 ng-keydown="$mdAutocompleteCtrl.keydown($event)"\
26118 ng-blur="$mdAutocompleteCtrl.blur($event)"\
26119 ng-focus="$mdAutocompleteCtrl.focus($event)"\
26120 placeholder="{{placeholder}}"\
26121 aria-owns="ul-{{$mdAutocompleteCtrl.id}}"\
26122 aria-label="{{placeholder}}"\
26123 aria-autocomplete="list"\
26124 role="combobox"\
26125 aria-haspopup="true"\
26126 aria-activedescendant=""\
26127 aria-expanded="{{!$mdAutocompleteCtrl.hidden}}"/>';
26128 }
26129 }
26130
26131 function getClearButton() {
26132 return '' +
26133 '<button ' +
26134 'type="button" ' +
26135 'aria-label="Clear Input" ' +
26136 'tabindex="-1" ' +
26137 'ng-if="clearButton && $mdAutocompleteCtrl.scope.searchText && !$mdAutocompleteCtrl.isDisabled" ' +
26138 'ng-click="$mdAutocompleteCtrl.clear($event)">' +
26139 '<md-icon md-svg-src="' + $$mdSvgRegistry.mdClose + '"></md-icon>' +
26140 '</button>';
26141 }
26142 }
26143 };
26144}
26145
26146})();
26147(function(){
26148"use strict";
26149
26150
26151MdAutocompleteItemScopeDirective.$inject = ["$compile", "$mdUtil"];angular
26152 .module('material.components.autocomplete')
26153 .directive('mdAutocompleteParentScope', MdAutocompleteItemScopeDirective);
26154
26155function MdAutocompleteItemScopeDirective($compile, $mdUtil) {
26156 return {
26157 restrict: 'AE',
26158 compile: compile,
26159 terminal: true,
26160 transclude: 'element'
26161 };
26162
26163 function compile(tElement, tAttr, transclude) {
26164 return function postLink(scope, element, attr) {
26165 var ctrl = scope.$mdAutocompleteCtrl;
26166 var newScope = ctrl.parent.$new();
26167 var itemName = ctrl.itemName;
26168
26169 // Watch for changes to our scope's variables and copy them to the new scope
26170 watchVariable('$index', '$index');
26171 watchVariable('item', itemName);
26172
26173 // Ensure that $digest calls on our scope trigger $digest on newScope.
26174 connectScopes();
26175
26176 // Link the element against newScope.
26177 transclude(newScope, function(clone) {
26178 element.after(clone);
26179 });
26180
26181 /**
26182 * Creates a watcher for variables that are copied from the parent scope
26183 * @param variable
26184 * @param alias
26185 */
26186 function watchVariable(variable, alias) {
26187 newScope[alias] = scope[variable];
26188
26189 scope.$watch(variable, function(value) {
26190 $mdUtil.nextTick(function() {
26191 newScope[alias] = value;
26192 });
26193 });
26194 }
26195
26196 /**
26197 * Creates watchers on scope and newScope that ensure that for any
26198 * $digest of scope, newScope is also $digested.
26199 */
26200 function connectScopes() {
26201 var scopeDigesting = false;
26202 var newScopeDigesting = false;
26203
26204 scope.$watch(function() {
26205 if (newScopeDigesting || scopeDigesting) {
26206 return;
26207 }
26208
26209 scopeDigesting = true;
26210 scope.$$postDigest(function() {
26211 if (!newScopeDigesting) {
26212 newScope.$digest();
26213 }
26214
26215 scopeDigesting = newScopeDigesting = false;
26216 });
26217 });
26218
26219 newScope.$watch(function() {
26220 newScopeDigesting = true;
26221 });
26222 }
26223 };
26224 }
26225}
26226})();
26227(function(){
26228"use strict";
26229
26230
26231MdHighlightCtrl.$inject = ["$scope", "$element", "$attrs"];angular
26232 .module('material.components.autocomplete')
26233 .controller('MdHighlightCtrl', MdHighlightCtrl);
26234
26235function MdHighlightCtrl ($scope, $element, $attrs) {
26236 this.$scope = $scope;
26237 this.$element = $element;
26238 this.$attrs = $attrs;
26239
26240 // Cache the Regex to avoid rebuilding each time.
26241 this.regex = null;
26242}
26243
26244MdHighlightCtrl.prototype.init = function(unsafeTermFn, unsafeContentFn) {
26245
26246 this.flags = this.$attrs.mdHighlightFlags || '';
26247
26248 this.unregisterFn = this.$scope.$watch(function($scope) {
26249 return {
26250 term: unsafeTermFn($scope),
26251 contentText: unsafeContentFn($scope)
26252 };
26253 }.bind(this), this.onRender.bind(this), true);
26254
26255 this.$element.on('$destroy', this.unregisterFn);
26256};
26257
26258/**
26259 * Triggered once a new change has been recognized and the highlighted
26260 * text needs to be updated.
26261 */
26262MdHighlightCtrl.prototype.onRender = function(state, prevState) {
26263
26264 var contentText = state.contentText;
26265
26266 /* Update the regex if it's outdated, because we don't want to rebuilt it constantly. */
26267 if (this.regex === null || state.term !== prevState.term) {
26268 this.regex = this.createRegex(state.term, this.flags);
26269 }
26270
26271 /* If a term is available apply the regex to the content */
26272 if (state.term) {
26273 this.applyRegex(contentText);
26274 } else {
26275 this.$element.text(contentText);
26276 }
26277
26278};
26279
26280/**
26281 * Decomposes the specified text into different tokens (whether match or not).
26282 * Breaking down the string guarantees proper XSS protection due to the native browser
26283 * escaping of unsafe text.
26284 */
26285MdHighlightCtrl.prototype.applyRegex = function(text) {
26286 var tokens = this.resolveTokens(text);
26287
26288 this.$element.empty();
26289
26290 tokens.forEach(function (token) {
26291
26292 if (token.isMatch) {
26293 var tokenEl = angular.element('<span class="highlight">').text(token.text);
26294
26295 this.$element.append(tokenEl);
26296 } else {
26297 this.$element.append(document.createTextNode(token));
26298 }
26299
26300 }.bind(this));
26301
26302};
26303
26304 /**
26305 * Decomposes the specified text into different tokens by running the regex against the text.
26306 */
26307MdHighlightCtrl.prototype.resolveTokens = function(string) {
26308 var tokens = [];
26309 var lastIndex = 0;
26310
26311 // Use replace here, because it supports global and single regular expressions at same time.
26312 string.replace(this.regex, function(match, index) {
26313 appendToken(lastIndex, index);
26314
26315 tokens.push({
26316 text: match,
26317 isMatch: true
26318 });
26319
26320 lastIndex = index + match.length;
26321 });
26322
26323 // Append the missing text as a token.
26324 appendToken(lastIndex);
26325
26326 return tokens;
26327
26328 function appendToken(from, to) {
26329 var targetText = string.slice(from, to);
26330 targetText && tokens.push(targetText);
26331 }
26332};
26333
26334/** Creates a regex for the specified text with the given flags. */
26335MdHighlightCtrl.prototype.createRegex = function(term, flags) {
26336 var startFlag = '', endFlag = '';
26337 var regexTerm = this.sanitizeRegex(term);
26338
26339 if (flags.indexOf('^') >= 0) startFlag = '^';
26340 if (flags.indexOf('$') >= 0) endFlag = '$';
26341
26342 return new RegExp(startFlag + regexTerm + endFlag, flags.replace(/[$\^]/g, ''));
26343};
26344
26345/** Sanitizes a regex by removing all common RegExp identifiers */
26346MdHighlightCtrl.prototype.sanitizeRegex = function(term) {
26347 return term && term.toString().replace(/[\\\^\$\*\+\?\.\(\)\|\{}\[\]]/g, '\\$&');
26348};
26349
26350})();
26351(function(){
26352"use strict";
26353
26354
26355MdHighlight.$inject = ["$interpolate", "$parse"];angular
26356 .module('material.components.autocomplete')
26357 .directive('mdHighlightText', MdHighlight);
26358
26359/**
26360 * @ngdoc directive
26361 * @name mdHighlightText
26362 * @module material.components.autocomplete
26363 *
26364 * @description
26365 * The `md-highlight-text` directive allows you to specify text that should be highlighted within
26366 * an element. Highlighted text will be wrapped in `<span class="highlight"></span>` which can
26367 * be styled through CSS. Please note that child elements may not be used with this directive.
26368 *
26369 * @param {string} md-highlight-text A model to be searched for
26370 * @param {string=} md-highlight-flags A list of flags (loosely based on JavaScript RexExp flags).
26371 * #### **Supported flags**:
26372 * - `g`: Find all matches within the provided text
26373 * - `i`: Ignore case when searching for matches
26374 * - `$`: Only match if the text ends with the search term
26375 * - `^`: Only match if the text begins with the search term
26376 *
26377 * @usage
26378 * <hljs lang="html">
26379 * <input placeholder="Enter a search term..." ng-model="searchTerm" type="text" />
26380 * <ul>
26381 * <li ng-repeat="result in results" md-highlight-text="searchTerm">
26382 * {{result.text}}
26383 * </li>
26384 * </ul>
26385 * </hljs>
26386 */
26387
26388function MdHighlight ($interpolate, $parse) {
26389 return {
26390 terminal: true,
26391 controller: 'MdHighlightCtrl',
26392 compile: function mdHighlightCompile(tElement, tAttr) {
26393 var termExpr = $parse(tAttr.mdHighlightText);
26394 var unsafeContentExpr = $interpolate(tElement.html());
26395
26396 return function mdHighlightLink(scope, element, attr, ctrl) {
26397 ctrl.init(termExpr, unsafeContentExpr);
26398 };
26399 }
26400 };
26401}
26402
26403})();
26404(function(){
26405"use strict";
26406
26407
26408MdChipCtrl.$inject = ["$scope", "$element", "$mdConstant", "$timeout", "$mdUtil"];angular
26409 .module('material.components.chips')
26410 .controller('MdChipCtrl', MdChipCtrl);
26411
26412/**
26413 * Controller for the MdChip component. Responsible for handling keyboard
26414 * events and editting the chip if needed.
26415 *
26416 * @param $scope
26417 * @param $element
26418 * @param $mdConstant
26419 * @param $timeout
26420 * @param $mdUtil
26421 * @constructor
26422 */
26423function MdChipCtrl ($scope, $element, $mdConstant, $timeout, $mdUtil) {
26424 /**
26425 * @type {$scope}
26426 */
26427 this.$scope = $scope;
26428
26429 /**
26430 * @type {$element}
26431 */
26432 this.$element = $element;
26433
26434 /**
26435 * @type {$mdConstant}
26436 */
26437 this.$mdConstant = $mdConstant;
26438
26439 /**
26440 * @type {$timeout}
26441 */
26442 this.$timeout = $timeout;
26443
26444 /**
26445 * @type {$mdUtil}
26446 */
26447 this.$mdUtil = $mdUtil;
26448
26449 /**
26450 * @type {boolean}
26451 */
26452 this.isEditting = false;
26453
26454 /**
26455 * @type {MdChipsCtrl}
26456 */
26457 this.parentController = undefined;
26458
26459 /**
26460 * @type {boolean}
26461 */
26462 this.enableChipEdit = false;
26463}
26464
26465
26466/**
26467 * @param {MdChipsCtrl} controller
26468 */
26469MdChipCtrl.prototype.init = function(controller) {
26470 this.parentController = controller;
26471 this.enableChipEdit = this.parentController.enableChipEdit;
26472
26473 if (this.enableChipEdit) {
26474 this.$element.on('keydown', this.chipKeyDown.bind(this));
26475 this.$element.on('mousedown', this.chipMouseDown.bind(this));
26476 this.getChipContent().addClass('_md-chip-content-edit-is-enabled');
26477 }
26478};
26479
26480
26481/**
26482 * @return {Object}
26483 */
26484MdChipCtrl.prototype.getChipContent = function() {
26485 var chipContents = this.$element[0].getElementsByClassName('md-chip-content');
26486 return angular.element(chipContents[0]);
26487};
26488
26489
26490/**
26491 * @return {Object}
26492 */
26493MdChipCtrl.prototype.getContentElement = function() {
26494 return angular.element(this.getChipContent().children()[0]);
26495};
26496
26497
26498/**
26499 * @return {number}
26500 */
26501MdChipCtrl.prototype.getChipIndex = function() {
26502 return parseInt(this.$element.attr('index'));
26503};
26504
26505
26506/**
26507 * Presents an input element to edit the contents of the chip.
26508 */
26509MdChipCtrl.prototype.goOutOfEditMode = function() {
26510 if (!this.isEditting) return;
26511
26512 this.isEditting = false;
26513 this.$element.removeClass('_md-chip-editing');
26514 this.getChipContent()[0].contentEditable = 'false';
26515 var chipIndex = this.getChipIndex();
26516
26517 var content = this.getContentElement().text();
26518 if (content) {
26519 this.parentController.updateChipContents(
26520 chipIndex,
26521 this.getContentElement().text()
26522 );
26523
26524 this.$mdUtil.nextTick(function() {
26525 if (this.parentController.selectedChip === chipIndex) {
26526 this.parentController.focusChip(chipIndex);
26527 }
26528 }.bind(this));
26529 } else {
26530 this.parentController.removeChipAndFocusInput(chipIndex);
26531 }
26532};
26533
26534
26535/**
26536 * Given an HTML element. Selects contents of it.
26537 * @param node
26538 */
26539MdChipCtrl.prototype.selectNodeContents = function(node) {
26540 var range, selection;
26541 if (document.body.createTextRange) {
26542 range = document.body.createTextRange();
26543 range.moveToElementText(node);
26544 range.select();
26545 } else if (window.getSelection) {
26546 selection = window.getSelection();
26547 range = document.createRange();
26548 range.selectNodeContents(node);
26549 selection.removeAllRanges();
26550 selection.addRange(range);
26551 }
26552};
26553
26554
26555/**
26556 * Presents an input element to edit the contents of the chip.
26557 */
26558MdChipCtrl.prototype.goInEditMode = function() {
26559 this.isEditting = true;
26560 this.$element.addClass('_md-chip-editing');
26561 this.getChipContent()[0].contentEditable = 'true';
26562 this.getChipContent().on('blur', function() {
26563 this.goOutOfEditMode();
26564 }.bind(this));
26565
26566 this.selectNodeContents(this.getChipContent()[0]);
26567};
26568
26569
26570/**
26571 * Handles the keydown event on the chip element. If enable-chip-edit attribute is
26572 * set to true, space or enter keys can trigger going into edit mode. Enter can also
26573 * trigger submitting if the chip is already being edited.
26574 * @param event
26575 */
26576MdChipCtrl.prototype.chipKeyDown = function(event) {
26577 if (!this.isEditting &&
26578 (event.keyCode === this.$mdConstant.KEY_CODE.ENTER ||
26579 event.keyCode === this.$mdConstant.KEY_CODE.SPACE)) {
26580 event.preventDefault();
26581 this.goInEditMode();
26582 } else if (this.isEditting &&
26583 event.keyCode === this.$mdConstant.KEY_CODE.ENTER) {
26584 event.preventDefault();
26585 this.goOutOfEditMode();
26586 }
26587};
26588
26589
26590/**
26591 * Handles the double click event
26592 */
26593MdChipCtrl.prototype.chipMouseDown = function() {
26594 if(this.getChipIndex() == this.parentController.selectedChip &&
26595 this.enableChipEdit &&
26596 !this.isEditting) {
26597 this.goInEditMode();
26598 }
26599};
26600
26601})();
26602(function(){
26603"use strict";
26604
26605
26606MdChip.$inject = ["$mdTheming", "$mdUtil", "$compile", "$timeout"];angular
26607 .module('material.components.chips')
26608 .directive('mdChip', MdChip);
26609
26610/**
26611 * @ngdoc directive
26612 * @name mdChip
26613 * @module material.components.chips
26614 *
26615 * @description
26616 * `<md-chip>` is a component used within `<md-chips>` and is responsible for rendering individual
26617 * chips.
26618 *
26619 *
26620 * @usage
26621 * <hljs lang="html">
26622 * <md-chip>{{$chip}}</md-chip>
26623 * </hljs>
26624 *
26625 */
26626
26627// This hint text is hidden within a chip but used by screen readers to
26628// inform the user how they can interact with a chip.
26629var DELETE_HINT_TEMPLATE = '\
26630 <span ng-if="!$mdChipsCtrl.readonly" class="md-visually-hidden">\
26631 {{$mdChipsCtrl.deleteHint}}\
26632 </span>';
26633
26634/**
26635 * MDChip Directive Definition
26636 *
26637 * @param $mdTheming
26638 * @param $mdUtil
26639 * @ngInject
26640 */
26641function MdChip($mdTheming, $mdUtil, $compile, $timeout) {
26642 var deleteHintTemplate = $mdUtil.processTemplate(DELETE_HINT_TEMPLATE);
26643
26644 return {
26645 restrict: 'E',
26646 require: ['^?mdChips', 'mdChip'],
26647 link: postLink,
26648 controller: 'MdChipCtrl'
26649 };
26650
26651 function postLink(scope, element, attr, ctrls) {
26652 var chipsController = ctrls.shift();
26653 var chipController = ctrls.shift();
26654 var chipContentElement = angular.element(element[0].querySelector('.md-chip-content'));
26655
26656 $mdTheming(element);
26657
26658 if (chipsController) {
26659 chipController.init(chipsController);
26660
26661 // Append our delete hint to the div.md-chip-content (which does not exist at compile time)
26662 chipContentElement.append($compile(deleteHintTemplate)(scope));
26663
26664 // When a chip is blurred, make sure to unset (or reset) the selected chip so that tabbing
26665 // through elements works properly
26666 chipContentElement.on('blur', function() {
26667 chipsController.resetSelectedChip();
26668 chipsController.$scope.$applyAsync();
26669 });
26670 }
26671
26672 // Use $timeout to ensure we run AFTER the element has been added to the DOM so we can focus it.
26673 $timeout(function() {
26674 if (!chipsController) {
26675 return;
26676 }
26677
26678 if (chipsController.shouldFocusLastChip) {
26679 chipsController.focusLastChipThenInput();
26680 }
26681 });
26682 }
26683}
26684
26685})();
26686(function(){
26687"use strict";
26688
26689
26690MdChipRemove.$inject = ["$timeout"];angular
26691 .module('material.components.chips')
26692 .directive('mdChipRemove', MdChipRemove);
26693
26694/**
26695 * @ngdoc directive
26696 * @name mdChipRemove
26697 * @restrict A
26698 * @module material.components.chips
26699 *
26700 * @description
26701 * Designates an element to be used as the delete button for a chip. <br/>
26702 * This element is passed as a child of the `md-chips` element.
26703 *
26704 * The designated button will be just appended to the chip and removes the given chip on click.<br/>
26705 * By default the button is not being styled by the `md-chips` component.
26706 *
26707 * @usage
26708 * <hljs lang="html">
26709 * <md-chips>
26710 * <button md-chip-remove="">
26711 * <md-icon md-svg-icon="md-close"></md-icon>
26712 * </button>
26713 * </md-chips>
26714 * </hljs>
26715 */
26716
26717
26718/**
26719 * MdChipRemove Directive Definition.
26720 *
26721 * @param $timeout
26722 * @returns {{restrict: string, require: string[], link: Function, scope: boolean}}
26723 * @constructor
26724 */
26725function MdChipRemove ($timeout) {
26726 return {
26727 restrict: 'A',
26728 require: '^mdChips',
26729 scope: false,
26730 link: postLink
26731 };
26732
26733 function postLink(scope, element, attr, ctrl) {
26734 element.on('click', function(event) {
26735 scope.$apply(function() {
26736 ctrl.removeChip(scope.$$replacedScope.$index);
26737 });
26738 });
26739
26740 // Child elements aren't available until after a $timeout tick as they are hidden by an
26741 // `ng-if`. see http://goo.gl/zIWfuw
26742 $timeout(function() {
26743 element.attr({ tabindex: -1, 'aria-hidden': true });
26744 element.find('button').attr('tabindex', '-1');
26745 });
26746 }
26747}
26748
26749})();
26750(function(){
26751"use strict";
26752
26753
26754MdChipTransclude.$inject = ["$compile"];angular
26755 .module('material.components.chips')
26756 .directive('mdChipTransclude', MdChipTransclude);
26757
26758function MdChipTransclude ($compile) {
26759 return {
26760 restrict: 'EA',
26761 terminal: true,
26762 link: link,
26763 scope: false
26764 };
26765 function link (scope, element, attr) {
26766 var ctrl = scope.$parent.$mdChipsCtrl,
26767 newScope = ctrl.parent.$new(false, ctrl.parent);
26768 newScope.$$replacedScope = scope;
26769 newScope.$chip = scope.$chip;
26770 newScope.$index = scope.$index;
26771 newScope.$mdChipsCtrl = ctrl;
26772
26773 var newHtml = ctrl.$scope.$eval(attr.mdChipTransclude);
26774
26775 element.html(newHtml);
26776 $compile(element.contents())(newScope);
26777 }
26778}
26779
26780})();
26781(function(){
26782"use strict";
26783
26784/**
26785 * The default chip append delay.
26786 *
26787 * @type {number}
26788 */
26789MdChipsCtrl.$inject = ["$scope", "$attrs", "$mdConstant", "$log", "$element", "$timeout", "$mdUtil"];
26790var DEFAULT_CHIP_APPEND_DELAY = 300;
26791
26792angular
26793 .module('material.components.chips')
26794 .controller('MdChipsCtrl', MdChipsCtrl);
26795
26796/**
26797 * Controller for the MdChips component. Responsible for adding to and
26798 * removing from the list of chips, marking chips as selected, and binding to
26799 * the models of various input components.
26800 *
26801 * @param $scope
26802 * @param $attrs
26803 * @param $mdConstant
26804 * @param $log
26805 * @param $element
26806 * @param $timeout
26807 * @param $mdUtil
26808 * @constructor
26809 */
26810function MdChipsCtrl ($scope, $attrs, $mdConstant, $log, $element, $timeout, $mdUtil) {
26811 /** @type {$timeout} **/
26812 this.$timeout = $timeout;
26813
26814 /** @type {Object} */
26815 this.$mdConstant = $mdConstant;
26816
26817 /** @type {angular.$scope} */
26818 this.$scope = $scope;
26819
26820 /** @type {angular.$scope} */
26821 this.parent = $scope.$parent;
26822
26823 /** @type {$mdUtil} */
26824 this.$mdUtil = $mdUtil;
26825
26826 /** @type {$log} */
26827 this.$log = $log;
26828
26829 /** @type {$element} */
26830 this.$element = $element;
26831
26832 /** @type {$attrs} */
26833 this.$attrs = $attrs;
26834
26835 /** @type {angular.NgModelController} */
26836 this.ngModelCtrl = null;
26837
26838 /** @type {angular.NgModelController} */
26839 this.userInputNgModelCtrl = null;
26840
26841 /** @type {MdAutocompleteCtrl} */
26842 this.autocompleteCtrl = null;
26843
26844 /** @type {Element} */
26845 this.userInputElement = null;
26846
26847 /** @type {Array.<Object>} */
26848 this.items = [];
26849
26850 /** @type {number} */
26851 this.selectedChip = -1;
26852
26853 /** @type {string} */
26854 this.enableChipEdit = $mdUtil.parseAttributeBoolean($attrs.mdEnableChipEdit);
26855
26856 /** @type {string} */
26857 this.addOnBlur = $mdUtil.parseAttributeBoolean($attrs.mdAddOnBlur);
26858
26859 /**
26860 * The text to be used as the aria-label for the input.
26861 * @type {string}
26862 */
26863 this.inputAriaLabel = 'Chips input.';
26864
26865 /**
26866 * Hidden hint text to describe the chips container. Used to give context to screen readers when
26867 * the chips are readonly and the input cannot be selected.
26868 *
26869 * @type {string}
26870 */
26871 this.containerHint = 'Chips container. Use arrow keys to select chips.';
26872
26873 /**
26874 * Hidden hint text for how to delete a chip. Used to give context to screen readers.
26875 * @type {string}
26876 */
26877 this.deleteHint = 'Press delete to remove this chip.';
26878
26879 /**
26880 * Hidden label for the delete button. Used to give context to screen readers.
26881 * @type {string}
26882 */
26883 this.deleteButtonLabel = 'Remove';
26884
26885 /**
26886 * Model used by the input element.
26887 * @type {string}
26888 */
26889 this.chipBuffer = '';
26890
26891 /**
26892 * Whether to use the transformChip expression to transform the chip buffer
26893 * before appending it to the list.
26894 * @type {boolean}
26895 */
26896 this.useTransformChip = false;
26897
26898 /**
26899 * Whether to use the onAdd expression to notify of chip additions.
26900 * @type {boolean}
26901 */
26902 this.useOnAdd = false;
26903
26904 /**
26905 * Whether to use the onRemove expression to notify of chip removals.
26906 * @type {boolean}
26907 */
26908 this.useOnRemove = false;
26909
26910 /**
26911 * The ID of the chips wrapper which is used to build unique IDs for the chips and the aria-owns
26912 * attribute.
26913 *
26914 * Defaults to '_md-chips-wrapper-' plus a unique number.
26915 *
26916 * @type {string}
26917 */
26918 this.wrapperId = '';
26919
26920 /**
26921 * Array of unique numbers which will be auto-generated any time the items change, and is used to
26922 * create unique IDs for the aria-owns attribute.
26923 *
26924 * @type {Array}
26925 */
26926 this.contentIds = [];
26927
26928 /**
26929 * The index of the chip that should have it's tabindex property set to 0 so it is selectable
26930 * via the keyboard.
26931 *
26932 * @type {int}
26933 */
26934 this.ariaTabIndex = null;
26935
26936 /**
26937 * After appending a chip, the chip will be focused for this number of milliseconds before the
26938 * input is refocused.
26939 *
26940 * **Note:** This is **required** for compatibility with certain screen readers in order for
26941 * them to properly allow keyboard access.
26942 *
26943 * @type {number}
26944 */
26945 this.chipAppendDelay = DEFAULT_CHIP_APPEND_DELAY;
26946
26947 this.init();
26948}
26949
26950/**
26951 * Initializes variables and sets up watchers
26952 */
26953MdChipsCtrl.prototype.init = function() {
26954 var ctrl = this;
26955
26956 // Set the wrapper ID
26957 ctrl.wrapperId = '_md-chips-wrapper-' + ctrl.$mdUtil.nextUid();
26958
26959 // Setup a watcher which manages the role and aria-owns attributes
26960 ctrl.$scope.$watchCollection('$mdChipsCtrl.items', function() {
26961 // Make sure our input and wrapper have the correct ARIA attributes
26962 ctrl.setupInputAria();
26963 ctrl.setupWrapperAria();
26964 });
26965
26966 ctrl.$attrs.$observe('mdChipAppendDelay', function(newValue) {
26967 ctrl.chipAppendDelay = parseInt(newValue) || DEFAULT_CHIP_APPEND_DELAY;
26968 });
26969};
26970
26971/**
26972 * If we have an input, ensure it has the appropriate ARIA attributes.
26973 */
26974MdChipsCtrl.prototype.setupInputAria = function() {
26975 var input = this.$element.find('input');
26976
26977 // If we have no input, just return
26978 if (!input) {
26979 return;
26980 }
26981
26982 input.attr('role', 'textbox');
26983 input.attr('aria-multiline', true);
26984};
26985
26986/**
26987 * Ensure our wrapper has the appropriate ARIA attributes.
26988 */
26989MdChipsCtrl.prototype.setupWrapperAria = function() {
26990 var ctrl = this,
26991 wrapper = this.$element.find('md-chips-wrap');
26992
26993 if (this.items && this.items.length) {
26994 // Dynamically add the listbox role on every change because it must be removed when there are
26995 // no items.
26996 wrapper.attr('role', 'listbox');
26997
26998 // Generate some random (but unique) IDs for each chip
26999 this.contentIds = this.items.map(function() {
27000 return ctrl.wrapperId + '-chip-' + ctrl.$mdUtil.nextUid();
27001 });
27002
27003 // Use the contentIDs above to generate the aria-owns attribute
27004 wrapper.attr('aria-owns', this.contentIds.join(' '));
27005 } else {
27006 // If we have no items, then the role and aria-owns attributes MUST be removed
27007 wrapper.removeAttr('role');
27008 wrapper.removeAttr('aria-owns');
27009 }
27010};
27011
27012/**
27013 * Handles the keydown event on the input element: by default <enter> appends
27014 * the buffer to the chip list, while backspace removes the last chip in the
27015 * list if the current buffer is empty.
27016 * @param event
27017 */
27018MdChipsCtrl.prototype.inputKeydown = function(event) {
27019 var chipBuffer = this.getChipBuffer();
27020
27021 // If we have an autocomplete, and it handled the event, we have nothing to do
27022 if (this.autocompleteCtrl && event.isDefaultPrevented && event.isDefaultPrevented()) {
27023 return;
27024 }
27025
27026 if (event.keyCode === this.$mdConstant.KEY_CODE.BACKSPACE) {
27027 // Only select and focus the previous chip, if the current caret position of the
27028 // input element is at the beginning.
27029 if (this.getCursorPosition(event.target) !== 0) {
27030 return;
27031 }
27032
27033 event.preventDefault();
27034 event.stopPropagation();
27035
27036 if (this.items.length) {
27037 this.selectAndFocusChipSafe(this.items.length - 1);
27038 }
27039
27040 return;
27041 }
27042
27043 // By default <enter> appends the buffer to the chip list.
27044 if (!this.separatorKeys || this.separatorKeys.length < 1) {
27045 this.separatorKeys = [this.$mdConstant.KEY_CODE.ENTER];
27046 }
27047
27048 // Support additional separator key codes in an array of `md-separator-keys`.
27049 if (this.separatorKeys.indexOf(event.keyCode) !== -1) {
27050 if ((this.autocompleteCtrl && this.requireMatch) || !chipBuffer) return;
27051 event.preventDefault();
27052
27053 // Only append the chip and reset the chip buffer if the max chips limit isn't reached.
27054 if (this.hasMaxChipsReached()) return;
27055
27056 this.appendChip(chipBuffer.trim());
27057 this.resetChipBuffer();
27058
27059 return false;
27060 }
27061};
27062
27063/**
27064 * Returns the cursor position of the specified input element.
27065 * @param element HTMLInputElement
27066 * @returns {Number} Cursor Position of the input.
27067 */
27068MdChipsCtrl.prototype.getCursorPosition = function(element) {
27069 /*
27070 * Figure out whether the current input for the chips buffer is valid for using
27071 * the selectionStart / end property to retrieve the cursor position.
27072 * Some browsers do not allow the use of those attributes, on different input types.
27073 */
27074 try {
27075 if (element.selectionStart === element.selectionEnd) {
27076 return element.selectionStart;
27077 }
27078 } catch (e) {
27079 if (!element.value) {
27080 return 0;
27081 }
27082 }
27083};
27084
27085
27086/**
27087 * Updates the content of the chip at given index
27088 * @param chipIndex
27089 * @param chipContents
27090 */
27091MdChipsCtrl.prototype.updateChipContents = function(chipIndex, chipContents){
27092 if(chipIndex >= 0 && chipIndex < this.items.length) {
27093 this.items[chipIndex] = chipContents;
27094 this.ngModelCtrl.$setDirty();
27095 }
27096};
27097
27098
27099/**
27100 * Returns true if a chip is currently being edited. False otherwise.
27101 * @return {boolean}
27102 */
27103MdChipsCtrl.prototype.isEditingChip = function() {
27104 return !!this.$element[0].querySelector('._md-chip-editing');
27105};
27106
27107
27108MdChipsCtrl.prototype.isRemovable = function() {
27109 // Return false if we have static chips
27110 if (!this.ngModelCtrl) {
27111 return false;
27112 }
27113
27114 return this.readonly ? this.removable :
27115 angular.isDefined(this.removable) ? this.removable : true;
27116};
27117
27118/**
27119 * Handles the keydown event on the chip elements: backspace removes the selected chip, arrow
27120 * keys switch which chips is active
27121 * @param event
27122 */
27123MdChipsCtrl.prototype.chipKeydown = function (event) {
27124 if (this.getChipBuffer()) return;
27125 if (this.isEditingChip()) return;
27126
27127 switch (event.keyCode) {
27128 case this.$mdConstant.KEY_CODE.BACKSPACE:
27129 case this.$mdConstant.KEY_CODE.DELETE:
27130 if (this.selectedChip < 0) return;
27131 event.preventDefault();
27132 // Cancel the delete action only after the event cancel. Otherwise the page will go back.
27133 if (!this.isRemovable()) return;
27134 this.removeAndSelectAdjacentChip(this.selectedChip);
27135 break;
27136 case this.$mdConstant.KEY_CODE.LEFT_ARROW:
27137 event.preventDefault();
27138 // By default, allow selection of -1 which will focus the input; if we're readonly, don't go
27139 // below 0
27140 if (this.selectedChip < 0 || (this.readonly && this.selectedChip == 0)) {
27141 this.selectedChip = this.items.length;
27142 }
27143 if (this.items.length) this.selectAndFocusChipSafe(this.selectedChip - 1);
27144 break;
27145 case this.$mdConstant.KEY_CODE.RIGHT_ARROW:
27146 event.preventDefault();
27147 this.selectAndFocusChipSafe(this.selectedChip + 1);
27148 break;
27149 case this.$mdConstant.KEY_CODE.ESCAPE:
27150 case this.$mdConstant.KEY_CODE.TAB:
27151 if (this.selectedChip < 0) return;
27152 event.preventDefault();
27153 this.onFocus();
27154 break;
27155 }
27156};
27157
27158/**
27159 * Get the input's placeholder - uses `placeholder` when list is empty and `secondary-placeholder`
27160 * when the list is non-empty. If `secondary-placeholder` is not provided, `placeholder` is used
27161 * always.
27162 */
27163MdChipsCtrl.prototype.getPlaceholder = function() {
27164 // Allow `secondary-placeholder` to be blank.
27165 var useSecondary = (this.items && this.items.length &&
27166 (this.secondaryPlaceholder == '' || this.secondaryPlaceholder));
27167 return useSecondary ? this.secondaryPlaceholder : this.placeholder;
27168};
27169
27170/**
27171 * Removes chip at {@code index} and selects the adjacent chip.
27172 * @param index
27173 */
27174MdChipsCtrl.prototype.removeAndSelectAdjacentChip = function(index) {
27175 var self = this;
27176 var selIndex = self.getAdjacentChipIndex(index);
27177 var wrap = this.$element[0].querySelector('md-chips-wrap');
27178 var chip = this.$element[0].querySelector('md-chip[index="' + index + '"]');
27179
27180 self.removeChip(index);
27181
27182 // The dobule-timeout is currently necessary to ensure that the DOM has finalized and the select()
27183 // will find the proper chip since the selection is index-based.
27184 //
27185 // TODO: Investigate calling from within chip $scope.$on('$destroy') to reduce/remove timeouts
27186 self.$timeout(function() {
27187 self.$timeout(function() {
27188 self.selectAndFocusChipSafe(selIndex);
27189 });
27190 });
27191};
27192
27193/**
27194 * Sets the selected chip index to -1.
27195 */
27196MdChipsCtrl.prototype.resetSelectedChip = function() {
27197 this.selectedChip = -1;
27198 this.ariaTabIndex = null;
27199};
27200
27201/**
27202 * Gets the index of an adjacent chip to select after deletion. Adjacency is
27203 * determined as the next chip in the list, unless the target chip is the
27204 * last in the list, then it is the chip immediately preceding the target. If
27205 * there is only one item in the list, -1 is returned (select none).
27206 * The number returned is the index to select AFTER the target has been
27207 * removed.
27208 * If the current chip is not selected, then -1 is returned to select none.
27209 */
27210MdChipsCtrl.prototype.getAdjacentChipIndex = function(index) {
27211 var len = this.items.length - 1;
27212 return (len == 0) ? -1 :
27213 (index == len) ? index -1 : index;
27214};
27215
27216/**
27217 * Append the contents of the buffer to the chip list. This method will first
27218 * call out to the md-transform-chip method, if provided.
27219 *
27220 * @param newChip
27221 */
27222MdChipsCtrl.prototype.appendChip = function(newChip) {
27223 this.shouldFocusLastChip = true;
27224 if (this.useTransformChip && this.transformChip) {
27225 var transformedChip = this.transformChip({'$chip': newChip});
27226
27227 // Check to make sure the chip is defined before assigning it, otherwise, we'll just assume
27228 // they want the string version.
27229 if (angular.isDefined(transformedChip)) {
27230 newChip = transformedChip;
27231 }
27232 }
27233
27234 // If items contains an identical object to newChip, do not append
27235 if (angular.isObject(newChip)){
27236 var identical = this.items.some(function(item){
27237 return angular.equals(newChip, item);
27238 });
27239 if (identical) return;
27240 }
27241
27242 // Check for a null (but not undefined), or existing chip and cancel appending
27243 if (newChip == null || this.items.indexOf(newChip) + 1) return;
27244
27245 // Append the new chip onto our list
27246 var length = this.items.push(newChip);
27247 var index = length - 1;
27248
27249 // Update model validation
27250 this.ngModelCtrl.$setDirty();
27251 this.validateModel();
27252
27253 // If they provide the md-on-add attribute, notify them of the chip addition
27254 if (this.useOnAdd && this.onAdd) {
27255 this.onAdd({ '$chip': newChip, '$index': index });
27256 }
27257};
27258
27259/**
27260 * Sets whether to use the md-transform-chip expression. This expression is
27261 * bound to scope and controller in {@code MdChipsDirective} as
27262 * {@code transformChip}. Due to the nature of directive scope bindings, the
27263 * controller cannot know on its own/from the scope whether an expression was
27264 * actually provided.
27265 */
27266MdChipsCtrl.prototype.useTransformChipExpression = function() {
27267 this.useTransformChip = true;
27268};
27269
27270/**
27271 * Sets whether to use the md-on-add expression. This expression is
27272 * bound to scope and controller in {@code MdChipsDirective} as
27273 * {@code onAdd}. Due to the nature of directive scope bindings, the
27274 * controller cannot know on its own/from the scope whether an expression was
27275 * actually provided.
27276 */
27277MdChipsCtrl.prototype.useOnAddExpression = function() {
27278 this.useOnAdd = true;
27279};
27280
27281/**
27282 * Sets whether to use the md-on-remove expression. This expression is
27283 * bound to scope and controller in {@code MdChipsDirective} as
27284 * {@code onRemove}. Due to the nature of directive scope bindings, the
27285 * controller cannot know on its own/from the scope whether an expression was
27286 * actually provided.
27287 */
27288MdChipsCtrl.prototype.useOnRemoveExpression = function() {
27289 this.useOnRemove = true;
27290};
27291
27292/*
27293 * Sets whether to use the md-on-select expression. This expression is
27294 * bound to scope and controller in {@code MdChipsDirective} as
27295 * {@code onSelect}. Due to the nature of directive scope bindings, the
27296 * controller cannot know on its own/from the scope whether an expression was
27297 * actually provided.
27298 */
27299MdChipsCtrl.prototype.useOnSelectExpression = function() {
27300 this.useOnSelect = true;
27301};
27302
27303/**
27304 * Gets the input buffer. The input buffer can be the model bound to the
27305 * default input item {@code this.chipBuffer}, the {@code selectedItem}
27306 * model of an {@code md-autocomplete}, or, through some magic, the model
27307 * bound to any inpput or text area element found within a
27308 * {@code md-input-container} element.
27309 * @return {string}
27310 */
27311MdChipsCtrl.prototype.getChipBuffer = function() {
27312 var chipBuffer = !this.userInputElement ? this.chipBuffer :
27313 this.userInputNgModelCtrl ? this.userInputNgModelCtrl.$viewValue :
27314 this.userInputElement[0].value;
27315
27316 // Ensure that the chip buffer is always a string. For example, the input element buffer might be falsy.
27317 return angular.isString(chipBuffer) ? chipBuffer : '';
27318};
27319
27320/**
27321 * Resets the input buffer for either the internal input or user provided input element.
27322 */
27323MdChipsCtrl.prototype.resetChipBuffer = function() {
27324 if (this.userInputElement) {
27325 if (this.userInputNgModelCtrl) {
27326 this.userInputNgModelCtrl.$setViewValue('');
27327 this.userInputNgModelCtrl.$render();
27328 } else {
27329 this.userInputElement[0].value = '';
27330 }
27331 } else {
27332 this.chipBuffer = '';
27333 }
27334};
27335
27336MdChipsCtrl.prototype.hasMaxChipsReached = function() {
27337 if (angular.isString(this.maxChips)) this.maxChips = parseInt(this.maxChips, 10) || 0;
27338
27339 return this.maxChips > 0 && this.items.length >= this.maxChips;
27340};
27341
27342/**
27343 * Updates the validity properties for the ngModel.
27344 */
27345MdChipsCtrl.prototype.validateModel = function() {
27346 this.ngModelCtrl.$setValidity('md-max-chips', !this.hasMaxChipsReached());
27347};
27348
27349/**
27350 * Removes the chip at the given index.
27351 * @param index
27352 */
27353MdChipsCtrl.prototype.removeChip = function(index) {
27354 var removed = this.items.splice(index, 1);
27355
27356 // Update model validation
27357 this.ngModelCtrl.$setDirty();
27358 this.validateModel();
27359
27360 if (removed && removed.length && this.useOnRemove && this.onRemove) {
27361 this.onRemove({ '$chip': removed[0], '$index': index });
27362 }
27363};
27364
27365MdChipsCtrl.prototype.removeChipAndFocusInput = function (index) {
27366 this.removeChip(index);
27367
27368 if (this.autocompleteCtrl) {
27369 // Always hide the autocomplete dropdown before focusing the autocomplete input.
27370 // Wait for the input to move horizontally, because the chip was removed.
27371 // This can lead to an incorrect dropdown position.
27372 this.autocompleteCtrl.hidden = true;
27373 this.$mdUtil.nextTick(this.onFocus.bind(this));
27374 } else {
27375 this.onFocus();
27376 }
27377
27378};
27379/**
27380 * Selects the chip at `index`,
27381 * @param index
27382 */
27383MdChipsCtrl.prototype.selectAndFocusChipSafe = function(index) {
27384 // If we have no chips, or are asked to select a chip before the first, just focus the input
27385 if (!this.items.length || index === -1) {
27386 return this.focusInput();
27387 }
27388
27389 // If we are asked to select a chip greater than the number of chips...
27390 if (index >= this.items.length) {
27391 if (this.readonly) {
27392 // If we are readonly, jump back to the start (because we have no input)
27393 index = 0;
27394 } else {
27395 // If we are not readonly, we should attempt to focus the input
27396 return this.onFocus();
27397 }
27398 }
27399
27400 index = Math.max(index, 0);
27401 index = Math.min(index, this.items.length - 1);
27402
27403 this.selectChip(index);
27404 this.focusChip(index);
27405};
27406
27407MdChipsCtrl.prototype.focusLastChipThenInput = function() {
27408 var ctrl = this;
27409
27410 ctrl.shouldFocusLastChip = false;
27411
27412 ctrl.focusChip(this.items.length - 1);
27413
27414 ctrl.$timeout(function() {
27415 ctrl.focusInput();
27416 }, ctrl.chipAppendDelay);
27417};
27418
27419MdChipsCtrl.prototype.focusInput = function() {
27420 this.selectChip(-1);
27421 this.onFocus();
27422};
27423
27424/**
27425 * Marks the chip at the given index as selected.
27426 * @param index
27427 */
27428MdChipsCtrl.prototype.selectChip = function(index) {
27429 if (index >= -1 && index <= this.items.length) {
27430 this.selectedChip = index;
27431
27432 // Fire the onSelect if provided
27433 if (this.useOnSelect && this.onSelect) {
27434 this.onSelect({'$chip': this.items[index] });
27435 }
27436 } else {
27437 this.$log.warn('Selected Chip index out of bounds; ignoring.');
27438 }
27439};
27440
27441/**
27442 * Selects the chip at `index` and gives it focus.
27443 * @param index
27444 */
27445MdChipsCtrl.prototype.selectAndFocusChip = function(index) {
27446 this.selectChip(index);
27447 if (index != -1) {
27448 this.focusChip(index);
27449 }
27450};
27451
27452/**
27453 * Call `focus()` on the chip at `index`
27454 */
27455MdChipsCtrl.prototype.focusChip = function(index) {
27456 var chipContent = this.$element[0].querySelector('md-chip[index="' + index + '"] .md-chip-content');
27457
27458 this.ariaTabIndex = index;
27459
27460 chipContent.focus();
27461};
27462
27463/**
27464 * Configures the required interactions with the ngModel Controller.
27465 * Specifically, set {@code this.items} to the {@code NgModelCtrl#$viewVale}.
27466 * @param ngModelCtrl
27467 */
27468MdChipsCtrl.prototype.configureNgModel = function(ngModelCtrl) {
27469 this.ngModelCtrl = ngModelCtrl;
27470
27471 var self = this;
27472 ngModelCtrl.$render = function() {
27473 // model is updated. do something.
27474 self.items = self.ngModelCtrl.$viewValue;
27475 };
27476};
27477
27478MdChipsCtrl.prototype.onFocus = function () {
27479 var input = this.$element[0].querySelector('input');
27480 input && input.focus();
27481 this.resetSelectedChip();
27482};
27483
27484MdChipsCtrl.prototype.onInputFocus = function () {
27485 this.inputHasFocus = true;
27486
27487 // Make sure we have the appropriate ARIA attributes
27488 this.setupInputAria();
27489
27490 // Make sure we don't have any chips selected
27491 this.resetSelectedChip();
27492};
27493
27494MdChipsCtrl.prototype.onInputBlur = function () {
27495 this.inputHasFocus = false;
27496
27497 if (this.shouldAddOnBlur()) {
27498 this.appendChip(this.getChipBuffer().trim());
27499 this.resetChipBuffer();
27500 }
27501};
27502
27503/**
27504 * Configure event bindings on a user-provided input element.
27505 * @param inputElement
27506 */
27507MdChipsCtrl.prototype.configureUserInput = function(inputElement) {
27508 this.userInputElement = inputElement;
27509
27510 // Find the NgModelCtrl for the input element
27511 var ngModelCtrl = inputElement.controller('ngModel');
27512 // `.controller` will look in the parent as well.
27513 if (ngModelCtrl != this.ngModelCtrl) {
27514 this.userInputNgModelCtrl = ngModelCtrl;
27515 }
27516
27517 var scope = this.$scope;
27518 var ctrl = this;
27519
27520 // Run all of the events using evalAsync because a focus may fire a blur in the same digest loop
27521 var scopeApplyFn = function(event, fn) {
27522 scope.$evalAsync(angular.bind(ctrl, fn, event));
27523 };
27524
27525 // Bind to keydown and focus events of input
27526 inputElement
27527 .attr({ tabindex: 0 })
27528 .on('keydown', function(event) { scopeApplyFn(event, ctrl.inputKeydown) })
27529 .on('focus', function(event) { scopeApplyFn(event, ctrl.onInputFocus) })
27530 .on('blur', function(event) { scopeApplyFn(event, ctrl.onInputBlur) })
27531};
27532
27533MdChipsCtrl.prototype.configureAutocomplete = function(ctrl) {
27534 if (ctrl) {
27535 this.autocompleteCtrl = ctrl;
27536
27537 ctrl.registerSelectedItemWatcher(angular.bind(this, function (item) {
27538 if (item) {
27539 // Only append the chip and reset the chip buffer if the max chips limit isn't reached.
27540 if (this.hasMaxChipsReached()) return;
27541
27542 this.appendChip(item);
27543 this.resetChipBuffer();
27544 }
27545 }));
27546
27547 this.$element.find('input')
27548 .on('focus',angular.bind(this, this.onInputFocus) )
27549 .on('blur', angular.bind(this, this.onInputBlur) );
27550 }
27551};
27552
27553/**
27554 * Whether the current chip buffer should be added on input blur or not.
27555 * @returns {boolean}
27556 */
27557MdChipsCtrl.prototype.shouldAddOnBlur = function() {
27558
27559 // Update the custom ngModel validators from the chips component.
27560 this.validateModel();
27561
27562 var chipBuffer = this.getChipBuffer().trim();
27563 var isModelValid = this.ngModelCtrl.$valid;
27564 var isAutocompleteShowing = this.autocompleteCtrl && !this.autocompleteCtrl.hidden;
27565
27566 if (this.userInputNgModelCtrl) {
27567 isModelValid = isModelValid && this.userInputNgModelCtrl.$valid;
27568 }
27569
27570 return this.addOnBlur && !this.requireMatch && chipBuffer && isModelValid && !isAutocompleteShowing;
27571};
27572
27573MdChipsCtrl.prototype.hasFocus = function () {
27574 return this.inputHasFocus || this.selectedChip >= 0;
27575};
27576
27577MdChipsCtrl.prototype.contentIdFor = function(index) {
27578 return this.contentIds[index];
27579};
27580
27581})();
27582(function(){
27583"use strict";
27584
27585
27586 MdChips.$inject = ["$mdTheming", "$mdUtil", "$compile", "$log", "$timeout", "$$mdSvgRegistry"];angular
27587 .module('material.components.chips')
27588 .directive('mdChips', MdChips);
27589
27590 /**
27591 * @ngdoc directive
27592 * @name mdChips
27593 * @module material.components.chips
27594 *
27595 * @description
27596 * `<md-chips>` is an input component for building lists of strings or objects. The list items are
27597 * displayed as 'chips'. This component can make use of an `<input>` element or an
27598 * `<md-autocomplete>` element.
27599 *
27600 * ### Custom templates
27601 * A custom template may be provided to render the content of each chip. This is achieved by
27602 * specifying an `<md-chip-template>` element containing the custom content as a child of
27603 * `<md-chips>`.
27604 *
27605 * Note: Any attributes on
27606 * `<md-chip-template>` will be dropped as only the innerHTML is used for the chip template. The
27607 * variables `$chip` and `$index` are available in the scope of `<md-chip-template>`, representing
27608 * the chip object and its index in the list of chips, respectively.
27609 * To override the chip delete control, include an element (ideally a button) with the attribute
27610 * `md-chip-remove`. A click listener to remove the chip will be added automatically. The element
27611 * is also placed as a sibling to the chip content (on which there are also click listeners) to
27612 * avoid a nested ng-click situation.
27613 *
27614 * <!-- Note: We no longer want to include this in the site docs; but it should remain here for
27615 * future developers and those looking at the documentation.
27616 *
27617 * <h3> Pending Features </h3>
27618 * <ul style="padding-left:20px;">
27619 *
27620 * <ul>Style
27621 * <li>Colours for hover, press states (ripple?).</li>
27622 * </ul>
27623 *
27624 * <ul>Validation
27625 * <li>allow a validation callback</li>
27626 * <li>hilighting style for invalid chips</li>
27627 * </ul>
27628 *
27629 * <ul>Item mutation
27630 * <li>Support `
27631 * <md-chip-edit>` template, show/hide the edit element on tap/click? double tap/double
27632 * click?
27633 * </li>
27634 * </ul>
27635 *
27636 * <ul>Truncation and Disambiguation (?)
27637 * <li>Truncate chip text where possible, but do not truncate entries such that two are
27638 * indistinguishable.</li>
27639 * </ul>
27640 *
27641 * <ul>Drag and Drop
27642 * <li>Drag and drop chips between related `<md-chips>` elements.
27643 * </li>
27644 * </ul>
27645 * </ul>
27646 *
27647 * //-->
27648 *
27649 * Sometimes developers want to limit the amount of possible chips.<br/>
27650 * You can specify the maximum amount of chips by using the following markup.
27651 *
27652 * <hljs lang="html">
27653 * <md-chips
27654 * ng-model="myItems"
27655 * placeholder="Add an item"
27656 * md-max-chips="5">
27657 * </md-chips>
27658 * </hljs>
27659 *
27660 * In some cases, you have an autocomplete inside of the `md-chips`.<br/>
27661 * When the maximum amount of chips has been reached, you can also disable the autocomplete selection.<br/>
27662 * Here is an example markup.
27663 *
27664 * <hljs lang="html">
27665 * <md-chips ng-model="myItems" md-max-chips="5">
27666 * <md-autocomplete ng-hide="myItems.length > 5" ...></md-autocomplete>
27667 * </md-chips>
27668 * </hljs>
27669 *
27670 * ### Accessibility
27671 *
27672 * The `md-chips` component supports keyboard and screen reader users since Version 1.1.2. In
27673 * order to achieve this, we modified the chips behavior to select newly appended chips for
27674 * `300ms` before re-focusing the input and allowing the user to type.
27675 *
27676 * For most users, this delay is small enough that it will not be noticeable but allows certain
27677 * screen readers to function properly (JAWS and NVDA in particular).
27678 *
27679 * We introduced a new `md-chip-append-delay` option to allow developers to better control this
27680 * behavior.
27681 *
27682 * Please refer to the documentation of this option (below) for more information.
27683 *
27684 * @param {string=|object=} ng-model A model to which the list of items will be bound.
27685 * @param {string=} placeholder Placeholder text that will be forwarded to the input.
27686 * @param {string=} secondary-placeholder Placeholder text that will be forwarded to the input,
27687 * displayed when there is at least one item in the list
27688 * @param {boolean=} md-removable Enables or disables the deletion of chips through the
27689 * removal icon or the Delete/Backspace key. Defaults to true.
27690 * @param {boolean=} readonly Disables list manipulation (deleting or adding list items), hiding
27691 * the input and delete buttons. If no `ng-model` is provided, the chips will automatically be
27692 * marked as readonly.<br/><br/>
27693 * When `md-removable` is not defined, the `md-remove` behavior will be overwritten and disabled.
27694 * @param {string=} md-enable-chip-edit Set this to "true" to enable editing of chip contents. The user can
27695 * go into edit mode with pressing "space", "enter", or double clicking on the chip. Chip edit is only
27696 * supported for chips with basic template.
27697 * @param {number=} md-max-chips The maximum number of chips allowed to add through user input.
27698 * <br/><br/>The validation property `md-max-chips` can be used when the max chips
27699 * amount is reached.
27700 * @param {boolean=} md-add-on-blur When set to true, remaining text inside of the input will
27701 * be converted into a new chip on blur.
27702 * @param {expression} md-transform-chip An expression of form `myFunction($chip)` that when called
27703 * expects one of the following return values:
27704 * - an object representing the `$chip` input string
27705 * - `undefined` to simply add the `$chip` input string, or
27706 * - `null` to prevent the chip from being appended
27707 * @param {expression=} md-on-add An expression which will be called when a chip has been
27708 * added.
27709 * @param {expression=} md-on-remove An expression which will be called when a chip has been
27710 * removed.
27711 * @param {expression=} md-on-select An expression which will be called when a chip is selected.
27712 * @param {boolean} md-require-match If true, and the chips template contains an autocomplete,
27713 * only allow selection of pre-defined chips (i.e. you cannot add new ones).
27714 * @param {string=} input-aria-label A string read by screen readers to identify the input.
27715 * @param {string=} container-hint A string read by screen readers informing users of how to
27716 * navigate the chips. Used in readonly mode.
27717 * @param {string=} delete-hint A string read by screen readers instructing users that pressing
27718 * the delete key will remove the chip.
27719 * @param {string=} delete-button-label A label for the delete button. Also hidden and read by
27720 * screen readers.
27721 * @param {expression=} md-separator-keys An array of key codes used to separate chips.
27722 * @param {string=} md-chip-append-delay The number of milliseconds that the component will select
27723 * a newly appended chip before allowing a user to type into the input. This is **necessary**
27724 * for keyboard accessibility for screen readers. It defaults to 300ms and any number less than
27725 * 300 can cause issues with screen readers (particularly JAWS and sometimes NVDA).
27726 *
27727 * _Available since Version 1.1.2._
27728 *
27729 * **Note:** You can safely set this to `0` in one of the following two instances:
27730 *
27731 * 1. You are targeting an iOS or Safari-only application (where users would use VoiceOver) or
27732 * only ChromeVox users.
27733 *
27734 * 2. If you have utilized the `md-separator-keys` to disable the `enter` keystroke in
27735 * favor of another one (such as `,` or `;`).
27736 *
27737 * @usage
27738 * <hljs lang="html">
27739 * <md-chips
27740 * ng-model="myItems"
27741 * placeholder="Add an item"
27742 * readonly="isReadOnly">
27743 * </md-chips>
27744 * </hljs>
27745 *
27746 * <h3>Validation</h3>
27747 * When using [ngMessages](https://docs.angularjs.org/api/ngMessages), you can show errors based
27748 * on our custom validators.
27749 * <hljs lang="html">
27750 * <form name="userForm">
27751 * <md-chips
27752 * name="fruits"
27753 * ng-model="myItems"
27754 * placeholder="Add an item"
27755 * md-max-chips="5">
27756 * </md-chips>
27757 * <div ng-messages="userForm.fruits.$error" ng-if="userForm.$dirty">
27758 * <div ng-message="md-max-chips">You reached the maximum amount of chips</div>
27759 * </div>
27760 * </form>
27761 * </hljs>
27762 *
27763 */
27764
27765 var MD_CHIPS_TEMPLATE = '\
27766 <md-chips-wrap\
27767 id="{{$mdChipsCtrl.wrapperId}}"\
27768 tabindex="{{$mdChipsCtrl.readonly ? 0 : -1}}"\
27769 ng-keydown="$mdChipsCtrl.chipKeydown($event)"\
27770 ng-class="{ \'md-focused\': $mdChipsCtrl.hasFocus(), \
27771 \'md-readonly\': !$mdChipsCtrl.ngModelCtrl || $mdChipsCtrl.readonly,\
27772 \'md-removable\': $mdChipsCtrl.isRemovable() }"\
27773 aria-setsize="{{$mdChipsCtrl.items.length}}"\
27774 class="md-chips">\
27775 <span ng-if="$mdChipsCtrl.readonly" class="md-visually-hidden">\
27776 {{$mdChipsCtrl.containerHint}}\
27777 </span>\
27778 <md-chip ng-repeat="$chip in $mdChipsCtrl.items"\
27779 index="{{$index}}"\
27780 ng-class="{\'md-focused\': $mdChipsCtrl.selectedChip == $index, \'md-readonly\': !$mdChipsCtrl.ngModelCtrl || $mdChipsCtrl.readonly}">\
27781 <div class="md-chip-content"\
27782 tabindex="{{$mdChipsCtrl.ariaTabIndex == $index ? 0 : -1}}"\
27783 id="{{$mdChipsCtrl.contentIdFor($index)}}"\
27784 role="option"\
27785 aria-selected="{{$mdChipsCtrl.selectedChip == $index}}" \
27786 aria-posinset="{{$index}}"\
27787 ng-click="!$mdChipsCtrl.readonly && $mdChipsCtrl.focusChip($index)"\
27788 ng-focus="!$mdChipsCtrl.readonly && $mdChipsCtrl.selectChip($index)"\
27789 md-chip-transclude="$mdChipsCtrl.chipContentsTemplate"></div>\
27790 <div ng-if="$mdChipsCtrl.isRemovable()"\
27791 class="md-chip-remove-container"\
27792 tabindex="-1"\
27793 md-chip-transclude="$mdChipsCtrl.chipRemoveTemplate"></div>\
27794 </md-chip>\
27795 <div class="md-chip-input-container" ng-if="!$mdChipsCtrl.readonly && $mdChipsCtrl.ngModelCtrl">\
27796 <div md-chip-transclude="$mdChipsCtrl.chipInputTemplate"></div>\
27797 </div>\
27798 </md-chips-wrap>';
27799
27800 var CHIP_INPUT_TEMPLATE = '\
27801 <input\
27802 class="md-input"\
27803 tabindex="0"\
27804 aria-label="{{$mdChipsCtrl.inputAriaLabel}}" \
27805 placeholder="{{$mdChipsCtrl.getPlaceholder()}}"\
27806 ng-model="$mdChipsCtrl.chipBuffer"\
27807 ng-focus="$mdChipsCtrl.onInputFocus()"\
27808 ng-blur="$mdChipsCtrl.onInputBlur()"\
27809 ng-keydown="$mdChipsCtrl.inputKeydown($event)">';
27810
27811 var CHIP_DEFAULT_TEMPLATE = '\
27812 <span>{{$chip}}</span>';
27813
27814 var CHIP_REMOVE_TEMPLATE = '\
27815 <button\
27816 class="md-chip-remove"\
27817 ng-if="$mdChipsCtrl.isRemovable()"\
27818 ng-click="$mdChipsCtrl.removeChipAndFocusInput($$replacedScope.$index)"\
27819 type="button"\
27820 tabindex="-1">\
27821 <md-icon md-svg-src="{{ $mdChipsCtrl.mdCloseIcon }}"></md-icon>\
27822 <span class="md-visually-hidden">\
27823 {{$mdChipsCtrl.deleteButtonLabel}}\
27824 </span>\
27825 </button>';
27826
27827 /**
27828 * MDChips Directive Definition
27829 */
27830 function MdChips ($mdTheming, $mdUtil, $compile, $log, $timeout, $$mdSvgRegistry) {
27831 // Run our templates through $mdUtil.processTemplate() to allow custom start/end symbols
27832 var templates = getTemplates();
27833
27834 return {
27835 template: function(element, attrs) {
27836 // Clone the element into an attribute. By prepending the attribute
27837 // name with '$', Angular won't write it into the DOM. The cloned
27838 // element propagates to the link function via the attrs argument,
27839 // where various contained-elements can be consumed.
27840 attrs['$mdUserTemplate'] = element.clone();
27841 return templates.chips;
27842 },
27843 require: ['mdChips'],
27844 restrict: 'E',
27845 controller: 'MdChipsCtrl',
27846 controllerAs: '$mdChipsCtrl',
27847 bindToController: true,
27848 compile: compile,
27849 scope: {
27850 readonly: '=readonly',
27851 removable: '=mdRemovable',
27852 placeholder: '@',
27853 secondaryPlaceholder: '@',
27854 maxChips: '@mdMaxChips',
27855 transformChip: '&mdTransformChip',
27856 onAppend: '&mdOnAppend',
27857 onAdd: '&mdOnAdd',
27858 onRemove: '&mdOnRemove',
27859 onSelect: '&mdOnSelect',
27860 inputAriaLabel: '@',
27861 containerHint: '@',
27862 deleteHint: '@',
27863 deleteButtonLabel: '@',
27864 separatorKeys: '=?mdSeparatorKeys',
27865 requireMatch: '=?mdRequireMatch',
27866 chipAppendDelayString: '@?mdChipAppendDelay'
27867 }
27868 };
27869
27870 /**
27871 * Builds the final template for `md-chips` and returns the postLink function.
27872 *
27873 * Building the template involves 3 key components:
27874 * static chips
27875 * chip template
27876 * input control
27877 *
27878 * If no `ng-model` is provided, only the static chip work needs to be done.
27879 *
27880 * If no user-passed `md-chip-template` exists, the default template is used. This resulting
27881 * template is appended to the chip content element.
27882 *
27883 * The remove button may be overridden by passing an element with an md-chip-remove attribute.
27884 *
27885 * If an `input` or `md-autocomplete` element is provided by the caller, it is set aside for
27886 * transclusion later. The transclusion happens in `postLink` as the parent scope is required.
27887 * If no user input is provided, a default one is appended to the input container node in the
27888 * template.
27889 *
27890 * Static Chips (i.e. `md-chip` elements passed from the caller) are gathered and set aside for
27891 * transclusion in the `postLink` function.
27892 *
27893 *
27894 * @param element
27895 * @param attr
27896 * @returns {Function}
27897 */
27898 function compile(element, attr) {
27899 // Grab the user template from attr and reset the attribute to null.
27900 var userTemplate = attr['$mdUserTemplate'];
27901 attr['$mdUserTemplate'] = null;
27902
27903 var chipTemplate = getTemplateByQuery('md-chips>md-chip-template');
27904
27905 var chipRemoveSelector = $mdUtil
27906 .prefixer()
27907 .buildList('md-chip-remove')
27908 .map(function(attr) {
27909 return 'md-chips>*[' + attr + ']';
27910 })
27911 .join(',');
27912
27913 // Set the chip remove, chip contents and chip input templates. The link function will put
27914 // them on the scope for transclusion later.
27915 var chipRemoveTemplate = getTemplateByQuery(chipRemoveSelector) || templates.remove,
27916 chipContentsTemplate = chipTemplate || templates.default,
27917 chipInputTemplate = getTemplateByQuery('md-chips>md-autocomplete')
27918 || getTemplateByQuery('md-chips>input')
27919 || templates.input,
27920 staticChips = userTemplate.find('md-chip');
27921
27922 // Warn of malformed template. See #2545
27923 if (userTemplate[0].querySelector('md-chip-template>*[md-chip-remove]')) {
27924 $log.warn('invalid placement of md-chip-remove within md-chip-template.');
27925 }
27926
27927 function getTemplateByQuery (query) {
27928 if (!attr.ngModel) return;
27929 var element = userTemplate[0].querySelector(query);
27930 return element && element.outerHTML;
27931 }
27932
27933 /**
27934 * Configures controller and transcludes.
27935 */
27936 return function postLink(scope, element, attrs, controllers) {
27937 $mdUtil.initOptionalProperties(scope, attr);
27938
27939 $mdTheming(element);
27940 var mdChipsCtrl = controllers[0];
27941 if(chipTemplate) {
27942 // Chip editing functionality assumes we are using the default chip template.
27943 mdChipsCtrl.enableChipEdit = false;
27944 }
27945
27946 mdChipsCtrl.chipContentsTemplate = chipContentsTemplate;
27947 mdChipsCtrl.chipRemoveTemplate = chipRemoveTemplate;
27948 mdChipsCtrl.chipInputTemplate = chipInputTemplate;
27949
27950 mdChipsCtrl.mdCloseIcon = $$mdSvgRegistry.mdClose;
27951
27952 element
27953 .attr({ tabindex: -1 })
27954 .on('focus', function () { mdChipsCtrl.onFocus(); });
27955
27956 if (attr.ngModel) {
27957 mdChipsCtrl.configureNgModel(element.controller('ngModel'));
27958
27959 // If an `md-transform-chip` attribute was set, tell the controller to use the expression
27960 // before appending chips.
27961 if (attrs.mdTransformChip) mdChipsCtrl.useTransformChipExpression();
27962
27963 // If an `md-on-append` attribute was set, tell the controller to use the expression
27964 // when appending chips.
27965 //
27966 // DEPRECATED: Will remove in official 1.0 release
27967 if (attrs.mdOnAppend) mdChipsCtrl.useOnAppendExpression();
27968
27969 // If an `md-on-add` attribute was set, tell the controller to use the expression
27970 // when adding chips.
27971 if (attrs.mdOnAdd) mdChipsCtrl.useOnAddExpression();
27972
27973 // If an `md-on-remove` attribute was set, tell the controller to use the expression
27974 // when removing chips.
27975 if (attrs.mdOnRemove) mdChipsCtrl.useOnRemoveExpression();
27976
27977 // If an `md-on-select` attribute was set, tell the controller to use the expression
27978 // when selecting chips.
27979 if (attrs.mdOnSelect) mdChipsCtrl.useOnSelectExpression();
27980
27981 // The md-autocomplete and input elements won't be compiled until after this directive
27982 // is complete (due to their nested nature). Wait a tick before looking for them to
27983 // configure the controller.
27984 if (chipInputTemplate != templates.input) {
27985 // The autocomplete will not appear until the readonly attribute is not true (i.e.
27986 // false or undefined), so we have to watch the readonly and then on the next tick
27987 // after the chip transclusion has run, we can configure the autocomplete and user
27988 // input.
27989 scope.$watch('$mdChipsCtrl.readonly', function(readonly) {
27990 if (!readonly) {
27991
27992 $mdUtil.nextTick(function(){
27993
27994 if (chipInputTemplate.indexOf('<md-autocomplete') === 0) {
27995 var autocompleteEl = element.find('md-autocomplete');
27996 mdChipsCtrl.configureAutocomplete(autocompleteEl.controller('mdAutocomplete'));
27997 }
27998
27999 mdChipsCtrl.configureUserInput(element.find('input'));
28000 });
28001 }
28002 });
28003 }
28004
28005 // At the next tick, if we find an input, make sure it has the md-input class
28006 $mdUtil.nextTick(function() {
28007 var input = element.find('input');
28008
28009 input && input.toggleClass('md-input', true);
28010 });
28011 }
28012
28013 // Compile with the parent's scope and prepend any static chips to the wrapper.
28014 if (staticChips.length > 0) {
28015 var compiledStaticChips = $compile(staticChips.clone())(scope.$parent);
28016 $timeout(function() { element.find('md-chips-wrap').prepend(compiledStaticChips); });
28017 }
28018 };
28019 }
28020
28021 function getTemplates() {
28022 return {
28023 chips: $mdUtil.processTemplate(MD_CHIPS_TEMPLATE),
28024 input: $mdUtil.processTemplate(CHIP_INPUT_TEMPLATE),
28025 default: $mdUtil.processTemplate(CHIP_DEFAULT_TEMPLATE),
28026 remove: $mdUtil.processTemplate(CHIP_REMOVE_TEMPLATE)
28027 };
28028 }
28029 }
28030
28031})();
28032(function(){
28033"use strict";
28034
28035angular
28036 .module('material.components.chips')
28037 .controller('MdContactChipsCtrl', MdContactChipsCtrl);
28038
28039
28040
28041/**
28042 * Controller for the MdContactChips component
28043 * @constructor
28044 */
28045function MdContactChipsCtrl () {
28046 /** @type {Object} */
28047 this.selectedItem = null;
28048
28049 /** @type {string} */
28050 this.searchText = '';
28051}
28052
28053
28054MdContactChipsCtrl.prototype.queryContact = function(searchText) {
28055 return this.contactQuery({'$query': searchText});
28056};
28057
28058
28059MdContactChipsCtrl.prototype.itemName = function(item) {
28060 return item[this.contactName];
28061};
28062
28063})();
28064(function(){
28065"use strict";
28066
28067
28068MdContactChips.$inject = ["$mdTheming", "$mdUtil"];angular
28069 .module('material.components.chips')
28070 .directive('mdContactChips', MdContactChips);
28071
28072/**
28073 * @ngdoc directive
28074 * @name mdContactChips
28075 * @module material.components.chips
28076 *
28077 * @description
28078 * `<md-contact-chips>` is an input component based on `md-chips` and makes use of an
28079 * `md-autocomplete` element. The component allows the caller to supply a query expression which
28080 * returns a list of possible contacts. The user can select one of these and add it to the list of
28081 * chips.
28082 *
28083 * You may also use the `md-highlight-text` directive along with its parameters to control the
28084 * appearance of the matched text inside of the contacts' autocomplete popup.
28085 *
28086 * @param {string=|object=} ng-model A model to bind the list of items to
28087 * @param {string=} placeholder Placeholder text that will be forwarded to the input.
28088 * @param {string=} secondary-placeholder Placeholder text that will be forwarded to the input,
28089 * displayed when there is at least on item in the list
28090 * @param {expression} md-contacts An expression expected to return contacts matching the search
28091 * test, `$query`. If this expression involves a promise, a loading bar is displayed while
28092 * waiting for it to resolve.
28093 * @param {string} md-contact-name The field name of the contact object representing the
28094 * contact's name.
28095 * @param {string} md-contact-email The field name of the contact object representing the
28096 * contact's email address.
28097 * @param {string} md-contact-image The field name of the contact object representing the
28098 * contact's image.
28099 * @param {number=} md-min-length Specifies the minimum length of text before autocomplete will
28100 * make suggestions
28101 *
28102 * @param {expression=} filter-selected Whether to filter selected contacts from the list of
28103 * suggestions shown in the autocomplete.
28104 *
28105 * ***Note:** This attribute has been removed but may come back.*
28106 *
28107 *
28108 *
28109 * @usage
28110 * <hljs lang="html">
28111 * <md-contact-chips
28112 * ng-model="ctrl.contacts"
28113 * md-contacts="ctrl.querySearch($query)"
28114 * md-contact-name="name"
28115 * md-contact-image="image"
28116 * md-contact-email="email"
28117 * placeholder="To">
28118 * </md-contact-chips>
28119 * </hljs>
28120 *
28121 */
28122
28123
28124var MD_CONTACT_CHIPS_TEMPLATE = '\
28125 <md-chips class="md-contact-chips"\
28126 ng-model="$mdContactChipsCtrl.contacts"\
28127 md-require-match="$mdContactChipsCtrl.requireMatch"\
28128 md-chip-append-delay="{{$mdContactChipsCtrl.chipAppendDelay}}" \
28129 md-autocomplete-snap>\
28130 <md-autocomplete\
28131 md-menu-class="md-contact-chips-suggestions"\
28132 md-selected-item="$mdContactChipsCtrl.selectedItem"\
28133 md-search-text="$mdContactChipsCtrl.searchText"\
28134 md-items="item in $mdContactChipsCtrl.queryContact($mdContactChipsCtrl.searchText)"\
28135 md-item-text="$mdContactChipsCtrl.itemName(item)"\
28136 md-no-cache="true"\
28137 md-min-length="$mdContactChipsCtrl.minLength"\
28138 md-autoselect\
28139 placeholder="{{$mdContactChipsCtrl.contacts.length == 0 ?\
28140 $mdContactChipsCtrl.placeholder : $mdContactChipsCtrl.secondaryPlaceholder}}">\
28141 <div class="md-contact-suggestion">\
28142 <img \
28143 ng-src="{{item[$mdContactChipsCtrl.contactImage]}}"\
28144 alt="{{item[$mdContactChipsCtrl.contactName]}}"\
28145 ng-if="item[$mdContactChipsCtrl.contactImage]" />\
28146 <span class="md-contact-name" md-highlight-text="$mdContactChipsCtrl.searchText"\
28147 md-highlight-flags="{{$mdContactChipsCtrl.highlightFlags}}">\
28148 {{item[$mdContactChipsCtrl.contactName]}}\
28149 </span>\
28150 <span class="md-contact-email" >{{item[$mdContactChipsCtrl.contactEmail]}}</span>\
28151 </div>\
28152 </md-autocomplete>\
28153 <md-chip-template>\
28154 <div class="md-contact-avatar">\
28155 <img \
28156 ng-src="{{$chip[$mdContactChipsCtrl.contactImage]}}"\
28157 alt="{{$chip[$mdContactChipsCtrl.contactName]}}"\
28158 ng-if="$chip[$mdContactChipsCtrl.contactImage]" />\
28159 </div>\
28160 <div class="md-contact-name">\
28161 {{$chip[$mdContactChipsCtrl.contactName]}}\
28162 </div>\
28163 </md-chip-template>\
28164 </md-chips>';
28165
28166
28167/**
28168 * MDContactChips Directive Definition
28169 *
28170 * @param $mdTheming
28171 * @returns {*}
28172 * @ngInject
28173 */
28174function MdContactChips($mdTheming, $mdUtil) {
28175 return {
28176 template: function(element, attrs) {
28177 return MD_CONTACT_CHIPS_TEMPLATE;
28178 },
28179 restrict: 'E',
28180 controller: 'MdContactChipsCtrl',
28181 controllerAs: '$mdContactChipsCtrl',
28182 bindToController: true,
28183 compile: compile,
28184 scope: {
28185 contactQuery: '&mdContacts',
28186 placeholder: '@',
28187 secondaryPlaceholder: '@',
28188 contactName: '@mdContactName',
28189 contactImage: '@mdContactImage',
28190 contactEmail: '@mdContactEmail',
28191 contacts: '=ngModel',
28192 requireMatch: '=?mdRequireMatch',
28193 minLength: '=?mdMinLength',
28194 highlightFlags: '@?mdHighlightFlags',
28195 chipAppendDelay: '@?mdChipAppendDelay'
28196 }
28197 };
28198
28199 function compile(element, attr) {
28200 return function postLink(scope, element, attrs, controllers) {
28201 var contactChipsController = controllers;
28202
28203 $mdUtil.initOptionalProperties(scope, attr);
28204 $mdTheming(element);
28205
28206 element.attr('tabindex', '-1');
28207
28208 attrs.$observe('mdChipAppendDelay', function(newValue) {
28209 contactChipsController.chipAppendDelay = newValue;
28210 });
28211 };
28212 }
28213}
28214
28215})();
28216(function(){
28217"use strict";
28218
28219(function() {
28220 'use strict';
28221
28222 /**
28223 * @ngdoc directive
28224 * @name mdCalendar
28225 * @module material.components.datepicker
28226 *
28227 * @param {Date} ng-model The component's model. Should be a Date object.
28228 * @param {Date=} md-min-date Expression representing the minimum date.
28229 * @param {Date=} md-max-date Expression representing the maximum date.
28230 * @param {(function(Date): boolean)=} md-date-filter Function expecting a date and returning a boolean whether it can be selected or not.
28231 *
28232 * @description
28233 * `<md-calendar>` is a component that renders a calendar that can be used to select a date.
28234 * It is a part of the `<md-datepicker` pane, however it can also be used on it's own.
28235 *
28236 * @usage
28237 *
28238 * <hljs lang="html">
28239 * <md-calendar ng-model="birthday"></md-calendar>
28240 * </hljs>
28241 */
28242 CalendarCtrl.$inject = ["$element", "$scope", "$$mdDateUtil", "$mdUtil", "$mdConstant", "$mdTheming", "$$rAF", "$attrs", "$mdDateLocale"];
28243 angular.module('material.components.datepicker')
28244 .directive('mdCalendar', calendarDirective);
28245
28246 // POST RELEASE
28247 // TODO(jelbourn): Mac Cmd + left / right == Home / End
28248 // TODO(jelbourn): Refactor month element creation to use cloneNode (performance).
28249 // TODO(jelbourn): Define virtual scrolling constants (compactness) users can override.
28250 // TODO(jelbourn): Animated month transition on ng-model change (virtual-repeat)
28251 // TODO(jelbourn): Scroll snapping (virtual repeat)
28252 // TODO(jelbourn): Remove superfluous row from short months (virtual-repeat)
28253 // TODO(jelbourn): Month headers stick to top when scrolling.
28254 // TODO(jelbourn): Previous month opacity is lowered when partially scrolled out of view.
28255 // TODO(jelbourn): Support md-calendar standalone on a page (as a tabstop w/ aria-live
28256 // announcement and key handling).
28257 // Read-only calendar (not just date-picker).
28258
28259 function calendarDirective() {
28260 return {
28261 template: function(tElement, tAttr) {
28262 // TODO(crisbeto): This is a workaround that allows the calendar to work, without
28263 // a datepicker, until issue #8585 gets resolved. It can safely be removed
28264 // afterwards. This ensures that the virtual repeater scrolls to the proper place on load by
28265 // deferring the execution until the next digest. It's necessary only if the calendar is used
28266 // without a datepicker, otherwise it's already wrapped in an ngIf.
28267 var extraAttrs = tAttr.hasOwnProperty('ngIf') ? '' : 'ng-if="calendarCtrl.isInitialized"';
28268 var template = '' +
28269 '<div ng-switch="calendarCtrl.currentView" ' + extraAttrs + '>' +
28270 '<md-calendar-year ng-switch-when="year"></md-calendar-year>' +
28271 '<md-calendar-month ng-switch-default></md-calendar-month>' +
28272 '</div>';
28273
28274 return template;
28275 },
28276 scope: {
28277 minDate: '=mdMinDate',
28278 maxDate: '=mdMaxDate',
28279 dateFilter: '=mdDateFilter',
28280 _currentView: '@mdCurrentView'
28281 },
28282 require: ['ngModel', 'mdCalendar'],
28283 controller: CalendarCtrl,
28284 controllerAs: 'calendarCtrl',
28285 bindToController: true,
28286 link: function(scope, element, attrs, controllers) {
28287 var ngModelCtrl = controllers[0];
28288 var mdCalendarCtrl = controllers[1];
28289 mdCalendarCtrl.configureNgModel(ngModelCtrl);
28290 }
28291 };
28292 }
28293
28294 /**
28295 * Occasionally the hideVerticalScrollbar method might read an element's
28296 * width as 0, because it hasn't been laid out yet. This value will be used
28297 * as a fallback, in order to prevent scenarios where the element's width
28298 * would otherwise have been set to 0. This value is the "usual" width of a
28299 * calendar within a floating calendar pane.
28300 */
28301 var FALLBACK_WIDTH = 340;
28302
28303 /** Next identifier for calendar instance. */
28304 var nextUniqueId = 0;
28305
28306 /**
28307 * Controller for the mdCalendar component.
28308 * @ngInject @constructor
28309 */
28310 function CalendarCtrl($element, $scope, $$mdDateUtil, $mdUtil,
28311 $mdConstant, $mdTheming, $$rAF, $attrs, $mdDateLocale) {
28312
28313 $mdTheming($element);
28314
28315 /** @final {!angular.JQLite} */
28316 this.$element = $element;
28317
28318 /** @final {!angular.Scope} */
28319 this.$scope = $scope;
28320
28321 /** @final */
28322 this.dateUtil = $$mdDateUtil;
28323
28324 /** @final */
28325 this.$mdUtil = $mdUtil;
28326
28327 /** @final */
28328 this.keyCode = $mdConstant.KEY_CODE;
28329
28330 /** @final */
28331 this.$$rAF = $$rAF;
28332
28333 /** @final */
28334 this.$mdDateLocale = $mdDateLocale;
28335
28336 /** @final {Date} */
28337 this.today = this.dateUtil.createDateAtMidnight();
28338
28339 /** @type {!angular.NgModelController} */
28340 this.ngModelCtrl = null;
28341
28342 /** @type {String} Class applied to the selected date cell. */
28343 this.SELECTED_DATE_CLASS = 'md-calendar-selected-date';
28344
28345 /** @type {String} Class applied to the cell for today. */
28346 this.TODAY_CLASS = 'md-calendar-date-today';
28347
28348 /** @type {String} Class applied to the focused cell. */
28349 this.FOCUSED_DATE_CLASS = 'md-focus';
28350
28351 /** @final {number} Unique ID for this calendar instance. */
28352 this.id = nextUniqueId++;
28353
28354 /**
28355 * The date that is currently focused or showing in the calendar. This will initially be set
28356 * to the ng-model value if set, otherwise to today. It will be updated as the user navigates
28357 * to other months. The cell corresponding to the displayDate does not necesarily always have
28358 * focus in the document (such as for cases when the user is scrolling the calendar).
28359 * @type {Date}
28360 */
28361 this.displayDate = null;
28362
28363 /**
28364 * The selected date. Keep track of this separately from the ng-model value so that we
28365 * can know, when the ng-model value changes, what the previous value was before it's updated
28366 * in the component's UI.
28367 *
28368 * @type {Date}
28369 */
28370 this.selectedDate = null;
28371
28372 /**
28373 * The first date that can be rendered by the calendar. The default is taken
28374 * from the mdDateLocale provider and is limited by the mdMinDate.
28375 * @type {Date}
28376 */
28377 this.firstRenderableDate = null;
28378
28379 /**
28380 * The last date that can be rendered by the calendar. The default comes
28381 * from the mdDateLocale provider and is limited by the maxDate.
28382 * @type {Date}
28383 */
28384 this.lastRenderableDate = null;
28385
28386 /**
28387 * Used to toggle initialize the root element in the next digest.
28388 * @type {Boolean}
28389 */
28390 this.isInitialized = false;
28391
28392 /**
28393 * Cache for the width of the element without a scrollbar. Used to hide the scrollbar later on
28394 * and to avoid extra reflows when switching between views.
28395 * @type {Number}
28396 */
28397 this.width = 0;
28398
28399 /**
28400 * Caches the width of the scrollbar in order to be used when hiding it and to avoid extra reflows.
28401 * @type {Number}
28402 */
28403 this.scrollbarWidth = 0;
28404
28405 // Unless the user specifies so, the calendar should not be a tab stop.
28406 // This is necessary because ngAria might add a tabindex to anything with an ng-model
28407 // (based on whether or not the user has turned that particular feature on/off).
28408 if (!$attrs.tabindex) {
28409 $element.attr('tabindex', '-1');
28410 }
28411
28412 var boundKeyHandler = angular.bind(this, this.handleKeyEvent);
28413
28414
28415
28416 // If use the md-calendar directly in the body without datepicker,
28417 // handleKeyEvent will disable other inputs on the page.
28418 // So only apply the handleKeyEvent on the body when the md-calendar inside datepicker,
28419 // otherwise apply on the calendar element only.
28420
28421 var handleKeyElement;
28422 if ($element.parent().hasClass('md-datepicker-calendar')) {
28423 handleKeyElement = angular.element(document.body);
28424 } else {
28425 handleKeyElement = $element;
28426 }
28427
28428 // Bind the keydown handler to the body, in order to handle cases where the focused
28429 // element gets removed from the DOM and stops propagating click events.
28430 handleKeyElement.on('keydown', boundKeyHandler);
28431
28432 $scope.$on('$destroy', function() {
28433 handleKeyElement.off('keydown', boundKeyHandler);
28434 });
28435
28436 // For Angular 1.4 and older, where there are no lifecycle hooks but bindings are pre-assigned,
28437 // manually call the $onInit hook.
28438 if (angular.version.major === 1 && angular.version.minor <= 4) {
28439 this.$onInit();
28440 }
28441
28442 }
28443
28444 /**
28445 * Angular Lifecycle hook for newer Angular versions.
28446 * Bindings are not guaranteed to have been assigned in the controller, but they are in the $onInit hook.
28447 */
28448 CalendarCtrl.prototype.$onInit = function() {
28449
28450 /**
28451 * The currently visible calendar view. Note the prefix on the scope value,
28452 * which is necessary, because the datepicker seems to reset the real one value if the
28453 * calendar is open, but the value on the datepicker's scope is empty.
28454 * @type {String}
28455 */
28456 this.currentView = this._currentView || 'month';
28457
28458 var dateLocale = this.$mdDateLocale;
28459
28460 if (this.minDate && this.minDate > dateLocale.firstRenderableDate) {
28461 this.firstRenderableDate = this.minDate;
28462 } else {
28463 this.firstRenderableDate = dateLocale.firstRenderableDate;
28464 }
28465
28466 if (this.maxDate && this.maxDate < dateLocale.lastRenderableDate) {
28467 this.lastRenderableDate = this.maxDate;
28468 } else {
28469 this.lastRenderableDate = dateLocale.lastRenderableDate;
28470 }
28471 };
28472
28473 /**
28474 * Sets up the controller's reference to ngModelController.
28475 * @param {!angular.NgModelController} ngModelCtrl
28476 */
28477 CalendarCtrl.prototype.configureNgModel = function(ngModelCtrl) {
28478 var self = this;
28479
28480 self.ngModelCtrl = ngModelCtrl;
28481
28482 self.$mdUtil.nextTick(function() {
28483 self.isInitialized = true;
28484 });
28485
28486 ngModelCtrl.$render = function() {
28487 var value = this.$viewValue;
28488
28489 // Notify the child scopes of any changes.
28490 self.$scope.$broadcast('md-calendar-parent-changed', value);
28491
28492 // Set up the selectedDate if it hasn't been already.
28493 if (!self.selectedDate) {
28494 self.selectedDate = value;
28495 }
28496
28497 // Also set up the displayDate.
28498 if (!self.displayDate) {
28499 self.displayDate = self.selectedDate || self.today;
28500 }
28501 };
28502 };
28503
28504 /**
28505 * Sets the ng-model value for the calendar and emits a change event.
28506 * @param {Date} date
28507 */
28508 CalendarCtrl.prototype.setNgModelValue = function(date) {
28509 var value = this.dateUtil.createDateAtMidnight(date);
28510 this.focus(value);
28511 this.$scope.$emit('md-calendar-change', value);
28512 this.ngModelCtrl.$setViewValue(value);
28513 this.ngModelCtrl.$render();
28514 return value;
28515 };
28516
28517 /**
28518 * Sets the current view that should be visible in the calendar
28519 * @param {string} newView View name to be set.
28520 * @param {number|Date} time Date object or a timestamp for the new display date.
28521 */
28522 CalendarCtrl.prototype.setCurrentView = function(newView, time) {
28523 var self = this;
28524
28525 self.$mdUtil.nextTick(function() {
28526 self.currentView = newView;
28527
28528 if (time) {
28529 self.displayDate = angular.isDate(time) ? time : new Date(time);
28530 }
28531 });
28532 };
28533
28534 /**
28535 * Focus the cell corresponding to the given date.
28536 * @param {Date} date The date to be focused.
28537 */
28538 CalendarCtrl.prototype.focus = function(date) {
28539 if (this.dateUtil.isValidDate(date)) {
28540 var previousFocus = this.$element[0].querySelector('.md-focus');
28541 if (previousFocus) {
28542 previousFocus.classList.remove(this.FOCUSED_DATE_CLASS);
28543 }
28544
28545 var cellId = this.getDateId(date, this.currentView);
28546 var cell = document.getElementById(cellId);
28547 if (cell) {
28548 cell.classList.add(this.FOCUSED_DATE_CLASS);
28549 cell.focus();
28550 this.displayDate = date;
28551 }
28552 } else {
28553 var rootElement = this.$element[0].querySelector('[ng-switch]');
28554
28555 if (rootElement) {
28556 rootElement.focus();
28557 }
28558 }
28559 };
28560
28561 /**
28562 * Normalizes the key event into an action name. The action will be broadcast
28563 * to the child controllers.
28564 * @param {KeyboardEvent} event
28565 * @returns {String} The action that should be taken, or null if the key
28566 * does not match a calendar shortcut.
28567 */
28568 CalendarCtrl.prototype.getActionFromKeyEvent = function(event) {
28569 var keyCode = this.keyCode;
28570
28571 switch (event.which) {
28572 case keyCode.ENTER: return 'select';
28573
28574 case keyCode.RIGHT_ARROW: return 'move-right';
28575 case keyCode.LEFT_ARROW: return 'move-left';
28576
28577 // TODO(crisbeto): Might want to reconsider using metaKey, because it maps
28578 // to the "Windows" key on PC, which opens the start menu or resizes the browser.
28579 case keyCode.DOWN_ARROW: return event.metaKey ? 'move-page-down' : 'move-row-down';
28580 case keyCode.UP_ARROW: return event.metaKey ? 'move-page-up' : 'move-row-up';
28581
28582 case keyCode.PAGE_DOWN: return 'move-page-down';
28583 case keyCode.PAGE_UP: return 'move-page-up';
28584
28585 case keyCode.HOME: return 'start';
28586 case keyCode.END: return 'end';
28587
28588 default: return null;
28589 }
28590 };
28591
28592 /**
28593 * Handles a key event in the calendar with the appropriate action. The action will either
28594 * be to select the focused date or to navigate to focus a new date.
28595 * @param {KeyboardEvent} event
28596 */
28597 CalendarCtrl.prototype.handleKeyEvent = function(event) {
28598 var self = this;
28599
28600 this.$scope.$apply(function() {
28601 // Capture escape and emit back up so that a wrapping component
28602 // (such as a date-picker) can decide to close.
28603 if (event.which == self.keyCode.ESCAPE || event.which == self.keyCode.TAB) {
28604 self.$scope.$emit('md-calendar-close');
28605
28606 if (event.which == self.keyCode.TAB) {
28607 event.preventDefault();
28608 }
28609
28610 return;
28611 }
28612
28613 // Broadcast the action that any child controllers should take.
28614 var action = self.getActionFromKeyEvent(event);
28615 if (action) {
28616 event.preventDefault();
28617 event.stopPropagation();
28618 self.$scope.$broadcast('md-calendar-parent-action', action);
28619 }
28620 });
28621 };
28622
28623 /**
28624 * Hides the vertical scrollbar on the calendar scroller of a child controller by
28625 * setting the width on the calendar scroller and the `overflow: hidden` wrapper
28626 * around the scroller, and then setting a padding-right on the scroller equal
28627 * to the width of the browser's scrollbar.
28628 *
28629 * This will cause a reflow.
28630 *
28631 * @param {object} childCtrl The child controller whose scrollbar should be hidden.
28632 */
28633 CalendarCtrl.prototype.hideVerticalScrollbar = function(childCtrl) {
28634 var self = this;
28635 var element = childCtrl.$element[0];
28636 var scrollMask = element.querySelector('.md-calendar-scroll-mask');
28637
28638 if (self.width > 0) {
28639 setWidth();
28640 } else {
28641 self.$$rAF(function() {
28642 var scroller = childCtrl.calendarScroller;
28643
28644 self.scrollbarWidth = scroller.offsetWidth - scroller.clientWidth;
28645 self.width = element.querySelector('table').offsetWidth;
28646 setWidth();
28647 });
28648 }
28649
28650 function setWidth() {
28651 var width = self.width || FALLBACK_WIDTH;
28652 var scrollbarWidth = self.scrollbarWidth;
28653 var scroller = childCtrl.calendarScroller;
28654
28655 scrollMask.style.width = width + 'px';
28656 scroller.style.width = (width + scrollbarWidth) + 'px';
28657 scroller.style.paddingRight = scrollbarWidth + 'px';
28658 }
28659 };
28660
28661 /**
28662 * Gets an identifier for a date unique to the calendar instance for internal
28663 * purposes. Not to be displayed.
28664 * @param {Date} date The date for which the id is being generated
28665 * @param {string} namespace Namespace for the id. (month, year etc.)
28666 * @returns {string}
28667 */
28668 CalendarCtrl.prototype.getDateId = function(date, namespace) {
28669 if (!namespace) {
28670 throw new Error('A namespace for the date id has to be specified.');
28671 }
28672
28673 return [
28674 'md',
28675 this.id,
28676 namespace,
28677 date.getFullYear(),
28678 date.getMonth(),
28679 date.getDate()
28680 ].join('-');
28681 };
28682
28683 /**
28684 * Util to trigger an extra digest on a parent scope, in order to to ensure that
28685 * any child virtual repeaters have updated. This is necessary, because the virtual
28686 * repeater doesn't update the $index the first time around since the content isn't
28687 * in place yet. The case, in which this is an issue, is when the repeater has less
28688 * than a page of content (e.g. a month or year view has a min or max date).
28689 */
28690 CalendarCtrl.prototype.updateVirtualRepeat = function() {
28691 var scope = this.$scope;
28692 var virtualRepeatResizeListener = scope.$on('$md-resize-enable', function() {
28693 if (!scope.$$phase) {
28694 scope.$apply();
28695 }
28696
28697 virtualRepeatResizeListener();
28698 });
28699 };
28700})();
28701
28702})();
28703(function(){
28704"use strict";
28705
28706(function() {
28707 'use strict';
28708
28709 CalendarMonthCtrl.$inject = ["$element", "$scope", "$animate", "$q", "$$mdDateUtil", "$mdDateLocale"];
28710 angular.module('material.components.datepicker')
28711 .directive('mdCalendarMonth', calendarDirective);
28712
28713 /**
28714 * Height of one calendar month tbody. This must be made known to the virtual-repeat and is
28715 * subsequently used for scrolling to specific months.
28716 */
28717 var TBODY_HEIGHT = 265;
28718
28719 /**
28720 * Height of a calendar month with a single row. This is needed to calculate the offset for
28721 * rendering an extra month in virtual-repeat that only contains one row.
28722 */
28723 var TBODY_SINGLE_ROW_HEIGHT = 45;
28724
28725 /** Private directive that represents a list of months inside the calendar. */
28726 function calendarDirective() {
28727 return {
28728 template:
28729 '<table aria-hidden="true" class="md-calendar-day-header"><thead></thead></table>' +
28730 '<div class="md-calendar-scroll-mask">' +
28731 '<md-virtual-repeat-container class="md-calendar-scroll-container" ' +
28732 'md-offset-size="' + (TBODY_SINGLE_ROW_HEIGHT - TBODY_HEIGHT) + '">' +
28733 '<table role="grid" tabindex="0" class="md-calendar" aria-readonly="true">' +
28734 '<tbody ' +
28735 'md-calendar-month-body ' +
28736 'role="rowgroup" ' +
28737 'md-virtual-repeat="i in monthCtrl.items" ' +
28738 'md-month-offset="$index" ' +
28739 'class="md-calendar-month" ' +
28740 'md-start-index="monthCtrl.getSelectedMonthIndex()" ' +
28741 'md-item-size="' + TBODY_HEIGHT + '">' +
28742
28743 // The <tr> ensures that the <tbody> will always have the
28744 // proper height, even if it's empty. If it's content is
28745 // compiled, the <tr> will be overwritten.
28746 '<tr aria-hidden="true" style="height:' + TBODY_HEIGHT + 'px;"></tr>' +
28747 '</tbody>' +
28748 '</table>' +
28749 '</md-virtual-repeat-container>' +
28750 '</div>',
28751 require: ['^^mdCalendar', 'mdCalendarMonth'],
28752 controller: CalendarMonthCtrl,
28753 controllerAs: 'monthCtrl',
28754 bindToController: true,
28755 link: function(scope, element, attrs, controllers) {
28756 var calendarCtrl = controllers[0];
28757 var monthCtrl = controllers[1];
28758 monthCtrl.initialize(calendarCtrl);
28759 }
28760 };
28761 }
28762
28763 /**
28764 * Controller for the calendar month component.
28765 * @ngInject @constructor
28766 */
28767 function CalendarMonthCtrl($element, $scope, $animate, $q,
28768 $$mdDateUtil, $mdDateLocale) {
28769
28770 /** @final {!angular.JQLite} */
28771 this.$element = $element;
28772
28773 /** @final {!angular.Scope} */
28774 this.$scope = $scope;
28775
28776 /** @final {!angular.$animate} */
28777 this.$animate = $animate;
28778
28779 /** @final {!angular.$q} */
28780 this.$q = $q;
28781
28782 /** @final */
28783 this.dateUtil = $$mdDateUtil;
28784
28785 /** @final */
28786 this.dateLocale = $mdDateLocale;
28787
28788 /** @final {HTMLElement} */
28789 this.calendarScroller = $element[0].querySelector('.md-virtual-repeat-scroller');
28790
28791 /** @type {boolean} */
28792 this.isInitialized = false;
28793
28794 /** @type {boolean} */
28795 this.isMonthTransitionInProgress = false;
28796
28797 var self = this;
28798
28799 /**
28800 * Handles a click event on a date cell.
28801 * Created here so that every cell can use the same function instance.
28802 * @this {HTMLTableCellElement} The cell that was clicked.
28803 */
28804 this.cellClickHandler = function() {
28805 var timestamp = $$mdDateUtil.getTimestampFromNode(this);
28806 self.$scope.$apply(function() {
28807 self.calendarCtrl.setNgModelValue(timestamp);
28808 });
28809 };
28810
28811 /**
28812 * Handles click events on the month headers. Switches
28813 * the calendar to the year view.
28814 * @this {HTMLTableCellElement} The cell that was clicked.
28815 */
28816 this.headerClickHandler = function() {
28817 self.calendarCtrl.setCurrentView('year', $$mdDateUtil.getTimestampFromNode(this));
28818 };
28819 }
28820
28821 /*** Initialization ***/
28822
28823 /**
28824 * Initialize the controller by saving a reference to the calendar and
28825 * setting up the object that will be iterated by the virtual repeater.
28826 */
28827 CalendarMonthCtrl.prototype.initialize = function(calendarCtrl) {
28828 /**
28829 * Dummy array-like object for virtual-repeat to iterate over. The length is the total
28830 * number of months that can be viewed. We add 2 months: one to include the current month
28831 * and one for the last dummy month.
28832 *
28833 * This is shorter than ideal because of a (potential) Firefox bug
28834 * https://bugzilla.mozilla.org/show_bug.cgi?id=1181658.
28835 */
28836
28837 this.items = {
28838 length: this.dateUtil.getMonthDistance(
28839 calendarCtrl.firstRenderableDate,
28840 calendarCtrl.lastRenderableDate
28841 ) + 2
28842 };
28843
28844 this.calendarCtrl = calendarCtrl;
28845 this.attachScopeListeners();
28846 calendarCtrl.updateVirtualRepeat();
28847
28848 // Fire the initial render, since we might have missed it the first time it fired.
28849 calendarCtrl.ngModelCtrl && calendarCtrl.ngModelCtrl.$render();
28850 };
28851
28852 /**
28853 * Gets the "index" of the currently selected date as it would be in the virtual-repeat.
28854 * @returns {number}
28855 */
28856 CalendarMonthCtrl.prototype.getSelectedMonthIndex = function() {
28857 var calendarCtrl = this.calendarCtrl;
28858
28859 return this.dateUtil.getMonthDistance(
28860 calendarCtrl.firstRenderableDate,
28861 calendarCtrl.displayDate || calendarCtrl.selectedDate || calendarCtrl.today
28862 );
28863 };
28864
28865 /**
28866 * Change the selected date in the calendar (ngModel value has already been changed).
28867 * @param {Date} date
28868 */
28869 CalendarMonthCtrl.prototype.changeSelectedDate = function(date) {
28870 var self = this;
28871 var calendarCtrl = self.calendarCtrl;
28872 var previousSelectedDate = calendarCtrl.selectedDate;
28873 calendarCtrl.selectedDate = date;
28874
28875 this.changeDisplayDate(date).then(function() {
28876 var selectedDateClass = calendarCtrl.SELECTED_DATE_CLASS;
28877 var namespace = 'month';
28878
28879 // Remove the selected class from the previously selected date, if any.
28880 if (previousSelectedDate) {
28881 var prevDateCell = document.getElementById(calendarCtrl.getDateId(previousSelectedDate, namespace));
28882 if (prevDateCell) {
28883 prevDateCell.classList.remove(selectedDateClass);
28884 prevDateCell.setAttribute('aria-selected', 'false');
28885 }
28886 }
28887
28888 // Apply the select class to the new selected date if it is set.
28889 if (date) {
28890 var dateCell = document.getElementById(calendarCtrl.getDateId(date, namespace));
28891 if (dateCell) {
28892 dateCell.classList.add(selectedDateClass);
28893 dateCell.setAttribute('aria-selected', 'true');
28894 }
28895 }
28896 });
28897 };
28898
28899 /**
28900 * Change the date that is being shown in the calendar. If the given date is in a different
28901 * month, the displayed month will be transitioned.
28902 * @param {Date} date
28903 */
28904 CalendarMonthCtrl.prototype.changeDisplayDate = function(date) {
28905 // Initialization is deferred until this function is called because we want to reflect
28906 // the starting value of ngModel.
28907 if (!this.isInitialized) {
28908 this.buildWeekHeader();
28909 this.calendarCtrl.hideVerticalScrollbar(this);
28910 this.isInitialized = true;
28911 return this.$q.when();
28912 }
28913
28914 // If trying to show an invalid date or a transition is in progress, do nothing.
28915 if (!this.dateUtil.isValidDate(date) || this.isMonthTransitionInProgress) {
28916 return this.$q.when();
28917 }
28918
28919 this.isMonthTransitionInProgress = true;
28920 var animationPromise = this.animateDateChange(date);
28921
28922 this.calendarCtrl.displayDate = date;
28923
28924 var self = this;
28925 animationPromise.then(function() {
28926 self.isMonthTransitionInProgress = false;
28927 });
28928
28929 return animationPromise;
28930 };
28931
28932 /**
28933 * Animates the transition from the calendar's current month to the given month.
28934 * @param {Date} date
28935 * @returns {angular.$q.Promise} The animation promise.
28936 */
28937 CalendarMonthCtrl.prototype.animateDateChange = function(date) {
28938 if (this.dateUtil.isValidDate(date)) {
28939 var monthDistance = this.dateUtil.getMonthDistance(this.calendarCtrl.firstRenderableDate, date);
28940 this.calendarScroller.scrollTop = monthDistance * TBODY_HEIGHT;
28941 }
28942
28943 return this.$q.when();
28944 };
28945
28946 /**
28947 * Builds and appends a day-of-the-week header to the calendar.
28948 * This should only need to be called once during initialization.
28949 */
28950 CalendarMonthCtrl.prototype.buildWeekHeader = function() {
28951 var firstDayOfWeek = this.dateLocale.firstDayOfWeek;
28952 var shortDays = this.dateLocale.shortDays;
28953
28954 var row = document.createElement('tr');
28955 for (var i = 0; i < 7; i++) {
28956 var th = document.createElement('th');
28957 th.textContent = shortDays[(i + firstDayOfWeek) % 7];
28958 row.appendChild(th);
28959 }
28960
28961 this.$element.find('thead').append(row);
28962 };
28963
28964 /**
28965 * Attaches listeners for the scope events that are broadcast by the calendar.
28966 */
28967 CalendarMonthCtrl.prototype.attachScopeListeners = function() {
28968 var self = this;
28969
28970 self.$scope.$on('md-calendar-parent-changed', function(event, value) {
28971 self.changeSelectedDate(value);
28972 });
28973
28974 self.$scope.$on('md-calendar-parent-action', angular.bind(this, this.handleKeyEvent));
28975 };
28976
28977 /**
28978 * Handles the month-specific keyboard interactions.
28979 * @param {Object} event Scope event object passed by the calendar.
28980 * @param {String} action Action, corresponding to the key that was pressed.
28981 */
28982 CalendarMonthCtrl.prototype.handleKeyEvent = function(event, action) {
28983 var calendarCtrl = this.calendarCtrl;
28984 var displayDate = calendarCtrl.displayDate;
28985
28986 if (action === 'select') {
28987 calendarCtrl.setNgModelValue(displayDate);
28988 } else {
28989 var date = null;
28990 var dateUtil = this.dateUtil;
28991
28992 switch (action) {
28993 case 'move-right': date = dateUtil.incrementDays(displayDate, 1); break;
28994 case 'move-left': date = dateUtil.incrementDays(displayDate, -1); break;
28995
28996 case 'move-page-down': date = dateUtil.incrementMonths(displayDate, 1); break;
28997 case 'move-page-up': date = dateUtil.incrementMonths(displayDate, -1); break;
28998
28999 case 'move-row-down': date = dateUtil.incrementDays(displayDate, 7); break;
29000 case 'move-row-up': date = dateUtil.incrementDays(displayDate, -7); break;
29001
29002 case 'start': date = dateUtil.getFirstDateOfMonth(displayDate); break;
29003 case 'end': date = dateUtil.getLastDateOfMonth(displayDate); break;
29004 }
29005
29006 if (date) {
29007 date = this.dateUtil.clampDate(date, calendarCtrl.minDate, calendarCtrl.maxDate);
29008
29009 this.changeDisplayDate(date).then(function() {
29010 calendarCtrl.focus(date);
29011 });
29012 }
29013 }
29014 };
29015})();
29016
29017})();
29018(function(){
29019"use strict";
29020
29021(function() {
29022 'use strict';
29023
29024 mdCalendarMonthBodyDirective.$inject = ["$compile", "$$mdSvgRegistry"];
29025 CalendarMonthBodyCtrl.$inject = ["$element", "$$mdDateUtil", "$mdDateLocale"];
29026 angular.module('material.components.datepicker')
29027 .directive('mdCalendarMonthBody', mdCalendarMonthBodyDirective);
29028
29029 /**
29030 * Private directive consumed by md-calendar-month. Having this directive lets the calender use
29031 * md-virtual-repeat and also cleanly separates the month DOM construction functions from
29032 * the rest of the calendar controller logic.
29033 * @ngInject
29034 */
29035 function mdCalendarMonthBodyDirective($compile, $$mdSvgRegistry) {
29036 var ARROW_ICON = $compile('<md-icon md-svg-src="' +
29037 $$mdSvgRegistry.mdTabsArrow + '"></md-icon>')({})[0];
29038
29039 return {
29040 require: ['^^mdCalendar', '^^mdCalendarMonth', 'mdCalendarMonthBody'],
29041 scope: { offset: '=mdMonthOffset' },
29042 controller: CalendarMonthBodyCtrl,
29043 controllerAs: 'mdMonthBodyCtrl',
29044 bindToController: true,
29045 link: function(scope, element, attrs, controllers) {
29046 var calendarCtrl = controllers[0];
29047 var monthCtrl = controllers[1];
29048 var monthBodyCtrl = controllers[2];
29049
29050 monthBodyCtrl.calendarCtrl = calendarCtrl;
29051 monthBodyCtrl.monthCtrl = monthCtrl;
29052 monthBodyCtrl.arrowIcon = ARROW_ICON.cloneNode(true);
29053
29054 // The virtual-repeat re-uses the same DOM elements, so there are only a limited number
29055 // of repeated items that are linked, and then those elements have their bindings updated.
29056 // Since the months are not generated by bindings, we simply regenerate the entire thing
29057 // when the binding (offset) changes.
29058 scope.$watch(function() { return monthBodyCtrl.offset; }, function(offset) {
29059 if (angular.isNumber(offset)) {
29060 monthBodyCtrl.generateContent();
29061 }
29062 });
29063 }
29064 };
29065 }
29066
29067 /**
29068 * Controller for a single calendar month.
29069 * @ngInject @constructor
29070 */
29071 function CalendarMonthBodyCtrl($element, $$mdDateUtil, $mdDateLocale) {
29072 /** @final {!angular.JQLite} */
29073 this.$element = $element;
29074
29075 /** @final */
29076 this.dateUtil = $$mdDateUtil;
29077
29078 /** @final */
29079 this.dateLocale = $mdDateLocale;
29080
29081 /** @type {Object} Reference to the month view. */
29082 this.monthCtrl = null;
29083
29084 /** @type {Object} Reference to the calendar. */
29085 this.calendarCtrl = null;
29086
29087 /**
29088 * Number of months from the start of the month "items" that the currently rendered month
29089 * occurs. Set via angular data binding.
29090 * @type {number}
29091 */
29092 this.offset = null;
29093
29094 /**
29095 * Date cell to focus after appending the month to the document.
29096 * @type {HTMLElement}
29097 */
29098 this.focusAfterAppend = null;
29099 }
29100
29101 /** Generate and append the content for this month to the directive element. */
29102 CalendarMonthBodyCtrl.prototype.generateContent = function() {
29103 var date = this.dateUtil.incrementMonths(this.calendarCtrl.firstRenderableDate, this.offset);
29104
29105 this.$element
29106 .empty()
29107 .append(this.buildCalendarForMonth(date));
29108
29109 if (this.focusAfterAppend) {
29110 this.focusAfterAppend.classList.add(this.calendarCtrl.FOCUSED_DATE_CLASS);
29111 this.focusAfterAppend.focus();
29112 this.focusAfterAppend = null;
29113 }
29114 };
29115
29116 /**
29117 * Creates a single cell to contain a date in the calendar with all appropriate
29118 * attributes and classes added. If a date is given, the cell content will be set
29119 * based on the date.
29120 * @param {Date=} opt_date
29121 * @returns {HTMLElement}
29122 */
29123 CalendarMonthBodyCtrl.prototype.buildDateCell = function(opt_date) {
29124 var monthCtrl = this.monthCtrl;
29125 var calendarCtrl = this.calendarCtrl;
29126
29127 // TODO(jelbourn): cloneNode is likely a faster way of doing this.
29128 var cell = document.createElement('td');
29129 cell.tabIndex = -1;
29130 cell.classList.add('md-calendar-date');
29131 cell.setAttribute('role', 'gridcell');
29132
29133 if (opt_date) {
29134 cell.setAttribute('tabindex', '-1');
29135 cell.setAttribute('aria-label', this.dateLocale.longDateFormatter(opt_date));
29136 cell.id = calendarCtrl.getDateId(opt_date, 'month');
29137
29138 // Use `data-timestamp` attribute because IE10 does not support the `dataset` property.
29139 cell.setAttribute('data-timestamp', opt_date.getTime());
29140
29141 // TODO(jelourn): Doing these comparisons for class addition during generation might be slow.
29142 // It may be better to finish the construction and then query the node and add the class.
29143 if (this.dateUtil.isSameDay(opt_date, calendarCtrl.today)) {
29144 cell.classList.add(calendarCtrl.TODAY_CLASS);
29145 }
29146
29147 if (this.dateUtil.isValidDate(calendarCtrl.selectedDate) &&
29148 this.dateUtil.isSameDay(opt_date, calendarCtrl.selectedDate)) {
29149 cell.classList.add(calendarCtrl.SELECTED_DATE_CLASS);
29150 cell.setAttribute('aria-selected', 'true');
29151 }
29152
29153 var cellText = this.dateLocale.dates[opt_date.getDate()];
29154
29155 if (this.isDateEnabled(opt_date)) {
29156 // Add a indicator for select, hover, and focus states.
29157 var selectionIndicator = document.createElement('span');
29158 selectionIndicator.classList.add('md-calendar-date-selection-indicator');
29159 selectionIndicator.textContent = cellText;
29160 cell.appendChild(selectionIndicator);
29161 cell.addEventListener('click', monthCtrl.cellClickHandler);
29162
29163 if (calendarCtrl.displayDate && this.dateUtil.isSameDay(opt_date, calendarCtrl.displayDate)) {
29164 this.focusAfterAppend = cell;
29165 }
29166 } else {
29167 cell.classList.add('md-calendar-date-disabled');
29168 cell.textContent = cellText;
29169 }
29170 }
29171
29172 return cell;
29173 };
29174
29175 /**
29176 * Check whether date is in range and enabled
29177 * @param {Date=} opt_date
29178 * @return {boolean} Whether the date is enabled.
29179 */
29180 CalendarMonthBodyCtrl.prototype.isDateEnabled = function(opt_date) {
29181 return this.dateUtil.isDateWithinRange(opt_date,
29182 this.calendarCtrl.minDate, this.calendarCtrl.maxDate) &&
29183 (!angular.isFunction(this.calendarCtrl.dateFilter)
29184 || this.calendarCtrl.dateFilter(opt_date));
29185 };
29186
29187 /**
29188 * Builds a `tr` element for the calendar grid.
29189 * @param rowNumber The week number within the month.
29190 * @returns {HTMLElement}
29191 */
29192 CalendarMonthBodyCtrl.prototype.buildDateRow = function(rowNumber) {
29193 var row = document.createElement('tr');
29194 row.setAttribute('role', 'row');
29195
29196 // Because of an NVDA bug (with Firefox), the row needs an aria-label in order
29197 // to prevent the entire row being read aloud when the user moves between rows.
29198 // See http://community.nvda-project.org/ticket/4643.
29199 row.setAttribute('aria-label', this.dateLocale.weekNumberFormatter(rowNumber));
29200
29201 return row;
29202 };
29203
29204 /**
29205 * Builds the <tbody> content for the given date's month.
29206 * @param {Date=} opt_dateInMonth
29207 * @returns {DocumentFragment} A document fragment containing the <tr> elements.
29208 */
29209 CalendarMonthBodyCtrl.prototype.buildCalendarForMonth = function(opt_dateInMonth) {
29210 var date = this.dateUtil.isValidDate(opt_dateInMonth) ? opt_dateInMonth : new Date();
29211
29212 var firstDayOfMonth = this.dateUtil.getFirstDateOfMonth(date);
29213 var firstDayOfTheWeek = this.getLocaleDay_(firstDayOfMonth);
29214 var numberOfDaysInMonth = this.dateUtil.getNumberOfDaysInMonth(date);
29215
29216 // Store rows for the month in a document fragment so that we can append them all at once.
29217 var monthBody = document.createDocumentFragment();
29218
29219 var rowNumber = 1;
29220 var row = this.buildDateRow(rowNumber);
29221 monthBody.appendChild(row);
29222
29223 // If this is the final month in the list of items, only the first week should render,
29224 // so we should return immediately after the first row is complete and has been
29225 // attached to the body.
29226 var isFinalMonth = this.offset === this.monthCtrl.items.length - 1;
29227
29228 // Add a label for the month. If the month starts on a Sun/Mon/Tues, the month label
29229 // goes on a row above the first of the month. Otherwise, the month label takes up the first
29230 // two cells of the first row.
29231 var blankCellOffset = 0;
29232 var monthLabelCell = document.createElement('td');
29233 var monthLabelCellContent = document.createElement('span');
29234
29235 monthLabelCellContent.textContent = this.dateLocale.monthHeaderFormatter(date);
29236 monthLabelCell.appendChild(monthLabelCellContent);
29237 monthLabelCell.classList.add('md-calendar-month-label');
29238 // If the entire month is after the max date, render the label as a disabled state.
29239 if (this.calendarCtrl.maxDate && firstDayOfMonth > this.calendarCtrl.maxDate) {
29240 monthLabelCell.classList.add('md-calendar-month-label-disabled');
29241 } else {
29242 monthLabelCell.addEventListener('click', this.monthCtrl.headerClickHandler);
29243 monthLabelCell.setAttribute('data-timestamp', firstDayOfMonth.getTime());
29244 monthLabelCell.setAttribute('aria-label', this.dateLocale.monthFormatter(date));
29245 monthLabelCell.appendChild(this.arrowIcon.cloneNode(true));
29246 }
29247
29248 if (firstDayOfTheWeek <= 2) {
29249 monthLabelCell.setAttribute('colspan', '7');
29250
29251 var monthLabelRow = this.buildDateRow();
29252 monthLabelRow.appendChild(monthLabelCell);
29253 monthBody.insertBefore(monthLabelRow, row);
29254
29255 if (isFinalMonth) {
29256 return monthBody;
29257 }
29258 } else {
29259 blankCellOffset = 3;
29260 monthLabelCell.setAttribute('colspan', '3');
29261 row.appendChild(monthLabelCell);
29262 }
29263
29264 // Add a blank cell for each day of the week that occurs before the first of the month.
29265 // For example, if the first day of the month is a Tuesday, add blank cells for Sun and Mon.
29266 // The blankCellOffset is needed in cases where the first N cells are used by the month label.
29267 for (var i = blankCellOffset; i < firstDayOfTheWeek; i++) {
29268 row.appendChild(this.buildDateCell());
29269 }
29270
29271 // Add a cell for each day of the month, keeping track of the day of the week so that
29272 // we know when to start a new row.
29273 var dayOfWeek = firstDayOfTheWeek;
29274 var iterationDate = firstDayOfMonth;
29275 for (var d = 1; d <= numberOfDaysInMonth; d++) {
29276 // If we've reached the end of the week, start a new row.
29277 if (dayOfWeek === 7) {
29278 // We've finished the first row, so we're done if this is the final month.
29279 if (isFinalMonth) {
29280 return monthBody;
29281 }
29282 dayOfWeek = 0;
29283 rowNumber++;
29284 row = this.buildDateRow(rowNumber);
29285 monthBody.appendChild(row);
29286 }
29287
29288 iterationDate.setDate(d);
29289 var cell = this.buildDateCell(iterationDate);
29290 row.appendChild(cell);
29291
29292 dayOfWeek++;
29293 }
29294
29295 // Ensure that the last row of the month has 7 cells.
29296 while (row.childNodes.length < 7) {
29297 row.appendChild(this.buildDateCell());
29298 }
29299
29300 // Ensure that all months have 6 rows. This is necessary for now because the virtual-repeat
29301 // requires that all items have exactly the same height.
29302 while (monthBody.childNodes.length < 6) {
29303 var whitespaceRow = this.buildDateRow();
29304 for (var j = 0; j < 7; j++) {
29305 whitespaceRow.appendChild(this.buildDateCell());
29306 }
29307 monthBody.appendChild(whitespaceRow);
29308 }
29309
29310 return monthBody;
29311 };
29312
29313 /**
29314 * Gets the day-of-the-week index for a date for the current locale.
29315 * @private
29316 * @param {Date} date
29317 * @returns {number} The column index of the date in the calendar.
29318 */
29319 CalendarMonthBodyCtrl.prototype.getLocaleDay_ = function(date) {
29320 return (date.getDay() + (7 - this.dateLocale.firstDayOfWeek)) % 7;
29321 };
29322})();
29323
29324})();
29325(function(){
29326"use strict";
29327
29328(function() {
29329 'use strict';
29330
29331 CalendarYearCtrl.$inject = ["$element", "$scope", "$animate", "$q", "$$mdDateUtil"];
29332 angular.module('material.components.datepicker')
29333 .directive('mdCalendarYear', calendarDirective);
29334
29335 /**
29336 * Height of one calendar year tbody. This must be made known to the virtual-repeat and is
29337 * subsequently used for scrolling to specific years.
29338 */
29339 var TBODY_HEIGHT = 88;
29340
29341 /** Private component, representing a list of years in the calendar. */
29342 function calendarDirective() {
29343 return {
29344 template:
29345 '<div class="md-calendar-scroll-mask">' +
29346 '<md-virtual-repeat-container class="md-calendar-scroll-container">' +
29347 '<table role="grid" tabindex="0" class="md-calendar" aria-readonly="true">' +
29348 '<tbody ' +
29349 'md-calendar-year-body ' +
29350 'role="rowgroup" ' +
29351 'md-virtual-repeat="i in yearCtrl.items" ' +
29352 'md-year-offset="$index" class="md-calendar-year" ' +
29353 'md-start-index="yearCtrl.getFocusedYearIndex()" ' +
29354 'md-item-size="' + TBODY_HEIGHT + '">' +
29355 // The <tr> ensures that the <tbody> will have the proper
29356 // height, even though it may be empty.
29357 '<tr aria-hidden="true" style="height:' + TBODY_HEIGHT + 'px;"></tr>' +
29358 '</tbody>' +
29359 '</table>' +
29360 '</md-virtual-repeat-container>' +
29361 '</div>',
29362 require: ['^^mdCalendar', 'mdCalendarYear'],
29363 controller: CalendarYearCtrl,
29364 controllerAs: 'yearCtrl',
29365 bindToController: true,
29366 link: function(scope, element, attrs, controllers) {
29367 var calendarCtrl = controllers[0];
29368 var yearCtrl = controllers[1];
29369 yearCtrl.initialize(calendarCtrl);
29370 }
29371 };
29372 }
29373
29374 /**
29375 * Controller for the mdCalendar component.
29376 * @ngInject @constructor
29377 */
29378 function CalendarYearCtrl($element, $scope, $animate, $q, $$mdDateUtil) {
29379
29380 /** @final {!angular.JQLite} */
29381 this.$element = $element;
29382
29383 /** @final {!angular.Scope} */
29384 this.$scope = $scope;
29385
29386 /** @final {!angular.$animate} */
29387 this.$animate = $animate;
29388
29389 /** @final {!angular.$q} */
29390 this.$q = $q;
29391
29392 /** @final */
29393 this.dateUtil = $$mdDateUtil;
29394
29395 /** @final {HTMLElement} */
29396 this.calendarScroller = $element[0].querySelector('.md-virtual-repeat-scroller');
29397
29398 /** @type {boolean} */
29399 this.isInitialized = false;
29400
29401 /** @type {boolean} */
29402 this.isMonthTransitionInProgress = false;
29403
29404 var self = this;
29405
29406 /**
29407 * Handles a click event on a date cell.
29408 * Created here so that every cell can use the same function instance.
29409 * @this {HTMLTableCellElement} The cell that was clicked.
29410 */
29411 this.cellClickHandler = function() {
29412 self.calendarCtrl.setCurrentView('month', $$mdDateUtil.getTimestampFromNode(this));
29413 };
29414 }
29415
29416 /**
29417 * Initialize the controller by saving a reference to the calendar and
29418 * setting up the object that will be iterated by the virtual repeater.
29419 */
29420 CalendarYearCtrl.prototype.initialize = function(calendarCtrl) {
29421 /**
29422 * Dummy array-like object for virtual-repeat to iterate over. The length is the total
29423 * number of years that can be viewed. We add 1 extra in order to include the current year.
29424 */
29425 this.items = {
29426 length: this.dateUtil.getYearDistance(
29427 calendarCtrl.firstRenderableDate,
29428 calendarCtrl.lastRenderableDate
29429 ) + 1
29430 };
29431
29432 this.calendarCtrl = calendarCtrl;
29433 this.attachScopeListeners();
29434 calendarCtrl.updateVirtualRepeat();
29435
29436 // Fire the initial render, since we might have missed it the first time it fired.
29437 calendarCtrl.ngModelCtrl && calendarCtrl.ngModelCtrl.$render();
29438 };
29439
29440 /**
29441 * Gets the "index" of the currently selected date as it would be in the virtual-repeat.
29442 * @returns {number}
29443 */
29444 CalendarYearCtrl.prototype.getFocusedYearIndex = function() {
29445 var calendarCtrl = this.calendarCtrl;
29446
29447 return this.dateUtil.getYearDistance(
29448 calendarCtrl.firstRenderableDate,
29449 calendarCtrl.displayDate || calendarCtrl.selectedDate || calendarCtrl.today
29450 );
29451 };
29452
29453 /**
29454 * Change the date that is highlighted in the calendar.
29455 * @param {Date} date
29456 */
29457 CalendarYearCtrl.prototype.changeDate = function(date) {
29458 // Initialization is deferred until this function is called because we want to reflect
29459 // the starting value of ngModel.
29460 if (!this.isInitialized) {
29461 this.calendarCtrl.hideVerticalScrollbar(this);
29462 this.isInitialized = true;
29463 return this.$q.when();
29464 } else if (this.dateUtil.isValidDate(date) && !this.isMonthTransitionInProgress) {
29465 var self = this;
29466 var animationPromise = this.animateDateChange(date);
29467
29468 self.isMonthTransitionInProgress = true;
29469 self.calendarCtrl.displayDate = date;
29470
29471 return animationPromise.then(function() {
29472 self.isMonthTransitionInProgress = false;
29473 });
29474 }
29475 };
29476
29477 /**
29478 * Animates the transition from the calendar's current month to the given month.
29479 * @param {Date} date
29480 * @returns {angular.$q.Promise} The animation promise.
29481 */
29482 CalendarYearCtrl.prototype.animateDateChange = function(date) {
29483 if (this.dateUtil.isValidDate(date)) {
29484 var monthDistance = this.dateUtil.getYearDistance(this.calendarCtrl.firstRenderableDate, date);
29485 this.calendarScroller.scrollTop = monthDistance * TBODY_HEIGHT;
29486 }
29487
29488 return this.$q.when();
29489 };
29490
29491 /**
29492 * Handles the year-view-specific keyboard interactions.
29493 * @param {Object} event Scope event object passed by the calendar.
29494 * @param {String} action Action, corresponding to the key that was pressed.
29495 */
29496 CalendarYearCtrl.prototype.handleKeyEvent = function(event, action) {
29497 var calendarCtrl = this.calendarCtrl;
29498 var displayDate = calendarCtrl.displayDate;
29499
29500 if (action === 'select') {
29501 this.changeDate(displayDate).then(function() {
29502 calendarCtrl.setCurrentView('month', displayDate);
29503 calendarCtrl.focus(displayDate);
29504 });
29505 } else {
29506 var date = null;
29507 var dateUtil = this.dateUtil;
29508
29509 switch (action) {
29510 case 'move-right': date = dateUtil.incrementMonths(displayDate, 1); break;
29511 case 'move-left': date = dateUtil.incrementMonths(displayDate, -1); break;
29512
29513 case 'move-row-down': date = dateUtil.incrementMonths(displayDate, 6); break;
29514 case 'move-row-up': date = dateUtil.incrementMonths(displayDate, -6); break;
29515 }
29516
29517 if (date) {
29518 var min = calendarCtrl.minDate ? dateUtil.getFirstDateOfMonth(calendarCtrl.minDate) : null;
29519 var max = calendarCtrl.maxDate ? dateUtil.getFirstDateOfMonth(calendarCtrl.maxDate) : null;
29520 date = dateUtil.getFirstDateOfMonth(this.dateUtil.clampDate(date, min, max));
29521
29522 this.changeDate(date).then(function() {
29523 calendarCtrl.focus(date);
29524 });
29525 }
29526 }
29527 };
29528
29529 /**
29530 * Attaches listeners for the scope events that are broadcast by the calendar.
29531 */
29532 CalendarYearCtrl.prototype.attachScopeListeners = function() {
29533 var self = this;
29534
29535 self.$scope.$on('md-calendar-parent-changed', function(event, value) {
29536 self.changeDate(value);
29537 });
29538
29539 self.$scope.$on('md-calendar-parent-action', angular.bind(self, self.handleKeyEvent));
29540 };
29541})();
29542
29543})();
29544(function(){
29545"use strict";
29546
29547(function() {
29548 'use strict';
29549
29550 CalendarYearBodyCtrl.$inject = ["$element", "$$mdDateUtil", "$mdDateLocale"];
29551 angular.module('material.components.datepicker')
29552 .directive('mdCalendarYearBody', mdCalendarYearDirective);
29553
29554 /**
29555 * Private component, consumed by the md-calendar-year, which separates the DOM construction logic
29556 * and allows for the year view to use md-virtual-repeat.
29557 */
29558 function mdCalendarYearDirective() {
29559 return {
29560 require: ['^^mdCalendar', '^^mdCalendarYear', 'mdCalendarYearBody'],
29561 scope: { offset: '=mdYearOffset' },
29562 controller: CalendarYearBodyCtrl,
29563 controllerAs: 'mdYearBodyCtrl',
29564 bindToController: true,
29565 link: function(scope, element, attrs, controllers) {
29566 var calendarCtrl = controllers[0];
29567 var yearCtrl = controllers[1];
29568 var yearBodyCtrl = controllers[2];
29569
29570 yearBodyCtrl.calendarCtrl = calendarCtrl;
29571 yearBodyCtrl.yearCtrl = yearCtrl;
29572
29573 scope.$watch(function() { return yearBodyCtrl.offset; }, function(offset) {
29574 if (angular.isNumber(offset)) {
29575 yearBodyCtrl.generateContent();
29576 }
29577 });
29578 }
29579 };
29580 }
29581
29582 /**
29583 * Controller for a single year.
29584 * @ngInject @constructor
29585 */
29586 function CalendarYearBodyCtrl($element, $$mdDateUtil, $mdDateLocale) {
29587 /** @final {!angular.JQLite} */
29588 this.$element = $element;
29589
29590 /** @final */
29591 this.dateUtil = $$mdDateUtil;
29592
29593 /** @final */
29594 this.dateLocale = $mdDateLocale;
29595
29596 /** @type {Object} Reference to the calendar. */
29597 this.calendarCtrl = null;
29598
29599 /** @type {Object} Reference to the year view. */
29600 this.yearCtrl = null;
29601
29602 /**
29603 * Number of months from the start of the month "items" that the currently rendered month
29604 * occurs. Set via angular data binding.
29605 * @type {number}
29606 */
29607 this.offset = null;
29608
29609 /**
29610 * Date cell to focus after appending the month to the document.
29611 * @type {HTMLElement}
29612 */
29613 this.focusAfterAppend = null;
29614 }
29615
29616 /** Generate and append the content for this year to the directive element. */
29617 CalendarYearBodyCtrl.prototype.generateContent = function() {
29618 var date = this.dateUtil.incrementYears(this.calendarCtrl.firstRenderableDate, this.offset);
29619
29620 this.$element
29621 .empty()
29622 .append(this.buildCalendarForYear(date));
29623
29624 if (this.focusAfterAppend) {
29625 this.focusAfterAppend.classList.add(this.calendarCtrl.FOCUSED_DATE_CLASS);
29626 this.focusAfterAppend.focus();
29627 this.focusAfterAppend = null;
29628 }
29629 };
29630
29631 /**
29632 * Creates a single cell to contain a year in the calendar.
29633 * @param {number} opt_year Four-digit year.
29634 * @param {number} opt_month Zero-indexed month.
29635 * @returns {HTMLElement}
29636 */
29637 CalendarYearBodyCtrl.prototype.buildMonthCell = function(year, month) {
29638 var calendarCtrl = this.calendarCtrl;
29639 var yearCtrl = this.yearCtrl;
29640 var cell = this.buildBlankCell();
29641
29642 // Represent this month/year as a date.
29643 var firstOfMonth = new Date(year, month, 1);
29644 cell.setAttribute('aria-label', this.dateLocale.monthFormatter(firstOfMonth));
29645 cell.id = calendarCtrl.getDateId(firstOfMonth, 'year');
29646
29647 // Use `data-timestamp` attribute because IE10 does not support the `dataset` property.
29648 cell.setAttribute('data-timestamp', firstOfMonth.getTime());
29649
29650 if (this.dateUtil.isSameMonthAndYear(firstOfMonth, calendarCtrl.today)) {
29651 cell.classList.add(calendarCtrl.TODAY_CLASS);
29652 }
29653
29654 if (this.dateUtil.isValidDate(calendarCtrl.selectedDate) &&
29655 this.dateUtil.isSameMonthAndYear(firstOfMonth, calendarCtrl.selectedDate)) {
29656 cell.classList.add(calendarCtrl.SELECTED_DATE_CLASS);
29657 cell.setAttribute('aria-selected', 'true');
29658 }
29659
29660 var cellText = this.dateLocale.shortMonths[month];
29661
29662 if (this.dateUtil.isMonthWithinRange(firstOfMonth,
29663 calendarCtrl.minDate, calendarCtrl.maxDate)) {
29664 var selectionIndicator = document.createElement('span');
29665 selectionIndicator.classList.add('md-calendar-date-selection-indicator');
29666 selectionIndicator.textContent = cellText;
29667 cell.appendChild(selectionIndicator);
29668 cell.addEventListener('click', yearCtrl.cellClickHandler);
29669
29670 if (calendarCtrl.displayDate && this.dateUtil.isSameMonthAndYear(firstOfMonth, calendarCtrl.displayDate)) {
29671 this.focusAfterAppend = cell;
29672 }
29673 } else {
29674 cell.classList.add('md-calendar-date-disabled');
29675 cell.textContent = cellText;
29676 }
29677
29678 return cell;
29679 };
29680
29681 /**
29682 * Builds a blank cell.
29683 * @return {HTMLTableCellElement}
29684 */
29685 CalendarYearBodyCtrl.prototype.buildBlankCell = function() {
29686 var cell = document.createElement('td');
29687 cell.tabIndex = -1;
29688 cell.classList.add('md-calendar-date');
29689 cell.setAttribute('role', 'gridcell');
29690
29691 cell.setAttribute('tabindex', '-1');
29692 return cell;
29693 };
29694
29695 /**
29696 * Builds the <tbody> content for the given year.
29697 * @param {Date} date Date for which the content should be built.
29698 * @returns {DocumentFragment} A document fragment containing the months within the year.
29699 */
29700 CalendarYearBodyCtrl.prototype.buildCalendarForYear = function(date) {
29701 // Store rows for the month in a document fragment so that we can append them all at once.
29702 var year = date.getFullYear();
29703 var yearBody = document.createDocumentFragment();
29704
29705 var monthCell, i;
29706 // First row contains label and Jan-Jun.
29707 var firstRow = document.createElement('tr');
29708 var labelCell = document.createElement('td');
29709 labelCell.className = 'md-calendar-month-label';
29710 labelCell.textContent = year;
29711 firstRow.appendChild(labelCell);
29712
29713 for (i = 0; i < 6; i++) {
29714 firstRow.appendChild(this.buildMonthCell(year, i));
29715 }
29716 yearBody.appendChild(firstRow);
29717
29718 // Second row contains a blank cell and Jul-Dec.
29719 var secondRow = document.createElement('tr');
29720 secondRow.appendChild(this.buildBlankCell());
29721 for (i = 6; i < 12; i++) {
29722 secondRow.appendChild(this.buildMonthCell(year, i));
29723 }
29724 yearBody.appendChild(secondRow);
29725
29726 return yearBody;
29727 };
29728})();
29729
29730})();
29731(function(){
29732"use strict";
29733
29734(function() {
29735 'use strict';
29736
29737 /**
29738 * @ngdoc service
29739 * @name $mdDateLocaleProvider
29740 * @module material.components.datepicker
29741 *
29742 * @description
29743 * The `$mdDateLocaleProvider` is the provider that creates the `$mdDateLocale` service.
29744 * This provider that allows the user to specify messages, formatters, and parsers for date
29745 * internationalization. The `$mdDateLocale` service itself is consumed by Angular Material
29746 * components that deal with dates.
29747 *
29748 * @property {(Array<string>)=} months Array of month names (in order).
29749 * @property {(Array<string>)=} shortMonths Array of abbreviated month names.
29750 * @property {(Array<string>)=} days Array of the days of the week (in order).
29751 * @property {(Array<string>)=} shortDays Array of abbreviated dayes of the week.
29752 * @property {(Array<string>)=} dates Array of dates of the month. Only necessary for locales
29753 * using a numeral system other than [1, 2, 3...].
29754 * @property {(Array<string>)=} firstDayOfWeek The first day of the week. Sunday = 0, Monday = 1,
29755 * etc.
29756 * @property {(function(string): Date)=} parseDate Function to parse a date object from a string.
29757 * @property {(function(Date, string): string)=} formatDate Function to format a date object to a
29758 * string. The datepicker directive also provides the time zone, if it was specified.
29759 * @property {(function(Date): string)=} monthHeaderFormatter Function that returns the label for
29760 * a month given a date.
29761 * @property {(function(Date): string)=} monthFormatter Function that returns the full name of a month
29762 * for a giben date.
29763 * @property {(function(number): string)=} weekNumberFormatter Function that returns a label for
29764 * a week given the week number.
29765 * @property {(string)=} msgCalendar Translation of the label "Calendar" for the current locale.
29766 * @property {(string)=} msgOpenCalendar Translation of the button label "Open calendar" for the
29767 * current locale.
29768 * @property {Date=} firstRenderableDate The date from which the datepicker calendar will begin
29769 * rendering. Note that this will be ignored if a minimum date is set. Defaults to January 1st 1880.
29770 * @property {Date=} lastRenderableDate The last date that will be rendered by the datepicker
29771 * calendar. Note that this will be ignored if a maximum date is set. Defaults to January 1st 2130.
29772 *
29773 * @usage
29774 * <hljs lang="js">
29775 * myAppModule.config(function($mdDateLocaleProvider) {
29776 *
29777 * // Example of a French localization.
29778 * $mdDateLocaleProvider.months = ['janvier', 'février', 'mars', ...];
29779 * $mdDateLocaleProvider.shortMonths = ['janv', 'févr', 'mars', ...];
29780 * $mdDateLocaleProvider.days = ['dimanche', 'lundi', 'mardi', ...];
29781 * $mdDateLocaleProvider.shortDays = ['Di', 'Lu', 'Ma', ...];
29782 *
29783 * // Can change week display to start on Monday.
29784 * $mdDateLocaleProvider.firstDayOfWeek = 1;
29785 *
29786 * // Optional.
29787 * $mdDateLocaleProvider.dates = [1, 2, 3, 4, 5, 6, ...];
29788 *
29789 * // Example uses moment.js to parse and format dates.
29790 * $mdDateLocaleProvider.parseDate = function(dateString) {
29791 * var m = moment(dateString, 'L', true);
29792 * return m.isValid() ? m.toDate() : new Date(NaN);
29793 * };
29794 *
29795 * $mdDateLocaleProvider.formatDate = function(date) {
29796 * var m = moment(date);
29797 * return m.isValid() ? m.format('L') : '';
29798 * };
29799 *
29800 * $mdDateLocaleProvider.monthHeaderFormatter = function(date) {
29801 * return myShortMonths[date.getMonth()] + ' ' + date.getFullYear();
29802 * };
29803 *
29804 * // In addition to date display, date components also need localized messages
29805 * // for aria-labels for screen-reader users.
29806 *
29807 * $mdDateLocaleProvider.weekNumberFormatter = function(weekNumber) {
29808 * return 'Semaine ' + weekNumber;
29809 * };
29810 *
29811 * $mdDateLocaleProvider.msgCalendar = 'Calendrier';
29812 * $mdDateLocaleProvider.msgOpenCalendar = 'Ouvrir le calendrier';
29813 *
29814 * // You can also set when your calendar begins and ends.
29815 * $mdDateLocaleProvider.firstRenderableDate = new Date(1776, 6, 4);
29816 * $mdDateLocaleProvider.lastRenderableDate = new Date(2012, 11, 21);
29817 * });
29818 * </hljs>
29819 *
29820 */
29821 angular.module('material.components.datepicker').config(["$provide", function($provide) {
29822 // TODO(jelbourn): Assert provided values are correctly formatted. Need assertions.
29823
29824 /** @constructor */
29825 function DateLocaleProvider() {
29826 /** Array of full month names. E.g., ['January', 'Febuary', ...] */
29827 this.months = null;
29828
29829 /** Array of abbreviated month names. E.g., ['Jan', 'Feb', ...] */
29830 this.shortMonths = null;
29831
29832 /** Array of full day of the week names. E.g., ['Monday', 'Tuesday', ...] */
29833 this.days = null;
29834
29835 /** Array of abbreviated dat of the week names. E.g., ['M', 'T', ...] */
29836 this.shortDays = null;
29837
29838 /** Array of dates of a month (1 - 31). Characters might be different in some locales. */
29839 this.dates = null;
29840
29841 /** Index of the first day of the week. 0 = Sunday, 1 = Monday, etc. */
29842 this.firstDayOfWeek = 0;
29843
29844 /**
29845 * Function that converts the date portion of a Date to a string.
29846 * @type {(function(Date): string)}
29847 */
29848 this.formatDate = null;
29849
29850 /**
29851 * Function that converts a date string to a Date object (the date portion)
29852 * @type {function(string): Date}
29853 */
29854 this.parseDate = null;
29855
29856 /**
29857 * Function that formats a Date into a month header string.
29858 * @type {function(Date): string}
29859 */
29860 this.monthHeaderFormatter = null;
29861
29862 /**
29863 * Function that formats a week number into a label for the week.
29864 * @type {function(number): string}
29865 */
29866 this.weekNumberFormatter = null;
29867
29868 /**
29869 * Function that formats a date into a long aria-label that is read
29870 * when the focused date changes.
29871 * @type {function(Date): string}
29872 */
29873 this.longDateFormatter = null;
29874
29875 /**
29876 * ARIA label for the calendar "dialog" used in the datepicker.
29877 * @type {string}
29878 */
29879 this.msgCalendar = '';
29880
29881 /**
29882 * ARIA label for the datepicker's "Open calendar" buttons.
29883 * @type {string}
29884 */
29885 this.msgOpenCalendar = '';
29886 }
29887
29888 /**
29889 * Factory function that returns an instance of the dateLocale service.
29890 * @ngInject
29891 * @param $locale
29892 * @returns {DateLocale}
29893 */
29894 DateLocaleProvider.prototype.$get = function($locale, $filter) {
29895 /**
29896 * Default date-to-string formatting function.
29897 * @param {!Date} date
29898 * @param {string=} timezone
29899 * @returns {string}
29900 */
29901 function defaultFormatDate(date, timezone) {
29902 if (!date) {
29903 return '';
29904 }
29905
29906 // All of the dates created through ng-material *should* be set to midnight.
29907 // If we encounter a date where the localeTime shows at 11pm instead of midnight,
29908 // we have run into an issue with DST where we need to increment the hour by one:
29909 // var d = new Date(1992, 9, 8, 0, 0, 0);
29910 // d.toLocaleString(); // == "10/7/1992, 11:00:00 PM"
29911 var localeTime = date.toLocaleTimeString();
29912 var formatDate = date;
29913 if (date.getHours() === 0 &&
29914 (localeTime.indexOf('11:') !== -1 || localeTime.indexOf('23:') !== -1)) {
29915 formatDate = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 1, 0, 0);
29916 }
29917
29918 return $filter('date')(formatDate, 'M/d/yyyy', timezone);
29919 }
29920
29921 /**
29922 * Default string-to-date parsing function.
29923 * @param {string} dateString
29924 * @returns {!Date}
29925 */
29926 function defaultParseDate(dateString) {
29927 return new Date(dateString);
29928 }
29929
29930 /**
29931 * Default function to determine whether a string makes sense to be
29932 * parsed to a Date object.
29933 *
29934 * This is very permissive and is just a basic sanity check to ensure that
29935 * things like single integers aren't able to be parsed into dates.
29936 * @param {string} dateString
29937 * @returns {boolean}
29938 */
29939 function defaultIsDateComplete(dateString) {
29940 dateString = dateString.trim();
29941
29942 // Looks for three chunks of content (either numbers or text) separated
29943 // by delimiters.
29944 var re = /^(([a-zA-Z]{3,}|[0-9]{1,4})([ \.,]+|[\/\-])){2}([a-zA-Z]{3,}|[0-9]{1,4})$/;
29945 return re.test(dateString);
29946 }
29947
29948 /**
29949 * Default date-to-string formatter to get a month header.
29950 * @param {!Date} date
29951 * @returns {string}
29952 */
29953 function defaultMonthHeaderFormatter(date) {
29954 return service.shortMonths[date.getMonth()] + ' ' + date.getFullYear();
29955 }
29956
29957 /**
29958 * Default formatter for a month.
29959 * @param {!Date} date
29960 * @returns {string}
29961 */
29962 function defaultMonthFormatter(date) {
29963 return service.months[date.getMonth()] + ' ' + date.getFullYear();
29964 }
29965
29966 /**
29967 * Default week number formatter.
29968 * @param number
29969 * @returns {string}
29970 */
29971 function defaultWeekNumberFormatter(number) {
29972 return 'Week ' + number;
29973 }
29974
29975 /**
29976 * Default formatter for date cell aria-labels.
29977 * @param {!Date} date
29978 * @returns {string}
29979 */
29980 function defaultLongDateFormatter(date) {
29981 // Example: 'Thursday June 18 2015'
29982 return [
29983 service.days[date.getDay()],
29984 service.months[date.getMonth()],
29985 service.dates[date.getDate()],
29986 date.getFullYear()
29987 ].join(' ');
29988 }
29989
29990 // The default "short" day strings are the first character of each day,
29991 // e.g., "Monday" => "M".
29992 var defaultShortDays = $locale.DATETIME_FORMATS.SHORTDAY.map(function(day) {
29993 return day.substring(0, 1);
29994 });
29995
29996 // The default dates are simply the numbers 1 through 31.
29997 var defaultDates = Array(32);
29998 for (var i = 1; i <= 31; i++) {
29999 defaultDates[i] = i;
30000 }
30001
30002 // Default ARIA messages are in English (US).
30003 var defaultMsgCalendar = 'Calendar';
30004 var defaultMsgOpenCalendar = 'Open calendar';
30005
30006 // Default start/end dates that are rendered in the calendar.
30007 var defaultFirstRenderableDate = new Date(1880, 0, 1);
30008 var defaultLastRendereableDate = new Date(defaultFirstRenderableDate.getFullYear() + 250, 0, 1);
30009
30010 var service = {
30011 months: this.months || $locale.DATETIME_FORMATS.MONTH,
30012 shortMonths: this.shortMonths || $locale.DATETIME_FORMATS.SHORTMONTH,
30013 days: this.days || $locale.DATETIME_FORMATS.DAY,
30014 shortDays: this.shortDays || defaultShortDays,
30015 dates: this.dates || defaultDates,
30016 firstDayOfWeek: this.firstDayOfWeek || 0,
30017 formatDate: this.formatDate || defaultFormatDate,
30018 parseDate: this.parseDate || defaultParseDate,
30019 isDateComplete: this.isDateComplete || defaultIsDateComplete,
30020 monthHeaderFormatter: this.monthHeaderFormatter || defaultMonthHeaderFormatter,
30021 monthFormatter: this.monthFormatter || defaultMonthFormatter,
30022 weekNumberFormatter: this.weekNumberFormatter || defaultWeekNumberFormatter,
30023 longDateFormatter: this.longDateFormatter || defaultLongDateFormatter,
30024 msgCalendar: this.msgCalendar || defaultMsgCalendar,
30025 msgOpenCalendar: this.msgOpenCalendar || defaultMsgOpenCalendar,
30026 firstRenderableDate: this.firstRenderableDate || defaultFirstRenderableDate,
30027 lastRenderableDate: this.lastRenderableDate || defaultLastRendereableDate
30028 };
30029
30030 return service;
30031 };
30032 DateLocaleProvider.prototype.$get.$inject = ["$locale", "$filter"];
30033
30034 $provide.provider('$mdDateLocale', new DateLocaleProvider());
30035 }]);
30036})();
30037
30038})();
30039(function(){
30040"use strict";
30041
30042(function() {
30043 'use strict';
30044
30045 /**
30046 * Utility for performing date calculations to facilitate operation of the calendar and
30047 * datepicker.
30048 */
30049 angular.module('material.components.datepicker').factory('$$mdDateUtil', function() {
30050 return {
30051 getFirstDateOfMonth: getFirstDateOfMonth,
30052 getNumberOfDaysInMonth: getNumberOfDaysInMonth,
30053 getDateInNextMonth: getDateInNextMonth,
30054 getDateInPreviousMonth: getDateInPreviousMonth,
30055 isInNextMonth: isInNextMonth,
30056 isInPreviousMonth: isInPreviousMonth,
30057 getDateMidpoint: getDateMidpoint,
30058 isSameMonthAndYear: isSameMonthAndYear,
30059 getWeekOfMonth: getWeekOfMonth,
30060 incrementDays: incrementDays,
30061 incrementMonths: incrementMonths,
30062 getLastDateOfMonth: getLastDateOfMonth,
30063 isSameDay: isSameDay,
30064 getMonthDistance: getMonthDistance,
30065 isValidDate: isValidDate,
30066 setDateTimeToMidnight: setDateTimeToMidnight,
30067 createDateAtMidnight: createDateAtMidnight,
30068 isDateWithinRange: isDateWithinRange,
30069 incrementYears: incrementYears,
30070 getYearDistance: getYearDistance,
30071 clampDate: clampDate,
30072 getTimestampFromNode: getTimestampFromNode,
30073 isMonthWithinRange: isMonthWithinRange
30074 };
30075
30076 /**
30077 * Gets the first day of the month for the given date's month.
30078 * @param {Date} date
30079 * @returns {Date}
30080 */
30081 function getFirstDateOfMonth(date) {
30082 return new Date(date.getFullYear(), date.getMonth(), 1);
30083 }
30084
30085 /**
30086 * Gets the number of days in the month for the given date's month.
30087 * @param date
30088 * @returns {number}
30089 */
30090 function getNumberOfDaysInMonth(date) {
30091 return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate();
30092 }
30093
30094 /**
30095 * Get an arbitrary date in the month after the given date's month.
30096 * @param date
30097 * @returns {Date}
30098 */
30099 function getDateInNextMonth(date) {
30100 return new Date(date.getFullYear(), date.getMonth() + 1, 1);
30101 }
30102
30103 /**
30104 * Get an arbitrary date in the month before the given date's month.
30105 * @param date
30106 * @returns {Date}
30107 */
30108 function getDateInPreviousMonth(date) {
30109 return new Date(date.getFullYear(), date.getMonth() - 1, 1);
30110 }
30111
30112 /**
30113 * Gets whether two dates have the same month and year.
30114 * @param {Date} d1
30115 * @param {Date} d2
30116 * @returns {boolean}
30117 */
30118 function isSameMonthAndYear(d1, d2) {
30119 return d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth();
30120 }
30121
30122 /**
30123 * Gets whether two dates are the same day (not not necesarily the same time).
30124 * @param {Date} d1
30125 * @param {Date} d2
30126 * @returns {boolean}
30127 */
30128 function isSameDay(d1, d2) {
30129 return d1.getDate() == d2.getDate() && isSameMonthAndYear(d1, d2);
30130 }
30131
30132 /**
30133 * Gets whether a date is in the month immediately after some date.
30134 * @param {Date} startDate The date from which to compare.
30135 * @param {Date} endDate The date to check.
30136 * @returns {boolean}
30137 */
30138 function isInNextMonth(startDate, endDate) {
30139 var nextMonth = getDateInNextMonth(startDate);
30140 return isSameMonthAndYear(nextMonth, endDate);
30141 }
30142
30143 /**
30144 * Gets whether a date is in the month immediately before some date.
30145 * @param {Date} startDate The date from which to compare.
30146 * @param {Date} endDate The date to check.
30147 * @returns {boolean}
30148 */
30149 function isInPreviousMonth(startDate, endDate) {
30150 var previousMonth = getDateInPreviousMonth(startDate);
30151 return isSameMonthAndYear(endDate, previousMonth);
30152 }
30153
30154 /**
30155 * Gets the midpoint between two dates.
30156 * @param {Date} d1
30157 * @param {Date} d2
30158 * @returns {Date}
30159 */
30160 function getDateMidpoint(d1, d2) {
30161 return createDateAtMidnight((d1.getTime() + d2.getTime()) / 2);
30162 }
30163
30164 /**
30165 * Gets the week of the month that a given date occurs in.
30166 * @param {Date} date
30167 * @returns {number} Index of the week of the month (zero-based).
30168 */
30169 function getWeekOfMonth(date) {
30170 var firstDayOfMonth = getFirstDateOfMonth(date);
30171 return Math.floor((firstDayOfMonth.getDay() + date.getDate() - 1) / 7);
30172 }
30173
30174 /**
30175 * Gets a new date incremented by the given number of days. Number of days can be negative.
30176 * @param {Date} date
30177 * @param {number} numberOfDays
30178 * @returns {Date}
30179 */
30180 function incrementDays(date, numberOfDays) {
30181 return new Date(date.getFullYear(), date.getMonth(), date.getDate() + numberOfDays);
30182 }
30183
30184 /**
30185 * Gets a new date incremented by the given number of months. Number of months can be negative.
30186 * If the date of the given month does not match the target month, the date will be set to the
30187 * last day of the month.
30188 * @param {Date} date
30189 * @param {number} numberOfMonths
30190 * @returns {Date}
30191 */
30192 function incrementMonths(date, numberOfMonths) {
30193 // If the same date in the target month does not actually exist, the Date object will
30194 // automatically advance *another* month by the number of missing days.
30195 // For example, if you try to go from Jan. 30 to Feb. 30, you'll end up on March 2.
30196 // So, we check if the month overflowed and go to the last day of the target month instead.
30197 var dateInTargetMonth = new Date(date.getFullYear(), date.getMonth() + numberOfMonths, 1);
30198 var numberOfDaysInMonth = getNumberOfDaysInMonth(dateInTargetMonth);
30199 if (numberOfDaysInMonth < date.getDate()) {
30200 dateInTargetMonth.setDate(numberOfDaysInMonth);
30201 } else {
30202 dateInTargetMonth.setDate(date.getDate());
30203 }
30204
30205 return dateInTargetMonth;
30206 }
30207
30208 /**
30209 * Get the integer distance between two months. This *only* considers the month and year
30210 * portion of the Date instances.
30211 *
30212 * @param {Date} start
30213 * @param {Date} end
30214 * @returns {number} Number of months between `start` and `end`. If `end` is before `start`
30215 * chronologically, this number will be negative.
30216 */
30217 function getMonthDistance(start, end) {
30218 return (12 * (end.getFullYear() - start.getFullYear())) + (end.getMonth() - start.getMonth());
30219 }
30220
30221 /**
30222 * Gets the last day of the month for the given date.
30223 * @param {Date} date
30224 * @returns {Date}
30225 */
30226 function getLastDateOfMonth(date) {
30227 return new Date(date.getFullYear(), date.getMonth(), getNumberOfDaysInMonth(date));
30228 }
30229
30230 /**
30231 * Checks whether a date is valid.
30232 * @param {Date} date
30233 * @return {boolean} Whether the date is a valid Date.
30234 */
30235 function isValidDate(date) {
30236 return date && date.getTime && !isNaN(date.getTime());
30237 }
30238
30239 /**
30240 * Sets a date's time to midnight.
30241 * @param {Date} date
30242 */
30243 function setDateTimeToMidnight(date) {
30244 if (isValidDate(date)) {
30245 date.setHours(0, 0, 0, 0);
30246 }
30247 }
30248
30249 /**
30250 * Creates a date with the time set to midnight.
30251 * Drop-in replacement for two forms of the Date constructor:
30252 * 1. No argument for Date representing now.
30253 * 2. Single-argument value representing number of seconds since Unix Epoch
30254 * or a Date object.
30255 * @param {number|Date=} opt_value
30256 * @return {Date} New date with time set to midnight.
30257 */
30258 function createDateAtMidnight(opt_value) {
30259 var date;
30260 if (angular.isUndefined(opt_value)) {
30261 date = new Date();
30262 } else {
30263 date = new Date(opt_value);
30264 }
30265 setDateTimeToMidnight(date);
30266 return date;
30267 }
30268
30269 /**
30270 * Checks if a date is within a min and max range, ignoring the time component.
30271 * If minDate or maxDate are not dates, they are ignored.
30272 * @param {Date} date
30273 * @param {Date} minDate
30274 * @param {Date} maxDate
30275 */
30276 function isDateWithinRange(date, minDate, maxDate) {
30277 var dateAtMidnight = createDateAtMidnight(date);
30278 var minDateAtMidnight = isValidDate(minDate) ? createDateAtMidnight(minDate) : null;
30279 var maxDateAtMidnight = isValidDate(maxDate) ? createDateAtMidnight(maxDate) : null;
30280 return (!minDateAtMidnight || minDateAtMidnight <= dateAtMidnight) &&
30281 (!maxDateAtMidnight || maxDateAtMidnight >= dateAtMidnight);
30282 }
30283
30284 /**
30285 * Gets a new date incremented by the given number of years. Number of years can be negative.
30286 * See `incrementMonths` for notes on overflow for specific dates.
30287 * @param {Date} date
30288 * @param {number} numberOfYears
30289 * @returns {Date}
30290 */
30291 function incrementYears(date, numberOfYears) {
30292 return incrementMonths(date, numberOfYears * 12);
30293 }
30294
30295 /**
30296 * Get the integer distance between two years. This *only* considers the year portion of the
30297 * Date instances.
30298 *
30299 * @param {Date} start
30300 * @param {Date} end
30301 * @returns {number} Number of months between `start` and `end`. If `end` is before `start`
30302 * chronologically, this number will be negative.
30303 */
30304 function getYearDistance(start, end) {
30305 return end.getFullYear() - start.getFullYear();
30306 }
30307
30308 /**
30309 * Clamps a date between a minimum and a maximum date.
30310 * @param {Date} date Date to be clamped
30311 * @param {Date=} minDate Minimum date
30312 * @param {Date=} maxDate Maximum date
30313 * @return {Date}
30314 */
30315 function clampDate(date, minDate, maxDate) {
30316 var boundDate = date;
30317 if (minDate && date < minDate) {
30318 boundDate = new Date(minDate.getTime());
30319 }
30320 if (maxDate && date > maxDate) {
30321 boundDate = new Date(maxDate.getTime());
30322 }
30323 return boundDate;
30324 }
30325
30326 /**
30327 * Extracts and parses the timestamp from a DOM node.
30328 * @param {HTMLElement} node Node from which the timestamp will be extracted.
30329 * @return {number} Time since epoch.
30330 */
30331 function getTimestampFromNode(node) {
30332 if (node && node.hasAttribute('data-timestamp')) {
30333 return Number(node.getAttribute('data-timestamp'));
30334 }
30335 }
30336
30337 /**
30338 * Checks if a month is within a min and max range, ignoring the date and time components.
30339 * If minDate or maxDate are not dates, they are ignored.
30340 * @param {Date} date
30341 * @param {Date} minDate
30342 * @param {Date} maxDate
30343 */
30344 function isMonthWithinRange(date, minDate, maxDate) {
30345 var month = date.getMonth();
30346 var year = date.getFullYear();
30347
30348 return (!minDate || minDate.getFullYear() < year || minDate.getMonth() <= month) &&
30349 (!maxDate || maxDate.getFullYear() > year || maxDate.getMonth() >= month);
30350 }
30351 });
30352})();
30353
30354})();
30355(function(){
30356"use strict";
30357
30358(function() {
30359 'use strict';
30360
30361 // POST RELEASE
30362 // TODO(jelbourn): Demo that uses moment.js
30363 // TODO(jelbourn): make sure this plays well with validation and ngMessages.
30364 // TODO(jelbourn): calendar pane doesn't open up outside of visible viewport.
30365 // TODO(jelbourn): forward more attributes to the internal input (required, autofocus, etc.)
30366 // TODO(jelbourn): something better for mobile (calendar panel takes up entire screen?)
30367 // TODO(jelbourn): input behavior (masking? auto-complete?)
30368
30369
30370 DatePickerCtrl.$inject = ["$scope", "$element", "$attrs", "$window", "$mdConstant", "$mdTheming", "$mdUtil", "$mdDateLocale", "$$mdDateUtil", "$$rAF", "$filter"];
30371 datePickerDirective.$inject = ["$$mdSvgRegistry", "$mdUtil", "$mdAria", "inputDirective"];
30372 angular.module('material.components.datepicker')
30373 .directive('mdDatepicker', datePickerDirective);
30374
30375 /**
30376 * @ngdoc directive
30377 * @name mdDatepicker
30378 * @module material.components.datepicker
30379 *
30380 * @param {Date} ng-model The component's model. Expects a JavaScript Date object.
30381 * @param {Object=} ng-model-options Allows tuning of the way in which `ng-model` is being updated. Also allows
30382 * for a timezone to be specified. <a href="https://docs.angularjs.org/api/ng/directive/ngModelOptions#usage">Read more at the ngModelOptions docs.</a>
30383 * @param {expression=} ng-change Expression evaluated when the model value changes.
30384 * @param {expression=} ng-focus Expression evaluated when the input is focused or the calendar is opened.
30385 * @param {expression=} ng-blur Expression evaluated when focus is removed from the input or the calendar is closed.
30386 * @param {boolean=} ng-disabled Whether the datepicker is disabled.
30387 * @param {boolean=} ng-required Whether a value is required for the datepicker.
30388 * @param {Date=} md-min-date Expression representing a min date (inclusive).
30389 * @param {Date=} md-max-date Expression representing a max date (inclusive).
30390 * @param {(function(Date): boolean)=} md-date-filter Function expecting a date and returning a boolean whether it can be selected or not.
30391 * @param {String=} md-placeholder The date input placeholder value.
30392 * @param {String=} md-open-on-focus When present, the calendar will be opened when the input is focused.
30393 * @param {Boolean=} md-is-open Expression that can be used to open the datepicker's calendar on-demand.
30394 * @param {String=} md-current-view Default open view of the calendar pane. Can be either "month" or "year".
30395 * @param {String=} md-hide-icons Determines which datepicker icons should be hidden. Note that this may cause the
30396 * datepicker to not align properly with other components. **Use at your own risk.** Possible values are:
30397 * * `"all"` - Hides all icons.
30398 * * `"calendar"` - Only hides the calendar icon.
30399 * * `"triangle"` - Only hides the triangle icon.
30400 * @param {Object=} md-date-locale Allows for the values from the `$mdDateLocaleProvider` to be
30401 * ovewritten on a per-element basis (e.g. `msgOpenCalendar` can be overwritten with
30402 * `md-date-locale="{ msgOpenCalendar: 'Open a special calendar' }"`).
30403 *
30404 * @description
30405 * `<md-datepicker>` is a component used to select a single date.
30406 * For information on how to configure internationalization for the date picker,
30407 * see `$mdDateLocaleProvider`.
30408 *
30409 * This component supports [ngMessages](https://docs.angularjs.org/api/ngMessages/directive/ngMessages).
30410 * Supported attributes are:
30411 * * `required`: whether a required date is not set.
30412 * * `mindate`: whether the selected date is before the minimum allowed date.
30413 * * `maxdate`: whether the selected date is after the maximum allowed date.
30414 * * `debounceInterval`: ms to delay input processing (since last debounce reset); default value 500ms
30415 *
30416 * @usage
30417 * <hljs lang="html">
30418 * <md-datepicker ng-model="birthday"></md-datepicker>
30419 * </hljs>
30420 *
30421 */
30422
30423 function datePickerDirective($$mdSvgRegistry, $mdUtil, $mdAria, inputDirective) {
30424 return {
30425 template: function(tElement, tAttrs) {
30426 // Buttons are not in the tab order because users can open the calendar via keyboard
30427 // interaction on the text input, and multiple tab stops for one component (picker)
30428 // may be confusing.
30429 var hiddenIcons = tAttrs.mdHideIcons;
30430 var ariaLabelValue = tAttrs.ariaLabel || tAttrs.mdPlaceholder;
30431
30432 var calendarButton = (hiddenIcons === 'all' || hiddenIcons === 'calendar') ? '' :
30433 '<md-button class="md-datepicker-button md-icon-button" type="button" ' +
30434 'tabindex="-1" aria-hidden="true" ' +
30435 'ng-click="ctrl.openCalendarPane($event)">' +
30436 '<md-icon class="md-datepicker-calendar-icon" aria-label="md-calendar" ' +
30437 'md-svg-src="' + $$mdSvgRegistry.mdCalendar + '"></md-icon>' +
30438 '</md-button>';
30439
30440 var triangleButton = '';
30441
30442 if (hiddenIcons !== 'all' && hiddenIcons !== 'triangle') {
30443 triangleButton = '' +
30444 '<md-button type="button" md-no-ink ' +
30445 'class="md-datepicker-triangle-button md-icon-button" ' +
30446 'ng-click="ctrl.openCalendarPane($event)" ' +
30447 'aria-label="{{::ctrl.locale.msgOpenCalendar}}">' +
30448 '<div class="md-datepicker-expand-triangle"></div>' +
30449 '</md-button>';
30450
30451 tElement.addClass(HAS_TRIANGLE_ICON_CLASS);
30452 }
30453
30454 return calendarButton +
30455 '<div class="md-datepicker-input-container" ng-class="{\'md-datepicker-focused\': ctrl.isFocused}">' +
30456 '<input ' +
30457 (ariaLabelValue ? 'aria-label="' + ariaLabelValue + '" ' : '') +
30458 'class="md-datepicker-input" ' +
30459 'aria-haspopup="true" ' +
30460 'aria-expanded="{{ctrl.isCalendarOpen}}" ' +
30461 'ng-focus="ctrl.setFocused(true)" ' +
30462 'ng-blur="ctrl.setFocused(false)"> ' +
30463 triangleButton +
30464 '</div>' +
30465
30466 // This pane will be detached from here and re-attached to the document body.
30467 '<div class="md-datepicker-calendar-pane md-whiteframe-z1" id="{{::ctrl.calendarPaneId}}">' +
30468 '<div class="md-datepicker-input-mask">' +
30469 '<div class="md-datepicker-input-mask-opaque"></div>' +
30470 '</div>' +
30471 '<div class="md-datepicker-calendar">' +
30472 '<md-calendar role="dialog" aria-label="{{::ctrl.locale.msgCalendar}}" ' +
30473 'md-current-view="{{::ctrl.currentView}}"' +
30474 'md-min-date="ctrl.minDate"' +
30475 'md-max-date="ctrl.maxDate"' +
30476 'md-date-filter="ctrl.dateFilter"' +
30477 'ng-model="ctrl.date" ng-if="ctrl.isCalendarOpen">' +
30478 '</md-calendar>' +
30479 '</div>' +
30480 '</div>';
30481 },
30482 require: ['ngModel', 'mdDatepicker', '?^mdInputContainer', '?^form'],
30483 scope: {
30484 minDate: '=mdMinDate',
30485 maxDate: '=mdMaxDate',
30486 placeholder: '@mdPlaceholder',
30487 currentView: '@mdCurrentView',
30488 dateFilter: '=mdDateFilter',
30489 isOpen: '=?mdIsOpen',
30490 debounceInterval: '=mdDebounceInterval',
30491 dateLocale: '=mdDateLocale'
30492 },
30493 controller: DatePickerCtrl,
30494 controllerAs: 'ctrl',
30495 bindToController: true,
30496 link: function(scope, element, attr, controllers) {
30497 var ngModelCtrl = controllers[0];
30498 var mdDatePickerCtrl = controllers[1];
30499 var mdInputContainer = controllers[2];
30500 var parentForm = controllers[3];
30501 var mdNoAsterisk = $mdUtil.parseAttributeBoolean(attr.mdNoAsterisk);
30502
30503 mdDatePickerCtrl.configureNgModel(ngModelCtrl, mdInputContainer, inputDirective);
30504
30505 if (mdInputContainer) {
30506 // We need to move the spacer after the datepicker itself,
30507 // because md-input-container adds it after the
30508 // md-datepicker-input by default. The spacer gets wrapped in a
30509 // div, because it floats and gets aligned next to the datepicker.
30510 // There are easier ways of working around this with CSS (making the
30511 // datepicker 100% wide, change the `display` etc.), however they
30512 // break the alignment with any other form controls.
30513 var spacer = element[0].querySelector('.md-errors-spacer');
30514
30515 if (spacer) {
30516 element.after(angular.element('<div>').append(spacer));
30517 }
30518
30519 mdInputContainer.setHasPlaceholder(attr.mdPlaceholder);
30520 mdInputContainer.input = element;
30521 mdInputContainer.element
30522 .addClass(INPUT_CONTAINER_CLASS)
30523 .toggleClass(HAS_CALENDAR_ICON_CLASS, attr.mdHideIcons !== 'calendar' && attr.mdHideIcons !== 'all');
30524
30525 if (!mdInputContainer.label) {
30526 $mdAria.expect(element, 'aria-label', attr.mdPlaceholder);
30527 } else if(!mdNoAsterisk) {
30528 attr.$observe('required', function(value) {
30529 mdInputContainer.label.toggleClass('md-required', !!value);
30530 });
30531 }
30532
30533 scope.$watch(mdInputContainer.isErrorGetter || function() {
30534 return ngModelCtrl.$invalid && (ngModelCtrl.$touched || (parentForm && parentForm.$submitted));
30535 }, mdInputContainer.setInvalid);
30536 } else if (parentForm) {
30537 // If invalid, highlights the input when the parent form is submitted.
30538 var parentSubmittedWatcher = scope.$watch(function() {
30539 return parentForm.$submitted;
30540 }, function(isSubmitted) {
30541 if (isSubmitted) {
30542 mdDatePickerCtrl.updateErrorState();
30543 parentSubmittedWatcher();
30544 }
30545 });
30546 }
30547 }
30548 };
30549 }
30550
30551 /** Additional offset for the input's `size` attribute, which is updated based on its content. */
30552 var EXTRA_INPUT_SIZE = 3;
30553
30554 /** Class applied to the container if the date is invalid. */
30555 var INVALID_CLASS = 'md-datepicker-invalid';
30556
30557 /** Class applied to the datepicker when it's open. */
30558 var OPEN_CLASS = 'md-datepicker-open';
30559
30560 /** Class applied to the md-input-container, if a datepicker is placed inside it */
30561 var INPUT_CONTAINER_CLASS = '_md-datepicker-floating-label';
30562
30563 /** Class to be applied when the calendar icon is enabled. */
30564 var HAS_CALENDAR_ICON_CLASS = '_md-datepicker-has-calendar-icon';
30565
30566 /** Class to be applied when the triangle icon is enabled. */
30567 var HAS_TRIANGLE_ICON_CLASS = '_md-datepicker-has-triangle-icon';
30568
30569 /** Default time in ms to debounce input event by. */
30570 var DEFAULT_DEBOUNCE_INTERVAL = 500;
30571
30572 /**
30573 * Height of the calendar pane used to check if the pane is going outside the boundary of
30574 * the viewport. See calendar.scss for how $md-calendar-height is computed; an extra 20px is
30575 * also added to space the pane away from the exact edge of the screen.
30576 *
30577 * This is computed statically now, but can be changed to be measured if the circumstances
30578 * of calendar sizing are changed.
30579 */
30580 var CALENDAR_PANE_HEIGHT = 368;
30581
30582 /**
30583 * Width of the calendar pane used to check if the pane is going outside the boundary of
30584 * the viewport. See calendar.scss for how $md-calendar-width is computed; an extra 20px is
30585 * also added to space the pane away from the exact edge of the screen.
30586 *
30587 * This is computed statically now, but can be changed to be measured if the circumstances
30588 * of calendar sizing are changed.
30589 */
30590 var CALENDAR_PANE_WIDTH = 360;
30591
30592 /** Used for checking whether the current user agent is on iOS or Android. */
30593 var IS_MOBILE_REGEX = /ipad|iphone|ipod|android/i;
30594
30595 /**
30596 * Controller for md-datepicker.
30597 *
30598 * @ngInject @constructor
30599 */
30600 function DatePickerCtrl($scope, $element, $attrs, $window, $mdConstant,
30601 $mdTheming, $mdUtil, $mdDateLocale, $$mdDateUtil, $$rAF, $filter) {
30602
30603 /** @final */
30604 this.$window = $window;
30605
30606 /** @final */
30607 this.dateUtil = $$mdDateUtil;
30608
30609 /** @final */
30610 this.$mdConstant = $mdConstant;
30611
30612 /* @final */
30613 this.$mdUtil = $mdUtil;
30614
30615 /** @final */
30616 this.$$rAF = $$rAF;
30617
30618 /** @final */
30619 this.$mdDateLocale = $mdDateLocale;
30620
30621 /**
30622 * The root document element. This is used for attaching a top-level click handler to
30623 * close the calendar panel when a click outside said panel occurs. We use `documentElement`
30624 * instead of body because, when scrolling is disabled, some browsers consider the body element
30625 * to be completely off the screen and propagate events directly to the html element.
30626 * @type {!angular.JQLite}
30627 */
30628 this.documentElement = angular.element(document.documentElement);
30629
30630 /** @type {!angular.NgModelController} */
30631 this.ngModelCtrl = null;
30632
30633 /** @type {HTMLInputElement} */
30634 this.inputElement = $element[0].querySelector('input');
30635
30636 /** @final {!angular.JQLite} */
30637 this.ngInputElement = angular.element(this.inputElement);
30638
30639 /** @type {HTMLElement} */
30640 this.inputContainer = $element[0].querySelector('.md-datepicker-input-container');
30641
30642 /** @type {HTMLElement} Floating calendar pane. */
30643 this.calendarPane = $element[0].querySelector('.md-datepicker-calendar-pane');
30644
30645 /** @type {HTMLElement} Calendar icon button. */
30646 this.calendarButton = $element[0].querySelector('.md-datepicker-button');
30647
30648 /**
30649 * Element covering everything but the input in the top of the floating calendar pane.
30650 * @type {!angular.JQLite}
30651 */
30652 this.inputMask = angular.element($element[0].querySelector('.md-datepicker-input-mask-opaque'));
30653
30654 /** @final {!angular.JQLite} */
30655 this.$element = $element;
30656
30657 /** @final {!angular.Attributes} */
30658 this.$attrs = $attrs;
30659
30660 /** @final {!angular.Scope} */
30661 this.$scope = $scope;
30662
30663 /** @type {Date} */
30664 this.date = null;
30665
30666 /** @type {boolean} */
30667 this.isFocused = false;
30668
30669 /** @type {boolean} */
30670 this.isDisabled;
30671 this.setDisabled($element[0].disabled || angular.isString($attrs.disabled));
30672
30673 /** @type {boolean} Whether the date-picker's calendar pane is open. */
30674 this.isCalendarOpen = false;
30675
30676 /** @type {boolean} Whether the calendar should open when the input is focused. */
30677 this.openOnFocus = $attrs.hasOwnProperty('mdOpenOnFocus');
30678
30679 /** @final */
30680 this.mdInputContainer = null;
30681
30682 /**
30683 * Element from which the calendar pane was opened. Keep track of this so that we can return
30684 * focus to it when the pane is closed.
30685 * @type {HTMLElement}
30686 */
30687 this.calendarPaneOpenedFrom = null;
30688
30689 /** @type {String} Unique id for the calendar pane. */
30690 this.calendarPaneId = 'md-date-pane-' + $mdUtil.nextUid();
30691
30692 /** Pre-bound click handler is saved so that the event listener can be removed. */
30693 this.bodyClickHandler = angular.bind(this, this.handleBodyClick);
30694
30695 /**
30696 * Name of the event that will trigger a close. Necessary to sniff the browser, because
30697 * the resize event doesn't make sense on mobile and can have a negative impact since it
30698 * triggers whenever the browser zooms in on a focused input.
30699 */
30700 this.windowEventName = IS_MOBILE_REGEX.test(
30701 navigator.userAgent || navigator.vendor || window.opera
30702 ) ? 'orientationchange' : 'resize';
30703
30704 /** Pre-bound close handler so that the event listener can be removed. */
30705 this.windowEventHandler = $mdUtil.debounce(angular.bind(this, this.closeCalendarPane), 100);
30706
30707 /** Pre-bound handler for the window blur event. Allows for it to be removed later. */
30708 this.windowBlurHandler = angular.bind(this, this.handleWindowBlur);
30709
30710 /** The built-in Angular date filter. */
30711 this.ngDateFilter = $filter('date');
30712
30713 /** @type {Number} Extra margin for the left side of the floating calendar pane. */
30714 this.leftMargin = 20;
30715
30716 /** @type {Number} Extra margin for the top of the floating calendar. Gets determined on the first open. */
30717 this.topMargin = null;
30718
30719 // Unless the user specifies so, the datepicker should not be a tab stop.
30720 // This is necessary because ngAria might add a tabindex to anything with an ng-model
30721 // (based on whether or not the user has turned that particular feature on/off).
30722 if ($attrs.tabindex) {
30723 this.ngInputElement.attr('tabindex', $attrs.tabindex);
30724 $attrs.$set('tabindex', null);
30725 } else {
30726 $attrs.$set('tabindex', '-1');
30727 }
30728
30729 $attrs.$set('aria-owns', this.calendarPaneId);
30730
30731 $mdTheming($element);
30732 $mdTheming(angular.element(this.calendarPane));
30733
30734 var self = this;
30735
30736 $scope.$on('$destroy', function() {
30737 self.detachCalendarPane();
30738 });
30739
30740 if ($attrs.mdIsOpen) {
30741 $scope.$watch('ctrl.isOpen', function(shouldBeOpen) {
30742 if (shouldBeOpen) {
30743 self.openCalendarPane({
30744 target: self.inputElement
30745 });
30746 } else {
30747 self.closeCalendarPane();
30748 }
30749 });
30750 }
30751
30752 // For Angular 1.4 and older, where there are no lifecycle hooks but bindings are pre-assigned,
30753 // manually call the $onInit hook.
30754 if (angular.version.major === 1 && angular.version.minor <= 4) {
30755 this.$onInit();
30756 }
30757
30758 }
30759
30760 /**
30761 * Angular Lifecycle hook for newer Angular versions.
30762 * Bindings are not guaranteed to have been assigned in the controller, but they are in the $onInit hook.
30763 */
30764 DatePickerCtrl.prototype.$onInit = function() {
30765
30766 /**
30767 * Holds locale-specific formatters, parsers, labels etc. Allows
30768 * the user to override specific ones from the $mdDateLocale provider.
30769 * @type {!Object}
30770 */
30771 this.locale = this.dateLocale ? angular.extend({}, this.$mdDateLocale, this.dateLocale) : this.$mdDateLocale;
30772
30773 this.installPropertyInterceptors();
30774 this.attachChangeListeners();
30775 this.attachInteractionListeners();
30776 };
30777
30778 /**
30779 * Sets up the controller's reference to ngModelController and
30780 * applies Angular's `input[type="date"]` directive.
30781 * @param {!angular.NgModelController} ngModelCtrl Instance of the ngModel controller.
30782 * @param {Object} mdInputContainer Instance of the mdInputContainer controller.
30783 * @param {Object} inputDirective Config for Angular's `input` directive.
30784 */
30785 DatePickerCtrl.prototype.configureNgModel = function(ngModelCtrl, mdInputContainer, inputDirective) {
30786 this.ngModelCtrl = ngModelCtrl;
30787 this.mdInputContainer = mdInputContainer;
30788
30789 // The input needs to be [type="date"] in order to be picked up by Angular.
30790 this.$attrs.$set('type', 'date');
30791
30792 // Invoke the `input` directive link function, adding a stub for the element.
30793 // This allows us to re-use Angular's logic for setting the timezone via ng-model-options.
30794 // It works by calling the link function directly which then adds the proper `$parsers` and
30795 // `$formatters` to the ngModel controller.
30796 inputDirective[0].link.pre(this.$scope, {
30797 on: angular.noop,
30798 val: angular.noop,
30799 0: {}
30800 }, this.$attrs, [ngModelCtrl]);
30801
30802 var self = this;
30803
30804 // Responds to external changes to the model value.
30805 self.ngModelCtrl.$formatters.push(function(value) {
30806 if (value && !(value instanceof Date)) {
30807 throw Error('The ng-model for md-datepicker must be a Date instance. ' +
30808 'Currently the model is a: ' + (typeof value));
30809 }
30810
30811 self.onExternalChange(value);
30812
30813 return value;
30814 });
30815
30816 // Responds to external error state changes (e.g. ng-required based on another input).
30817 ngModelCtrl.$viewChangeListeners.unshift(angular.bind(this, this.updateErrorState));
30818
30819 // Forwards any events from the input to the root element. This is necessary to get `updateOn`
30820 // working for events that don't bubble (e.g. 'blur') since Angular binds the handlers to
30821 // the `<md-datepicker>`.
30822 var updateOn = self.$mdUtil.getModelOption(ngModelCtrl, 'updateOn');
30823
30824 if (updateOn) {
30825 this.ngInputElement.on(
30826 updateOn,
30827 angular.bind(this.$element, this.$element.triggerHandler, updateOn)
30828 );
30829 }
30830 };
30831
30832 /**
30833 * Attach event listeners for both the text input and the md-calendar.
30834 * Events are used instead of ng-model so that updates don't infinitely update the other
30835 * on a change. This should also be more performant than using a $watch.
30836 */
30837 DatePickerCtrl.prototype.attachChangeListeners = function() {
30838 var self = this;
30839
30840 self.$scope.$on('md-calendar-change', function(event, date) {
30841 self.setModelValue(date);
30842 self.onExternalChange(date);
30843 self.closeCalendarPane();
30844 });
30845
30846 self.ngInputElement.on('input', angular.bind(self, self.resizeInputElement));
30847
30848 var debounceInterval = angular.isDefined(this.debounceInterval) ?
30849 this.debounceInterval : DEFAULT_DEBOUNCE_INTERVAL;
30850 self.ngInputElement.on('input', self.$mdUtil.debounce(self.handleInputEvent,
30851 debounceInterval, self));
30852 };
30853
30854 /** Attach event listeners for user interaction. */
30855 DatePickerCtrl.prototype.attachInteractionListeners = function() {
30856 var self = this;
30857 var $scope = this.$scope;
30858 var keyCodes = this.$mdConstant.KEY_CODE;
30859
30860 // Add event listener through angular so that we can triggerHandler in unit tests.
30861 self.ngInputElement.on('keydown', function(event) {
30862 if (event.altKey && event.keyCode == keyCodes.DOWN_ARROW) {
30863 self.openCalendarPane(event);
30864 $scope.$digest();
30865 }
30866 });
30867
30868 if (self.openOnFocus) {
30869 self.ngInputElement.on('focus', angular.bind(self, self.openCalendarPane));
30870 angular.element(self.$window).on('blur', self.windowBlurHandler);
30871
30872 $scope.$on('$destroy', function() {
30873 angular.element(self.$window).off('blur', self.windowBlurHandler);
30874 });
30875 }
30876
30877 $scope.$on('md-calendar-close', function() {
30878 self.closeCalendarPane();
30879 });
30880 };
30881
30882 /**
30883 * Capture properties set to the date-picker and imperitively handle internal changes.
30884 * This is done to avoid setting up additional $watches.
30885 */
30886 DatePickerCtrl.prototype.installPropertyInterceptors = function() {
30887 var self = this;
30888
30889 if (this.$attrs.ngDisabled) {
30890 // The expression is to be evaluated against the directive element's scope and not
30891 // the directive's isolate scope.
30892 var scope = this.$scope.$parent;
30893
30894 if (scope) {
30895 scope.$watch(this.$attrs.ngDisabled, function(isDisabled) {
30896 self.setDisabled(isDisabled);
30897 });
30898 }
30899 }
30900
30901 Object.defineProperty(this, 'placeholder', {
30902 get: function() { return self.inputElement.placeholder; },
30903 set: function(value) { self.inputElement.placeholder = value || ''; }
30904 });
30905 };
30906
30907 /**
30908 * Sets whether the date-picker is disabled.
30909 * @param {boolean} isDisabled
30910 */
30911 DatePickerCtrl.prototype.setDisabled = function(isDisabled) {
30912 this.isDisabled = isDisabled;
30913 this.inputElement.disabled = isDisabled;
30914
30915 if (this.calendarButton) {
30916 this.calendarButton.disabled = isDisabled;
30917 }
30918 };
30919
30920 /**
30921 * Sets the custom ngModel.$error flags to be consumed by ngMessages. Flags are:
30922 * - mindate: whether the selected date is before the minimum date.
30923 * - maxdate: whether the selected flag is after the maximum date.
30924 * - filtered: whether the selected date is allowed by the custom filtering function.
30925 * - valid: whether the entered text input is a valid date
30926 *
30927 * The 'required' flag is handled automatically by ngModel.
30928 *
30929 * @param {Date=} opt_date Date to check. If not given, defaults to the datepicker's model value.
30930 */
30931 DatePickerCtrl.prototype.updateErrorState = function(opt_date) {
30932 var date = opt_date || this.date;
30933
30934 // Clear any existing errors to get rid of anything that's no longer relevant.
30935 this.clearErrorState();
30936
30937 if (this.dateUtil.isValidDate(date)) {
30938 // Force all dates to midnight in order to ignore the time portion.
30939 date = this.dateUtil.createDateAtMidnight(date);
30940
30941 if (this.dateUtil.isValidDate(this.minDate)) {
30942 var minDate = this.dateUtil.createDateAtMidnight(this.minDate);
30943 this.ngModelCtrl.$setValidity('mindate', date >= minDate);
30944 }
30945
30946 if (this.dateUtil.isValidDate(this.maxDate)) {
30947 var maxDate = this.dateUtil.createDateAtMidnight(this.maxDate);
30948 this.ngModelCtrl.$setValidity('maxdate', date <= maxDate);
30949 }
30950
30951 if (angular.isFunction(this.dateFilter)) {
30952 this.ngModelCtrl.$setValidity('filtered', this.dateFilter(date));
30953 }
30954 } else {
30955 // The date is seen as "not a valid date" if there is *something* set
30956 // (i.e.., not null or undefined), but that something isn't a valid date.
30957 this.ngModelCtrl.$setValidity('valid', date == null);
30958 }
30959
30960 angular.element(this.inputContainer).toggleClass(INVALID_CLASS, !this.ngModelCtrl.$valid);
30961 };
30962
30963 /** Clears any error flags set by `updateErrorState`. */
30964 DatePickerCtrl.prototype.clearErrorState = function() {
30965 this.inputContainer.classList.remove(INVALID_CLASS);
30966 ['mindate', 'maxdate', 'filtered', 'valid'].forEach(function(field) {
30967 this.ngModelCtrl.$setValidity(field, true);
30968 }, this);
30969 };
30970
30971 /** Resizes the input element based on the size of its content. */
30972 DatePickerCtrl.prototype.resizeInputElement = function() {
30973 this.inputElement.size = this.inputElement.value.length + EXTRA_INPUT_SIZE;
30974 };
30975
30976 /**
30977 * Sets the model value if the user input is a valid date.
30978 * Adds an invalid class to the input element if not.
30979 */
30980 DatePickerCtrl.prototype.handleInputEvent = function() {
30981 var inputString = this.inputElement.value;
30982 var parsedDate = inputString ? this.locale.parseDate(inputString) : null;
30983 this.dateUtil.setDateTimeToMidnight(parsedDate);
30984
30985 // An input string is valid if it is either empty (representing no date)
30986 // or if it parses to a valid date that the user is allowed to select.
30987 var isValidInput = inputString == '' || (
30988 this.dateUtil.isValidDate(parsedDate) &&
30989 this.locale.isDateComplete(inputString) &&
30990 this.isDateEnabled(parsedDate)
30991 );
30992
30993 // The datepicker's model is only updated when there is a valid input.
30994 if (isValidInput) {
30995 this.setModelValue(parsedDate);
30996 this.date = parsedDate;
30997 }
30998
30999 this.updateErrorState(parsedDate);
31000 };
31001
31002 /**
31003 * Check whether date is in range and enabled
31004 * @param {Date=} opt_date
31005 * @return {boolean} Whether the date is enabled.
31006 */
31007 DatePickerCtrl.prototype.isDateEnabled = function(opt_date) {
31008 return this.dateUtil.isDateWithinRange(opt_date, this.minDate, this.maxDate) &&
31009 (!angular.isFunction(this.dateFilter) || this.dateFilter(opt_date));
31010 };
31011
31012 /** Position and attach the floating calendar to the document. */
31013 DatePickerCtrl.prototype.attachCalendarPane = function() {
31014 var calendarPane = this.calendarPane;
31015 var body = document.body;
31016
31017 calendarPane.style.transform = '';
31018 this.$element.addClass(OPEN_CLASS);
31019 this.mdInputContainer && this.mdInputContainer.element.addClass(OPEN_CLASS);
31020 angular.element(body).addClass('md-datepicker-is-showing');
31021
31022 var elementRect = this.inputContainer.getBoundingClientRect();
31023 var bodyRect = body.getBoundingClientRect();
31024
31025 if (!this.topMargin || this.topMargin < 0) {
31026 this.topMargin = (this.inputMask.parent().prop('clientHeight') - this.ngInputElement.prop('clientHeight')) / 2;
31027 }
31028
31029 // Check to see if the calendar pane would go off the screen. If so, adjust position
31030 // accordingly to keep it within the viewport.
31031 var paneTop = elementRect.top - bodyRect.top - this.topMargin;
31032 var paneLeft = elementRect.left - bodyRect.left - this.leftMargin;
31033
31034 // If ng-material has disabled body scrolling (for example, if a dialog is open),
31035 // then it's possible that the already-scrolled body has a negative top/left. In this case,
31036 // we want to treat the "real" top as (0 - bodyRect.top). In a normal scrolling situation,
31037 // though, the top of the viewport should just be the body's scroll position.
31038 var viewportTop = (bodyRect.top < 0 && document.body.scrollTop == 0) ?
31039 -bodyRect.top :
31040 document.body.scrollTop;
31041
31042 var viewportLeft = (bodyRect.left < 0 && document.body.scrollLeft == 0) ?
31043 -bodyRect.left :
31044 document.body.scrollLeft;
31045
31046 var viewportBottom = viewportTop + this.$window.innerHeight;
31047 var viewportRight = viewportLeft + this.$window.innerWidth;
31048
31049 // Creates an overlay with a hole the same size as element. We remove a pixel or two
31050 // on each end to make it overlap slightly. The overlay's background is added in
31051 // the theme in the form of a box-shadow with a huge spread.
31052 this.inputMask.css({
31053 position: 'absolute',
31054 left: this.leftMargin + 'px',
31055 top: this.topMargin + 'px',
31056 width: (elementRect.width - 1) + 'px',
31057 height: (elementRect.height - 2) + 'px'
31058 });
31059
31060 // If the right edge of the pane would be off the screen and shifting it left by the
31061 // difference would not go past the left edge of the screen. If the calendar pane is too
31062 // big to fit on the screen at all, move it to the left of the screen and scale the entire
31063 // element down to fit.
31064 if (paneLeft + CALENDAR_PANE_WIDTH > viewportRight) {
31065 if (viewportRight - CALENDAR_PANE_WIDTH > 0) {
31066 paneLeft = viewportRight - CALENDAR_PANE_WIDTH;
31067 } else {
31068 paneLeft = viewportLeft;
31069 var scale = this.$window.innerWidth / CALENDAR_PANE_WIDTH;
31070 calendarPane.style.transform = 'scale(' + scale + ')';
31071 }
31072
31073 calendarPane.classList.add('md-datepicker-pos-adjusted');
31074 }
31075
31076 // If the bottom edge of the pane would be off the screen and shifting it up by the
31077 // difference would not go past the top edge of the screen.
31078 if (paneTop + CALENDAR_PANE_HEIGHT > viewportBottom &&
31079 viewportBottom - CALENDAR_PANE_HEIGHT > viewportTop) {
31080 paneTop = viewportBottom - CALENDAR_PANE_HEIGHT;
31081 calendarPane.classList.add('md-datepicker-pos-adjusted');
31082 }
31083
31084 calendarPane.style.left = paneLeft + 'px';
31085 calendarPane.style.top = paneTop + 'px';
31086 document.body.appendChild(calendarPane);
31087
31088 // Add CSS class after one frame to trigger open animation.
31089 this.$$rAF(function() {
31090 calendarPane.classList.add('md-pane-open');
31091 });
31092 };
31093
31094 /** Detach the floating calendar pane from the document. */
31095 DatePickerCtrl.prototype.detachCalendarPane = function() {
31096 this.$element.removeClass(OPEN_CLASS);
31097 this.mdInputContainer && this.mdInputContainer.element.removeClass(OPEN_CLASS);
31098 angular.element(document.body).removeClass('md-datepicker-is-showing');
31099 this.calendarPane.classList.remove('md-pane-open');
31100 this.calendarPane.classList.remove('md-datepicker-pos-adjusted');
31101
31102 if (this.isCalendarOpen) {
31103 this.$mdUtil.enableScrolling();
31104 }
31105
31106 if (this.calendarPane.parentNode) {
31107 // Use native DOM removal because we do not want any of the
31108 // angular state of this element to be disposed.
31109 this.calendarPane.parentNode.removeChild(this.calendarPane);
31110 }
31111 };
31112
31113 /**
31114 * Open the floating calendar pane.
31115 * @param {Event} event
31116 */
31117 DatePickerCtrl.prototype.openCalendarPane = function(event) {
31118 if (!this.isCalendarOpen && !this.isDisabled && !this.inputFocusedOnWindowBlur) {
31119 this.isCalendarOpen = this.isOpen = true;
31120 this.calendarPaneOpenedFrom = event.target;
31121
31122 // Because the calendar pane is attached directly to the body, it is possible that the
31123 // rest of the component (input, etc) is in a different scrolling container, such as
31124 // an md-content. This means that, if the container is scrolled, the pane would remain
31125 // stationary. To remedy this, we disable scrolling while the calendar pane is open, which
31126 // also matches the native behavior for things like `<select>` on Mac and Windows.
31127 this.$mdUtil.disableScrollAround(this.calendarPane);
31128
31129 this.attachCalendarPane();
31130 this.focusCalendar();
31131 this.evalAttr('ngFocus');
31132
31133 // Attach click listener inside of a timeout because, if this open call was triggered by a
31134 // click, we don't want it to be immediately propogated up to the body and handled.
31135 var self = this;
31136 this.$mdUtil.nextTick(function() {
31137 // Use 'touchstart` in addition to click in order to work on iOS Safari, where click
31138 // events aren't propogated under most circumstances.
31139 // See http://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html
31140 self.documentElement.on('click touchstart', self.bodyClickHandler);
31141 }, false);
31142
31143 window.addEventListener(this.windowEventName, this.windowEventHandler);
31144 }
31145 };
31146
31147 /** Close the floating calendar pane. */
31148 DatePickerCtrl.prototype.closeCalendarPane = function() {
31149 if (this.isCalendarOpen) {
31150 var self = this;
31151
31152 self.detachCalendarPane();
31153 self.ngModelCtrl.$setTouched();
31154 self.evalAttr('ngBlur');
31155
31156 self.documentElement.off('click touchstart', self.bodyClickHandler);
31157 window.removeEventListener(self.windowEventName, self.windowEventHandler);
31158
31159 self.calendarPaneOpenedFrom.focus();
31160 self.calendarPaneOpenedFrom = null;
31161
31162 if (self.openOnFocus) {
31163 // Ensures that all focus events have fired before resetting
31164 // the calendar. Prevents the calendar from reopening immediately
31165 // in IE when md-open-on-focus is set. Also it needs to trigger
31166 // a digest, in order to prevent issues where the calendar wasn't
31167 // showing up on the next open.
31168 self.$mdUtil.nextTick(reset);
31169 } else {
31170 reset();
31171 }
31172 }
31173
31174 function reset(){
31175 self.isCalendarOpen = self.isOpen = false;
31176 }
31177 };
31178
31179 /** Gets the controller instance for the calendar in the floating pane. */
31180 DatePickerCtrl.prototype.getCalendarCtrl = function() {
31181 return angular.element(this.calendarPane.querySelector('md-calendar')).controller('mdCalendar');
31182 };
31183
31184 /** Focus the calendar in the floating pane. */
31185 DatePickerCtrl.prototype.focusCalendar = function() {
31186 // Use a timeout in order to allow the calendar to be rendered, as it is gated behind an ng-if.
31187 var self = this;
31188 this.$mdUtil.nextTick(function() {
31189 self.getCalendarCtrl().focus();
31190 }, false);
31191 };
31192
31193 /**
31194 * Sets whether the input is currently focused.
31195 * @param {boolean} isFocused
31196 */
31197 DatePickerCtrl.prototype.setFocused = function(isFocused) {
31198 if (!isFocused) {
31199 this.ngModelCtrl.$setTouched();
31200 }
31201
31202 // The ng* expressions shouldn't be evaluated when mdOpenOnFocus is on,
31203 // because they also get called when the calendar is opened/closed.
31204 if (!this.openOnFocus) {
31205 this.evalAttr(isFocused ? 'ngFocus' : 'ngBlur');
31206 }
31207
31208 this.isFocused = isFocused;
31209 };
31210
31211 /**
31212 * Handles a click on the document body when the floating calendar pane is open.
31213 * Closes the floating calendar pane if the click is not inside of it.
31214 * @param {MouseEvent} event
31215 */
31216 DatePickerCtrl.prototype.handleBodyClick = function(event) {
31217 if (this.isCalendarOpen) {
31218 var isInCalendar = this.$mdUtil.getClosest(event.target, 'md-calendar');
31219
31220 if (!isInCalendar) {
31221 this.closeCalendarPane();
31222 }
31223
31224 this.$scope.$digest();
31225 }
31226 };
31227
31228 /**
31229 * Handles the event when the user navigates away from the current tab. Keeps track of
31230 * whether the input was focused when the event happened, in order to prevent the calendar
31231 * from re-opening.
31232 */
31233 DatePickerCtrl.prototype.handleWindowBlur = function() {
31234 this.inputFocusedOnWindowBlur = document.activeElement === this.inputElement;
31235 };
31236
31237 /**
31238 * Evaluates an attribute expression against the parent scope.
31239 * @param {String} attr Name of the attribute to be evaluated.
31240 */
31241 DatePickerCtrl.prototype.evalAttr = function(attr) {
31242 if (this.$attrs[attr]) {
31243 this.$scope.$parent.$eval(this.$attrs[attr]);
31244 }
31245 };
31246
31247 /**
31248 * Sets the ng-model value by first converting the date object into a strng. Converting it
31249 * is necessary, in order to pass Angular's `input[type="date"]` validations. Angular turns
31250 * the value into a Date object afterwards, before setting it on the model.
31251 * @param {Date=} value Date to be set as the model value.
31252 */
31253 DatePickerCtrl.prototype.setModelValue = function(value) {
31254 var timezone = this.$mdUtil.getModelOption(this.ngModelCtrl, 'timezone');
31255 this.ngModelCtrl.$setViewValue(this.ngDateFilter(value, 'yyyy-MM-dd', timezone));
31256 };
31257
31258 /**
31259 * Updates the datepicker when a model change occurred externally.
31260 * @param {Date=} value Value that was set to the model.
31261 */
31262 DatePickerCtrl.prototype.onExternalChange = function(value) {
31263 var timezone = this.$mdUtil.getModelOption(this.ngModelCtrl, 'timezone');
31264
31265 this.date = value;
31266 this.inputElement.value = this.locale.formatDate(value, timezone);
31267 this.mdInputContainer && this.mdInputContainer.setHasValue(!!value);
31268 this.resizeInputElement();
31269 this.updateErrorState();
31270 };
31271})();
31272
31273})();
31274(function(){
31275"use strict";
31276
31277angular
31278 .module('material.components.icon')
31279 .directive('mdIcon', ['$mdIcon', '$mdTheming', '$mdAria', '$sce', mdIconDirective]);
31280
31281/**
31282 * @ngdoc directive
31283 * @name mdIcon
31284 * @module material.components.icon
31285 *
31286 * @restrict E
31287 *
31288 * @description
31289 * The `md-icon` directive makes it easier to use vector-based icons in your app (as opposed to
31290 * raster-based icons types like PNG). The directive supports both icon fonts and SVG icons.
31291 *
31292 * Icons should be considered view-only elements that should not be used directly as buttons; instead nest a `<md-icon>`
31293 * inside a `md-button` to add hover and click features.
31294 *
31295 * ### Icon fonts
31296 * Icon fonts are a technique in which you use a font where the glyphs in the font are
31297 * your icons instead of text. Benefits include a straightforward way to bundle everything into a
31298 * single HTTP request, simple scaling, easy color changing, and more.
31299 *
31300 * `md-icon` lets you consume an icon font by letting you reference specific icons in that font
31301 * by name rather than character code.
31302 *
31303 * ### SVG
31304 * For SVGs, the problem with using `<img>` or a CSS `background-image` is that you can't take
31305 * advantage of some SVG features, such as styling specific parts of the icon with CSS or SVG
31306 * animation.
31307 *
31308 * `md-icon` makes it easier to use SVG icons by *inlining* the SVG into an `<svg>` element in the
31309 * document. The most straightforward way of referencing an SVG icon is via URL, just like a
31310 * traditional `<img>`. `$mdIconProvider`, as a convenience, lets you _name_ an icon so you can
31311 * reference it by name instead of URL throughout your templates.
31312 *
31313 * Additionally, you may not want to make separate HTTP requests for every icon, so you can bundle
31314 * your SVG icons together and pre-load them with $mdIconProvider as an icon set. An icon set can
31315 * also be given a name, which acts as a namespace for individual icons, so you can reference them
31316 * like `"social:cake"`.
31317 *
31318 * When using SVGs, both external SVGs (via URLs) or sets of SVGs [from icon sets] can be
31319 * easily loaded and used. When using font-icons, developers must follow three (3) simple steps:
31320 *
31321 * <ol>
31322 * <li>Load the font library. e.g.<br/>
31323 * `<link href="https://fonts.googleapis.com/icon?family=Material+Icons"
31324 * rel="stylesheet">`
31325 * </li>
31326 * <li>
31327 * Use either (a) font-icon class names or (b) a fontset and a font ligature to render the font glyph by
31328 * using its textual name _or_ numerical character reference. Note that `material-icons` is the default fontset when
31329 * none is specified.
31330 * </li>
31331 * <li> Use any of the following templates: <br/>
31332 * <ul>
31333 * <li>`<md-icon md-font-icon="classname"></md-icon>`</li>
31334 * <li>`<md-icon md-font-set="font library classname or alias">textual_name</md-icon>`</li>
31335 * <li>`<md-icon> numerical_character_reference </md-icon>`</li>
31336 * <li>`<md-icon ng_bind="'textual_name'"></md-icon>`</li>
31337 * <li>`<md-icon ng-bind="scopeVariable"></md-icon>`</li>
31338 * </ul>
31339 * </li>
31340 * </ol>
31341 *
31342 * Full details for these steps can be found:
31343 *
31344 * <ul>
31345 * <li>http://google.github.io/material-design-icons/</li>
31346 * <li>http://google.github.io/material-design-icons/#icon-font-for-the-web</li>
31347 * </ul>
31348 *
31349 * The Material Design icon style <code>.material-icons</code> and the icon font references are published in
31350 * Material Design Icons:
31351 *
31352 * <ul>
31353 * <li>https://design.google.com/icons/</li>
31354 * <li>https://design.google.com/icons/#ic_accessibility</li>
31355 * </ul>
31356 *
31357 * ### Localization
31358 *
31359 * Because an `md-icon` element's text content is not intended to translated, it is recommended to declare the text
31360 * content for an `md-icon` element in its start tag. Instead of using the HTML text content, consider using `ng-bind`
31361 * with a scope variable or literal string.
31362 *
31363 * Examples:
31364 *
31365 * <ul>
31366 * <li>`<md-icon ng-bind="myIconVariable"></md-icon>`</li>
31367 * <li>`<md-icon ng-bind="'menu'"></md-icon>`
31368 * </ul>
31369 *
31370 * <h2 id="material_design_icons">Material Design Icons</h2>
31371 * Using the Material Design Icon-Selector, developers can easily and quickly search for a Material Design font-icon and
31372 * determine its textual name and character reference code. Click on any icon to see the slide-up information
31373 * panel with details regarding a SVG download or information on the font-icon usage.
31374 *
31375 * <a href="https://www.google.com/design/icons/#ic_accessibility" target="_blank" style="border-bottom:none;">
31376 * <img src="https://cloud.githubusercontent.com/assets/210413/7902490/fe8dd14c-0780-11e5-98fb-c821cc6475e6.png"
31377 * aria-label="Material Design Icon-Selector" style="max-width:75%;padding-left:10%">
31378 * </a>
31379 *
31380 * <span class="image_caption">
31381 * Click on the image above to link to the
31382 * <a href="https://design.google.com/icons/#ic_accessibility" target="_blank">Material Design Icon-Selector</a>.
31383 * </span>
31384 *
31385 * @param {string} md-font-icon String name of CSS icon associated with the font-face will be used
31386 * to render the icon. Requires the fonts and the named CSS styles to be preloaded.
31387 * @param {string} md-font-set CSS style name associated with the font library; which will be assigned as
31388 * the class for the font-icon ligature. This value may also be an alias that is used to lookup the classname;
31389 * internally use `$mdIconProvider.fontSet(<alias>)` to determine the style name.
31390 * @param {string} md-svg-src String URL (or expression) used to load, cache, and display an
31391 * external SVG.
31392 * @param {string} md-svg-icon md-svg-icon String name used for lookup of the icon from the internal cache;
31393 * interpolated strings or expressions may also be used. Specific set names can be used with
31394 * the syntax `<set name>:<icon name>`.<br/><br/>
31395 * To use icon sets, developers are required to pre-register the sets using the `$mdIconProvider` service.
31396 * @param {string=} aria-label Labels icon for accessibility. If an empty string is provided, icon
31397 * will be hidden from accessibility layer with `aria-hidden="true"`. If there's no aria-label on the icon
31398 * nor a label on the parent element, a warning will be logged to the console.
31399 * @param {string=} alt Labels icon for accessibility. If an empty string is provided, icon
31400 * will be hidden from accessibility layer with `aria-hidden="true"`. If there's no alt on the icon
31401 * nor a label on the parent element, a warning will be logged to the console.
31402 *
31403 * @usage
31404 * When using SVGs:
31405 * <hljs lang="html">
31406 *
31407 * <!-- Icon ID; may contain optional icon set prefix; icons must registered using $mdIconProvider -->
31408 * <md-icon md-svg-icon="social:android" aria-label="android " ></md-icon>
31409 *
31410 * <!-- Icon urls; may be preloaded in templateCache -->
31411 * <md-icon md-svg-src="/android.svg" aria-label="android " ></md-icon>
31412 * <md-icon md-svg-src="{{ getAndroid() }}" aria-label="android " ></md-icon>
31413 *
31414 * </hljs>
31415 *
31416 * Use the <code>$mdIconProvider</code> to configure your application with
31417 * svg iconsets.
31418 *
31419 * <hljs lang="js">
31420 * angular.module('appSvgIconSets', ['ngMaterial'])
31421 * .controller('DemoCtrl', function($scope) {})
31422 * .config(function($mdIconProvider) {
31423 * $mdIconProvider
31424 * .iconSet('social', 'img/icons/sets/social-icons.svg', 24)
31425 * .defaultIconSet('img/icons/sets/core-icons.svg', 24);
31426 * });
31427 * </hljs>
31428 *
31429 *
31430 * When using Font Icons with classnames:
31431 * <hljs lang="html">
31432 *
31433 * <md-icon md-font-icon="android" aria-label="android" ></md-icon>
31434 * <md-icon class="icon_home" aria-label="Home" ></md-icon>
31435 *
31436 * </hljs>
31437 *
31438 * When using Material Font Icons with ligatures:
31439 * <hljs lang="html">
31440 * <!--
31441 * For Material Design Icons
31442 * The class '.material-icons' is auto-added if a style has NOT been specified
31443 * since `material-icons` is the default fontset. So your markup:
31444 * -->
31445 * <md-icon> face </md-icon>
31446 * <!-- becomes this at runtime: -->
31447 * <md-icon md-font-set="material-icons"> face </md-icon>
31448 * <!-- If the fontset does not support ligature names, then we need to use the ligature unicode.-->
31449 * <md-icon>  </md-icon>
31450 * <!-- The class '.material-icons' must be manually added if other styles are also specified-->
31451 * <md-icon class="material-icons md-light md-48"> face </md-icon>
31452 * </hljs>
31453 *
31454 * When using other Font-Icon libraries:
31455 *
31456 * <hljs lang="js">
31457 * // Specify a font-icon style alias
31458 * angular.config(function($mdIconProvider) {
31459 * $mdIconProvider.fontSet('md', 'material-icons');
31460 * });
31461 * </hljs>
31462 *
31463 * <hljs lang="html">
31464 * <md-icon md-font-set="md">favorite</md-icon>
31465 * </hljs>
31466 *
31467 */
31468function mdIconDirective($mdIcon, $mdTheming, $mdAria, $sce) {
31469
31470 return {
31471 restrict: 'E',
31472 link : postLink
31473 };
31474
31475
31476 /**
31477 * Directive postLink
31478 * Supports embedded SVGs, font-icons, & external SVGs
31479 */
31480 function postLink(scope, element, attr) {
31481 $mdTheming(element);
31482 var lastFontIcon = attr.mdFontIcon;
31483 var lastFontSet = $mdIcon.fontSet(attr.mdFontSet);
31484
31485 prepareForFontIcon();
31486
31487 attr.$observe('mdFontIcon', fontIconChanged);
31488 attr.$observe('mdFontSet', fontIconChanged);
31489
31490 // Keep track of the content of the svg src so we can compare against it later to see if the
31491 // attribute is static (and thus safe).
31492 var originalSvgSrc = element[0].getAttribute(attr.$attr.mdSvgSrc);
31493
31494 // If using a font-icon, then the textual name of the icon itself
31495 // provides the aria-label.
31496
31497 var label = attr.alt || attr.mdFontIcon || attr.mdSvgIcon || element.text();
31498 var attrName = attr.$normalize(attr.$attr.mdSvgIcon || attr.$attr.mdSvgSrc || '');
31499
31500 if ( !attr['aria-label'] ) {
31501
31502 if (label !== '' && !parentsHaveText() ) {
31503
31504 $mdAria.expect(element, 'aria-label', label);
31505 $mdAria.expect(element, 'role', 'img');
31506
31507 } else if ( !element.text() ) {
31508 // If not a font-icon with ligature, then
31509 // hide from the accessibility layer.
31510
31511 $mdAria.expect(element, 'aria-hidden', 'true');
31512 }
31513 }
31514
31515 if (attrName) {
31516 // Use either pre-configured SVG or URL source, respectively.
31517 attr.$observe(attrName, function(attrVal) {
31518 element.empty();
31519 if (attrVal) {
31520 $mdIcon(attrVal)
31521 .then(function(svg) {
31522 element.empty();
31523 element.append(svg);
31524 });
31525 }
31526
31527 });
31528 }
31529
31530 function parentsHaveText() {
31531 var parent = element.parent();
31532 if (parent.attr('aria-label') || parent.text()) {
31533 return true;
31534 }
31535 else if(parent.parent().attr('aria-label') || parent.parent().text()) {
31536 return true;
31537 }
31538 return false;
31539 }
31540
31541 function prepareForFontIcon() {
31542 if (!attr.mdSvgIcon && !attr.mdSvgSrc) {
31543 if (attr.mdFontIcon) {
31544 element.addClass('md-font ' + attr.mdFontIcon);
31545 }
31546
31547 element.addClass(lastFontSet);
31548 }
31549 }
31550
31551 function fontIconChanged() {
31552 if (!attr.mdSvgIcon && !attr.mdSvgSrc) {
31553 if (attr.mdFontIcon) {
31554 element.removeClass(lastFontIcon);
31555 element.addClass(attr.mdFontIcon);
31556
31557 lastFontIcon = attr.mdFontIcon;
31558 }
31559
31560 var fontSet = $mdIcon.fontSet(attr.mdFontSet);
31561
31562 if (lastFontSet !== fontSet) {
31563 element.removeClass(lastFontSet);
31564 element.addClass(fontSet);
31565
31566 lastFontSet = fontSet;
31567 }
31568 }
31569 }
31570 }
31571}
31572
31573})();
31574(function(){
31575"use strict";
31576
31577
31578MdIconService.$inject = ["config", "$templateRequest", "$q", "$log", "$mdUtil", "$sce"];angular
31579 .module('material.components.icon')
31580 .constant('$$mdSvgRegistry', {
31581 'mdTabsArrow': 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxnPjxwb2x5Z29uIHBvaW50cz0iMTUuNCw3LjQgMTQsNiA4LDEyIDE0LDE4IDE1LjQsMTYuNiAxMC44LDEyICIvPjwvZz48L3N2Zz4=',
31582 'mdClose': 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxnPjxwYXRoIGQ9Ik0xOSA2LjQxbC0xLjQxLTEuNDEtNS41OSA1LjU5LTUuNTktNS41OS0xLjQxIDEuNDEgNS41OSA1LjU5LTUuNTkgNS41OSAxLjQxIDEuNDEgNS41OS01LjU5IDUuNTkgNS41OSAxLjQxLTEuNDEtNS41OS01LjU5eiIvPjwvZz48L3N2Zz4=',
31583 'mdCancel': 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxnPjxwYXRoIGQ9Ik0xMiAyYy01LjUzIDAtMTAgNC40Ny0xMCAxMHM0LjQ3IDEwIDEwIDEwIDEwLTQuNDcgMTAtMTAtNC40Ny0xMC0xMC0xMHptNSAxMy41OWwtMS40MSAxLjQxLTMuNTktMy41OS0zLjU5IDMuNTktMS40MS0xLjQxIDMuNTktMy41OS0zLjU5LTMuNTkgMS40MS0xLjQxIDMuNTkgMy41OSAzLjU5LTMuNTkgMS40MSAxLjQxLTMuNTkgMy41OSAzLjU5IDMuNTl6Ii8+PC9nPjwvc3ZnPg==',
31584 'mdMenu': 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0zLDZIMjFWOEgzVjZNMywxMUgyMVYxM0gzVjExTTMsMTZIMjFWMThIM1YxNloiIC8+PC9zdmc+',
31585 'mdToggleArrow': 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgNDggNDgiPjxwYXRoIGQ9Ik0yNCAxNmwtMTIgMTIgMi44MyAyLjgzIDkuMTctOS4xNyA5LjE3IDkuMTcgMi44My0yLjgzeiIvPjxwYXRoIGQ9Ik0wIDBoNDh2NDhoLTQ4eiIgZmlsbD0ibm9uZSIvPjwvc3ZnPg==',
31586 'mdCalendar': 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTkgM2gtMVYxaC0ydjJIOFYxSDZ2Mkg1Yy0xLjExIDAtMS45OS45LTEuOTkgMkwzIDE5YzAgMS4xLjg5IDIgMiAyaDE0YzEuMSAwIDItLjkgMi0yVjVjMC0xLjEtLjktMi0yLTJ6bTAgMTZINVY4aDE0djExek03IDEwaDV2NUg3eiIvPjwvc3ZnPg==',
31587 'mdChecked': 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxnPjxwYXRoIGQ9Ik05IDE2LjE3TDQuODMgMTJsLTEuNDIgMS40MUw5IDE5IDIxIDdsLTEuNDEtMS40MXoiLz48L2c+PC9zdmc+'
31588 })
31589 .provider('$mdIcon', MdIconProvider);
31590
31591/**
31592 * @ngdoc service
31593 * @name $mdIconProvider
31594 * @module material.components.icon
31595 *
31596 * @description
31597 * `$mdIconProvider` is used only to register icon IDs with URLs. These configuration features allow
31598 * icons and icon sets to be pre-registered and associated with source URLs **before** the `<md-icon />`
31599 * directives are compiled.
31600 *
31601 * If using font-icons, the developer is responsible for loading the fonts.
31602 *
31603 * If using SVGs, loading of the actual svg files are deferred to on-demand requests and are loaded
31604 * internally by the `$mdIcon` service using the `$templateRequest` service. When an SVG is
31605 * requested by name/ID, the `$mdIcon` service searches its registry for the associated source URL;
31606 * that URL is used to on-demand load and parse the SVG dynamically.
31607 *
31608 * The `$templateRequest` service expects the icons source to be loaded over trusted URLs.<br/>
31609 * This means, when loading icons from an external URL, you have to trust the URL in the `$sceDelegateProvider`.
31610 *
31611 * <hljs lang="js">
31612 * app.config(function($sceDelegateProvider) {
31613 * $sceDelegateProvider.resourceUrlWhitelist([
31614 * // Adding 'self' to the whitelist, will allow requests from the current origin.
31615 * 'self',
31616 * // Using double asterisks here, will allow all URLs to load.
31617 * // We recommend to only specify the given domain you want to allow.
31618 * '**'
31619 * ]);
31620 * });
31621 * </hljs>
31622 *
31623 * Read more about the [$sceDelegateProvider](https://docs.angularjs.org/api/ng/provider/$sceDelegateProvider).
31624 *
31625 * **Notice:** Most font-icons libraries do not support ligatures (for example `fontawesome`).<br/>
31626 * In such cases you are not able to use the icon's ligature name - Like so:
31627 *
31628 * <hljs lang="html">
31629 * <md-icon md-font-set="fa">fa-bell</md-icon>
31630 * </hljs>
31631 *
31632 * You should instead use the given unicode, instead of the ligature name.
31633 *
31634 * <p ng-hide="true"> ##// Notice we can't use a hljs element here, because the characters will be escaped.</p>
31635 * ```html
31636 * <md-icon md-font-set="fa"></md-icon>
31637 * ```
31638 *
31639 * All unicode ligatures are prefixed with the `&#x` string.
31640 *
31641 * @usage
31642 * <hljs lang="js">
31643 * app.config(function($mdIconProvider) {
31644 *
31645 * // Configure URLs for icons specified by [set:]id.
31646 *
31647 * $mdIconProvider
31648 * .defaultFontSet( 'fa' ) // This sets our default fontset className.
31649 * .defaultIconSet('my/app/icons.svg') // Register a default set of SVG icons
31650 * .iconSet('social', 'my/app/social.svg') // Register a named icon set of SVGs
31651 * .icon('android', 'my/app/android.svg') // Register a specific icon (by name)
31652 * .icon('work:chair', 'my/app/chair.svg'); // Register icon in a specific set
31653 * });
31654 * </hljs>
31655 *
31656 * SVG icons and icon sets can be easily pre-loaded and cached using either (a) a build process or (b) a runtime
31657 * **startup** process (shown below):
31658 *
31659 * <hljs lang="js">
31660 * app.config(function($mdIconProvider) {
31661 *
31662 * // Register a default set of SVG icon definitions
31663 * $mdIconProvider.defaultIconSet('my/app/icons.svg')
31664 *
31665 * })
31666 * .run(function($templateRequest){
31667 *
31668 * // Pre-fetch icons sources by URL and cache in the $templateCache...
31669 * // subsequent $templateRequest calls will look there first.
31670 *
31671 * var urls = [ 'imy/app/icons.svg', 'img/icons/android.svg'];
31672 *
31673 * angular.forEach(urls, function(url) {
31674 * $templateRequest(url);
31675 * });
31676 *
31677 * });
31678 *
31679 * </hljs>
31680 *
31681 * > <b>Note:</b> The loaded SVG data is subsequently cached internally for future requests.
31682 *
31683 */
31684
31685/**
31686 * @ngdoc method
31687 * @name $mdIconProvider#icon
31688 *
31689 * @description
31690 * Register a source URL for a specific icon name; the name may include optional 'icon set' name prefix.
31691 * These icons will later be retrieved from the cache using `$mdIcon( <icon name> )`
31692 *
31693 * @param {string} id Icon name/id used to register the icon
31694 * @param {string} url specifies the external location for the data file. Used internally by
31695 * `$templateRequest` to load the data or as part of the lookup in `$templateCache` if pre-loading
31696 * was configured.
31697 * @param {number=} viewBoxSize Sets the width and height the icon's viewBox.
31698 * It is ignored for icons with an existing viewBox. Default size is 24.
31699 *
31700 * @returns {obj} an `$mdIconProvider` reference; used to support method call chains for the API
31701 *
31702 * @usage
31703 * <hljs lang="js">
31704 * app.config(function($mdIconProvider) {
31705 *
31706 * // Configure URLs for icons specified by [set:]id.
31707 *
31708 * $mdIconProvider
31709 * .icon('android', 'my/app/android.svg') // Register a specific icon (by name)
31710 * .icon('work:chair', 'my/app/chair.svg'); // Register icon in a specific set
31711 * });
31712 * </hljs>
31713 *
31714 */
31715/**
31716 * @ngdoc method
31717 * @name $mdIconProvider#iconSet
31718 *
31719 * @description
31720 * Register a source URL for a 'named' set of icons; group of SVG definitions where each definition
31721 * has an icon id. Individual icons can be subsequently retrieved from this cached set using
31722 * `$mdIcon(<icon set name>:<icon name>)`
31723 *
31724 * @param {string} id Icon name/id used to register the iconset
31725 * @param {string} url specifies the external location for the data file. Used internally by
31726 * `$templateRequest` to load the data or as part of the lookup in `$templateCache` if pre-loading
31727 * was configured.
31728 * @param {number=} viewBoxSize Sets the width and height of the viewBox of all icons in the set.
31729 * It is ignored for icons with an existing viewBox. All icons in the icon set should be the same size.
31730 * Default value is 24.
31731 *
31732 * @returns {obj} an `$mdIconProvider` reference; used to support method call chains for the API
31733 *
31734 *
31735 * @usage
31736 * <hljs lang="js">
31737 * app.config(function($mdIconProvider) {
31738 *
31739 * // Configure URLs for icons specified by [set:]id.
31740 *
31741 * $mdIconProvider
31742 * .iconSet('social', 'my/app/social.svg') // Register a named icon set
31743 * });
31744 * </hljs>
31745 *
31746 */
31747/**
31748 * @ngdoc method
31749 * @name $mdIconProvider#defaultIconSet
31750 *
31751 * @description
31752 * Register a source URL for the default 'named' set of icons. Unless explicitly registered,
31753 * subsequent lookups of icons will failover to search this 'default' icon set.
31754 * Icon can be retrieved from this cached, default set using `$mdIcon(<name>)`
31755 *
31756 * @param {string} url specifies the external location for the data file. Used internally by
31757 * `$templateRequest` to load the data or as part of the lookup in `$templateCache` if pre-loading
31758 * was configured.
31759 * @param {number=} viewBoxSize Sets the width and height of the viewBox of all icons in the set.
31760 * It is ignored for icons with an existing viewBox. All icons in the icon set should be the same size.
31761 * Default value is 24.
31762 *
31763 * @returns {obj} an `$mdIconProvider` reference; used to support method call chains for the API
31764 *
31765 * @usage
31766 * <hljs lang="js">
31767 * app.config(function($mdIconProvider) {
31768 *
31769 * // Configure URLs for icons specified by [set:]id.
31770 *
31771 * $mdIconProvider
31772 * .defaultIconSet( 'my/app/social.svg' ) // Register a default icon set
31773 * });
31774 * </hljs>
31775 *
31776 */
31777/**
31778 * @ngdoc method
31779 * @name $mdIconProvider#defaultFontSet
31780 *
31781 * @description
31782 * When using Font-Icons, Angular Material assumes the the Material Design icons will be used and automatically
31783 * configures the default font-set == 'material-icons'. Note that the font-set references the font-icon library
31784 * class style that should be applied to the `<md-icon>`.
31785 *
31786 * Configuring the default means that the attributes
31787 * `md-font-set="material-icons"` or `class="material-icons"` do not need to be explicitly declared on the
31788 * `<md-icon>` markup. For example:
31789 *
31790 * `<md-icon> face </md-icon>`
31791 * will render as
31792 * `<span class="material-icons"> face </span>`, and
31793 *
31794 * `<md-icon md-font-set="fa"> face </md-icon>`
31795 * will render as
31796 * `<span class="fa"> face </span>`
31797 *
31798 * @param {string} name of the font-library style that should be applied to the md-icon DOM element
31799 *
31800 * @usage
31801 * <hljs lang="js">
31802 * app.config(function($mdIconProvider) {
31803 * $mdIconProvider.defaultFontSet( 'fa' );
31804 * });
31805 * </hljs>
31806 *
31807 */
31808
31809/**
31810 * @ngdoc method
31811 * @name $mdIconProvider#fontSet
31812 *
31813 * @description
31814 * When using a font set for `<md-icon>` you must specify the correct font classname in the `md-font-set`
31815 * attribute. If the fonset className is really long, your markup may become cluttered... an easy
31816 * solution is to define an `alias` for your fontset:
31817 *
31818 * @param {string} alias of the specified fontset.
31819 * @param {string} className of the fontset.
31820 *
31821 * @usage
31822 * <hljs lang="js">
31823 * app.config(function($mdIconProvider) {
31824 * // In this case, we set an alias for the `material-icons` fontset.
31825 * $mdIconProvider.fontSet('md', 'material-icons');
31826 * });
31827 * </hljs>
31828 *
31829 */
31830
31831/**
31832 * @ngdoc method
31833 * @name $mdIconProvider#defaultViewBoxSize
31834 *
31835 * @description
31836 * While `<md-icon />` markup can also be style with sizing CSS, this method configures
31837 * the default width **and** height used for all icons; unless overridden by specific CSS.
31838 * The default sizing is (24px, 24px).
31839 * @param {number=} viewBoxSize Sets the width and height of the viewBox for an icon or an icon set.
31840 * All icons in a set should be the same size. The default value is 24.
31841 *
31842 * @returns {obj} an `$mdIconProvider` reference; used to support method call chains for the API
31843 *
31844 * @usage
31845 * <hljs lang="js">
31846 * app.config(function($mdIconProvider) {
31847 *
31848 * // Configure URLs for icons specified by [set:]id.
31849 *
31850 * $mdIconProvider
31851 * .defaultViewBoxSize(36) // Register a default icon size (width == height)
31852 * });
31853 * </hljs>
31854 *
31855 */
31856
31857var config = {
31858 defaultViewBoxSize: 24,
31859 defaultFontSet: 'material-icons',
31860 fontSets: []
31861};
31862
31863function MdIconProvider() {
31864}
31865
31866MdIconProvider.prototype = {
31867 icon: function(id, url, viewBoxSize) {
31868 if (id.indexOf(':') == -1) id = '$default:' + id;
31869
31870 config[id] = new ConfigurationItem(url, viewBoxSize);
31871 return this;
31872 },
31873
31874 iconSet: function(id, url, viewBoxSize) {
31875 config[id] = new ConfigurationItem(url, viewBoxSize);
31876 return this;
31877 },
31878
31879 defaultIconSet: function(url, viewBoxSize) {
31880 var setName = '$default';
31881
31882 if (!config[setName]) {
31883 config[setName] = new ConfigurationItem(url, viewBoxSize);
31884 }
31885
31886 config[setName].viewBoxSize = viewBoxSize || config.defaultViewBoxSize;
31887
31888 return this;
31889 },
31890
31891 defaultViewBoxSize: function(viewBoxSize) {
31892 config.defaultViewBoxSize = viewBoxSize;
31893 return this;
31894 },
31895
31896 /**
31897 * Register an alias name associated with a font-icon library style ;
31898 */
31899 fontSet: function fontSet(alias, className) {
31900 config.fontSets.push({
31901 alias: alias,
31902 fontSet: className || alias
31903 });
31904 return this;
31905 },
31906
31907 /**
31908 * Specify a default style name associated with a font-icon library
31909 * fallback to Material Icons.
31910 *
31911 */
31912 defaultFontSet: function defaultFontSet(className) {
31913 config.defaultFontSet = !className ? '' : className;
31914 return this;
31915 },
31916
31917 defaultIconSize: function defaultIconSize(iconSize) {
31918 config.defaultIconSize = iconSize;
31919 return this;
31920 },
31921
31922 $get: ['$templateRequest', '$q', '$log', '$mdUtil', '$sce', function($templateRequest, $q, $log, $mdUtil, $sce) {
31923 return MdIconService(config, $templateRequest, $q, $log, $mdUtil, $sce);
31924 }]
31925};
31926
31927/**
31928 * Configuration item stored in the Icon registry; used for lookups
31929 * to load if not already cached in the `loaded` cache
31930 */
31931function ConfigurationItem(url, viewBoxSize) {
31932 this.url = url;
31933 this.viewBoxSize = viewBoxSize || config.defaultViewBoxSize;
31934}
31935
31936/**
31937 * @ngdoc service
31938 * @name $mdIcon
31939 * @module material.components.icon
31940 *
31941 * @description
31942 * The `$mdIcon` service is a function used to lookup SVG icons.
31943 *
31944 * @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
31945 * 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
31946 * performed. The Id may be a unique icon ID or may include an iconSet ID prefix.
31947 *
31948 * For the **id** query to work properly, this means that all id-to-URL mappings must have been previously configured
31949 * using the `$mdIconProvider`.
31950 *
31951 * @returns {angular.$q.Promise} A promise that gets resolved to a clone of the initial SVG DOM element; which was
31952 * created from the SVG markup in the SVG data file. If an error occurs (e.g. the icon cannot be found) the promise
31953 * will get rejected.
31954 *
31955 * @usage
31956 * <hljs lang="js">
31957 * function SomeDirective($mdIcon) {
31958 *
31959 * // See if the icon has already been loaded, if not
31960 * // then lookup the icon from the registry cache, load and cache
31961 * // it for future requests.
31962 * // NOTE: ID queries require configuration with $mdIconProvider
31963 *
31964 * $mdIcon('android').then(function(iconEl) { element.append(iconEl); });
31965 * $mdIcon('work:chair').then(function(iconEl) { element.append(iconEl); });
31966 *
31967 * // Load and cache the external SVG using a URL
31968 *
31969 * $mdIcon('img/icons/android.svg').then(function(iconEl) {
31970 * element.append(iconEl);
31971 * });
31972 * };
31973 * </hljs>
31974 *
31975 * > <b>Note:</b> The `<md-icon>` directive internally uses the `$mdIcon` service to query, loaded,
31976 * and instantiate SVG DOM elements.
31977 */
31978
31979/* @ngInject */
31980function MdIconService(config, $templateRequest, $q, $log, $mdUtil, $sce) {
31981 var iconCache = {};
31982 var svgCache = {};
31983 var urlRegex = /[-\w@:%\+.~#?&//=]{2,}\.[a-z]{2,4}\b(\/[-\w@:%\+.~#?&//=]*)?/i;
31984 var dataUrlRegex = /^data:image\/svg\+xml[\s*;\w\-\=]*?(base64)?,(.*)$/i;
31985
31986 Icon.prototype = {clone: cloneSVG, prepare: prepareAndStyle};
31987 getIcon.fontSet = findRegisteredFontSet;
31988
31989 // Publish service...
31990 return getIcon;
31991
31992 /**
31993 * Actual $mdIcon service is essentially a lookup function
31994 */
31995 function getIcon(id) {
31996 id = id || '';
31997
31998 // If the "id" provided is not a string, the only other valid value is a $sce trust wrapper
31999 // over a URL string. If the value is not trusted, this will intentionally throw an error
32000 // because the user is attempted to use an unsafe URL, potentially opening themselves up
32001 // to an XSS attack.
32002 if (!angular.isString(id)) {
32003 id = $sce.getTrustedUrl(id);
32004 }
32005
32006 // If already loaded and cached, use a clone of the cached icon.
32007 // Otherwise either load by URL, or lookup in the registry and then load by URL, and cache.
32008
32009 if (iconCache[id]) {
32010 return $q.when(transformClone(iconCache[id]));
32011 }
32012
32013 if (urlRegex.test(id) || dataUrlRegex.test(id)) {
32014 return loadByURL(id).then(cacheIcon(id));
32015 }
32016
32017 if (id.indexOf(':') == -1) {
32018 id = '$default:' + id;
32019 }
32020
32021 var load = config[id] ? loadByID : loadFromIconSet;
32022 return load(id)
32023 .then(cacheIcon(id));
32024 }
32025
32026 /**
32027 * Lookup registered fontSet style using its alias...
32028 * If not found,
32029 */
32030 function findRegisteredFontSet(alias) {
32031 var useDefault = angular.isUndefined(alias) || !(alias && alias.length);
32032 if (useDefault) return config.defaultFontSet;
32033
32034 var result = alias;
32035 angular.forEach(config.fontSets, function(it) {
32036 if (it.alias == alias) result = it.fontSet || result;
32037 });
32038
32039 return result;
32040 }
32041
32042 function transformClone(cacheElement) {
32043 var clone = cacheElement.clone();
32044 var cacheSuffix = '_cache' + $mdUtil.nextUid();
32045
32046 // We need to modify for each cached icon the id attributes.
32047 // This is needed because SVG id's are treated as normal DOM ids
32048 // and should not have a duplicated id.
32049 if (clone.id) clone.id += cacheSuffix;
32050 angular.forEach(clone.querySelectorAll('[id]'), function(item) {
32051 item.id += cacheSuffix;
32052 });
32053
32054 return clone;
32055 }
32056
32057 /**
32058 * Prepare and cache the loaded icon for the specified `id`
32059 */
32060 function cacheIcon(id) {
32061
32062 return function updateCache(icon) {
32063 iconCache[id] = isIcon(icon) ? icon : new Icon(icon, config[id]);
32064
32065 return iconCache[id].clone();
32066 };
32067 }
32068
32069 /**
32070 * Lookup the configuration in the registry, if !registered throw an error
32071 * otherwise load the icon [on-demand] using the registered URL.
32072 *
32073 */
32074 function loadByID(id) {
32075 var iconConfig = config[id];
32076 return loadByURL(iconConfig.url).then(function(icon) {
32077 return new Icon(icon, iconConfig);
32078 });
32079 }
32080
32081 /**
32082 * Loads the file as XML and uses querySelector( <id> ) to find
32083 * the desired node...
32084 */
32085 function loadFromIconSet(id) {
32086 var setName = id.substring(0, id.lastIndexOf(':')) || '$default';
32087 var iconSetConfig = config[setName];
32088
32089 return !iconSetConfig ? announceIdNotFound(id) : loadByURL(iconSetConfig.url).then(extractFromSet);
32090
32091 function extractFromSet(set) {
32092 var iconName = id.slice(id.lastIndexOf(':') + 1);
32093 var icon = set.querySelector('#' + iconName);
32094 return icon ? new Icon(icon, iconSetConfig) : announceIdNotFound(id);
32095 }
32096
32097 function announceIdNotFound(id) {
32098 var msg = 'icon ' + id + ' not found';
32099 $log.warn(msg);
32100
32101 return $q.reject(msg || id);
32102 }
32103 }
32104
32105 /**
32106 * Load the icon by URL (may use the $templateCache).
32107 * Extract the data for later conversion to Icon
32108 */
32109 function loadByURL(url) {
32110 /* Load the icon from embedded data URL. */
32111 function loadByDataUrl(url) {
32112 var results = dataUrlRegex.exec(url);
32113 var isBase64 = /base64/i.test(url);
32114 var data = isBase64 ? window.atob(results[2]) : results[2];
32115
32116 return $q.when(angular.element(data)[0]);
32117 }
32118
32119 /* Load the icon by URL using HTTP. */
32120 function loadByHttpUrl(url) {
32121 return $q(function(resolve, reject) {
32122 // Catch HTTP or generic errors not related to incorrect icon IDs.
32123 var announceAndReject = function(err) {
32124 var msg = angular.isString(err) ? err : (err.message || err.data || err.statusText);
32125 $log.warn(msg);
32126 reject(err);
32127 },
32128 extractSvg = function(response) {
32129 if (!svgCache[url]) {
32130 svgCache[url] = angular.element('<div>').append(response)[0].querySelector('svg');
32131 }
32132 resolve(svgCache[url]);
32133 };
32134
32135 $templateRequest(url, true).then(extractSvg, announceAndReject);
32136 });
32137 }
32138
32139 return dataUrlRegex.test(url)
32140 ? loadByDataUrl(url)
32141 : loadByHttpUrl(url);
32142 }
32143
32144 /**
32145 * Check target signature to see if it is an Icon instance.
32146 */
32147 function isIcon(target) {
32148 return angular.isDefined(target.element) && angular.isDefined(target.config);
32149 }
32150
32151 /**
32152 * Define the Icon class
32153 */
32154 function Icon(el, config) {
32155 if (el && el.tagName != 'svg') {
32156 el = angular.element('<svg xmlns="http://www.w3.org/2000/svg">').append(el.cloneNode(true))[0];
32157 }
32158
32159 // Inject the namespace if not available...
32160 if (!el.getAttribute('xmlns')) {
32161 el.setAttribute('xmlns', "http://www.w3.org/2000/svg");
32162 }
32163
32164 this.element = el;
32165 this.config = config;
32166 this.prepare();
32167 }
32168
32169 /**
32170 * Prepare the DOM element that will be cached in the
32171 * loaded iconCache store.
32172 */
32173 function prepareAndStyle() {
32174 var viewBoxSize = this.config ? this.config.viewBoxSize : config.defaultViewBoxSize;
32175 angular.forEach({
32176 'fit': '',
32177 'height': '100%',
32178 'width': '100%',
32179 'preserveAspectRatio': 'xMidYMid meet',
32180 'viewBox': this.element.getAttribute('viewBox') || ('0 0 ' + viewBoxSize + ' ' + viewBoxSize),
32181 'focusable': false // Disable IE11s default behavior to make SVGs focusable
32182 }, function(val, attr) {
32183 this.element.setAttribute(attr, val);
32184 }, this);
32185 }
32186
32187 /**
32188 * Clone the Icon DOM element.
32189 */
32190 function cloneSVG() {
32191 // If the element or any of its children have a style attribute, then a CSP policy without
32192 // 'unsafe-inline' in the style-src directive, will result in a violation.
32193 return this.element.cloneNode(true);
32194 }
32195
32196}
32197
32198})();
32199(function(){
32200"use strict";
32201
32202
32203
32204MenuController.$inject = ["$mdMenu", "$attrs", "$element", "$scope", "$mdUtil", "$timeout", "$rootScope", "$q", "$log"];
32205angular
32206 .module('material.components.menu')
32207 .controller('mdMenuCtrl', MenuController);
32208
32209/**
32210 * @ngInject
32211 */
32212function MenuController($mdMenu, $attrs, $element, $scope, $mdUtil, $timeout, $rootScope, $q, $log) {
32213
32214 var prefixer = $mdUtil.prefixer();
32215 var menuContainer;
32216 var self = this;
32217 var triggerElement;
32218
32219 this.nestLevel = parseInt($attrs.mdNestLevel, 10) || 0;
32220
32221 /**
32222 * Called by our linking fn to provide access to the menu-content
32223 * element removed during link
32224 */
32225 this.init = function init(setMenuContainer, opts) {
32226 opts = opts || {};
32227 menuContainer = setMenuContainer;
32228
32229 // Default element for ARIA attributes has the ngClick or ngMouseenter expression
32230 triggerElement = $element[0].querySelector(prefixer.buildSelector(['ng-click', 'ng-mouseenter']));
32231 triggerElement.setAttribute('aria-expanded', 'false');
32232
32233 this.isInMenuBar = opts.isInMenuBar;
32234 this.nestedMenus = $mdUtil.nodesToArray(menuContainer[0].querySelectorAll('.md-nested-menu'));
32235
32236 menuContainer.on('$mdInterimElementRemove', function() {
32237 self.isOpen = false;
32238 $mdUtil.nextTick(function(){ self.onIsOpenChanged(self.isOpen);});
32239 });
32240 $mdUtil.nextTick(function(){ self.onIsOpenChanged(self.isOpen);});
32241
32242 var menuContainerId = 'menu_container_' + $mdUtil.nextUid();
32243 menuContainer.attr('id', menuContainerId);
32244 angular.element(triggerElement).attr({
32245 'aria-owns': menuContainerId,
32246 'aria-haspopup': 'true'
32247 });
32248
32249 $scope.$on('$destroy', angular.bind(this, function() {
32250 this.disableHoverListener();
32251 $mdMenu.destroy();
32252 }));
32253
32254 menuContainer.on('$destroy', function() {
32255 $mdMenu.destroy();
32256 });
32257 };
32258
32259 var openMenuTimeout, menuItems, deregisterScopeListeners = [];
32260 this.enableHoverListener = function() {
32261 deregisterScopeListeners.push($rootScope.$on('$mdMenuOpen', function(event, el) {
32262 if (menuContainer[0].contains(el[0])) {
32263 self.currentlyOpenMenu = el.controller('mdMenu');
32264 self.isAlreadyOpening = false;
32265 self.currentlyOpenMenu.registerContainerProxy(self.triggerContainerProxy.bind(self));
32266 }
32267 }));
32268 deregisterScopeListeners.push($rootScope.$on('$mdMenuClose', function(event, el) {
32269 if (menuContainer[0].contains(el[0])) {
32270 self.currentlyOpenMenu = undefined;
32271 }
32272 }));
32273 menuItems = angular.element($mdUtil.nodesToArray(menuContainer[0].children[0].children));
32274 menuItems.on('mouseenter', self.handleMenuItemHover);
32275 menuItems.on('mouseleave', self.handleMenuItemMouseLeave);
32276 };
32277
32278 this.disableHoverListener = function() {
32279 while (deregisterScopeListeners.length) {
32280 deregisterScopeListeners.shift()();
32281 }
32282 menuItems && menuItems.off('mouseenter', self.handleMenuItemHover);
32283 menuItems && menuItems.off('mouseleave', self.handleMenuItemMouseLeave);
32284 };
32285
32286 this.handleMenuItemHover = function(event) {
32287 if (self.isAlreadyOpening) return;
32288 var nestedMenu = (
32289 event.target.querySelector('md-menu')
32290 || $mdUtil.getClosest(event.target, 'MD-MENU')
32291 );
32292 openMenuTimeout = $timeout(function() {
32293 if (nestedMenu) {
32294 nestedMenu = angular.element(nestedMenu).controller('mdMenu');
32295 }
32296
32297 if (self.currentlyOpenMenu && self.currentlyOpenMenu != nestedMenu) {
32298 var closeTo = self.nestLevel + 1;
32299 self.currentlyOpenMenu.close(true, { closeTo: closeTo });
32300 self.isAlreadyOpening = !!nestedMenu;
32301 nestedMenu && nestedMenu.open();
32302 } else if (nestedMenu && !nestedMenu.isOpen && nestedMenu.open) {
32303 self.isAlreadyOpening = !!nestedMenu;
32304 nestedMenu && nestedMenu.open();
32305 }
32306 }, nestedMenu ? 100 : 250);
32307 var focusableTarget = event.currentTarget.querySelector('.md-button:not([disabled])');
32308 focusableTarget && focusableTarget.focus();
32309 };
32310
32311 this.handleMenuItemMouseLeave = function() {
32312 if (openMenuTimeout) {
32313 $timeout.cancel(openMenuTimeout);
32314 openMenuTimeout = undefined;
32315 }
32316 };
32317
32318
32319 /**
32320 * Uses the $mdMenu interim element service to open the menu contents
32321 */
32322 this.open = function openMenu(ev) {
32323 ev && ev.stopPropagation();
32324 ev && ev.preventDefault();
32325 if (self.isOpen) return;
32326 self.enableHoverListener();
32327 self.isOpen = true;
32328 $mdUtil.nextTick(function(){ self.onIsOpenChanged(self.isOpen);});
32329 triggerElement = triggerElement || (ev ? ev.target : $element[0]);
32330 triggerElement.setAttribute('aria-expanded', 'true');
32331 $scope.$emit('$mdMenuOpen', $element);
32332 $mdMenu.show({
32333 scope: $scope,
32334 mdMenuCtrl: self,
32335 nestLevel: self.nestLevel,
32336 element: menuContainer,
32337 target: triggerElement,
32338 preserveElement: true,
32339 parent: 'body'
32340 }).finally(function() {
32341 triggerElement.setAttribute('aria-expanded', 'false');
32342 self.disableHoverListener();
32343 });
32344 };
32345
32346 this.onIsOpenChanged = function(isOpen) {
32347 if (isOpen) {
32348 menuContainer.attr('aria-hidden', 'false');
32349 $element[0].classList.add('md-open');
32350 angular.forEach(self.nestedMenus, function(el) {
32351 el.classList.remove('md-open');
32352 });
32353 } else {
32354 menuContainer.attr('aria-hidden', 'true');
32355 $element[0].classList.remove('md-open');
32356 }
32357 $scope.$mdMenuIsOpen = self.isOpen;
32358 };
32359
32360 this.focusMenuContainer = function focusMenuContainer() {
32361 var focusTarget = menuContainer[0]
32362 .querySelector(prefixer.buildSelector(['md-menu-focus-target', 'md-autofocus']));
32363
32364 if (!focusTarget) focusTarget = menuContainer[0].querySelector('.md-button:not([disabled])');
32365 focusTarget.focus();
32366 };
32367
32368 this.registerContainerProxy = function registerContainerProxy(handler) {
32369 this.containerProxy = handler;
32370 };
32371
32372 this.triggerContainerProxy = function triggerContainerProxy(ev) {
32373 this.containerProxy && this.containerProxy(ev);
32374 };
32375
32376 this.destroy = function() {
32377 return self.isOpen ? $mdMenu.destroy() : $q.when(false);
32378 };
32379
32380 // Use the $mdMenu interim element service to close the menu contents
32381 this.close = function closeMenu(skipFocus, closeOpts) {
32382 if ( !self.isOpen ) return;
32383 self.isOpen = false;
32384 $mdUtil.nextTick(function(){ self.onIsOpenChanged(self.isOpen);});
32385
32386 var eventDetails = angular.extend({}, closeOpts, { skipFocus: skipFocus });
32387 $scope.$emit('$mdMenuClose', $element, eventDetails);
32388 $mdMenu.hide(null, closeOpts);
32389
32390 if (!skipFocus) {
32391 var el = self.restoreFocusTo || $element.find('button')[0];
32392 if (el instanceof angular.element) el = el[0];
32393 if (el) el.focus();
32394 }
32395 };
32396
32397 /**
32398 * Build a nice object out of our string attribute which specifies the
32399 * target mode for left and top positioning
32400 */
32401 this.positionMode = function positionMode() {
32402 var attachment = ($attrs.mdPositionMode || 'target').split(' ');
32403
32404 // If attachment is a single item, duplicate it for our second value.
32405 // ie. 'target' -> 'target target'
32406 if (attachment.length == 1) {
32407 attachment.push(attachment[0]);
32408 }
32409
32410 return {
32411 left: attachment[0],
32412 top: attachment[1]
32413 };
32414 };
32415
32416 /**
32417 * Build a nice object out of our string attribute which specifies
32418 * the offset of top and left in pixels.
32419 */
32420 this.offsets = function offsets() {
32421 var position = ($attrs.mdOffset || '0 0').split(' ').map(parseFloat);
32422 if (position.length == 2) {
32423 return {
32424 left: position[0],
32425 top: position[1]
32426 };
32427 } else if (position.length == 1) {
32428 return {
32429 top: position[0],
32430 left: position[0]
32431 };
32432 } else {
32433 throw Error('Invalid offsets specified. Please follow format <x, y> or <n>');
32434 }
32435 };
32436
32437 // Functionality that is exposed in the view.
32438 $scope.$mdMenu = {
32439 open: this.open,
32440 close: this.close
32441 };
32442
32443 // Deprecated APIs
32444 $scope.$mdOpenMenu = angular.bind(this, function() {
32445 $log.warn('mdMenu: The $mdOpenMenu method is deprecated. Please use `$mdMenu.open`.');
32446 return this.open.apply(this, arguments);
32447 });
32448}
32449
32450})();
32451(function(){
32452"use strict";
32453
32454/**
32455 * @ngdoc directive
32456 * @name mdMenu
32457 * @module material.components.menu
32458 * @restrict E
32459 * @description
32460 *
32461 * Menus are elements that open when clicked. They are useful for displaying
32462 * additional options within the context of an action.
32463 *
32464 * Every `md-menu` must specify exactly two child elements. The first element is what is
32465 * left in the DOM and is used to open the menu. This element is called the trigger element.
32466 * The trigger element's scope has access to `$mdMenu.open($event)`
32467 * which it may call to open the menu. By passing $event as argument, the
32468 * corresponding event is stopped from propagating up the DOM-tree. Similarly, `$mdMenu.close()`
32469 * can be used to close the menu.
32470 *
32471 * The second element is the `md-menu-content` element which represents the
32472 * contents of the menu when it is open. Typically this will contain `md-menu-item`s,
32473 * but you can do custom content as well.
32474 *
32475 * <hljs lang="html">
32476 * <md-menu>
32477 * <!-- Trigger element is a md-button with an icon -->
32478 * <md-button ng-click="$mdMenu.open($event)" class="md-icon-button" aria-label="Open sample menu">
32479 * <md-icon md-svg-icon="call:phone"></md-icon>
32480 * </md-button>
32481 * <md-menu-content>
32482 * <md-menu-item><md-button ng-click="doSomething()">Do Something</md-button></md-menu-item>
32483 * </md-menu-content>
32484 * </md-menu>
32485 * </hljs>
32486
32487 * ## Sizing Menus
32488 *
32489 * The width of the menu when it is open may be specified by specifying a `width`
32490 * attribute on the `md-menu-content` element.
32491 * See the [Material Design Spec](https://material.google.com/components/menus.html#menus-simple-menus)
32492 * for more information.
32493 *
32494 *
32495 * ## Aligning Menus
32496 *
32497 * When a menu opens, it is important that the content aligns with the trigger element.
32498 * Failure to align menus can result in jarring experiences for users as content
32499 * suddenly shifts. To help with this, `md-menu` provides serveral APIs to help
32500 * with alignment.
32501 *
32502 * ### Target Mode
32503 *
32504 * By default, `md-menu` will attempt to align the `md-menu-content` by aligning
32505 * designated child elements in both the trigger and the menu content.
32506 *
32507 * To specify the alignment element in the `trigger` you can use the `md-menu-origin`
32508 * attribute on a child element. If no `md-menu-origin` is specified, the `md-menu`
32509 * will be used as the origin element.
32510 *
32511 * Similarly, the `md-menu-content` may specify a `md-menu-align-target` for a
32512 * `md-menu-item` to specify the node that it should try and align with.
32513 *
32514 * In this example code, we specify an icon to be our origin element, and an
32515 * icon in our menu content to be our alignment target. This ensures that both
32516 * icons are aligned when the menu opens.
32517 *
32518 * <hljs lang="html">
32519 * <md-menu>
32520 * <md-button ng-click="$mdMenu.open($event)" class="md-icon-button" aria-label="Open some menu">
32521 * <md-icon md-menu-origin md-svg-icon="call:phone"></md-icon>
32522 * </md-button>
32523 * <md-menu-content>
32524 * <md-menu-item>
32525 * <md-button ng-click="doSomething()" aria-label="Do something">
32526 * <md-icon md-menu-align-target md-svg-icon="call:phone"></md-icon>
32527 * Do Something
32528 * </md-button>
32529 * </md-menu-item>
32530 * </md-menu-content>
32531 * </md-menu>
32532 * </hljs>
32533 *
32534 * Sometimes we want to specify alignment on the right side of an element, for example
32535 * if we have a menu on the right side a toolbar, we want to right align our menu content.
32536 *
32537 * We can specify the origin by using the `md-position-mode` attribute on both
32538 * the `x` and `y` axis. Right now only the `x-axis` has more than one option.
32539 * You may specify the default mode of `target target` or
32540 * `target-right target` to specify a right-oriented alignment target. See the
32541 * position section of the demos for more examples.
32542 *
32543 * ### Menu Offsets
32544 *
32545 * It is sometimes unavoidable to need to have a deeper level of control for
32546 * the positioning of a menu to ensure perfect alignment. `md-menu` provides
32547 * the `md-offset` attribute to allow pixel level specificty of adjusting the
32548 * exact positioning.
32549 *
32550 * This offset is provided in the format of `x y` or `n` where `n` will be used
32551 * in both the `x` and `y` axis.
32552 *
32553 * For example, to move a menu by `2px` down from the top, we can use:
32554 * <hljs lang="html">
32555 * <md-menu md-offset="0 2">
32556 * <!-- menu-content -->
32557 * </md-menu>
32558 * </hljs>
32559 *
32560 * ### Auto Focus
32561 * By default, when a menu opens, `md-menu` focuses the first button in the menu content.
32562 *
32563 * But sometimes you would like to focus another specific menu item instead of the first.<br/>
32564 * This can be done by applying the `md-autofocus` directive on the given element.
32565 *
32566 * <hljs lang="html">
32567 * <md-menu-item>
32568 * <md-button md-autofocus ng-click="doSomething()">
32569 * Auto Focus
32570 * </md-button>
32571 * </md-menu-item>
32572 * </hljs>
32573 *
32574 *
32575 * ### Preventing close
32576 *
32577 * Sometimes you would like to be able to click on a menu item without having the menu
32578 * close. To do this, ngMaterial exposes the `md-prevent-menu-close` attribute which
32579 * can be added to a button inside a menu to stop the menu from automatically closing.
32580 * You can then close the menu either by using `$mdMenu.close()` in the template,
32581 * or programatically by injecting `$mdMenu` and calling `$mdMenu.hide()`.
32582 *
32583 * <hljs lang="html">
32584 * <md-menu-content ng-mouseleave="$mdMenu.close()">
32585 * <md-menu-item>
32586 * <md-button ng-click="doSomething()" aria-label="Do something" md-prevent-menu-close="md-prevent-menu-close">
32587 * <md-icon md-menu-align-target md-svg-icon="call:phone"></md-icon>
32588 * Do Something
32589 * </md-button>
32590 * </md-menu-item>
32591 * </md-menu-content>
32592 * </hljs>
32593 *
32594 * @usage
32595 * <hljs lang="html">
32596 * <md-menu>
32597 * <md-button ng-click="$mdMenu.open($event)" class="md-icon-button">
32598 * <md-icon md-svg-icon="call:phone"></md-icon>
32599 * </md-button>
32600 * <md-menu-content>
32601 * <md-menu-item><md-button ng-click="doSomething()">Do Something</md-button></md-menu-item>
32602 * </md-menu-content>
32603 * </md-menu>
32604 * </hljs>
32605 *
32606 * @param {string} md-position-mode The position mode in the form of
32607 * `x`, `y`. Default value is `target`,`target`. Right now the `x` axis
32608 * also supports `target-right`.
32609 * @param {string} md-offset An offset to apply to the dropdown after positioning
32610 * `x`, `y`. Default value is `0`,`0`.
32611 *
32612 */
32613
32614MenuDirective.$inject = ["$mdUtil"];
32615angular
32616 .module('material.components.menu')
32617 .directive('mdMenu', MenuDirective);
32618
32619/**
32620 * @ngInject
32621 */
32622function MenuDirective($mdUtil) {
32623 var INVALID_PREFIX = 'Invalid HTML for md-menu: ';
32624 return {
32625 restrict: 'E',
32626 require: ['mdMenu', '?^mdMenuBar'],
32627 controller: 'mdMenuCtrl', // empty function to be built by link
32628 scope: true,
32629 compile: compile
32630 };
32631
32632 function compile(templateElement) {
32633 templateElement.addClass('md-menu');
32634
32635 var triggerEl = templateElement.children()[0];
32636 var contentEl = templateElement.children()[1];
32637
32638 var prefixer = $mdUtil.prefixer();
32639
32640 if (!prefixer.hasAttribute(triggerEl, 'ng-click')) {
32641 triggerEl = triggerEl
32642 .querySelector(prefixer.buildSelector(['ng-click', 'ng-mouseenter'])) || triggerEl;
32643 }
32644
32645 var isButtonTrigger = triggerEl.nodeName === 'MD-BUTTON' || triggerEl.nodeName === 'BUTTON';
32646
32647 if (triggerEl && isButtonTrigger && !triggerEl.hasAttribute('type')) {
32648 triggerEl.setAttribute('type', 'button');
32649 }
32650
32651 if (!triggerEl) {
32652 throw Error(INVALID_PREFIX + 'Expected the menu to have a trigger element.');
32653 }
32654
32655 if (!contentEl || contentEl.nodeName !== 'MD-MENU-CONTENT') {
32656 throw Error(INVALID_PREFIX + 'Expected the menu to contain a `md-menu-content` element.');
32657 }
32658
32659 // Default element for ARIA attributes has the ngClick or ngMouseenter expression
32660 triggerEl && triggerEl.setAttribute('aria-haspopup', 'true');
32661
32662 var nestedMenus = templateElement[0].querySelectorAll('md-menu');
32663 var nestingDepth = parseInt(templateElement[0].getAttribute('md-nest-level'), 10) || 0;
32664 if (nestedMenus) {
32665 angular.forEach($mdUtil.nodesToArray(nestedMenus), function(menuEl) {
32666 if (!menuEl.hasAttribute('md-position-mode')) {
32667 menuEl.setAttribute('md-position-mode', 'cascade');
32668 }
32669 menuEl.classList.add('_md-nested-menu');
32670 menuEl.setAttribute('md-nest-level', nestingDepth + 1);
32671 });
32672 }
32673 return link;
32674 }
32675
32676 function link(scope, element, attr, ctrls) {
32677 var mdMenuCtrl = ctrls[0];
32678 var isInMenuBar = !!ctrls[1];
32679 // Move everything into a md-menu-container and pass it to the controller
32680 var menuContainer = angular.element( '<div class="_md md-open-menu-container md-whiteframe-z2"></div>');
32681 var menuContents = element.children()[1];
32682
32683 element.addClass('_md'); // private md component indicator for styling
32684
32685 if (!menuContents.hasAttribute('role')) {
32686 menuContents.setAttribute('role', 'menu');
32687 }
32688 menuContainer.append(menuContents);
32689
32690 element.on('$destroy', function() {
32691 menuContainer.remove();
32692 });
32693
32694 element.append(menuContainer);
32695 menuContainer[0].style.display = 'none';
32696 mdMenuCtrl.init(menuContainer, { isInMenuBar: isInMenuBar });
32697
32698 }
32699}
32700
32701})();
32702(function(){
32703"use strict";
32704
32705
32706MenuProvider.$inject = ["$$interimElementProvider"];angular
32707 .module('material.components.menu')
32708 .provider('$mdMenu', MenuProvider);
32709
32710/*
32711 * Interim element provider for the menu.
32712 * Handles behavior for a menu while it is open, including:
32713 * - handling animating the menu opening/closing
32714 * - handling key/mouse events on the menu element
32715 * - handling enabling/disabling scroll while the menu is open
32716 * - handling redrawing during resizes and orientation changes
32717 *
32718 */
32719
32720function MenuProvider($$interimElementProvider) {
32721 menuDefaultOptions.$inject = ["$mdUtil", "$mdTheming", "$mdConstant", "$document", "$window", "$q", "$$rAF", "$animateCss", "$animate", "$log"];
32722 var MENU_EDGE_MARGIN = 8;
32723
32724 return $$interimElementProvider('$mdMenu')
32725 .setDefaults({
32726 methods: ['target'],
32727 options: menuDefaultOptions
32728 });
32729
32730 /* @ngInject */
32731 function menuDefaultOptions($mdUtil, $mdTheming, $mdConstant, $document, $window, $q, $$rAF,
32732 $animateCss, $animate, $log) {
32733
32734 var prefixer = $mdUtil.prefixer();
32735 var animator = $mdUtil.dom.animator;
32736
32737 return {
32738 parent: 'body',
32739 onShow: onShow,
32740 onRemove: onRemove,
32741 hasBackdrop: true,
32742 disableParentScroll: true,
32743 skipCompile: true,
32744 preserveScope: true,
32745 multiple: true,
32746 themable: true
32747 };
32748
32749 /**
32750 * Show modal backdrop element...
32751 * @returns {function(): void} A function that removes this backdrop
32752 */
32753 function showBackdrop(scope, element, options) {
32754 if (options.nestLevel) return angular.noop;
32755
32756 // If we are not within a dialog...
32757 if (options.disableParentScroll && !$mdUtil.getClosest(options.target, 'MD-DIALOG')) {
32758 // !! DO this before creating the backdrop; since disableScrollAround()
32759 // configures the scroll offset; which is used by mdBackDrop postLink()
32760 options.restoreScroll = $mdUtil.disableScrollAround(options.element, options.parent);
32761 } else {
32762 options.disableParentScroll = false;
32763 }
32764
32765 if (options.hasBackdrop) {
32766 options.backdrop = $mdUtil.createBackdrop(scope, "md-menu-backdrop md-click-catcher");
32767
32768 $animate.enter(options.backdrop, $document[0].body);
32769 }
32770
32771 /**
32772 * Hide and destroys the backdrop created by showBackdrop()
32773 */
32774 return function hideBackdrop() {
32775 if (options.backdrop) options.backdrop.remove();
32776 if (options.disableParentScroll) options.restoreScroll();
32777 };
32778 }
32779
32780 /**
32781 * Removing the menu element from the DOM and remove all associated event listeners
32782 * and backdrop
32783 */
32784 function onRemove(scope, element, opts) {
32785 opts.cleanupInteraction();
32786 opts.cleanupBackdrop();
32787 opts.cleanupResizing();
32788 opts.hideBackdrop();
32789
32790 // Before the menu is closing remove the clickable class.
32791 element.removeClass('md-clickable');
32792
32793 // For navigation $destroy events, do a quick, non-animated removal,
32794 // but for normal closes (from clicks, etc) animate the removal
32795
32796 return (opts.$destroy === true) ? detachAndClean() : animateRemoval().then( detachAndClean );
32797
32798 /**
32799 * For normal closes, animate the removal.
32800 * For forced closes (like $destroy events), skip the animations
32801 */
32802 function animateRemoval() {
32803 return $animateCss(element, {addClass: 'md-leave'}).start();
32804 }
32805
32806 /**
32807 * Detach the element
32808 */
32809 function detachAndClean() {
32810 element.removeClass('md-active');
32811 detachElement(element, opts);
32812 opts.alreadyOpen = false;
32813 }
32814
32815 }
32816
32817 /**
32818 * Inserts and configures the staged Menu element into the DOM, positioning it,
32819 * and wiring up various interaction events
32820 */
32821 function onShow(scope, element, opts) {
32822 sanitizeAndConfigure(opts);
32823
32824 if (opts.menuContentEl[0]) {
32825 // Inherit the theme from the target element.
32826 $mdTheming.inherit(opts.menuContentEl, opts.target);
32827 } else {
32828 $log.warn(
32829 '$mdMenu: Menu elements should always contain a `md-menu-content` element,' +
32830 'otherwise interactivity features will not work properly.',
32831 element
32832 );
32833 }
32834
32835 // Register various listeners to move menu on resize/orientation change
32836 opts.cleanupResizing = startRepositioningOnResize();
32837 opts.hideBackdrop = showBackdrop(scope, element, opts);
32838
32839 // Return the promise for when our menu is done animating in
32840 return showMenu()
32841 .then(function(response) {
32842 opts.alreadyOpen = true;
32843 opts.cleanupInteraction = activateInteraction();
32844 opts.cleanupBackdrop = setupBackdrop();
32845
32846 // Since the menu finished its animation, mark the menu as clickable.
32847 element.addClass('md-clickable');
32848
32849 return response;
32850 });
32851
32852 /**
32853 * Place the menu into the DOM and call positioning related functions
32854 */
32855 function showMenu() {
32856 opts.parent.append(element);
32857 element[0].style.display = '';
32858
32859 return $q(function(resolve) {
32860 var position = calculateMenuPosition(element, opts);
32861
32862 element.removeClass('md-leave');
32863
32864 // Animate the menu scaling, and opacity [from its position origin (default == top-left)]
32865 // to normal scale.
32866 $animateCss(element, {
32867 addClass: 'md-active',
32868 from: animator.toCss(position),
32869 to: animator.toCss({transform: ''})
32870 })
32871 .start()
32872 .then(resolve);
32873
32874 });
32875 }
32876
32877 /**
32878 * Check for valid opts and set some sane defaults
32879 */
32880 function sanitizeAndConfigure() {
32881 if (!opts.target) {
32882 throw Error(
32883 '$mdMenu.show() expected a target to animate from in options.target'
32884 );
32885 }
32886 angular.extend(opts, {
32887 alreadyOpen: false,
32888 isRemoved: false,
32889 target: angular.element(opts.target), //make sure it's not a naked dom node
32890 parent: angular.element(opts.parent),
32891 menuContentEl: angular.element(element[0].querySelector('md-menu-content'))
32892 });
32893 }
32894
32895 /**
32896 * Configure various resize listeners for screen changes
32897 */
32898 function startRepositioningOnResize() {
32899
32900 var repositionMenu = (function(target, options) {
32901 return $$rAF.throttle(function() {
32902 if (opts.isRemoved) return;
32903 var position = calculateMenuPosition(target, options);
32904
32905 target.css(animator.toCss(position));
32906 });
32907 })(element, opts);
32908
32909 $window.addEventListener('resize', repositionMenu);
32910 $window.addEventListener('orientationchange', repositionMenu);
32911
32912 return function stopRepositioningOnResize() {
32913
32914 // Disable resizing handlers
32915 $window.removeEventListener('resize', repositionMenu);
32916 $window.removeEventListener('orientationchange', repositionMenu);
32917
32918 };
32919 }
32920
32921 /**
32922 * Sets up the backdrop and listens for click elements.
32923 * Once the backdrop will be clicked, the menu will automatically close.
32924 * @returns {!Function} Function to remove the backdrop.
32925 */
32926 function setupBackdrop() {
32927 if (!opts.backdrop) return angular.noop;
32928
32929 opts.backdrop.on('click', onBackdropClick);
32930
32931 return function() {
32932 opts.backdrop.off('click', onBackdropClick);
32933 }
32934 }
32935
32936 /**
32937 * Function to be called whenever the backdrop is clicked.
32938 * @param {!MouseEvent} event
32939 */
32940 function onBackdropClick(event) {
32941 event.preventDefault();
32942 event.stopPropagation();
32943
32944 scope.$apply(function() {
32945 opts.mdMenuCtrl.close(true, { closeAll: true });
32946 });
32947 }
32948
32949 /**
32950 * Activate interaction on the menu. Resolves the focus target and closes the menu on
32951 * escape or option click.
32952 * @returns {!Function} Function to deactivate the interaction listeners.
32953 */
32954 function activateInteraction() {
32955 if (!opts.menuContentEl[0]) return angular.noop;
32956
32957 // Wire up keyboard listeners.
32958 // - Close on escape,
32959 // - focus next item on down arrow,
32960 // - focus prev item on up
32961 opts.menuContentEl.on('keydown', onMenuKeyDown);
32962 opts.menuContentEl[0].addEventListener('click', captureClickListener, true);
32963
32964 // kick off initial focus in the menu on the first enabled element
32965 var focusTarget = opts.menuContentEl[0]
32966 .querySelector(prefixer.buildSelector(['md-menu-focus-target', 'md-autofocus']));
32967
32968 if ( !focusTarget ) {
32969 var childrenLen = opts.menuContentEl[0].children.length;
32970 for(var childIndex = 0; childIndex < childrenLen; childIndex++) {
32971 var child = opts.menuContentEl[0].children[childIndex];
32972 focusTarget = child.querySelector('.md-button:not([disabled])');
32973 if (focusTarget) {
32974 break;
32975 }
32976 if (child.firstElementChild && !child.firstElementChild.disabled) {
32977 focusTarget = child.firstElementChild;
32978 break;
32979 }
32980 }
32981 }
32982
32983 focusTarget && focusTarget.focus();
32984
32985 return function cleanupInteraction() {
32986 opts.menuContentEl.off('keydown', onMenuKeyDown);
32987 opts.menuContentEl[0].removeEventListener('click', captureClickListener, true);
32988 };
32989
32990 // ************************************
32991 // internal functions
32992 // ************************************
32993
32994 function onMenuKeyDown(ev) {
32995 var handled;
32996 switch (ev.keyCode) {
32997 case $mdConstant.KEY_CODE.ESCAPE:
32998 opts.mdMenuCtrl.close(false, { closeAll: true });
32999 handled = true;
33000 break;
33001 case $mdConstant.KEY_CODE.UP_ARROW:
33002 if (!focusMenuItem(ev, opts.menuContentEl, opts, -1) && !opts.nestLevel) {
33003 opts.mdMenuCtrl.triggerContainerProxy(ev);
33004 }
33005 handled = true;
33006 break;
33007 case $mdConstant.KEY_CODE.DOWN_ARROW:
33008 if (!focusMenuItem(ev, opts.menuContentEl, opts, 1) && !opts.nestLevel) {
33009 opts.mdMenuCtrl.triggerContainerProxy(ev);
33010 }
33011 handled = true;
33012 break;
33013 case $mdConstant.KEY_CODE.LEFT_ARROW:
33014 if (opts.nestLevel) {
33015 opts.mdMenuCtrl.close();
33016 } else {
33017 opts.mdMenuCtrl.triggerContainerProxy(ev);
33018 }
33019 handled = true;
33020 break;
33021 case $mdConstant.KEY_CODE.RIGHT_ARROW:
33022 var parentMenu = $mdUtil.getClosest(ev.target, 'MD-MENU');
33023 if (parentMenu && parentMenu != opts.parent[0]) {
33024 ev.target.click();
33025 } else {
33026 opts.mdMenuCtrl.triggerContainerProxy(ev);
33027 }
33028 handled = true;
33029 break;
33030 }
33031 if (handled) {
33032 ev.preventDefault();
33033 ev.stopImmediatePropagation();
33034 }
33035 }
33036
33037 function onBackdropClick(e) {
33038 e.preventDefault();
33039 e.stopPropagation();
33040 scope.$apply(function() {
33041 opts.mdMenuCtrl.close(true, { closeAll: true });
33042 });
33043 }
33044
33045 // Close menu on menu item click, if said menu-item is not disabled
33046 function captureClickListener(e) {
33047 var target = e.target;
33048 // Traverse up the event until we get to the menuContentEl to see if
33049 // there is an ng-click and that the ng-click is not disabled
33050 do {
33051 if (target == opts.menuContentEl[0]) return;
33052 if ((hasAnyAttribute(target, ['ng-click', 'ng-href', 'ui-sref']) ||
33053 target.nodeName == 'BUTTON' || target.nodeName == 'MD-BUTTON') && !hasAnyAttribute(target, ['md-prevent-menu-close'])) {
33054 var closestMenu = $mdUtil.getClosest(target, 'MD-MENU');
33055 if (!target.hasAttribute('disabled') && (!closestMenu || closestMenu == opts.parent[0])) {
33056 close();
33057 }
33058 break;
33059 }
33060 } while (target = target.parentNode);
33061
33062 function close() {
33063 scope.$apply(function() {
33064 opts.mdMenuCtrl.close(true, { closeAll: true });
33065 });
33066 }
33067
33068 function hasAnyAttribute(target, attrs) {
33069 if (!target) return false;
33070
33071 for (var i = 0, attr; attr = attrs[i]; ++i) {
33072 if (prefixer.hasAttribute(target, attr)) {
33073 return true;
33074 }
33075 }
33076
33077 return false;
33078 }
33079 }
33080
33081 }
33082 }
33083
33084 /**
33085 * Takes a keypress event and focuses the next/previous menu
33086 * item from the emitting element
33087 * @param {event} e - The origin keypress event
33088 * @param {angular.element} menuEl - The menu element
33089 * @param {object} opts - The interim element options for the mdMenu
33090 * @param {number} direction - The direction to move in (+1 = next, -1 = prev)
33091 */
33092 function focusMenuItem(e, menuEl, opts, direction) {
33093 var currentItem = $mdUtil.getClosest(e.target, 'MD-MENU-ITEM');
33094
33095 var items = $mdUtil.nodesToArray(menuEl[0].children);
33096 var currentIndex = items.indexOf(currentItem);
33097
33098 // Traverse through our elements in the specified direction (+/-1) and try to
33099 // focus them until we find one that accepts focus
33100 var didFocus;
33101 for (var i = currentIndex + direction; i >= 0 && i < items.length; i = i + direction) {
33102 var focusTarget = items[i].querySelector('.md-button');
33103 didFocus = attemptFocus(focusTarget);
33104 if (didFocus) {
33105 break;
33106 }
33107 }
33108 return didFocus;
33109 }
33110
33111 /**
33112 * Attempts to focus an element. Checks whether that element is the currently
33113 * focused element after attempting.
33114 * @param {HTMLElement} el - the element to attempt focus on
33115 * @returns {bool} - whether the element was successfully focused
33116 */
33117 function attemptFocus(el) {
33118 if (el && el.getAttribute('tabindex') != -1) {
33119 el.focus();
33120 return ($document[0].activeElement == el);
33121 }
33122 }
33123
33124 /**
33125 * Use browser to remove this element without triggering a $destroy event
33126 */
33127 function detachElement(element, opts) {
33128 if (!opts.preserveElement) {
33129 if (toNode(element).parentNode === toNode(opts.parent)) {
33130 toNode(opts.parent).removeChild(toNode(element));
33131 }
33132 } else {
33133 toNode(element).style.display = 'none';
33134 }
33135 }
33136
33137 /**
33138 * Computes menu position and sets the style on the menu container
33139 * @param {HTMLElement} el - the menu container element
33140 * @param {object} opts - the interim element options object
33141 */
33142 function calculateMenuPosition(el, opts) {
33143
33144 var containerNode = el[0],
33145 openMenuNode = el[0].firstElementChild,
33146 openMenuNodeRect = openMenuNode.getBoundingClientRect(),
33147 boundryNode = $document[0].body,
33148 boundryNodeRect = boundryNode.getBoundingClientRect();
33149
33150 var menuStyle = $window.getComputedStyle(openMenuNode);
33151
33152 var originNode = opts.target[0].querySelector(prefixer.buildSelector('md-menu-origin')) || opts.target[0],
33153 originNodeRect = originNode.getBoundingClientRect();
33154
33155 var bounds = {
33156 left: boundryNodeRect.left + MENU_EDGE_MARGIN,
33157 top: Math.max(boundryNodeRect.top, 0) + MENU_EDGE_MARGIN,
33158 bottom: Math.max(boundryNodeRect.bottom, Math.max(boundryNodeRect.top, 0) + boundryNodeRect.height) - MENU_EDGE_MARGIN,
33159 right: boundryNodeRect.right - MENU_EDGE_MARGIN
33160 };
33161
33162 var alignTarget, alignTargetRect = { top:0, left : 0, right:0, bottom:0 }, existingOffsets = { top:0, left : 0, right:0, bottom:0 };
33163 var positionMode = opts.mdMenuCtrl.positionMode();
33164
33165 if (positionMode.top == 'target' || positionMode.left == 'target' || positionMode.left == 'target-right') {
33166 alignTarget = firstVisibleChild();
33167 if ( alignTarget ) {
33168 // TODO: Allow centering on an arbitrary node, for now center on first menu-item's child
33169 alignTarget = alignTarget.firstElementChild || alignTarget;
33170 alignTarget = alignTarget.querySelector(prefixer.buildSelector('md-menu-align-target')) || alignTarget;
33171 alignTargetRect = alignTarget.getBoundingClientRect();
33172
33173 existingOffsets = {
33174 top: parseFloat(containerNode.style.top || 0),
33175 left: parseFloat(containerNode.style.left || 0)
33176 };
33177 }
33178 }
33179
33180 var position = {};
33181 var transformOrigin = 'top ';
33182
33183 switch (positionMode.top) {
33184 case 'target':
33185 position.top = existingOffsets.top + originNodeRect.top - alignTargetRect.top;
33186 break;
33187 case 'cascade':
33188 position.top = originNodeRect.top - parseFloat(menuStyle.paddingTop) - originNode.style.top;
33189 break;
33190 case 'bottom':
33191 position.top = originNodeRect.top + originNodeRect.height;
33192 break;
33193 default:
33194 throw new Error('Invalid target mode "' + positionMode.top + '" specified for md-menu on Y axis.');
33195 }
33196
33197 var rtl = ($mdUtil.bidi() == 'rtl');
33198
33199 switch (positionMode.left) {
33200 case 'target':
33201 position.left = existingOffsets.left + originNodeRect.left - alignTargetRect.left;
33202 transformOrigin += rtl ? 'right' : 'left';
33203 break;
33204 case 'target-left':
33205 position.left = originNodeRect.left;
33206 transformOrigin += 'left';
33207 break;
33208 case 'target-right':
33209 position.left = originNodeRect.right - openMenuNodeRect.width + (openMenuNodeRect.right - alignTargetRect.right);
33210 transformOrigin += 'right';
33211 break;
33212 case 'cascade':
33213 var willFitRight = rtl ? (originNodeRect.left - openMenuNodeRect.width) < bounds.left : (originNodeRect.right + openMenuNodeRect.width) < bounds.right;
33214 position.left = willFitRight ? originNodeRect.right - originNode.style.left : originNodeRect.left - originNode.style.left - openMenuNodeRect.width;
33215 transformOrigin += willFitRight ? 'left' : 'right';
33216 break;
33217 case 'right':
33218 if (rtl) {
33219 position.left = originNodeRect.right - originNodeRect.width;
33220 transformOrigin += 'left';
33221 } else {
33222 position.left = originNodeRect.right - openMenuNodeRect.width;
33223 transformOrigin += 'right';
33224 }
33225 break;
33226 case 'left':
33227 if (rtl) {
33228 position.left = originNodeRect.right - openMenuNodeRect.width;
33229 transformOrigin += 'right';
33230 } else {
33231 position.left = originNodeRect.left;
33232 transformOrigin += 'left';
33233 }
33234 break;
33235 default:
33236 throw new Error('Invalid target mode "' + positionMode.left + '" specified for md-menu on X axis.');
33237 }
33238
33239 var offsets = opts.mdMenuCtrl.offsets();
33240 position.top += offsets.top;
33241 position.left += offsets.left;
33242
33243 clamp(position);
33244
33245 var scaleX = Math.round(100 * Math.min(originNodeRect.width / containerNode.offsetWidth, 1.0)) / 100;
33246 var scaleY = Math.round(100 * Math.min(originNodeRect.height / containerNode.offsetHeight, 1.0)) / 100;
33247
33248 return {
33249 top: Math.round(position.top),
33250 left: Math.round(position.left),
33251 // Animate a scale out if we aren't just repositioning
33252 transform: !opts.alreadyOpen ? $mdUtil.supplant('scale({0},{1})', [scaleX, scaleY]) : undefined,
33253 transformOrigin: transformOrigin
33254 };
33255
33256 /**
33257 * Clamps the repositioning of the menu within the confines of
33258 * bounding element (often the screen/body)
33259 */
33260 function clamp(pos) {
33261 pos.top = Math.max(Math.min(pos.top, bounds.bottom - containerNode.offsetHeight), bounds.top);
33262 pos.left = Math.max(Math.min(pos.left, bounds.right - containerNode.offsetWidth), bounds.left);
33263 }
33264
33265 /**
33266 * Gets the first visible child in the openMenuNode
33267 * Necessary incase menu nodes are being dynamically hidden
33268 */
33269 function firstVisibleChild() {
33270 for (var i = 0; i < openMenuNode.children.length; ++i) {
33271 if ($window.getComputedStyle(openMenuNode.children[i]).display != 'none') {
33272 return openMenuNode.children[i];
33273 }
33274 }
33275 }
33276 }
33277 }
33278 function toNode(el) {
33279 if (el instanceof angular.element) {
33280 el = el[0];
33281 }
33282 return el;
33283 }
33284}
33285
33286})();
33287(function(){
33288"use strict";
33289
33290
33291MenuBarController.$inject = ["$scope", "$rootScope", "$element", "$attrs", "$mdConstant", "$document", "$mdUtil", "$timeout"];
33292angular
33293 .module('material.components.menuBar')
33294 .controller('MenuBarController', MenuBarController);
33295
33296var BOUND_MENU_METHODS = ['handleKeyDown', 'handleMenuHover', 'scheduleOpenHoveredMenu', 'cancelScheduledOpen'];
33297
33298/**
33299 * @ngInject
33300 */
33301function MenuBarController($scope, $rootScope, $element, $attrs, $mdConstant, $document, $mdUtil, $timeout) {
33302 this.$element = $element;
33303 this.$attrs = $attrs;
33304 this.$mdConstant = $mdConstant;
33305 this.$mdUtil = $mdUtil;
33306 this.$document = $document;
33307 this.$scope = $scope;
33308 this.$rootScope = $rootScope;
33309 this.$timeout = $timeout;
33310
33311 var self = this;
33312 angular.forEach(BOUND_MENU_METHODS, function(methodName) {
33313 self[methodName] = angular.bind(self, self[methodName]);
33314 });
33315}
33316
33317MenuBarController.prototype.init = function() {
33318 var $element = this.$element;
33319 var $mdUtil = this.$mdUtil;
33320 var $scope = this.$scope;
33321
33322 var self = this;
33323 var deregisterFns = [];
33324 $element.on('keydown', this.handleKeyDown);
33325 this.parentToolbar = $mdUtil.getClosest($element, 'MD-TOOLBAR');
33326
33327 deregisterFns.push(this.$rootScope.$on('$mdMenuOpen', function(event, el) {
33328 if (self.getMenus().indexOf(el[0]) != -1) {
33329 $element[0].classList.add('md-open');
33330 el[0].classList.add('md-open');
33331 self.currentlyOpenMenu = el.controller('mdMenu');
33332 self.currentlyOpenMenu.registerContainerProxy(self.handleKeyDown);
33333 self.enableOpenOnHover();
33334 }
33335 }));
33336
33337 deregisterFns.push(this.$rootScope.$on('$mdMenuClose', function(event, el, opts) {
33338 var rootMenus = self.getMenus();
33339 if (rootMenus.indexOf(el[0]) != -1) {
33340 $element[0].classList.remove('md-open');
33341 el[0].classList.remove('md-open');
33342 }
33343
33344 if ($element[0].contains(el[0])) {
33345 var parentMenu = el[0];
33346 while (parentMenu && rootMenus.indexOf(parentMenu) == -1) {
33347 parentMenu = $mdUtil.getClosest(parentMenu, 'MD-MENU', true);
33348 }
33349 if (parentMenu) {
33350 if (!opts.skipFocus) parentMenu.querySelector('button:not([disabled])').focus();
33351 self.currentlyOpenMenu = undefined;
33352 self.disableOpenOnHover();
33353 self.setKeyboardMode(true);
33354 }
33355 }
33356 }));
33357
33358 $scope.$on('$destroy', function() {
33359 self.disableOpenOnHover();
33360 while (deregisterFns.length) {
33361 deregisterFns.shift()();
33362 }
33363 });
33364
33365
33366 this.setKeyboardMode(true);
33367};
33368
33369MenuBarController.prototype.setKeyboardMode = function(enabled) {
33370 if (enabled) this.$element[0].classList.add('md-keyboard-mode');
33371 else this.$element[0].classList.remove('md-keyboard-mode');
33372};
33373
33374MenuBarController.prototype.enableOpenOnHover = function() {
33375 if (this.openOnHoverEnabled) return;
33376
33377 var self = this;
33378
33379 self.openOnHoverEnabled = true;
33380
33381 if (self.parentToolbar) {
33382 self.parentToolbar.classList.add('md-has-open-menu');
33383
33384 // Needs to be on the next tick so it doesn't close immediately.
33385 self.$mdUtil.nextTick(function() {
33386 angular.element(self.parentToolbar).on('click', self.handleParentClick);
33387 }, false);
33388 }
33389
33390 angular
33391 .element(self.getMenus())
33392 .on('mouseenter', self.handleMenuHover);
33393};
33394
33395MenuBarController.prototype.handleMenuHover = function(e) {
33396 this.setKeyboardMode(false);
33397 if (this.openOnHoverEnabled) {
33398 this.scheduleOpenHoveredMenu(e);
33399 }
33400};
33401
33402MenuBarController.prototype.disableOpenOnHover = function() {
33403 if (!this.openOnHoverEnabled) return;
33404
33405 this.openOnHoverEnabled = false;
33406
33407 if (this.parentToolbar) {
33408 this.parentToolbar.classList.remove('md-has-open-menu');
33409 angular.element(this.parentToolbar).off('click', this.handleParentClick);
33410 }
33411
33412 angular
33413 .element(this.getMenus())
33414 .off('mouseenter', this.handleMenuHover);
33415};
33416
33417MenuBarController.prototype.scheduleOpenHoveredMenu = function(e) {
33418 var menuEl = angular.element(e.currentTarget);
33419 var menuCtrl = menuEl.controller('mdMenu');
33420 this.setKeyboardMode(false);
33421 this.scheduleOpenMenu(menuCtrl);
33422};
33423
33424MenuBarController.prototype.scheduleOpenMenu = function(menuCtrl) {
33425 var self = this;
33426 var $timeout = this.$timeout;
33427 if (menuCtrl != self.currentlyOpenMenu) {
33428 $timeout.cancel(self.pendingMenuOpen);
33429 self.pendingMenuOpen = $timeout(function() {
33430 self.pendingMenuOpen = undefined;
33431 if (self.currentlyOpenMenu) {
33432 self.currentlyOpenMenu.close(true, { closeAll: true });
33433 }
33434 menuCtrl.open();
33435 }, 200, false);
33436 }
33437};
33438
33439MenuBarController.prototype.handleKeyDown = function(e) {
33440 var keyCodes = this.$mdConstant.KEY_CODE;
33441 var currentMenu = this.currentlyOpenMenu;
33442 var wasOpen = currentMenu && currentMenu.isOpen;
33443 this.setKeyboardMode(true);
33444 var handled, newMenu, newMenuCtrl;
33445 switch (e.keyCode) {
33446 case keyCodes.DOWN_ARROW:
33447 if (currentMenu) {
33448 currentMenu.focusMenuContainer();
33449 } else {
33450 this.openFocusedMenu();
33451 }
33452 handled = true;
33453 break;
33454 case keyCodes.UP_ARROW:
33455 currentMenu && currentMenu.close();
33456 handled = true;
33457 break;
33458 case keyCodes.LEFT_ARROW:
33459 newMenu = this.focusMenu(-1);
33460 if (wasOpen) {
33461 newMenuCtrl = angular.element(newMenu).controller('mdMenu');
33462 this.scheduleOpenMenu(newMenuCtrl);
33463 }
33464 handled = true;
33465 break;
33466 case keyCodes.RIGHT_ARROW:
33467 newMenu = this.focusMenu(+1);
33468 if (wasOpen) {
33469 newMenuCtrl = angular.element(newMenu).controller('mdMenu');
33470 this.scheduleOpenMenu(newMenuCtrl);
33471 }
33472 handled = true;
33473 break;
33474 }
33475 if (handled) {
33476 e && e.preventDefault && e.preventDefault();
33477 e && e.stopImmediatePropagation && e.stopImmediatePropagation();
33478 }
33479};
33480
33481MenuBarController.prototype.focusMenu = function(direction) {
33482 var menus = this.getMenus();
33483 var focusedIndex = this.getFocusedMenuIndex();
33484
33485 if (focusedIndex == -1) { focusedIndex = this.getOpenMenuIndex(); }
33486
33487 var changed = false;
33488
33489 if (focusedIndex == -1) { focusedIndex = 0; changed = true; }
33490 else if (
33491 direction < 0 && focusedIndex > 0 ||
33492 direction > 0 && focusedIndex < menus.length - direction
33493 ) {
33494 focusedIndex += direction;
33495 changed = true;
33496 }
33497 if (changed) {
33498 menus[focusedIndex].querySelector('button').focus();
33499 return menus[focusedIndex];
33500 }
33501};
33502
33503MenuBarController.prototype.openFocusedMenu = function() {
33504 var menu = this.getFocusedMenu();
33505 menu && angular.element(menu).controller('mdMenu').open();
33506};
33507
33508MenuBarController.prototype.getMenus = function() {
33509 var $element = this.$element;
33510 return this.$mdUtil.nodesToArray($element[0].children)
33511 .filter(function(el) { return el.nodeName == 'MD-MENU'; });
33512};
33513
33514MenuBarController.prototype.getFocusedMenu = function() {
33515 return this.getMenus()[this.getFocusedMenuIndex()];
33516};
33517
33518MenuBarController.prototype.getFocusedMenuIndex = function() {
33519 var $mdUtil = this.$mdUtil;
33520 var focusedEl = $mdUtil.getClosest(
33521 this.$document[0].activeElement,
33522 'MD-MENU'
33523 );
33524 if (!focusedEl) return -1;
33525
33526 var focusedIndex = this.getMenus().indexOf(focusedEl);
33527 return focusedIndex;
33528};
33529
33530MenuBarController.prototype.getOpenMenuIndex = function() {
33531 var menus = this.getMenus();
33532 for (var i = 0; i < menus.length; ++i) {
33533 if (menus[i].classList.contains('md-open')) return i;
33534 }
33535 return -1;
33536};
33537
33538MenuBarController.prototype.handleParentClick = function(event) {
33539 var openMenu = this.querySelector('md-menu.md-open');
33540
33541 if (openMenu && !openMenu.contains(event.target)) {
33542 angular.element(openMenu).controller('mdMenu').close(true, {
33543 closeAll: true
33544 });
33545 }
33546};
33547
33548})();
33549(function(){
33550"use strict";
33551
33552/**
33553 * @ngdoc directive
33554 * @name mdMenuBar
33555 * @module material.components.menuBar
33556 * @restrict E
33557 * @description
33558 *
33559 * Menu bars are containers that hold multiple menus. They change the behavior and appearence
33560 * of the `md-menu` directive to behave similar to an operating system provided menu.
33561 *
33562 * @usage
33563 * <hljs lang="html">
33564 * <md-menu-bar>
33565 * <md-menu>
33566 * <button ng-click="$mdMenu.open()">
33567 * File
33568 * </button>
33569 * <md-menu-content>
33570 * <md-menu-item>
33571 * <md-button ng-click="ctrl.sampleAction('share', $event)">
33572 * Share...
33573 * </md-button>
33574 * </md-menu-item>
33575 * <md-menu-divider></md-menu-divider>
33576 * <md-menu-item>
33577 * <md-menu-item>
33578 * <md-menu>
33579 * <md-button ng-click="$mdMenu.open()">New</md-button>
33580 * <md-menu-content>
33581 * <md-menu-item><md-button ng-click="ctrl.sampleAction('New Document', $event)">Document</md-button></md-menu-item>
33582 * <md-menu-item><md-button ng-click="ctrl.sampleAction('New Spreadsheet', $event)">Spreadsheet</md-button></md-menu-item>
33583 * <md-menu-item><md-button ng-click="ctrl.sampleAction('New Presentation', $event)">Presentation</md-button></md-menu-item>
33584 * <md-menu-item><md-button ng-click="ctrl.sampleAction('New Form', $event)">Form</md-button></md-menu-item>
33585 * <md-menu-item><md-button ng-click="ctrl.sampleAction('New Drawing', $event)">Drawing</md-button></md-menu-item>
33586 * </md-menu-content>
33587 * </md-menu>
33588 * </md-menu-item>
33589 * </md-menu-content>
33590 * </md-menu>
33591 * </md-menu-bar>
33592 * </hljs>
33593 *
33594 * ## Menu Bar Controls
33595 *
33596 * You may place `md-menu-items` that function as controls within menu bars.
33597 * There are two modes that are exposed via the `type` attribute of the `md-menu-item`.
33598 * `type="checkbox"` will function as a boolean control for the `ng-model` attribute of the
33599 * `md-menu-item`. `type="radio"` will function like a radio button, setting the `ngModel`
33600 * to the `string` value of the `value` attribute. If you need non-string values, you can use
33601 * `ng-value` to provide an expression (this is similar to how angular's native `input[type=radio]` works.
33602 *
33603 * <hljs lang="html">
33604 * <md-menu-bar>
33605 * <md-menu>
33606 * <button ng-click="$mdMenu.open()">
33607 * Sample Menu
33608 * </button>
33609 * <md-menu-content>
33610 * <md-menu-item type="checkbox" ng-model="settings.allowChanges">Allow changes</md-menu-item>
33611 * <md-menu-divider></md-menu-divider>
33612 * <md-menu-item type="radio" ng-model="settings.mode" ng-value="1">Mode 1</md-menu-item>
33613 * <md-menu-item type="radio" ng-model="settings.mode" ng-value="1">Mode 2</md-menu-item>
33614 * <md-menu-item type="radio" ng-model="settings.mode" ng-value="1">Mode 3</md-menu-item>
33615 * </md-menu-content>
33616 * </md-menu>
33617 * </md-menu-bar>
33618 * </hljs>
33619 *
33620 *
33621 * ### Nesting Menus
33622 *
33623 * Menus may be nested within menu bars. This is commonly called cascading menus.
33624 * To nest a menu place the nested menu inside the content of the `md-menu-item`.
33625 * <hljs lang="html">
33626 * <md-menu-item>
33627 * <md-menu>
33628 * <button ng-click="$mdMenu.open()">New</md-button>
33629 * <md-menu-content>
33630 * <md-menu-item><md-button ng-click="ctrl.sampleAction('New Document', $event)">Document</md-button></md-menu-item>
33631 * <md-menu-item><md-button ng-click="ctrl.sampleAction('New Spreadsheet', $event)">Spreadsheet</md-button></md-menu-item>
33632 * <md-menu-item><md-button ng-click="ctrl.sampleAction('New Presentation', $event)">Presentation</md-button></md-menu-item>
33633 * <md-menu-item><md-button ng-click="ctrl.sampleAction('New Form', $event)">Form</md-button></md-menu-item>
33634 * <md-menu-item><md-button ng-click="ctrl.sampleAction('New Drawing', $event)">Drawing</md-button></md-menu-item>
33635 * </md-menu-content>
33636 * </md-menu>
33637 * </md-menu-item>
33638 * </hljs>
33639 *
33640 */
33641
33642MenuBarDirective.$inject = ["$mdUtil", "$mdTheming"];
33643angular
33644 .module('material.components.menuBar')
33645 .directive('mdMenuBar', MenuBarDirective);
33646
33647/* @ngInject */
33648function MenuBarDirective($mdUtil, $mdTheming) {
33649 return {
33650 restrict: 'E',
33651 require: 'mdMenuBar',
33652 controller: 'MenuBarController',
33653
33654 compile: function compile(templateEl, templateAttrs) {
33655 if (!templateAttrs.ariaRole) {
33656 templateEl[0].setAttribute('role', 'menubar');
33657 }
33658 angular.forEach(templateEl[0].children, function(menuEl) {
33659 if (menuEl.nodeName == 'MD-MENU') {
33660 if (!menuEl.hasAttribute('md-position-mode')) {
33661 menuEl.setAttribute('md-position-mode', 'left bottom');
33662
33663 // Since we're in the compile function and actual `md-buttons` are not compiled yet,
33664 // we need to query for possible `md-buttons` as well.
33665 menuEl.querySelector('button, a, md-button').setAttribute('role', 'menuitem');
33666 }
33667 var contentEls = $mdUtil.nodesToArray(menuEl.querySelectorAll('md-menu-content'));
33668 angular.forEach(contentEls, function(contentEl) {
33669 contentEl.classList.add('md-menu-bar-menu');
33670 contentEl.classList.add('md-dense');
33671 if (!contentEl.hasAttribute('width')) {
33672 contentEl.setAttribute('width', 5);
33673 }
33674 });
33675 }
33676 });
33677
33678 // Mark the child menu items that they're inside a menu bar. This is necessary,
33679 // because mnMenuItem has special behaviour during compilation, depending on
33680 // whether it is inside a mdMenuBar. We can usually figure this out via the DOM,
33681 // however if a directive that uses documentFragment is applied to the child (e.g. ngRepeat),
33682 // the element won't have a parent and won't compile properly.
33683 templateEl.find('md-menu-item').addClass('md-in-menu-bar');
33684
33685 return function postLink(scope, el, attr, ctrl) {
33686 el.addClass('_md'); // private md component indicator for styling
33687 $mdTheming(scope, el);
33688 ctrl.init();
33689 };
33690 }
33691 };
33692
33693}
33694
33695})();
33696(function(){
33697"use strict";
33698
33699
33700angular
33701 .module('material.components.menuBar')
33702 .directive('mdMenuDivider', MenuDividerDirective);
33703
33704
33705function MenuDividerDirective() {
33706 return {
33707 restrict: 'E',
33708 compile: function(templateEl, templateAttrs) {
33709 if (!templateAttrs.role) {
33710 templateEl[0].setAttribute('role', 'separator');
33711 }
33712 }
33713 };
33714}
33715
33716})();
33717(function(){
33718"use strict";
33719
33720
33721MenuItemController.$inject = ["$scope", "$element", "$attrs"];
33722angular
33723 .module('material.components.menuBar')
33724 .controller('MenuItemController', MenuItemController);
33725
33726
33727/**
33728 * @ngInject
33729 */
33730function MenuItemController($scope, $element, $attrs) {
33731 this.$element = $element;
33732 this.$attrs = $attrs;
33733 this.$scope = $scope;
33734}
33735
33736MenuItemController.prototype.init = function(ngModel) {
33737 var $element = this.$element;
33738 var $attrs = this.$attrs;
33739
33740 this.ngModel = ngModel;
33741 if ($attrs.type == 'checkbox' || $attrs.type == 'radio') {
33742 this.mode = $attrs.type;
33743 this.iconEl = $element[0].children[0];
33744 this.buttonEl = $element[0].children[1];
33745 if (ngModel) {
33746 // Clear ngAria set attributes
33747 this.initClickListeners();
33748 }
33749 }
33750};
33751
33752// ngAria auto sets attributes on a menu-item with a ngModel.
33753// We don't want this because our content (buttons) get the focus
33754// and set their own aria attributes appropritately. Having both
33755// breaks NVDA / JAWS. This undeoes ngAria's attrs.
33756MenuItemController.prototype.clearNgAria = function() {
33757 var el = this.$element[0];
33758 var clearAttrs = ['role', 'tabindex', 'aria-invalid', 'aria-checked'];
33759 angular.forEach(clearAttrs, function(attr) {
33760 el.removeAttribute(attr);
33761 });
33762};
33763
33764MenuItemController.prototype.initClickListeners = function() {
33765 var self = this;
33766 var ngModel = this.ngModel;
33767 var $scope = this.$scope;
33768 var $attrs = this.$attrs;
33769 var $element = this.$element;
33770 var mode = this.mode;
33771
33772 this.handleClick = angular.bind(this, this.handleClick);
33773
33774 var icon = this.iconEl;
33775 var button = angular.element(this.buttonEl);
33776 var handleClick = this.handleClick;
33777
33778 $attrs.$observe('disabled', setDisabled);
33779 setDisabled($attrs.disabled);
33780
33781 ngModel.$render = function render() {
33782 self.clearNgAria();
33783 if (isSelected()) {
33784 icon.style.display = '';
33785 button.attr('aria-checked', 'true');
33786 } else {
33787 icon.style.display = 'none';
33788 button.attr('aria-checked', 'false');
33789 }
33790 };
33791
33792 $scope.$$postDigest(ngModel.$render);
33793
33794 function isSelected() {
33795 if (mode == 'radio') {
33796 var val = $attrs.ngValue ? $scope.$eval($attrs.ngValue) : $attrs.value;
33797 return ngModel.$modelValue == val;
33798 } else {
33799 return ngModel.$modelValue;
33800 }
33801 }
33802
33803 function setDisabled(disabled) {
33804 if (disabled) {
33805 button.off('click', handleClick);
33806 } else {
33807 button.on('click', handleClick);
33808 }
33809 }
33810};
33811
33812MenuItemController.prototype.handleClick = function(e) {
33813 var mode = this.mode;
33814 var ngModel = this.ngModel;
33815 var $attrs = this.$attrs;
33816 var newVal;
33817 if (mode == 'checkbox') {
33818 newVal = !ngModel.$modelValue;
33819 } else if (mode == 'radio') {
33820 newVal = $attrs.ngValue ? this.$scope.$eval($attrs.ngValue) : $attrs.value;
33821 }
33822 ngModel.$setViewValue(newVal);
33823 ngModel.$render();
33824};
33825
33826})();
33827(function(){
33828"use strict";
33829
33830
33831MenuItemDirective.$inject = ["$mdUtil", "$mdConstant", "$$mdSvgRegistry"];
33832angular
33833 .module('material.components.menuBar')
33834 .directive('mdMenuItem', MenuItemDirective);
33835
33836 /* @ngInject */
33837function MenuItemDirective($mdUtil, $mdConstant, $$mdSvgRegistry) {
33838 return {
33839 controller: 'MenuItemController',
33840 require: ['mdMenuItem', '?ngModel'],
33841 priority: $mdConstant.BEFORE_NG_ARIA,
33842 compile: function(templateEl, templateAttrs) {
33843 var type = templateAttrs.type;
33844 var inMenuBarClass = 'md-in-menu-bar';
33845
33846 // Note: This allows us to show the `check` icon for the md-menu-bar items.
33847 // The `md-in-menu-bar` class is set by the mdMenuBar directive.
33848 if ((type == 'checkbox' || type == 'radio') && templateEl.hasClass(inMenuBarClass)) {
33849 var text = templateEl[0].textContent;
33850 var buttonEl = angular.element('<md-button type="button"></md-button>');
33851 var iconTemplate = '<md-icon md-svg-src="' + $$mdSvgRegistry.mdChecked + '"></md-icon>';
33852
33853 buttonEl.html(text);
33854 buttonEl.attr('tabindex', '0');
33855
33856 templateEl.html('');
33857 templateEl.append(angular.element(iconTemplate));
33858 templateEl.append(buttonEl);
33859 templateEl.addClass('md-indent').removeClass(inMenuBarClass);
33860
33861 setDefault('role', type == 'checkbox' ? 'menuitemcheckbox' : 'menuitemradio', buttonEl);
33862 moveAttrToButton('ng-disabled');
33863
33864 } else {
33865 setDefault('role', 'menuitem', templateEl[0].querySelector('md-button, button, a'));
33866 }
33867
33868
33869 return function(scope, el, attrs, ctrls) {
33870 var ctrl = ctrls[0];
33871 var ngModel = ctrls[1];
33872 ctrl.init(ngModel);
33873 };
33874
33875 function setDefault(attr, val, el) {
33876 el = el || templateEl;
33877 if (el instanceof angular.element) {
33878 el = el[0];
33879 }
33880 if (!el.hasAttribute(attr)) {
33881 el.setAttribute(attr, val);
33882 }
33883 }
33884
33885 function moveAttrToButton(attribute) {
33886 var attributes = $mdUtil.prefixer(attribute);
33887
33888 angular.forEach(attributes, function(attr) {
33889 if (templateEl[0].hasAttribute(attr)) {
33890 var val = templateEl[0].getAttribute(attr);
33891 buttonEl[0].setAttribute(attr, val);
33892 templateEl[0].removeAttribute(attr);
33893 }
33894 });
33895 }
33896 }
33897 };
33898}
33899
33900})();
33901(function(){
33902"use strict";
33903
33904/**
33905 * @ngdoc directive
33906 * @name mdProgressCircular
33907 * @module material.components.progressCircular
33908 * @restrict E
33909 *
33910 * @description
33911 * The circular progress directive is used to make loading content in your app as delightful and
33912 * painless as possible by minimizing the amount of visual change a user sees before they can view
33913 * and interact with content.
33914 *
33915 * For operations where the percentage of the operation completed can be determined, use a
33916 * determinate indicator. They give users a quick sense of how long an operation will take.
33917 *
33918 * For operations where the user is asked to wait a moment while something finishes up, and it’s
33919 * not necessary to expose what's happening behind the scenes and how long it will take, use an
33920 * indeterminate indicator.
33921 *
33922 * @param {string} md-mode Select from one of two modes: **'determinate'** and **'indeterminate'**.
33923 *
33924 * Note: if the `md-mode` value is set as undefined or specified as not 1 of the two (2) valid modes, then **'indeterminate'**
33925 * will be auto-applied as the mode.
33926 *
33927 * Note: if not configured, the `md-mode="indeterminate"` will be auto injected as an attribute.
33928 * If `value=""` is also specified, however, then `md-mode="determinate"` would be auto-injected instead.
33929 * @param {number=} value In determinate mode, this number represents the percentage of the
33930 * circular progress. Default: 0
33931 * @param {number=} md-diameter This specifies the diameter of the circular progress. The value
33932 * should be a pixel-size value (eg '100'). If this attribute is
33933 * not present then a default value of '50px' is assumed.
33934 *
33935 * @param {boolean=} ng-disabled Determines whether to disable the progress element.
33936 *
33937 * @usage
33938 * <hljs lang="html">
33939 * <md-progress-circular md-mode="determinate" value="..."></md-progress-circular>
33940 *
33941 * <md-progress-circular md-mode="determinate" ng-value="..."></md-progress-circular>
33942 *
33943 * <md-progress-circular md-mode="determinate" value="..." md-diameter="100"></md-progress-circular>
33944 *
33945 * <md-progress-circular md-mode="indeterminate"></md-progress-circular>
33946 * </hljs>
33947 */
33948
33949MdProgressCircularDirective.$inject = ["$window", "$mdProgressCircular", "$mdTheming", "$mdUtil", "$interval", "$log"];
33950angular
33951 .module('material.components.progressCircular')
33952 .directive('mdProgressCircular', MdProgressCircularDirective);
33953
33954/* @ngInject */
33955function MdProgressCircularDirective($window, $mdProgressCircular, $mdTheming,
33956 $mdUtil, $interval, $log) {
33957
33958 // Note that this shouldn't use use $$rAF, because it can cause an infinite loop
33959 // in any tests that call $animate.flush.
33960 var rAF = $window.requestAnimationFrame ||
33961 $window.webkitRequestAnimationFrame ||
33962 angular.noop;
33963
33964 var cAF = $window.cancelAnimationFrame ||
33965 $window.webkitCancelAnimationFrame ||
33966 $window.webkitCancelRequestAnimationFrame ||
33967 angular.noop;
33968
33969 var MODE_DETERMINATE = 'determinate';
33970 var MODE_INDETERMINATE = 'indeterminate';
33971 var DISABLED_CLASS = '_md-progress-circular-disabled';
33972 var INDETERMINATE_CLASS = 'md-mode-indeterminate';
33973
33974 return {
33975 restrict: 'E',
33976 scope: {
33977 value: '@',
33978 mdDiameter: '@',
33979 mdMode: '@'
33980 },
33981 template:
33982 '<svg xmlns="http://www.w3.org/2000/svg">' +
33983 '<path fill="none"/>' +
33984 '</svg>',
33985 compile: function(element, attrs) {
33986 element.attr({
33987 'aria-valuemin': 0,
33988 'aria-valuemax': 100,
33989 'role': 'progressbar'
33990 });
33991
33992 if (angular.isUndefined(attrs.mdMode)) {
33993 var mode = attrs.hasOwnProperty('value') ? MODE_DETERMINATE : MODE_INDETERMINATE;
33994 attrs.$set('mdMode', mode);
33995 } else {
33996 attrs.$set('mdMode', attrs.mdMode.trim());
33997 }
33998
33999 return MdProgressCircularLink;
34000 }
34001 };
34002
34003 function MdProgressCircularLink(scope, element, attrs) {
34004 var node = element[0];
34005 var svg = angular.element(node.querySelector('svg'));
34006 var path = angular.element(node.querySelector('path'));
34007 var startIndeterminate = $mdProgressCircular.startIndeterminate;
34008 var endIndeterminate = $mdProgressCircular.endIndeterminate;
34009 var iterationCount = 0;
34010 var lastAnimationId = 0;
34011 var lastDrawFrame;
34012 var interval;
34013
34014 $mdTheming(element);
34015 element.toggleClass(DISABLED_CLASS, attrs.hasOwnProperty('disabled'));
34016
34017 // If the mode is indeterminate, it doesn't need to
34018 // wait for the next digest. It can start right away.
34019 if(scope.mdMode === MODE_INDETERMINATE){
34020 startIndeterminateAnimation();
34021 }
34022
34023 scope.$on('$destroy', function(){
34024 cleanupIndeterminateAnimation();
34025
34026 if (lastDrawFrame) {
34027 cAF(lastDrawFrame);
34028 }
34029 });
34030
34031 scope.$watchGroup(['value', 'mdMode', function() {
34032 var isDisabled = node.disabled;
34033
34034 // Sometimes the browser doesn't return a boolean, in
34035 // which case we should check whether the attribute is
34036 // present.
34037 if (isDisabled === true || isDisabled === false){
34038 return isDisabled;
34039 }
34040
34041 return angular.isDefined(element.attr('disabled'));
34042 }], function(newValues, oldValues) {
34043 var mode = newValues[1];
34044 var isDisabled = newValues[2];
34045 var wasDisabled = oldValues[2];
34046
34047 if (isDisabled !== wasDisabled) {
34048 element.toggleClass(DISABLED_CLASS, !!isDisabled);
34049 }
34050
34051 if (isDisabled) {
34052 cleanupIndeterminateAnimation();
34053 } else {
34054 if (mode !== MODE_DETERMINATE && mode !== MODE_INDETERMINATE) {
34055 mode = MODE_INDETERMINATE;
34056 attrs.$set('mdMode', mode);
34057 }
34058
34059 if (mode === MODE_INDETERMINATE) {
34060 startIndeterminateAnimation();
34061 } else {
34062 var newValue = clamp(newValues[0]);
34063
34064 cleanupIndeterminateAnimation();
34065
34066 element.attr('aria-valuenow', newValue);
34067 renderCircle(clamp(oldValues[0]), newValue);
34068 }
34069 }
34070
34071 });
34072
34073 // This is in a separate watch in order to avoid layout, unless
34074 // the value has actually changed.
34075 scope.$watch('mdDiameter', function(newValue) {
34076 var diameter = getSize(newValue);
34077 var strokeWidth = getStroke(diameter);
34078 var value = clamp(scope.value);
34079 var transformOrigin = (diameter / 2) + 'px';
34080 var dimensions = {
34081 width: diameter + 'px',
34082 height: diameter + 'px'
34083 };
34084
34085 // The viewBox has to be applied via setAttribute, because it is
34086 // case-sensitive. If jQuery is included in the page, `.attr` lowercases
34087 // all attribute names.
34088 svg[0].setAttribute('viewBox', '0 0 ' + diameter + ' ' + diameter);
34089
34090 // Usually viewBox sets the dimensions for the SVG, however that doesn't
34091 // seem to be the case on IE10.
34092 // Important! The transform origin has to be set from here and it has to
34093 // be in the format of "Ypx Ypx Ypx", otherwise the rotation wobbles in
34094 // IE and Edge, because they don't account for the stroke width when
34095 // rotating. Also "center" doesn't help in this case, it has to be a
34096 // precise value.
34097 svg
34098 .css(dimensions)
34099 .css('transform-origin', transformOrigin + ' ' + transformOrigin + ' ' + transformOrigin);
34100
34101 element.css(dimensions);
34102
34103 path.attr('stroke-width', strokeWidth);
34104 path.attr('stroke-linecap', 'square');
34105 if (scope.mdMode == MODE_INDETERMINATE) {
34106 path.attr('d', getSvgArc(diameter, strokeWidth, true));
34107 path.attr('stroke-dasharray', (diameter - strokeWidth) * $window.Math.PI * 0.75);
34108 path.attr('stroke-dashoffset', getDashLength(diameter, strokeWidth, 1, 75));
34109 } else {
34110 path.attr('d', getSvgArc(diameter, strokeWidth, false));
34111 path.attr('stroke-dasharray', (diameter - strokeWidth) * $window.Math.PI);
34112 path.attr('stroke-dashoffset', getDashLength(diameter, strokeWidth, 0, 100));
34113 renderCircle(value, value);
34114 }
34115
34116 });
34117
34118 function renderCircle(animateFrom, animateTo, easing, duration, iterationCount, maxValue) {
34119 var id = ++lastAnimationId;
34120 var startTime = $mdUtil.now();
34121 var changeInValue = animateTo - animateFrom;
34122 var diameter = getSize(scope.mdDiameter);
34123 var strokeWidth = getStroke(diameter);
34124 var ease = easing || $mdProgressCircular.easeFn;
34125 var animationDuration = duration || $mdProgressCircular.duration;
34126 var rotation = -90 * (iterationCount || 0);
34127 var dashLimit = maxValue || 100;
34128
34129 // No need to animate it if the values are the same
34130 if (animateTo === animateFrom) {
34131 renderFrame(animateTo);
34132 } else {
34133 lastDrawFrame = rAF(function animation() {
34134 var currentTime = $window.Math.max(0, $window.Math.min($mdUtil.now() - startTime, animationDuration));
34135
34136 renderFrame(ease(currentTime, animateFrom, changeInValue, animationDuration));
34137
34138 // Do not allow overlapping animations
34139 if (id === lastAnimationId && currentTime < animationDuration) {
34140 lastDrawFrame = rAF(animation);
34141 }
34142 });
34143 }
34144
34145 function renderFrame(value) {
34146 path.attr('stroke-dashoffset', getDashLength(diameter, strokeWidth, value, dashLimit));
34147 path.attr('transform','rotate(' + (rotation) + ' ' + diameter/2 + ' ' + diameter/2 + ')');
34148 }
34149 }
34150
34151 function animateIndeterminate() {
34152 renderCircle(
34153 startIndeterminate,
34154 endIndeterminate,
34155 $mdProgressCircular.easeFnIndeterminate,
34156 $mdProgressCircular.durationIndeterminate,
34157 iterationCount,
34158 75
34159 );
34160
34161 // The %4 technically isn't necessary, but it keeps the rotation
34162 // under 360, instead of becoming a crazy large number.
34163 iterationCount = ++iterationCount % 4;
34164
34165 }
34166
34167 function startIndeterminateAnimation() {
34168 if (!interval) {
34169 // Note that this interval isn't supposed to trigger a digest.
34170 interval = $interval(
34171 animateIndeterminate,
34172 $mdProgressCircular.durationIndeterminate,
34173 0,
34174 false
34175 );
34176
34177 animateIndeterminate();
34178
34179 element
34180 .addClass(INDETERMINATE_CLASS)
34181 .removeAttr('aria-valuenow');
34182 }
34183 }
34184
34185 function cleanupIndeterminateAnimation() {
34186 if (interval) {
34187 $interval.cancel(interval);
34188 interval = null;
34189 element.removeClass(INDETERMINATE_CLASS);
34190 }
34191 }
34192 }
34193
34194 /**
34195 * Returns SVG path data for progress circle
34196 * Syntax spec: https://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands
34197 *
34198 * @param {number} diameter Diameter of the container.
34199 * @param {number} strokeWidth Stroke width to be used when drawing circle
34200 * @param {boolean} indeterminate Use if progress circle will be used for indeterminate
34201 *
34202 * @returns {string} String representation of an SVG arc.
34203 */
34204 function getSvgArc(diameter, strokeWidth, indeterminate) {
34205 var radius = diameter / 2;
34206 var offset = strokeWidth / 2;
34207 var start = radius + ',' + offset; // ie: (25, 2.5) or 12 o'clock
34208 var end = offset + ',' + radius; // ie: (2.5, 25) or 9 o'clock
34209 var arcRadius = radius - offset;
34210 return 'M' + start
34211 + 'A' + arcRadius + ',' + arcRadius + ' 0 1 1 ' + end // 75% circle
34212 + (indeterminate ? '' : 'A' + arcRadius + ',' + arcRadius + ' 0 0 1 ' + start); // loop to start
34213 }
34214
34215 /**
34216 * Return stroke length for progress circle
34217 *
34218 * @param {number} diameter Diameter of the container.
34219 * @param {number} strokeWidth Stroke width to be used when drawing circle
34220 * @param {number} value Percentage of circle (between 0 and 100)
34221 * @param {number} limit Max percentage for circle
34222 *
34223 * @returns {number} Stroke length for progres circle
34224 */
34225 function getDashLength(diameter, strokeWidth, value, limit) {
34226 return (diameter - strokeWidth) * $window.Math.PI * ( (3 * (limit || 100) / 100) - (value/100) );
34227 }
34228
34229 /**
34230 * Limits a value between 0 and 100.
34231 */
34232 function clamp(value) {
34233 return $window.Math.max(0, $window.Math.min(value || 0, 100));
34234 }
34235
34236 /**
34237 * Determines the size of a progress circle, based on the provided
34238 * value in the following formats: `X`, `Ypx`, `Z%`.
34239 */
34240 function getSize(value) {
34241 var defaultValue = $mdProgressCircular.progressSize;
34242
34243 if (value) {
34244 var parsed = parseFloat(value);
34245
34246 if (value.lastIndexOf('%') === value.length - 1) {
34247 parsed = (parsed / 100) * defaultValue;
34248 }
34249
34250 return parsed;
34251 }
34252
34253 return defaultValue;
34254 }
34255
34256 /**
34257 * Determines the circle's stroke width, based on
34258 * the provided diameter.
34259 */
34260 function getStroke(diameter) {
34261 return $mdProgressCircular.strokeWidth / 100 * diameter;
34262 }
34263
34264}
34265
34266})();
34267(function(){
34268"use strict";
34269
34270/**
34271 * @ngdoc service
34272 * @name $mdProgressCircular
34273 * @module material.components.progressCircular
34274 *
34275 * @description
34276 * Allows the user to specify the default options for the `progressCircular` directive.
34277 *
34278 * @property {number} progressSize Diameter of the progress circle in pixels.
34279 * @property {number} strokeWidth Width of the circle's stroke as a percentage of the circle's size.
34280 * @property {number} duration Length of the circle animation in milliseconds.
34281 * @property {function} easeFn Default easing animation function.
34282 * @property {object} easingPresets Collection of pre-defined easing functions.
34283 *
34284 * @property {number} durationIndeterminate Duration of the indeterminate animation.
34285 * @property {number} startIndeterminate Indeterminate animation start point.
34286 * @property {number} endIndeterminate Indeterminate animation end point.
34287 * @property {function} easeFnIndeterminate Easing function to be used when animating
34288 * between the indeterminate values.
34289 *
34290 * @property {(function(object): object)} configure Used to modify the default options.
34291 *
34292 * @usage
34293 * <hljs lang="js">
34294 * myAppModule.config(function($mdProgressCircularProvider) {
34295 *
34296 * // Example of changing the default progress options.
34297 * $mdProgressCircularProvider.configure({
34298 * progressSize: 100,
34299 * strokeWidth: 20,
34300 * duration: 800
34301 * });
34302 * });
34303 * </hljs>
34304 *
34305 */
34306
34307angular
34308 .module('material.components.progressCircular')
34309 .provider("$mdProgressCircular", MdProgressCircularProvider);
34310
34311function MdProgressCircularProvider() {
34312 var progressConfig = {
34313 progressSize: 50,
34314 strokeWidth: 10,
34315 duration: 100,
34316 easeFn: linearEase,
34317
34318 durationIndeterminate: 1333,
34319 startIndeterminate: 1,
34320 endIndeterminate: 149,
34321 easeFnIndeterminate: materialEase,
34322
34323 easingPresets: {
34324 linearEase: linearEase,
34325 materialEase: materialEase
34326 }
34327 };
34328
34329 return {
34330 configure: function(options) {
34331 progressConfig = angular.extend(progressConfig, options || {});
34332 return progressConfig;
34333 },
34334 $get: function() { return progressConfig; }
34335 };
34336
34337 function linearEase(t, b, c, d) {
34338 return c * t / d + b;
34339 }
34340
34341 function materialEase(t, b, c, d) {
34342 // via http://www.timotheegroleau.com/Flash/experiments/easing_function_generator.htm
34343 // with settings of [0, 0, 1, 1]
34344 var ts = (t /= d) * t;
34345 var tc = ts * t;
34346 return b + c * (6 * tc * ts + -15 * ts * ts + 10 * tc);
34347 }
34348}
34349
34350})();
34351(function(){
34352"use strict";
34353
34354/**
34355 * @ngdoc directive
34356 * @name mdTab
34357 * @module material.components.tabs
34358 *
34359 * @restrict E
34360 *
34361 * @description
34362 * Use the `<md-tab>` a nested directive used within `<md-tabs>` to specify a tab with a **label** and optional *view content*.
34363 *
34364 * If the `label` attribute is not specified, then an optional `<md-tab-label>` tag can be used to specify more
34365 * complex tab header markup. If neither the **label** nor the **md-tab-label** are specified, then the nested
34366 * markup of the `<md-tab>` is used as the tab header markup.
34367 *
34368 * Please note that if you use `<md-tab-label>`, your content **MUST** be wrapped in the `<md-tab-body>` tag. This
34369 * is to define a clear separation between the tab content and the tab label.
34370 *
34371 * This container is used by the TabsController to show/hide the active tab's content view. This synchronization is
34372 * automatically managed by the internal TabsController whenever the tab selection changes. Selection changes can
34373 * be initiated via data binding changes, programmatic invocation, or user gestures.
34374 *
34375 * @param {string=} label Optional attribute to specify a simple string as the tab label
34376 * @param {boolean=} ng-disabled If present and expression evaluates to truthy, disabled tab selection.
34377 * @param {expression=} md-on-deselect Expression to be evaluated after the tab has been de-selected.
34378 * @param {expression=} md-on-select Expression to be evaluated after the tab has been selected.
34379 * @param {boolean=} md-active When true, sets the active tab. Note: There can only be one active tab at a time.
34380 *
34381 *
34382 * @usage
34383 *
34384 * <hljs lang="html">
34385 * <md-tab label="" ng-disabled md-on-select="" md-on-deselect="" >
34386 * <h3>My Tab content</h3>
34387 * </md-tab>
34388 *
34389 * <md-tab >
34390 * <md-tab-label>
34391 * <h3>My Tab content</h3>
34392 * </md-tab-label>
34393 * <md-tab-body>
34394 * <p>
34395 * Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium,
34396 * totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae
34397 * dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit,
34398 * sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.
34399 * </p>
34400 * </md-tab-body>
34401 * </md-tab>
34402 * </hljs>
34403 *
34404 */
34405angular
34406 .module('material.components.tabs')
34407 .directive('mdTab', MdTab);
34408
34409function MdTab () {
34410 return {
34411 require: '^?mdTabs',
34412 terminal: true,
34413 compile: function (element, attr) {
34414 var label = firstChild(element, 'md-tab-label'),
34415 body = firstChild(element, 'md-tab-body');
34416
34417 if (label.length === 0) {
34418 label = angular.element('<md-tab-label></md-tab-label>');
34419 if (attr.label) label.text(attr.label);
34420 else label.append(element.contents());
34421
34422 if (body.length === 0) {
34423 var contents = element.contents().detach();
34424 body = angular.element('<md-tab-body></md-tab-body>');
34425 body.append(contents);
34426 }
34427 }
34428
34429 element.append(label);
34430 if (body.html()) element.append(body);
34431
34432 return postLink;
34433 },
34434 scope: {
34435 active: '=?mdActive',
34436 disabled: '=?ngDisabled',
34437 select: '&?mdOnSelect',
34438 deselect: '&?mdOnDeselect'
34439 }
34440 };
34441
34442 function postLink (scope, element, attr, ctrl) {
34443 if (!ctrl) return;
34444 var index = ctrl.getTabElementIndex(element),
34445 body = firstChild(element, 'md-tab-body').remove(),
34446 label = firstChild(element, 'md-tab-label').remove(),
34447 data = ctrl.insertTab({
34448 scope: scope,
34449 parent: scope.$parent,
34450 index: index,
34451 element: element,
34452 template: body.html(),
34453 label: label.html()
34454 }, index);
34455
34456 scope.select = scope.select || angular.noop;
34457 scope.deselect = scope.deselect || angular.noop;
34458
34459 scope.$watch('active', function (active) { if (active) ctrl.select(data.getIndex(), true); });
34460 scope.$watch('disabled', function () { ctrl.refreshIndex(); });
34461 scope.$watch(
34462 function () {
34463 return ctrl.getTabElementIndex(element);
34464 },
34465 function (newIndex) {
34466 data.index = newIndex;
34467 ctrl.updateTabOrder();
34468 }
34469 );
34470 scope.$on('$destroy', function () { ctrl.removeTab(data); });
34471 }
34472
34473 function firstChild (element, tagName) {
34474 var children = element[0].children;
34475 for (var i = 0, len = children.length; i < len; i++) {
34476 var child = children[i];
34477 if (child.tagName === tagName.toUpperCase()) return angular.element(child);
34478 }
34479 return angular.element();
34480 }
34481}
34482
34483})();
34484(function(){
34485"use strict";
34486
34487angular
34488 .module('material.components.tabs')
34489 .directive('mdTabItem', MdTabItem);
34490
34491function MdTabItem () {
34492 return {
34493 require: '^?mdTabs',
34494 link: function link (scope, element, attr, ctrl) {
34495 if (!ctrl) return;
34496 ctrl.attachRipple(scope, element);
34497 }
34498 };
34499}
34500
34501})();
34502(function(){
34503"use strict";
34504
34505angular
34506 .module('material.components.tabs')
34507 .directive('mdTabLabel', MdTabLabel);
34508
34509function MdTabLabel () {
34510 return { terminal: true };
34511}
34512
34513
34514})();
34515(function(){
34516"use strict";
34517
34518
34519MdTabScroll.$inject = ["$parse"];angular.module('material.components.tabs')
34520 .directive('mdTabScroll', MdTabScroll);
34521
34522function MdTabScroll ($parse) {
34523 return {
34524 restrict: 'A',
34525 compile: function ($element, attr) {
34526 var fn = $parse(attr.mdTabScroll, null, true);
34527 return function ngEventHandler (scope, element) {
34528 element.on('mousewheel', function (event) {
34529 scope.$apply(function () { fn(scope, { $event: event }); });
34530 });
34531 };
34532 }
34533 };
34534}
34535
34536})();
34537(function(){
34538"use strict";
34539
34540
34541MdTabsController.$inject = ["$scope", "$element", "$window", "$mdConstant", "$mdTabInkRipple", "$mdUtil", "$animateCss", "$attrs", "$compile", "$mdTheming", "$mdInteraction"];angular
34542 .module('material.components.tabs')
34543 .controller('MdTabsController', MdTabsController);
34544
34545/**
34546 * @ngInject
34547 */
34548function MdTabsController ($scope, $element, $window, $mdConstant, $mdTabInkRipple, $mdUtil,
34549 $animateCss, $attrs, $compile, $mdTheming, $mdInteraction) {
34550 // define private properties
34551 var ctrl = this,
34552 locked = false,
34553 elements = getElements(),
34554 queue = [],
34555 destroyed = false,
34556 loaded = false;
34557
34558
34559 // Define public methods
34560 ctrl.$onInit = $onInit;
34561 ctrl.updatePagination = $mdUtil.debounce(updatePagination, 100);
34562 ctrl.redirectFocus = redirectFocus;
34563 ctrl.attachRipple = attachRipple;
34564 ctrl.insertTab = insertTab;
34565 ctrl.removeTab = removeTab;
34566 ctrl.select = select;
34567 ctrl.scroll = scroll;
34568 ctrl.nextPage = nextPage;
34569 ctrl.previousPage = previousPage;
34570 ctrl.keydown = keydown;
34571 ctrl.canPageForward = canPageForward;
34572 ctrl.canPageBack = canPageBack;
34573 ctrl.refreshIndex = refreshIndex;
34574 ctrl.incrementIndex = incrementIndex;
34575 ctrl.getTabElementIndex = getTabElementIndex;
34576 ctrl.updateInkBarStyles = $mdUtil.debounce(updateInkBarStyles, 100);
34577 ctrl.updateTabOrder = $mdUtil.debounce(updateTabOrder, 100);
34578 ctrl.getFocusedTabId = getFocusedTabId;
34579
34580 // For Angular 1.4 and older, where there are no lifecycle hooks but bindings are pre-assigned,
34581 // manually call the $onInit hook.
34582 if (angular.version.major === 1 && angular.version.minor <= 4) {
34583 this.$onInit();
34584 }
34585
34586 /**
34587 * Angular Lifecycle hook for newer Angular versions.
34588 * Bindings are not guaranteed to have been assigned in the controller, but they are in the $onInit hook.
34589 */
34590 function $onInit() {
34591 // Define one-way bindings
34592 defineOneWayBinding('stretchTabs', handleStretchTabs);
34593
34594 // Define public properties with change handlers
34595 defineProperty('focusIndex', handleFocusIndexChange, ctrl.selectedIndex || 0);
34596 defineProperty('offsetLeft', handleOffsetChange, 0);
34597 defineProperty('hasContent', handleHasContent, false);
34598 defineProperty('maxTabWidth', handleMaxTabWidth, getMaxTabWidth());
34599 defineProperty('shouldPaginate', handleShouldPaginate, false);
34600
34601 // Define boolean attributes
34602 defineBooleanAttribute('noInkBar', handleInkBar);
34603 defineBooleanAttribute('dynamicHeight', handleDynamicHeight);
34604 defineBooleanAttribute('noPagination');
34605 defineBooleanAttribute('swipeContent');
34606 defineBooleanAttribute('noDisconnect');
34607 defineBooleanAttribute('autoselect');
34608 defineBooleanAttribute('noSelectClick');
34609 defineBooleanAttribute('centerTabs', handleCenterTabs, false);
34610 defineBooleanAttribute('enableDisconnect');
34611
34612 // Define public properties
34613 ctrl.scope = $scope;
34614 ctrl.parent = $scope.$parent;
34615 ctrl.tabs = [];
34616 ctrl.lastSelectedIndex = null;
34617 ctrl.hasFocus = false;
34618 ctrl.styleTabItemFocus = false;
34619 ctrl.shouldCenterTabs = shouldCenterTabs();
34620 ctrl.tabContentPrefix = 'tab-content-';
34621
34622 // Setup the tabs controller after all bindings are available.
34623 setupTabsController();
34624 }
34625
34626 /**
34627 * Perform setup for the controller, setup events and watcher(s)
34628 */
34629 function setupTabsController () {
34630 ctrl.selectedIndex = ctrl.selectedIndex || 0;
34631 compileTemplate();
34632 configureWatchers();
34633 bindEvents();
34634 $mdTheming($element);
34635 $mdUtil.nextTick(function () {
34636 // Note that the element references need to be updated, because certain "browsers"
34637 // (IE/Edge) lose them and start throwing "Invalid calling object" errors, when we
34638 // compile the element contents down in `compileElement`.
34639 elements = getElements();
34640 updateHeightFromContent();
34641 adjustOffset();
34642 updateInkBarStyles();
34643 ctrl.tabs[ ctrl.selectedIndex ] && ctrl.tabs[ ctrl.selectedIndex ].scope.select();
34644 loaded = true;
34645 updatePagination();
34646 });
34647 }
34648
34649 /**
34650 * Compiles the template provided by the user. This is passed as an attribute from the tabs
34651 * directive's template function.
34652 */
34653 function compileTemplate () {
34654 var template = $attrs.$mdTabsTemplate,
34655 element = angular.element($element[0].querySelector('md-tab-data'));
34656
34657 element.html(template);
34658 $compile(element.contents())(ctrl.parent);
34659 delete $attrs.$mdTabsTemplate;
34660 }
34661
34662 /**
34663 * Binds events used by the tabs component.
34664 */
34665 function bindEvents () {
34666 angular.element($window).on('resize', handleWindowResize);
34667 $scope.$on('$destroy', cleanup);
34668 }
34669
34670 /**
34671 * Configure watcher(s) used by Tabs
34672 */
34673 function configureWatchers () {
34674 $scope.$watch('$mdTabsCtrl.selectedIndex', handleSelectedIndexChange);
34675 }
34676
34677 /**
34678 * Creates a one-way binding manually rather than relying on Angular's isolated scope
34679 * @param key
34680 * @param handler
34681 */
34682 function defineOneWayBinding (key, handler) {
34683 var attr = $attrs.$normalize('md-' + key);
34684 if (handler) defineProperty(key, handler);
34685 $attrs.$observe(attr, function (newValue) { ctrl[ key ] = newValue; });
34686 }
34687
34688 /**
34689 * Defines boolean attributes with default value set to true. (ie. md-stretch-tabs with no value
34690 * will be treated as being truthy)
34691 * @param key
34692 * @param handler
34693 */
34694 function defineBooleanAttribute (key, handler) {
34695 var attr = $attrs.$normalize('md-' + key);
34696 if (handler) defineProperty(key, handler);
34697 if ($attrs.hasOwnProperty(attr)) updateValue($attrs[attr]);
34698 $attrs.$observe(attr, updateValue);
34699 function updateValue (newValue) {
34700 ctrl[ key ] = newValue !== 'false';
34701 }
34702 }
34703
34704 /**
34705 * Remove any events defined by this controller
34706 */
34707 function cleanup () {
34708 destroyed = true;
34709 angular.element($window).off('resize', handleWindowResize);
34710 }
34711
34712 // Change handlers
34713
34714 /**
34715 * Toggles stretch tabs class and updates inkbar when tab stretching changes
34716 * @param stretchTabs
34717 */
34718 function handleStretchTabs (stretchTabs) {
34719 var elements = getElements();
34720 angular.element(elements.wrapper).toggleClass('md-stretch-tabs', shouldStretchTabs());
34721 updateInkBarStyles();
34722 }
34723
34724 function handleCenterTabs (newValue) {
34725 ctrl.shouldCenterTabs = shouldCenterTabs();
34726 }
34727
34728 function handleMaxTabWidth (newWidth, oldWidth) {
34729 if (newWidth !== oldWidth) {
34730 var elements = getElements();
34731
34732 angular.forEach(elements.tabs, function(tab) {
34733 tab.style.maxWidth = newWidth + 'px';
34734 });
34735 $mdUtil.nextTick(ctrl.updateInkBarStyles);
34736 }
34737 }
34738
34739 function handleShouldPaginate (newValue, oldValue) {
34740 if (newValue !== oldValue) {
34741 ctrl.maxTabWidth = getMaxTabWidth();
34742 ctrl.shouldCenterTabs = shouldCenterTabs();
34743 $mdUtil.nextTick(function () {
34744 ctrl.maxTabWidth = getMaxTabWidth();
34745 adjustOffset(ctrl.selectedIndex);
34746 });
34747 }
34748 }
34749
34750 /**
34751 * Add/remove the `md-no-tab-content` class depending on `ctrl.hasContent`
34752 * @param hasContent
34753 */
34754 function handleHasContent (hasContent) {
34755 $element[ hasContent ? 'removeClass' : 'addClass' ]('md-no-tab-content');
34756 }
34757
34758 /**
34759 * Apply ctrl.offsetLeft to the paging element when it changes
34760 * @param left
34761 */
34762 function handleOffsetChange (left) {
34763 var elements = getElements();
34764 var newValue = ctrl.shouldCenterTabs ? '' : '-' + left + 'px';
34765
34766 angular.element(elements.paging).css($mdConstant.CSS.TRANSFORM, 'translate3d(' + newValue + ', 0, 0)');
34767 $scope.$broadcast('$mdTabsPaginationChanged');
34768 }
34769
34770 /**
34771 * Update the UI whenever `ctrl.focusIndex` is updated
34772 * @param newIndex
34773 * @param oldIndex
34774 */
34775 function handleFocusIndexChange (newIndex, oldIndex) {
34776 if (newIndex === oldIndex) return;
34777 if (!getElements().tabs[ newIndex ]) return;
34778 adjustOffset();
34779 redirectFocus();
34780 }
34781
34782 /**
34783 * Update the UI whenever the selected index changes. Calls user-defined select/deselect methods.
34784 * @param newValue
34785 * @param oldValue
34786 */
34787 function handleSelectedIndexChange (newValue, oldValue) {
34788 if (newValue === oldValue) return;
34789
34790 ctrl.selectedIndex = getNearestSafeIndex(newValue);
34791 ctrl.lastSelectedIndex = oldValue;
34792 ctrl.updateInkBarStyles();
34793 updateHeightFromContent();
34794 adjustOffset(newValue);
34795 $scope.$broadcast('$mdTabsChanged');
34796 ctrl.tabs[ oldValue ] && ctrl.tabs[ oldValue ].scope.deselect();
34797 ctrl.tabs[ newValue ] && ctrl.tabs[ newValue ].scope.select();
34798 }
34799
34800 function getTabElementIndex(tabEl){
34801 var tabs = $element[0].getElementsByTagName('md-tab');
34802 return Array.prototype.indexOf.call(tabs, tabEl[0]);
34803 }
34804
34805 /**
34806 * Queues up a call to `handleWindowResize` when a resize occurs while the tabs component is
34807 * hidden.
34808 */
34809 function handleResizeWhenVisible () {
34810 // if there is already a watcher waiting for resize, do nothing
34811 if (handleResizeWhenVisible.watcher) return;
34812 // otherwise, we will abuse the $watch function to check for visible
34813 handleResizeWhenVisible.watcher = $scope.$watch(function () {
34814 // since we are checking for DOM size, we use $mdUtil.nextTick() to wait for after the DOM updates
34815 $mdUtil.nextTick(function () {
34816 // if the watcher has already run (ie. multiple digests in one cycle), do nothing
34817 if (!handleResizeWhenVisible.watcher) return;
34818
34819 if ($element.prop('offsetParent')) {
34820 handleResizeWhenVisible.watcher();
34821 handleResizeWhenVisible.watcher = null;
34822
34823 handleWindowResize();
34824 }
34825 }, false);
34826 });
34827 }
34828
34829 // Event handlers / actions
34830
34831 /**
34832 * Handle user keyboard interactions
34833 * @param event
34834 */
34835 function keydown (event) {
34836 switch (event.keyCode) {
34837 case $mdConstant.KEY_CODE.LEFT_ARROW:
34838 event.preventDefault();
34839 incrementIndex(-1, true);
34840 break;
34841 case $mdConstant.KEY_CODE.RIGHT_ARROW:
34842 event.preventDefault();
34843 incrementIndex(1, true);
34844 break;
34845 case $mdConstant.KEY_CODE.SPACE:
34846 case $mdConstant.KEY_CODE.ENTER:
34847 event.preventDefault();
34848 if (!locked) select(ctrl.focusIndex);
34849 break;
34850 }
34851 }
34852
34853 /**
34854 * Update the selected index. Triggers a click event on the original `md-tab` element in order
34855 * to fire user-added click events if canSkipClick or `md-no-select-click` are false.
34856 * @param index
34857 * @param canSkipClick Optionally allow not firing the click event if `md-no-select-click` is also true.
34858 */
34859 function select (index, canSkipClick) {
34860 if (!locked) ctrl.focusIndex = ctrl.selectedIndex = index;
34861 // skip the click event if noSelectClick is enabled
34862 if (canSkipClick && ctrl.noSelectClick) return;
34863 // nextTick is required to prevent errors in user-defined click events
34864 $mdUtil.nextTick(function () {
34865 ctrl.tabs[ index ].element.triggerHandler('click');
34866 }, false);
34867 }
34868
34869 /**
34870 * When pagination is on, this makes sure the selected index is in view.
34871 * @param event
34872 */
34873 function scroll (event) {
34874 if (!ctrl.shouldPaginate) return;
34875 event.preventDefault();
34876 ctrl.offsetLeft = fixOffset(ctrl.offsetLeft - event.wheelDelta);
34877 }
34878
34879 /**
34880 * Slides the tabs over approximately one page forward.
34881 */
34882 function nextPage () {
34883 var elements = getElements();
34884 var viewportWidth = elements.canvas.clientWidth,
34885 totalWidth = viewportWidth + ctrl.offsetLeft,
34886 i, tab;
34887 for (i = 0; i < elements.tabs.length; i++) {
34888 tab = elements.tabs[ i ];
34889 if (tab.offsetLeft + tab.offsetWidth > totalWidth) break;
34890 }
34891
34892 if (viewportWidth > tab.offsetWidth) {
34893 //Canvas width *greater* than tab width: usual positioning
34894 ctrl.offsetLeft = fixOffset(tab.offsetLeft);
34895 } else {
34896 /**
34897 * Canvas width *smaller* than tab width: positioning at the *end* of current tab to let
34898 * pagination "for loop" to proceed correctly on next tab when nextPage() is called again
34899 */
34900 ctrl.offsetLeft = fixOffset(tab.offsetLeft + (tab.offsetWidth - viewportWidth + 1));
34901 }
34902 }
34903
34904 /**
34905 * Slides the tabs over approximately one page backward.
34906 */
34907 function previousPage () {
34908 var i, tab, elements = getElements();
34909
34910 for (i = 0; i < elements.tabs.length; i++) {
34911 tab = elements.tabs[ i ];
34912 if (tab.offsetLeft + tab.offsetWidth >= ctrl.offsetLeft) break;
34913 }
34914
34915 if (elements.canvas.clientWidth > tab.offsetWidth) {
34916 //Canvas width *greater* than tab width: usual positioning
34917 ctrl.offsetLeft = fixOffset(tab.offsetLeft + tab.offsetWidth - elements.canvas.clientWidth);
34918 } else {
34919 /**
34920 * Canvas width *smaller* than tab width: positioning at the *beginning* of current tab to let
34921 * pagination "for loop" to break correctly on previous tab when previousPage() is called again
34922 */
34923 ctrl.offsetLeft = fixOffset(tab.offsetLeft);
34924 }
34925 }
34926
34927 /**
34928 * Update size calculations when the window is resized.
34929 */
34930 function handleWindowResize () {
34931 ctrl.lastSelectedIndex = ctrl.selectedIndex;
34932 ctrl.offsetLeft = fixOffset(ctrl.offsetLeft);
34933 $mdUtil.nextTick(function () {
34934 ctrl.updateInkBarStyles();
34935 updatePagination();
34936 });
34937 }
34938
34939 function handleInkBar (hide) {
34940 angular.element(getElements().inkBar).toggleClass('ng-hide', hide);
34941 }
34942
34943 /**
34944 * Toggle dynamic height class when value changes
34945 * @param value
34946 */
34947 function handleDynamicHeight (value) {
34948 $element.toggleClass('md-dynamic-height', value);
34949 }
34950
34951 /**
34952 * Remove a tab from the data and select the nearest valid tab.
34953 * @param tabData
34954 */
34955 function removeTab (tabData) {
34956 if (destroyed) return;
34957 var selectedIndex = ctrl.selectedIndex,
34958 tab = ctrl.tabs.splice(tabData.getIndex(), 1)[ 0 ];
34959 refreshIndex();
34960 // when removing a tab, if the selected index did not change, we have to manually trigger the
34961 // tab select/deselect events
34962 if (ctrl.selectedIndex === selectedIndex) {
34963 tab.scope.deselect();
34964 ctrl.tabs[ ctrl.selectedIndex ] && ctrl.tabs[ ctrl.selectedIndex ].scope.select();
34965 }
34966 $mdUtil.nextTick(function () {
34967 updatePagination();
34968 ctrl.offsetLeft = fixOffset(ctrl.offsetLeft);
34969 });
34970 }
34971
34972 /**
34973 * Create an entry in the tabs array for a new tab at the specified index.
34974 * @param tabData
34975 * @param index
34976 * @returns {*}
34977 */
34978 function insertTab (tabData, index) {
34979 var hasLoaded = loaded;
34980 var proto = {
34981 getIndex: function () { return ctrl.tabs.indexOf(tab); },
34982 isActive: function () { return this.getIndex() === ctrl.selectedIndex; },
34983 isLeft: function () { return this.getIndex() < ctrl.selectedIndex; },
34984 isRight: function () { return this.getIndex() > ctrl.selectedIndex; },
34985 shouldRender: function () { return !ctrl.noDisconnect || this.isActive(); },
34986 hasFocus: function () {
34987 return ctrl.styleTabItemFocus
34988 && ctrl.hasFocus && this.getIndex() === ctrl.focusIndex;
34989 },
34990 id: $mdUtil.nextUid(),
34991 hasContent: !!(tabData.template && tabData.template.trim())
34992 },
34993 tab = angular.extend(proto, tabData);
34994 if (angular.isDefined(index)) {
34995 ctrl.tabs.splice(index, 0, tab);
34996 } else {
34997 ctrl.tabs.push(tab);
34998 }
34999
35000 processQueue();
35001 updateHasContent();
35002 $mdUtil.nextTick(function () {
35003 updatePagination();
35004 setAriaControls(tab);
35005
35006 // if autoselect is enabled, select the newly added tab
35007 if (hasLoaded && ctrl.autoselect) $mdUtil.nextTick(function () {
35008 $mdUtil.nextTick(function () { select(ctrl.tabs.indexOf(tab)); });
35009 });
35010 });
35011 return tab;
35012 }
35013
35014 // Getter methods
35015
35016 /**
35017 * Gathers references to all of the DOM elements used by this controller.
35018 * @returns {{}}
35019 */
35020 function getElements () {
35021 var elements = {};
35022 var node = $element[0];
35023
35024 // gather tab bar elements
35025 elements.wrapper = node.querySelector('md-tabs-wrapper');
35026 elements.canvas = elements.wrapper.querySelector('md-tabs-canvas');
35027 elements.paging = elements.canvas.querySelector('md-pagination-wrapper');
35028 elements.inkBar = elements.paging.querySelector('md-ink-bar');
35029
35030 elements.contents = node.querySelectorAll('md-tabs-content-wrapper > md-tab-content');
35031 elements.tabs = elements.paging.querySelectorAll('md-tab-item');
35032 elements.dummies = elements.canvas.querySelectorAll('md-dummy-tab');
35033
35034 return elements;
35035 }
35036
35037 /**
35038 * Determines whether or not the left pagination arrow should be enabled.
35039 * @returns {boolean}
35040 */
35041 function canPageBack () {
35042 return ctrl.offsetLeft > 0;
35043 }
35044
35045 /**
35046 * Determines whether or not the right pagination arrow should be enabled.
35047 * @returns {*|boolean}
35048 */
35049 function canPageForward () {
35050 var elements = getElements();
35051 var lastTab = elements.tabs[ elements.tabs.length - 1 ];
35052 return lastTab && lastTab.offsetLeft + lastTab.offsetWidth > elements.canvas.clientWidth +
35053 ctrl.offsetLeft;
35054 }
35055
35056 /**
35057 * Returns currently focused tab item's element ID
35058 */
35059 function getFocusedTabId() {
35060 var focusedTab = ctrl.tabs[ctrl.focusIndex];
35061 if (!focusedTab || !focusedTab.id) {
35062 return null;
35063 }
35064 return 'tab-item-' + focusedTab.id;
35065 }
35066
35067 /**
35068 * Determines if the UI should stretch the tabs to fill the available space.
35069 * @returns {*}
35070 */
35071 function shouldStretchTabs () {
35072 switch (ctrl.stretchTabs) {
35073 case 'always':
35074 return true;
35075 case 'never':
35076 return false;
35077 default:
35078 return !ctrl.shouldPaginate
35079 && $window.matchMedia('(max-width: 600px)').matches;
35080 }
35081 }
35082
35083 /**
35084 * Determines if the tabs should appear centered.
35085 * @returns {string|boolean}
35086 */
35087 function shouldCenterTabs () {
35088 return ctrl.centerTabs && !ctrl.shouldPaginate;
35089 }
35090
35091 /**
35092 * Determines if pagination is necessary to display the tabs within the available space.
35093 * @returns {boolean}
35094 */
35095 function shouldPaginate () {
35096 if (ctrl.noPagination || !loaded) return false;
35097 var canvasWidth = $element.prop('clientWidth');
35098
35099 angular.forEach(getElements().dummies, function (tab) {
35100 canvasWidth -= tab.offsetWidth;
35101 });
35102
35103 return canvasWidth < 0;
35104 }
35105
35106 /**
35107 * Finds the nearest tab index that is available. This is primarily used for when the active
35108 * tab is removed.
35109 * @param newIndex
35110 * @returns {*}
35111 */
35112 function getNearestSafeIndex (newIndex) {
35113 if (newIndex === -1) return -1;
35114 var maxOffset = Math.max(ctrl.tabs.length - newIndex, newIndex),
35115 i, tab;
35116 for (i = 0; i <= maxOffset; i++) {
35117 tab = ctrl.tabs[ newIndex + i ];
35118 if (tab && (tab.scope.disabled !== true)) return tab.getIndex();
35119 tab = ctrl.tabs[ newIndex - i ];
35120 if (tab && (tab.scope.disabled !== true)) return tab.getIndex();
35121 }
35122 return newIndex;
35123 }
35124
35125 // Utility methods
35126
35127 /**
35128 * Defines a property using a getter and setter in order to trigger a change handler without
35129 * using `$watch` to observe changes.
35130 * @param key
35131 * @param handler
35132 * @param value
35133 */
35134 function defineProperty (key, handler, value) {
35135 Object.defineProperty(ctrl, key, {
35136 get: function () { return value; },
35137 set: function (newValue) {
35138 var oldValue = value;
35139 value = newValue;
35140 handler && handler(newValue, oldValue);
35141 }
35142 });
35143 }
35144
35145 /**
35146 * Updates whether or not pagination should be displayed.
35147 */
35148 function updatePagination () {
35149 ctrl.maxTabWidth = getMaxTabWidth();
35150 ctrl.shouldPaginate = shouldPaginate();
35151 }
35152
35153 /**
35154 * Calculates the width of the pagination wrapper by summing the widths of the dummy tabs.
35155 * @returns {number}
35156 */
35157 function calcPagingWidth () {
35158 return calcTabsWidth(getElements().dummies);
35159 }
35160
35161 function calcTabsWidth(tabs) {
35162 var width = 0;
35163
35164 angular.forEach(tabs, function (tab) {
35165 //-- Uses the larger value between `getBoundingClientRect().width` and `offsetWidth`. This
35166 // prevents `offsetWidth` value from being rounded down and causing wrapping issues, but
35167 // also handles scenarios where `getBoundingClientRect()` is inaccurate (ie. tabs inside
35168 // of a dialog)
35169 width += Math.max(tab.offsetWidth, tab.getBoundingClientRect().width);
35170 });
35171
35172 return Math.ceil(width);
35173 }
35174
35175 function getMaxTabWidth () {
35176 return $element.prop('clientWidth');
35177 }
35178
35179 /**
35180 * Re-orders the tabs and updates the selected and focus indexes to their new positions.
35181 * This is triggered by `tabDirective.js` when the user's tabs have been re-ordered.
35182 */
35183 function updateTabOrder () {
35184 var selectedItem = ctrl.tabs[ ctrl.selectedIndex ],
35185 focusItem = ctrl.tabs[ ctrl.focusIndex ];
35186 ctrl.tabs = ctrl.tabs.sort(function (a, b) {
35187 return a.index - b.index;
35188 });
35189 ctrl.selectedIndex = ctrl.tabs.indexOf(selectedItem);
35190 ctrl.focusIndex = ctrl.tabs.indexOf(focusItem);
35191 }
35192
35193 /**
35194 * This moves the selected or focus index left or right. This is used by the keydown handler.
35195 * @param inc
35196 */
35197 function incrementIndex (inc, focus) {
35198 var newIndex,
35199 key = focus ? 'focusIndex' : 'selectedIndex',
35200 index = ctrl[ key ];
35201 for (newIndex = index + inc;
35202 ctrl.tabs[ newIndex ] && ctrl.tabs[ newIndex ].scope.disabled;
35203 newIndex += inc) {}
35204 if (ctrl.tabs[ newIndex ]) {
35205 ctrl[ key ] = newIndex;
35206 }
35207 }
35208
35209 /**
35210 * This is used to forward focus to dummy elements. This method is necessary to avoid animation
35211 * issues when attempting to focus an item that is out of view.
35212 */
35213 function redirectFocus () {
35214 ctrl.styleTabItemFocus = ($mdInteraction.getLastInteractionType() === 'keyboard');
35215 getElements().dummies[ ctrl.focusIndex ].focus();
35216 }
35217
35218 /**
35219 * Forces the pagination to move the focused tab into view.
35220 */
35221 function adjustOffset (index) {
35222 var elements = getElements();
35223
35224 if (!angular.isNumber(index)) index = ctrl.focusIndex;
35225 if (!elements.tabs[ index ]) return;
35226 if (ctrl.shouldCenterTabs) return;
35227 var tab = elements.tabs[ index ],
35228 left = tab.offsetLeft,
35229 right = tab.offsetWidth + left;
35230 ctrl.offsetLeft = Math.max(ctrl.offsetLeft, fixOffset(right - elements.canvas.clientWidth + 32 * 2));
35231 ctrl.offsetLeft = Math.min(ctrl.offsetLeft, fixOffset(left));
35232 }
35233
35234 /**
35235 * Iterates through all queued functions and clears the queue. This is used for functions that
35236 * are called before the UI is ready, such as size calculations.
35237 */
35238 function processQueue () {
35239 queue.forEach(function (func) { $mdUtil.nextTick(func); });
35240 queue = [];
35241 }
35242
35243 /**
35244 * Determines if the tab content area is needed.
35245 */
35246 function updateHasContent () {
35247 var hasContent = false;
35248
35249 for (var i = 0; i < ctrl.tabs.length; i++) {
35250 if (ctrl.tabs[i].hasContent) {
35251 hasContent = true;
35252 break;
35253 }
35254 }
35255
35256 ctrl.hasContent = hasContent;
35257 }
35258
35259 /**
35260 * Moves the indexes to their nearest valid values.
35261 */
35262 function refreshIndex () {
35263 ctrl.selectedIndex = getNearestSafeIndex(ctrl.selectedIndex);
35264 ctrl.focusIndex = getNearestSafeIndex(ctrl.focusIndex);
35265 }
35266
35267 /**
35268 * Calculates the content height of the current tab.
35269 * @returns {*}
35270 */
35271 function updateHeightFromContent () {
35272 if (!ctrl.dynamicHeight) return $element.css('height', '');
35273 if (!ctrl.tabs.length) return queue.push(updateHeightFromContent);
35274
35275 var elements = getElements();
35276
35277 var tabContent = elements.contents[ ctrl.selectedIndex ],
35278 contentHeight = tabContent ? tabContent.offsetHeight : 0,
35279 tabsHeight = elements.wrapper.offsetHeight,
35280 newHeight = contentHeight + tabsHeight,
35281 currentHeight = $element.prop('clientHeight');
35282
35283 if (currentHeight === newHeight) return;
35284
35285 // Adjusts calculations for when the buttons are bottom-aligned since this relies on absolute
35286 // positioning. This should probably be cleaned up if a cleaner solution is possible.
35287 if ($element.attr('md-align-tabs') === 'bottom') {
35288 currentHeight -= tabsHeight;
35289 newHeight -= tabsHeight;
35290 // Need to include bottom border in these calculations
35291 if ($element.attr('md-border-bottom') !== undefined) ++currentHeight;
35292 }
35293
35294 // Lock during animation so the user can't change tabs
35295 locked = true;
35296
35297 var fromHeight = { height: currentHeight + 'px' },
35298 toHeight = { height: newHeight + 'px' };
35299
35300 // Set the height to the current, specific pixel height to fix a bug on iOS where the height
35301 // first animates to 0, then back to the proper height causing a visual glitch
35302 $element.css(fromHeight);
35303
35304 // Animate the height from the old to the new
35305 $animateCss($element, {
35306 from: fromHeight,
35307 to: toHeight,
35308 easing: 'cubic-bezier(0.35, 0, 0.25, 1)',
35309 duration: 0.5
35310 }).start().done(function () {
35311 // Then (to fix the same iOS issue as above), disable transitions and remove the specific
35312 // pixel height so the height can size with browser width/content changes, etc.
35313 $element.css({
35314 transition: 'none',
35315 height: ''
35316 });
35317
35318 // In the next tick, re-allow transitions (if we do it all at once, $element.css is "smart"
35319 // enough to batch it for us instead of doing it immediately, which undoes the original
35320 // transition: none)
35321 $mdUtil.nextTick(function() {
35322 $element.css('transition', '');
35323 });
35324
35325 // And unlock so tab changes can occur
35326 locked = false;
35327 });
35328 }
35329
35330 /**
35331 * Repositions the ink bar to the selected tab.
35332 * @returns {*}
35333 */
35334 function updateInkBarStyles () {
35335 var elements = getElements();
35336
35337 if (!elements.tabs[ ctrl.selectedIndex ]) {
35338 angular.element(elements.inkBar).css({ left: 'auto', right: 'auto' });
35339 return;
35340 }
35341
35342 if (!ctrl.tabs.length) return queue.push(ctrl.updateInkBarStyles);
35343 // if the element is not visible, we will not be able to calculate sizes until it is
35344 // we should treat that as a resize event rather than just updating the ink bar
35345 if (!$element.prop('offsetParent')) return handleResizeWhenVisible();
35346
35347 var index = ctrl.selectedIndex,
35348 totalWidth = elements.paging.offsetWidth,
35349 tab = elements.tabs[ index ],
35350 left = tab.offsetLeft,
35351 right = totalWidth - left - tab.offsetWidth;
35352
35353 if (ctrl.shouldCenterTabs) {
35354 // We need to use the same calculate process as in the pagination wrapper, to avoid rounding deviations.
35355 var tabWidth = calcTabsWidth(elements.tabs);
35356
35357 if (totalWidth > tabWidth) {
35358 $mdUtil.nextTick(updateInkBarStyles, false);
35359 }
35360 }
35361 updateInkBarClassName();
35362 angular.element(elements.inkBar).css({ left: left + 'px', right: right + 'px' });
35363 }
35364
35365 /**
35366 * Adds left/right classes so that the ink bar will animate properly.
35367 */
35368 function updateInkBarClassName () {
35369 var elements = getElements();
35370 var newIndex = ctrl.selectedIndex,
35371 oldIndex = ctrl.lastSelectedIndex,
35372 ink = angular.element(elements.inkBar);
35373 if (!angular.isNumber(oldIndex)) return;
35374 ink
35375 .toggleClass('md-left', newIndex < oldIndex)
35376 .toggleClass('md-right', newIndex > oldIndex);
35377 }
35378
35379 /**
35380 * Takes an offset value and makes sure that it is within the min/max allowed values.
35381 * @param value
35382 * @returns {*}
35383 */
35384 function fixOffset (value) {
35385 var elements = getElements();
35386
35387 if (!elements.tabs.length || !ctrl.shouldPaginate) return 0;
35388 var lastTab = elements.tabs[ elements.tabs.length - 1 ],
35389 totalWidth = lastTab.offsetLeft + lastTab.offsetWidth;
35390 value = Math.max(0, value);
35391 value = Math.min(totalWidth - elements.canvas.clientWidth, value);
35392 return value;
35393 }
35394
35395 /**
35396 * Attaches a ripple to the tab item element.
35397 * @param scope
35398 * @param element
35399 */
35400 function attachRipple (scope, element) {
35401 var elements = getElements();
35402 var options = { colorElement: angular.element(elements.inkBar) };
35403 $mdTabInkRipple.attach(scope, element, options);
35404 }
35405
35406 /**
35407 * Sets the `aria-controls` attribute to the elements that
35408 * correspond to the passed-in tab.
35409 * @param tab
35410 */
35411 function setAriaControls (tab) {
35412 if (tab.hasContent) {
35413 var nodes = $element[0].querySelectorAll('[md-tab-id="' + tab.id + '"]');
35414 angular.element(nodes).attr('aria-controls', ctrl.tabContentPrefix + tab.id);
35415 }
35416 }
35417}
35418
35419})();
35420(function(){
35421"use strict";
35422
35423/**
35424 * @ngdoc directive
35425 * @name mdTabs
35426 * @module material.components.tabs
35427 *
35428 * @restrict E
35429 *
35430 * @description
35431 * The `<md-tabs>` directive serves as the container for 1..n `<md-tab>` child directives to
35432 * produces a Tabs components. In turn, the nested `<md-tab>` directive is used to specify a tab
35433 * label for the **header button** and a [optional] tab view content that will be associated with
35434 * each tab button.
35435 *
35436 * Below is the markup for its simplest usage:
35437 *
35438 * <hljs lang="html">
35439 * <md-tabs>
35440 * <md-tab label="Tab #1"></md-tab>
35441 * <md-tab label="Tab #2"></md-tab>
35442 * <md-tab label="Tab #3"></md-tab>
35443 * </md-tabs>
35444 * </hljs>
35445 *
35446 * Tabs supports three (3) usage scenarios:
35447 *
35448 * 1. Tabs (buttons only)
35449 * 2. Tabs with internal view content
35450 * 3. Tabs with external view content
35451 *
35452 * **Tab-only** support is useful when tab buttons are used for custom navigation regardless of any
35453 * other components, content, or views.
35454 *
35455 * <i><b>Note:</b> If you are using the Tabs component for page-level navigation, please take a look
35456 * at the <a ng-href="./api/directive/mdNavBar">NavBar component</a> instead as it can handle this
35457 * case a bit more natively.</i>
35458 *
35459 * **Tabs with internal views** are the traditional usages where each tab has associated view
35460 * content and the view switching is managed internally by the Tabs component.
35461 *
35462 * **Tabs with external view content** is often useful when content associated with each tab is
35463 * independently managed and data-binding notifications announce tab selection changes.
35464 *
35465 * Additional features also include:
35466 *
35467 * * Content can include any markup.
35468 * * If a tab is disabled while active/selected, then the next tab will be auto-selected.
35469 *
35470 * ### Explanation of tab stretching
35471 *
35472 * 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.
35473 *
35474 * On mobile devices, tabs will be expanded to fill the available horizontal space. When this happens, all tabs will become the same size.
35475 *
35476 * On desktops, by default, stretching will never occur.
35477 *
35478 * This default behavior can be overridden through the `md-stretch-tabs` attribute. Here is a table showing when stretching will occur:
35479 *
35480 * `md-stretch-tabs` | mobile | desktop
35481 * ------------------|-----------|--------
35482 * `auto` | stretched | ---
35483 * `always` | stretched | stretched
35484 * `never` | --- | ---
35485 *
35486 * @param {integer=} md-selected Index of the active/selected tab
35487 * @param {boolean=} md-no-ink If present, disables ink ripple effects.
35488 * @param {boolean=} md-no-ink-bar If present, disables the selection ink bar.
35489 * @param {string=} md-align-tabs Attribute to indicate position of tab buttons: `bottom` or `top`; default is `top`
35490 * @param {string=} md-stretch-tabs Attribute to indicate whether or not to stretch tabs: `auto`, `always`, or `never`; default is `auto`
35491 * @param {boolean=} md-dynamic-height When enabled, the tab wrapper will resize based on the contents of the selected tab
35492 * @param {boolean=} md-border-bottom If present, shows a solid `1px` border between the tabs and their content
35493 * @param {boolean=} md-center-tabs When enabled, tabs will be centered provided there is no need for pagination
35494 * @param {boolean=} md-no-pagination When enabled, pagination will remain off
35495 * @param {boolean=} md-swipe-content When enabled, swipe gestures will be enabled for the content area to jump between tabs
35496 * @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.
35497 * @param {boolean=} md-autoselect When present, any tabs added after the initial load will be automatically selected
35498 * @param {boolean=} md-no-select-click When enabled, click events will not be fired when selecting tabs
35499 *
35500 * @usage
35501 * <hljs lang="html">
35502 * <md-tabs md-selected="selectedIndex" >
35503 * <img ng-src="img/angular.png" class="centered">
35504 * <md-tab
35505 * ng-repeat="tab in tabs | orderBy:predicate:reversed"
35506 * md-on-select="onTabSelected(tab)"
35507 * md-on-deselect="announceDeselected(tab)"
35508 * ng-disabled="tab.disabled">
35509 * <md-tab-label>
35510 * {{tab.title}}
35511 * <img src="img/removeTab.png" ng-click="removeTab(tab)" class="delete">
35512 * </md-tab-label>
35513 * <md-tab-body>
35514 * {{tab.content}}
35515 * </md-tab-body>
35516 * </md-tab>
35517 * </md-tabs>
35518 * </hljs>
35519 *
35520 */
35521MdTabs.$inject = ["$$mdSvgRegistry"];
35522angular
35523 .module('material.components.tabs')
35524 .directive('mdTabs', MdTabs);
35525
35526function MdTabs ($$mdSvgRegistry) {
35527 return {
35528 scope: {
35529 selectedIndex: '=?mdSelected'
35530 },
35531 template: function (element, attr) {
35532 attr.$mdTabsTemplate = element.html();
35533 return '' +
35534 '<md-tabs-wrapper> ' +
35535 '<md-tab-data></md-tab-data> ' +
35536 '<md-prev-button ' +
35537 'tabindex="-1" ' +
35538 'role="button" ' +
35539 'aria-label="Previous Page" ' +
35540 'aria-disabled="{{!$mdTabsCtrl.canPageBack()}}" ' +
35541 'ng-class="{ \'md-disabled\': !$mdTabsCtrl.canPageBack() }" ' +
35542 'ng-if="$mdTabsCtrl.shouldPaginate" ' +
35543 'ng-click="$mdTabsCtrl.previousPage()"> ' +
35544 '<md-icon md-svg-src="'+ $$mdSvgRegistry.mdTabsArrow +'"></md-icon> ' +
35545 '</md-prev-button> ' +
35546 '<md-next-button ' +
35547 'tabindex="-1" ' +
35548 'role="button" ' +
35549 'aria-label="Next Page" ' +
35550 'aria-disabled="{{!$mdTabsCtrl.canPageForward()}}" ' +
35551 'ng-class="{ \'md-disabled\': !$mdTabsCtrl.canPageForward() }" ' +
35552 'ng-if="$mdTabsCtrl.shouldPaginate" ' +
35553 'ng-click="$mdTabsCtrl.nextPage()"> ' +
35554 '<md-icon md-svg-src="'+ $$mdSvgRegistry.mdTabsArrow +'"></md-icon> ' +
35555 '</md-next-button> ' +
35556 '<md-tabs-canvas ' +
35557 'tabindex="{{ $mdTabsCtrl.hasFocus ? -1 : 0 }}" ' +
35558 'aria-activedescendant="{{$mdTabsCtrl.getFocusedTabId()}}" ' +
35559 'ng-focus="$mdTabsCtrl.redirectFocus()" ' +
35560 'ng-class="{ ' +
35561 '\'md-paginated\': $mdTabsCtrl.shouldPaginate, ' +
35562 '\'md-center-tabs\': $mdTabsCtrl.shouldCenterTabs ' +
35563 '}" ' +
35564 'ng-keydown="$mdTabsCtrl.keydown($event)" ' +
35565 'role="tablist"> ' +
35566 '<md-pagination-wrapper ' +
35567 'ng-class="{ \'md-center-tabs\': $mdTabsCtrl.shouldCenterTabs }" ' +
35568 'md-tab-scroll="$mdTabsCtrl.scroll($event)"> ' +
35569 '<md-tab-item ' +
35570 'tabindex="-1" ' +
35571 'class="md-tab" ' +
35572 'ng-repeat="tab in $mdTabsCtrl.tabs" ' +
35573 'role="tab" ' +
35574 'md-tab-id="{{::tab.id}}"' +
35575 'aria-selected="{{tab.isActive()}}" ' +
35576 'aria-disabled="{{tab.scope.disabled || \'false\'}}" ' +
35577 'ng-click="$mdTabsCtrl.select(tab.getIndex())" ' +
35578 'ng-class="{ ' +
35579 '\'md-active\': tab.isActive(), ' +
35580 '\'md-focused\': tab.hasFocus(), ' +
35581 '\'md-disabled\': tab.scope.disabled ' +
35582 '}" ' +
35583 'ng-disabled="tab.scope.disabled" ' +
35584 'md-swipe-left="$mdTabsCtrl.nextPage()" ' +
35585 'md-swipe-right="$mdTabsCtrl.previousPage()" ' +
35586 'md-tabs-template="::tab.label" ' +
35587 'md-scope="::tab.parent"></md-tab-item> ' +
35588 '<md-ink-bar></md-ink-bar> ' +
35589 '</md-pagination-wrapper> ' +
35590 '<md-tabs-dummy-wrapper class="md-visually-hidden md-dummy-wrapper"> ' +
35591 '<md-dummy-tab ' +
35592 'class="md-tab" ' +
35593 'tabindex="-1" ' +
35594 'id="tab-item-{{::tab.id}}" ' +
35595 'md-tab-id="{{::tab.id}}"' +
35596 'aria-selected="{{tab.isActive()}}" ' +
35597 'aria-disabled="{{tab.scope.disabled || \'false\'}}" ' +
35598 'ng-focus="$mdTabsCtrl.hasFocus = true" ' +
35599 'ng-blur="$mdTabsCtrl.hasFocus = false" ' +
35600 'ng-repeat="tab in $mdTabsCtrl.tabs" ' +
35601 'md-tabs-template="::tab.label" ' +
35602 'md-scope="::tab.parent"></md-dummy-tab> ' +
35603 '</md-tabs-dummy-wrapper> ' +
35604 '</md-tabs-canvas> ' +
35605 '</md-tabs-wrapper> ' +
35606 '<md-tabs-content-wrapper ng-show="$mdTabsCtrl.hasContent && $mdTabsCtrl.selectedIndex >= 0" class="_md"> ' +
35607 '<md-tab-content ' +
35608 'id="{{:: $mdTabsCtrl.tabContentPrefix + tab.id}}" ' +
35609 'class="_md" ' +
35610 'role="tabpanel" ' +
35611 'aria-labelledby="tab-item-{{::tab.id}}" ' +
35612 'md-swipe-left="$mdTabsCtrl.swipeContent && $mdTabsCtrl.incrementIndex(1)" ' +
35613 'md-swipe-right="$mdTabsCtrl.swipeContent && $mdTabsCtrl.incrementIndex(-1)" ' +
35614 'ng-if="tab.hasContent" ' +
35615 'ng-repeat="(index, tab) in $mdTabsCtrl.tabs" ' +
35616 'ng-class="{ ' +
35617 '\'md-no-transition\': $mdTabsCtrl.lastSelectedIndex == null, ' +
35618 '\'md-active\': tab.isActive(), ' +
35619 '\'md-left\': tab.isLeft(), ' +
35620 '\'md-right\': tab.isRight(), ' +
35621 '\'md-no-scroll\': $mdTabsCtrl.dynamicHeight ' +
35622 '}"> ' +
35623 '<div ' +
35624 'md-tabs-template="::tab.template" ' +
35625 'md-connected-if="tab.isActive()" ' +
35626 'md-scope="::tab.parent" ' +
35627 'ng-if="$mdTabsCtrl.enableDisconnect || tab.shouldRender()"></div> ' +
35628 '</md-tab-content> ' +
35629 '</md-tabs-content-wrapper>';
35630 },
35631 controller: 'MdTabsController',
35632 controllerAs: '$mdTabsCtrl',
35633 bindToController: true
35634 };
35635}
35636
35637})();
35638(function(){
35639"use strict";
35640
35641
35642MdTabsDummyWrapper.$inject = ["$mdUtil", "$window"];angular
35643 .module('material.components.tabs')
35644 .directive('mdTabsDummyWrapper', MdTabsDummyWrapper);
35645
35646/**
35647 * @private
35648 *
35649 * @param $mdUtil
35650 * @param $window
35651 * @returns {{require: string, link: link}}
35652 * @constructor
35653 *
35654 * @ngInject
35655 */
35656function MdTabsDummyWrapper ($mdUtil, $window) {
35657 return {
35658 require: '^?mdTabs',
35659 link: function link (scope, element, attr, ctrl) {
35660 if (!ctrl) return;
35661
35662 var observer;
35663 var disconnect;
35664
35665 var mutationCallback = function() {
35666 ctrl.updatePagination();
35667 ctrl.updateInkBarStyles();
35668 };
35669
35670 if('MutationObserver' in $window) {
35671 var config = {
35672 childList: true,
35673 subtree: true,
35674 // Per https://bugzilla.mozilla.org/show_bug.cgi?id=1138368, browsers will not fire
35675 // the childList mutation, once a <span> element's innerText changes.
35676 // The characterData of the <span> element will change.
35677 characterData: true
35678 };
35679
35680 observer = new MutationObserver(mutationCallback);
35681 observer.observe(element[0], config);
35682 disconnect = observer.disconnect.bind(observer);
35683 } else {
35684 var debounced = $mdUtil.debounce(mutationCallback, 15, null, false);
35685
35686 element.on('DOMSubtreeModified', debounced);
35687 disconnect = element.off.bind(element, 'DOMSubtreeModified', debounced);
35688 }
35689
35690 // Disconnect the observer
35691 scope.$on('$destroy', function() {
35692 disconnect();
35693 });
35694 }
35695 };
35696}
35697
35698})();
35699(function(){
35700"use strict";
35701
35702
35703MdTabsTemplate.$inject = ["$compile", "$mdUtil"];angular
35704 .module('material.components.tabs')
35705 .directive('mdTabsTemplate', MdTabsTemplate);
35706
35707function MdTabsTemplate ($compile, $mdUtil) {
35708 return {
35709 restrict: 'A',
35710 link: link,
35711 scope: {
35712 template: '=mdTabsTemplate',
35713 connected: '=?mdConnectedIf',
35714 compileScope: '=mdScope'
35715 },
35716 require: '^?mdTabs'
35717 };
35718 function link (scope, element, attr, ctrl) {
35719 if (!ctrl) return;
35720
35721 var compileScope = ctrl.enableDisconnect ? scope.compileScope.$new() : scope.compileScope;
35722
35723 element.html(scope.template);
35724 $compile(element.contents())(compileScope);
35725
35726 return $mdUtil.nextTick(handleScope);
35727
35728 function handleScope () {
35729 scope.$watch('connected', function (value) { value === false ? disconnect() : reconnect(); });
35730 scope.$on('$destroy', reconnect);
35731 }
35732
35733 function disconnect () {
35734 if (ctrl.enableDisconnect) $mdUtil.disconnectScope(compileScope);
35735 }
35736
35737 function reconnect () {
35738 if (ctrl.enableDisconnect) $mdUtil.reconnectScope(compileScope);
35739 }
35740 }
35741}
35742
35743})();
35744(function(){
35745angular.module("material.core").constant("$MD_THEME_CSS", "md-autocomplete.md-THEME_NAME-theme{background:\"{{background-A100}}\"}md-autocomplete.md-THEME_NAME-theme[disabled]:not([md-floating-label]){background:\"{{background-100}}\"}md-autocomplete.md-THEME_NAME-theme button md-icon path{fill:\"{{background-600}}\"}md-autocomplete.md-THEME_NAME-theme button:after{background:\"{{background-600-0.3}}\"}.md-autocomplete-suggestions-container.md-THEME_NAME-theme{background:\"{{background-A100}}\"}.md-autocomplete-suggestions-container.md-THEME_NAME-theme li{color:\"{{background-900}}\"}.md-autocomplete-suggestions-container.md-THEME_NAME-theme li .highlight{color:\"{{background-600}}\"}.md-autocomplete-suggestions-container.md-THEME_NAME-theme li.selected,.md-autocomplete-suggestions-container.md-THEME_NAME-theme li:hover{background:\"{{background-200}}\"}md-backdrop{background-color:\"{{background-900-0.0}}\"}md-backdrop.md-opaque.md-THEME_NAME-theme{background-color:\"{{background-900-1.0}}\"}md-bottom-sheet.md-THEME_NAME-theme{background-color:\"{{background-50}}\";border-top-color:\"{{background-300}}\"}md-bottom-sheet.md-THEME_NAME-theme.md-list md-list-item{color:\"{{foreground-1}}\"}md-bottom-sheet.md-THEME_NAME-theme .md-subheader{background-color:\"{{background-50}}\";color:\"{{foreground-1}}\"}.md-button.md-THEME_NAME-theme:not([disabled]).md-focused,.md-button.md-THEME_NAME-theme:not([disabled]):hover{background-color:\"{{background-500-0.2}}\"}.md-button.md-THEME_NAME-theme:not([disabled]).md-icon-button:hover{background-color:transparent}.md-button.md-THEME_NAME-theme.md-fab md-icon{color:\"{{accent-contrast}}\"}.md-button.md-THEME_NAME-theme.md-primary{color:\"{{primary-color}}\"}.md-button.md-THEME_NAME-theme.md-primary.md-fab,.md-button.md-THEME_NAME-theme.md-primary.md-raised{color:\"{{primary-contrast}}\";background-color:\"{{primary-color}}\"}.md-button.md-THEME_NAME-theme.md-primary.md-fab:not([disabled]) md-icon,.md-button.md-THEME_NAME-theme.md-primary.md-raised:not([disabled]) md-icon{color:\"{{primary-contrast}}\"}.md-button.md-THEME_NAME-theme.md-primary.md-fab:not([disabled]).md-focused,.md-button.md-THEME_NAME-theme.md-primary.md-fab:not([disabled]):hover,.md-button.md-THEME_NAME-theme.md-primary.md-raised:not([disabled]).md-focused,.md-button.md-THEME_NAME-theme.md-primary.md-raised:not([disabled]):hover{background-color:\"{{primary-600}}\"}.md-button.md-THEME_NAME-theme.md-primary:not([disabled]) md-icon{color:\"{{primary-color}}\"}.md-button.md-THEME_NAME-theme.md-fab{background-color:\"{{accent-color}}\";color:\"{{accent-contrast}}\"}.md-button.md-THEME_NAME-theme.md-fab:not([disabled]) .md-icon{color:\"{{accent-contrast}}\"}.md-button.md-THEME_NAME-theme.md-fab:not([disabled]).md-focused,.md-button.md-THEME_NAME-theme.md-fab:not([disabled]):hover{background-color:\"{{accent-A700}}\"}.md-button.md-THEME_NAME-theme.md-raised{color:\"{{background-900}}\";background-color:\"{{background-50}}\"}.md-button.md-THEME_NAME-theme.md-raised:not([disabled]) md-icon{color:\"{{background-900}}\"}.md-button.md-THEME_NAME-theme.md-raised:not([disabled]):hover{background-color:\"{{background-50}}\"}.md-button.md-THEME_NAME-theme.md-raised:not([disabled]).md-focused{background-color:\"{{background-200}}\"}.md-button.md-THEME_NAME-theme.md-warn{color:\"{{warn-color}}\"}.md-button.md-THEME_NAME-theme.md-warn.md-fab,.md-button.md-THEME_NAME-theme.md-warn.md-raised{color:\"{{warn-contrast}}\";background-color:\"{{warn-color}}\"}.md-button.md-THEME_NAME-theme.md-warn.md-fab:not([disabled]) md-icon,.md-button.md-THEME_NAME-theme.md-warn.md-raised:not([disabled]) md-icon{color:\"{{warn-contrast}}\"}.md-button.md-THEME_NAME-theme.md-warn.md-fab:not([disabled]).md-focused,.md-button.md-THEME_NAME-theme.md-warn.md-fab:not([disabled]):hover,.md-button.md-THEME_NAME-theme.md-warn.md-raised:not([disabled]).md-focused,.md-button.md-THEME_NAME-theme.md-warn.md-raised:not([disabled]):hover{background-color:\"{{warn-600}}\"}.md-button.md-THEME_NAME-theme.md-warn:not([disabled]) md-icon{color:\"{{warn-color}}\"}.md-button.md-THEME_NAME-theme.md-accent{color:\"{{accent-color}}\"}.md-button.md-THEME_NAME-theme.md-accent.md-fab,.md-button.md-THEME_NAME-theme.md-accent.md-raised{color:\"{{accent-contrast}}\";background-color:\"{{accent-color}}\"}.md-button.md-THEME_NAME-theme.md-accent.md-fab:not([disabled]) md-icon,.md-button.md-THEME_NAME-theme.md-accent.md-raised:not([disabled]) md-icon{color:\"{{accent-contrast}}\"}.md-button.md-THEME_NAME-theme.md-accent.md-fab:not([disabled]).md-focused,.md-button.md-THEME_NAME-theme.md-accent.md-fab:not([disabled]):hover,.md-button.md-THEME_NAME-theme.md-accent.md-raised:not([disabled]).md-focused,.md-button.md-THEME_NAME-theme.md-accent.md-raised:not([disabled]):hover{background-color:\"{{accent-A700}}\"}.md-button.md-THEME_NAME-theme.md-accent:not([disabled]) md-icon{color:\"{{accent-color}}\"}.md-button.md-THEME_NAME-theme.md-accent[disabled],.md-button.md-THEME_NAME-theme.md-fab[disabled],.md-button.md-THEME_NAME-theme.md-raised[disabled],.md-button.md-THEME_NAME-theme.md-warn[disabled],.md-button.md-THEME_NAME-theme[disabled]{color:\"{{foreground-3}}\";cursor:default}.md-button.md-THEME_NAME-theme.md-accent[disabled] md-icon,.md-button.md-THEME_NAME-theme.md-fab[disabled] md-icon,.md-button.md-THEME_NAME-theme.md-raised[disabled] md-icon,.md-button.md-THEME_NAME-theme.md-warn[disabled] md-icon,.md-button.md-THEME_NAME-theme[disabled] md-icon{color:\"{{foreground-3}}\"}.md-button.md-THEME_NAME-theme.md-fab[disabled],.md-button.md-THEME_NAME-theme.md-raised[disabled]{background-color:\"{{foreground-4}}\"}.md-button.md-THEME_NAME-theme[disabled]{background-color:transparent}._md a.md-THEME_NAME-theme:not(.md-button).md-primary{color:\"{{primary-color}}\"}._md a.md-THEME_NAME-theme:not(.md-button).md-primary:hover{color:\"{{primary-700}}\"}._md a.md-THEME_NAME-theme:not(.md-button).md-accent: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,md-card.md-THEME_NAME-theme md-card-title md-card-title-text:not(:only-child) .md-subhead{color:\"{{foreground-2}}\"}md-checkbox.md-THEME_NAME-theme .md-ripple{color:\"{{accent-A700}}\"}md-checkbox.md-THEME_NAME-theme.md-checked .md-ripple{color:\"{{background-600}}\"}md-checkbox.md-THEME_NAME-theme.md-checked.md-focused .md-container:before{background-color:\"{{accent-color-0.26}}\"}md-checkbox.md-THEME_NAME-theme .md-ink-ripple{color:\"{{foreground-2}}\"}md-checkbox.md-THEME_NAME-theme.md-checked .md-ink-ripple{color:\"{{accent-color-0.87}}\"}md-checkbox.md-THEME_NAME-theme:not(.md-checked) .md-icon{border-color:\"{{foreground-2}}\"}md-checkbox.md-THEME_NAME-theme.md-checked .md-icon{background-color:\"{{accent-color-0.87}}\"}md-checkbox.md-THEME_NAME-theme.md-checked .md-icon:after{border-color:\"{{accent-contrast-0.87}}\"}md-checkbox.md-THEME_NAME-theme:not([disabled]).md-primary .md-ripple{color:\"{{primary-600}}\"}md-checkbox.md-THEME_NAME-theme:not([disabled]).md-primary.md-checked .md-ripple{color:\"{{background-600}}\"}md-checkbox.md-THEME_NAME-theme:not([disabled]).md-primary .md-ink-ripple{color:\"{{foreground-2}}\"}md-checkbox.md-THEME_NAME-theme:not([disabled]).md-primary.md-checked .md-ink-ripple{color:\"{{primary-color-0.87}}\"}md-checkbox.md-THEME_NAME-theme:not([disabled]).md-primary:not(.md-checked) .md-icon{border-color:\"{{foreground-2}}\"}md-checkbox.md-THEME_NAME-theme:not([disabled]).md-primary.md-checked .md-icon{background-color:\"{{primary-color-0.87}}\"}md-checkbox.md-THEME_NAME-theme:not([disabled]).md-primary.md-checked.md-focused .md-container:before{background-color:\"{{primary-color-0.26}}\"}md-checkbox.md-THEME_NAME-theme:not([disabled]).md-primary.md-checked .md-icon:after{border-color:\"{{primary-contrast-0.87}}\"}md-checkbox.md-THEME_NAME-theme:not([disabled]).md-primary .md-indeterminate[disabled] .md-container{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-chips.md-THEME_NAME-theme .md-chips{box-shadow:0 1px \"{{foreground-4}}\"}md-chips.md-THEME_NAME-theme .md-chips.md-focused{box-shadow:0 2px \"{{primary-color}}\"}md-chips.md-THEME_NAME-theme .md-chips .md-chip-input-container input{color:\"{{foreground-1}}\"}md-chips.md-THEME_NAME-theme .md-chips .md-chip-input-container input:-moz-placeholder,md-chips.md-THEME_NAME-theme .md-chips .md-chip-input-container input::-moz-placeholder{color:\"{{foreground-3}}\"}md-chips.md-THEME_NAME-theme .md-chips .md-chip-input-container input:-ms-input-placeholder{color:\"{{foreground-3}}\"}md-chips.md-THEME_NAME-theme .md-chips .md-chip-input-container input::-webkit-input-placeholder{color:\"{{foreground-3}}\"}md-chips.md-THEME_NAME-theme md-chip{background:\"{{background-300}}\";color:\"{{background-800}}\"}md-chips.md-THEME_NAME-theme md-chip md-icon{color:\"{{background-700}}\"}md-chips.md-THEME_NAME-theme md-chip.md-focused{background:\"{{primary-color}}\";color:\"{{primary-contrast}}\"}md-chips.md-THEME_NAME-theme md-chip.md-focused md-icon{color:\"{{primary-contrast}}\"}md-chips.md-THEME_NAME-theme md-chip._md-chip-editing{background:transparent;color:\"{{background-800}}\"}md-chips.md-THEME_NAME-theme md-chip-remove .md-button md-icon path{fill:\"{{background-500}}\"}.md-contact-suggestion span.md-contact-email{color:\"{{background-400}}\"}md-content.md-THEME_NAME-theme{color:\"{{foreground-1}}\";background-color:\"{{background-default}}\"}.md-calendar.md-THEME_NAME-theme{background:\"{{background-A100}}\";color:\"{{background-A200-0.87}}\"}.md-calendar.md-THEME_NAME-theme tr:last-child td{border-bottom-color:\"{{background-200}}\"}.md-THEME_NAME-theme .md-calendar-day-header{background:\"{{background-300}}\";color:\"{{background-A200-0.87}}\"}.md-THEME_NAME-theme .md-calendar-date.md-calendar-date-today .md-calendar-date-selection-indicator{border:1px solid \"{{primary-500}}\"}.md-THEME_NAME-theme .md-calendar-date.md-calendar-date-today.md-calendar-date-disabled{color:\"{{primary-500-0.6}}\"}.md-calendar-date.md-focus .md-THEME_NAME-theme .md-calendar-date-selection-indicator,.md-THEME_NAME-theme .md-calendar-date-selection-indicator:hover{background:\"{{background-300}}\"}.md-THEME_NAME-theme .md-calendar-date.md-calendar-selected-date .md-calendar-date-selection-indicator,.md-THEME_NAME-theme .md-calendar-date.md-focus.md-calendar-selected-date .md-calendar-date-selection-indicator{background:\"{{primary-500}}\";color:\"{{primary-500-contrast}}\";border-color:transparent}.md-THEME_NAME-theme .md-calendar-date-disabled,.md-THEME_NAME-theme .md-calendar-month-label-disabled{color:\"{{background-A200-0.435}}\"}.md-THEME_NAME-theme .md-datepicker-input{color:\"{{foreground-1}}\"}.md-THEME_NAME-theme .md-datepicker-input:-moz-placeholder,.md-THEME_NAME-theme .md-datepicker-input::-moz-placeholder{color:\"{{foreground-3}}\"}.md-THEME_NAME-theme .md-datepicker-input:-ms-input-placeholder{color:\"{{foreground-3}}\"}.md-THEME_NAME-theme .md-datepicker-input::-webkit-input-placeholder{color:\"{{foreground-3}}\"}.md-THEME_NAME-theme .md-datepicker-input-container{border-bottom-color:\"{{foreground-4}}\"}.md-THEME_NAME-theme .md-datepicker-input-container.md-datepicker-focused{border-bottom-color:\"{{primary-color}}\"}.md-accent .md-THEME_NAME-theme .md-datepicker-input-container.md-datepicker-focused{border-bottom-color:\"{{accent-color}}\"}.md-THEME_NAME-theme .md-datepicker-input-container.md-datepicker-invalid,.md-warn .md-THEME_NAME-theme .md-datepicker-input-container.md-datepicker-focused{border-bottom-color:\"{{warn-A700}}\"}.md-THEME_NAME-theme .md-datepicker-calendar-pane{border-color:\"{{background-hue-1}}\"}.md-THEME_NAME-theme .md-datepicker-triangle-button .md-datepicker-expand-triangle{border-top-color:\"{{foreground-2}}\"}.md-THEME_NAME-theme .md-datepicker-open .md-datepicker-calendar-icon{color:\"{{primary-color}}\"}.md-accent .md-THEME_NAME-theme .md-datepicker-open .md-datepicker-calendar-icon,.md-THEME_NAME-theme .md-datepicker-open.md-accent .md-datepicker-calendar-icon{color:\"{{accent-color}}\"}.md-THEME_NAME-theme .md-datepicker-open.md-warn .md-datepicker-calendar-icon,.md-warn .md-THEME_NAME-theme .md-datepicker-open .md-datepicker-calendar-icon{color:\"{{warn-A700}}\"}.md-THEME_NAME-theme .md-datepicker-calendar{background:\"{{background-A100}}\"}.md-THEME_NAME-theme .md-datepicker-input-mask-opaque{box-shadow:0 0 0 9999px \"{{background-hue-1}}\"}.md-THEME_NAME-theme .md-datepicker-open .md-datepicker-input-container{background:\"{{background-hue-1}}\"}md-dialog.md-THEME_NAME-theme{border-radius:4px;background-color:\"{{background-hue-1}}\";color:\"{{foreground-1}}\"}md-dialog.md-THEME_NAME-theme.md-content-overflow .md-actions,md-dialog.md-THEME_NAME-theme.md-content-overflow md-dialog-actions,md-divider.md-THEME_NAME-theme{border-top-color:\"{{foreground-4}}\"}.layout-gt-lg-row>md-divider.md-THEME_NAME-theme,.layout-gt-md-row>md-divider.md-THEME_NAME-theme,.layout-gt-sm-row>md-divider.md-THEME_NAME-theme,.layout-gt-xs-row>md-divider.md-THEME_NAME-theme,.layout-lg-row>md-divider.md-THEME_NAME-theme,.layout-md-row>md-divider.md-THEME_NAME-theme,.layout-row>md-divider.md-THEME_NAME-theme,.layout-sm-row>md-divider.md-THEME_NAME-theme,.layout-xl-row>md-divider.md-THEME_NAME-theme,.layout-xs-row>md-divider.md-THEME_NAME-theme{border-right-color:\"{{foreground-4}}\"}md-icon.md-THEME_NAME-theme{color:\"{{foreground-2}}\"}md-icon.md-THEME_NAME-theme.md-primary{color:\"{{primary-color}}\"}md-icon.md-THEME_NAME-theme.md-accent{color:\"{{accent-color}}\"}md-icon.md-THEME_NAME-theme.md-warn{color:\"{{warn-color}}\"}md-input-container.md-THEME_NAME-theme .md-input{color:\"{{foreground-1}}\";border-color:\"{{foreground-4}}\"}md-input-container.md-THEME_NAME-theme .md-input:-moz-placeholder,md-input-container.md-THEME_NAME-theme .md-input::-moz-placeholder{color:\"{{foreground-3}}\"}md-input-container.md-THEME_NAME-theme .md-input:-ms-input-placeholder{color:\"{{foreground-3}}\"}md-input-container.md-THEME_NAME-theme .md-input::-webkit-input-placeholder{color:\"{{foreground-3}}\"}md-input-container.md-THEME_NAME-theme>md-icon{color:\"{{foreground-1}}\"}md-input-container.md-THEME_NAME-theme .md-placeholder,md-input-container.md-THEME_NAME-theme label{color:\"{{foreground-3}}\"}md-input-container.md-THEME_NAME-theme label.md-required:after{color:\"{{warn-A700}}\"}md-input-container.md-THEME_NAME-theme:not(.md-input-focused):not(.md-input-invalid) label.md-required:after{color:\"{{foreground-2}}\"}md-input-container.md-THEME_NAME-theme .md-input-message-animation,md-input-container.md-THEME_NAME-theme .md-input-messages-animation{color:\"{{warn-A700}}\"}md-input-container.md-THEME_NAME-theme .md-input-message-animation .md-char-counter,md-input-container.md-THEME_NAME-theme .md-input-messages-animation .md-char-counter{color:\"{{foreground-1}}\"}md-input-container.md-THEME_NAME-theme.md-input-focused .md-input:-moz-placeholder,md-input-container.md-THEME_NAME-theme.md-input-focused .md-input::-moz-placeholder{color:\"{{foreground-2}}\"}md-input-container.md-THEME_NAME-theme.md-input-focused .md-input:-ms-input-placeholder{color:\"{{foreground-2}}\"}md-input-container.md-THEME_NAME-theme.md-input-focused .md-input::-webkit-input-placeholder{color:\"{{foreground-2}}\"}md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-has-value label{color:\"{{foreground-2}}\"}md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused .md-input,md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-resized .md-input{border-color:\"{{primary-color}}\"}md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused label,md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused md-icon{color:\"{{primary-color}}\"}md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused.md-accent .md-input{border-color:\"{{accent-color}}\"}md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused.md-accent label,md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused.md-accent md-icon{color:\"{{accent-color}}\"}md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused.md-warn .md-input{border-color:\"{{warn-A700}}\"}md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused.md-warn label,md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused.md-warn md-icon{color:\"{{warn-A700}}\"}md-input-container.md-THEME_NAME-theme.md-input-invalid .md-input{border-color:\"{{warn-A700}}\"}md-input-container.md-THEME_NAME-theme.md-input-invalid .md-char-counter,md-input-container.md-THEME_NAME-theme.md-input-invalid .md-input-message-animation,md-input-container.md-THEME_NAME-theme.md-input-invalid label{color:\"{{warn-A700}}\"}[disabled] md-input-container.md-THEME_NAME-theme .md-input,md-input-container.md-THEME_NAME-theme .md-input[disabled]{border-bottom-color:transparent;color:\"{{foreground-3}}\";background-image:linear-gradient(90deg,\"{{foreground-3}}\" 0,\"{{foreground-3}}\" 33%,transparent 0);background-image:-ms-linear-gradient(left,transparent 0,\"{{foreground-3}}\" 100%)}md-list.md-THEME_NAME-theme md-list-item.md-2-line .md-list-item-text h3,md-list.md-THEME_NAME-theme md-list-item.md-2-line .md-list-item-text h4,md-list.md-THEME_NAME-theme md-list-item.md-3-line .md-list-item-text h3,md-list.md-THEME_NAME-theme md-list-item.md-3-line .md-list-item-text h4{color:\"{{foreground-1}}\"}md-list.md-THEME_NAME-theme md-list-item.md-2-line .md-list-item-text p,md-list.md-THEME_NAME-theme md-list-item.md-3-line .md-list-item-text p{color:\"{{foreground-2}}\"}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],md-menu-content.md-THEME_NAME-theme md-menu-item .md-button[disabled] md-icon{color:\"{{background-A200-0.25}}\"}md-menu-content.md-THEME_NAME-theme md-menu-divider{background-color:\"{{background-A200-0.11}}\"}md-menu-bar.md-THEME_NAME-theme>button.md-button{color:\"{{foreground-2}}\";border-radius:2px}md-menu-bar.md-THEME_NAME-theme md-menu.md-open>button,md-menu-bar.md-THEME_NAME-theme md-menu>button:focus{outline:none;background:\"{{background-200}}\"}md-menu-bar.md-THEME_NAME-theme.md-open:not(.md-keyboard-mode) md-menu:hover>button{background-color:\"{{ background-500-0.2}}\"}md-menu-bar.md-THEME_NAME-theme:not(.md-keyboard-mode):not(.md-open) md-menu button:focus,md-menu-bar.md-THEME_NAME-theme:not(.md-keyboard-mode):not(.md-open) md-menu button:hover{background:transparent}md-menu-content.md-THEME_NAME-theme .md-menu>.md-button:after{color:\"{{background-A200-0.54}}\"}md-menu-content.md-THEME_NAME-theme .md-menu.md-open>.md-button{background-color:\"{{ background-500-0.2}}\"}md-toolbar.md-THEME_NAME-theme.md-menu-toolbar{background-color:\"{{background-A100}}\";color:\"{{background-A200}}\"}md-toolbar.md-THEME_NAME-theme.md-menu-toolbar md-toolbar-filler{background-color:\"{{primary-color}}\";color:\"{{background-A100-0.87}}\"}md-toolbar.md-THEME_NAME-theme.md-menu-toolbar md-toolbar-filler md-icon{color:\"{{background-A100-0.87}}\"}._md-panel-backdrop.md-THEME_NAME-theme{background-color:\"{{background-900-1.0}}\"}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-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-button.md-THEME_NAME-theme:not([disabled]).md-primary .md-on,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-primary .md-on,md-radio-group.md-THEME_NAME-theme:not([disabled]).md-primary .md-on,md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-primary .md-on{background-color:\"{{primary-color-0.87}}\"}md-radio-button.md-THEME_NAME-theme:not([disabled]).md-primary.md-checked .md-off,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-primary.md-checked .md-off,md-radio-button.md-THEME_NAME-theme:not([disabled]).md-primary .md-checked .md-off,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-primary .md-checked .md-off,md-radio-group.md-THEME_NAME-theme:not([disabled]).md-primary.md-checked .md-off,md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-primary.md-checked .md-off,md-radio-group.md-THEME_NAME-theme:not([disabled]).md-primary .md-checked .md-off,md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-primary .md-checked .md-off{border-color:\"{{primary-color-0.87}}\"}md-radio-button.md-THEME_NAME-theme:not([disabled]).md-primary.md-checked .md-ink-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-primary.md-checked .md-ink-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]).md-primary .md-checked .md-ink-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-primary .md-checked .md-ink-ripple,md-radio-group.md-THEME_NAME-theme:not([disabled]).md-primary.md-checked .md-ink-ripple,md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-primary.md-checked .md-ink-ripple,md-radio-group.md-THEME_NAME-theme:not([disabled]).md-primary .md-checked .md-ink-ripple,md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-primary .md-checked .md-ink-ripple{color:\"{{primary-color-0.87}}\"}md-radio-button.md-THEME_NAME-theme:not([disabled]).md-primary .md-container .md-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-primary .md-container .md-ripple,md-radio-group.md-THEME_NAME-theme:not([disabled]).md-primary .md-container .md-ripple,md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-primary .md-container .md-ripple{color:\"{{primary-600}}\"}md-radio-button.md-THEME_NAME-theme:not([disabled]).md-warn .md-on,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-warn .md-on,md-radio-group.md-THEME_NAME-theme:not([disabled]).md-warn .md-on,md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-warn .md-on{background-color:\"{{warn-color-0.87}}\"}md-radio-button.md-THEME_NAME-theme:not([disabled]).md-warn.md-checked .md-off,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-warn.md-checked .md-off,md-radio-button.md-THEME_NAME-theme:not([disabled]).md-warn .md-checked .md-off,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-warn .md-checked .md-off,md-radio-group.md-THEME_NAME-theme:not([disabled]).md-warn.md-checked .md-off,md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-warn.md-checked .md-off,md-radio-group.md-THEME_NAME-theme:not([disabled]).md-warn .md-checked .md-off,md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-warn .md-checked .md-off{border-color:\"{{warn-color-0.87}}\"}md-radio-button.md-THEME_NAME-theme:not([disabled]).md-warn.md-checked .md-ink-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-warn.md-checked .md-ink-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]).md-warn .md-checked .md-ink-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-warn .md-checked .md-ink-ripple,md-radio-group.md-THEME_NAME-theme:not([disabled]).md-warn.md-checked .md-ink-ripple,md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-warn.md-checked .md-ink-ripple,md-radio-group.md-THEME_NAME-theme:not([disabled]).md-warn .md-checked .md-ink-ripple,md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-warn .md-checked .md-ink-ripple{color:\"{{warn-color-0.87}}\"}md-radio-button.md-THEME_NAME-theme:not([disabled]).md-warn .md-container .md-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-warn .md-container .md-ripple,md-radio-group.md-THEME_NAME-theme:not([disabled]).md-warn .md-container .md-ripple,md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-warn .md-container .md-ripple{color:\"{{warn-600}}\"}md-radio-button.md-THEME_NAME-theme[disabled],md-radio-group.md-THEME_NAME-theme[disabled]{color:\"{{foreground-3}}\"}md-radio-button.md-THEME_NAME-theme[disabled] .md-container .md-off,md-radio-button.md-THEME_NAME-theme[disabled] .md-container .md-on,md-radio-group.md-THEME_NAME-theme[disabled] .md-container .md-off,md-radio-group.md-THEME_NAME-theme[disabled] .md-container .md-on{border-color:\"{{foreground-3}}\"}md-radio-group.md-THEME_NAME-theme .md-checked .md-ink-ripple{color:\"{{accent-color-0.26}}\"}md-radio-group.md-THEME_NAME-theme .md-checked:not([disabled]).md-primary .md-ink-ripple,md-radio-group.md-THEME_NAME-theme.md-primary .md-checked:not([disabled]) .md-ink-ripple{color:\"{{primary-color-0.26}}\"}md-radio-group.md-THEME_NAME-theme .md-checked.md-primary .md-ink-ripple{color:\"{{warn-color-0.26}}\"}md-radio-group.md-THEME_NAME-theme.md-focused:not(:empty) .md-checked .md-container:before{background-color:\"{{accent-color-0.26}}\"}md-radio-group.md-THEME_NAME-theme.md-focused:not(:empty) .md-checked.md-primary .md-container:before,md-radio-group.md-THEME_NAME-theme.md-focused:not(:empty).md-primary .md-checked .md-container:before{background-color:\"{{primary-color-0.26}}\"}md-radio-group.md-THEME_NAME-theme.md-focused:not(:empty) .md-checked.md-warn .md-container:before,md-radio-group.md-THEME_NAME-theme.md-focused:not(:empty).md-warn .md-checked .md-container:before{background-color:\"{{warn-color-0.26}}\"}md-input-container md-select.md-THEME_NAME-theme .md-select-value span:first-child:after{color:\"{{warn-A700}}\"}md-input-container:not(.md-input-focused):not(.md-input-invalid) md-select.md-THEME_NAME-theme .md-select-value span:first-child:after{color:\"{{foreground-3}}\"}md-input-container.md-input-focused:not(.md-input-has-value) md-select.md-THEME_NAME-theme .md-select-value,md-input-container.md-input-focused:not(.md-input-has-value) md-select.md-THEME_NAME-theme .md-select-value.md-select-placeholder{color:\"{{primary-color}}\"}md-input-container.md-input-invalid md-select.md-THEME_NAME-theme .md-select-value{color:\"{{warn-A700}}\"!important;border-bottom-color:\"{{warn-A700}}\"!important}md-input-container.md-input-invalid md-select.md-THEME_NAME-theme.md-no-underline .md-select-value{border-bottom-color:transparent!important}md-select.md-THEME_NAME-theme[disabled] .md-select-value{border-bottom-color:transparent;background-image:linear-gradient(90deg,\"{{foreground-3}}\" 0,\"{{foreground-3}}\" 33%,transparent 0);background-image:-ms-linear-gradient(left,transparent 0,\"{{foreground-3}}\" 100%)}md-select.md-THEME_NAME-theme .md-select-value{border-bottom-color:\"{{foreground-4}}\"}md-select.md-THEME_NAME-theme .md-select-value.md-select-placeholder{color:\"{{foreground-3}}\"}md-select.md-THEME_NAME-theme .md-select-value span:first-child:after{color:\"{{warn-A700}}\"}md-select.md-THEME_NAME-theme.md-no-underline .md-select-value{border-bottom-color:transparent!important}md-select.md-THEME_NAME-theme.ng-invalid.ng-touched .md-select-value{color:\"{{warn-A700}}\"!important;border-bottom-color:\"{{warn-A700}}\"!important}md-select.md-THEME_NAME-theme.ng-invalid.ng-touched.md-no-underline .md-select-value{border-bottom-color:transparent!important}md-select.md-THEME_NAME-theme:not([disabled]):focus .md-select-value{border-bottom-color:\"{{primary-color}}\";color:\"{{ foreground-1 }}\"}md-select.md-THEME_NAME-theme:not([disabled]):focus .md-select-value.md-select-placeholder{color:\"{{ foreground-1 }}\"}md-select.md-THEME_NAME-theme:not([disabled]):focus.md-no-underline .md-select-value{border-bottom-color:transparent!important}md-select.md-THEME_NAME-theme:not([disabled]):focus.md-accent .md-select-value{border-bottom-color:\"{{accent-color}}\"}md-select.md-THEME_NAME-theme:not([disabled]):focus.md-warn .md-select-value{border-bottom-color:\"{{warn-color}}\"}md-select.md-THEME_NAME-theme[disabled] .md-select-icon,md-select.md-THEME_NAME-theme[disabled] .md-select-value,md-select.md-THEME_NAME-theme[disabled] .md-select-value.md-select-placeholder{color:\"{{foreground-3}}\"}md-select.md-THEME_NAME-theme .md-select-icon{color:\"{{foreground-2}}\"}md-select-menu.md-THEME_NAME-theme md-content{background:\"{{background-A100}}\"}md-select-menu.md-THEME_NAME-theme md-content md-optgroup{color:\"{{background-600-0.87}}\"}md-select-menu.md-THEME_NAME-theme md-content md-option{color:\"{{background-900-0.87}}\"}md-select-menu.md-THEME_NAME-theme md-content md-option[disabled] .md-text{color:\"{{background-400-0.87}}\"}md-select-menu.md-THEME_NAME-theme md-content md-option:not([disabled]):focus,md-select-menu.md-THEME_NAME-theme md-content md-option:not([disabled]):hover{background:\"{{background-200}}\"}md-select-menu.md-THEME_NAME-theme md-content md-option[selected]{color:\"{{primary-500}}\"}md-select-menu.md-THEME_NAME-theme md-content md-option[selected]:focus{color:\"{{primary-600}}\"}md-select-menu.md-THEME_NAME-theme md-content md-option[selected].md-accent{color:\"{{accent-color}}\"}md-select-menu.md-THEME_NAME-theme md-content md-option[selected].md-accent:focus{color:\"{{accent-A700}}\"}.md-checkbox-enabled.md-THEME_NAME-theme .md-ripple{color:\"{{primary-600}}\"}.md-checkbox-enabled.md-THEME_NAME-theme[selected] .md-ripple{color:\"{{background-600}}\"}.md-checkbox-enabled.md-THEME_NAME-theme .md-ink-ripple{color:\"{{foreground-2}}\"}.md-checkbox-enabled.md-THEME_NAME-theme[selected] .md-ink-ripple{color:\"{{primary-color-0.87}}\"}.md-checkbox-enabled.md-THEME_NAME-theme:not(.md-checked) .md-icon{border-color:\"{{foreground-2}}\"}.md-checkbox-enabled.md-THEME_NAME-theme[selected] .md-icon{background-color:\"{{primary-color-0.87}}\"}.md-checkbox-enabled.md-THEME_NAME-theme[selected].md-focused .md-container:before{background-color:\"{{primary-color-0.26}}\"}.md-checkbox-enabled.md-THEME_NAME-theme[selected] .md-icon:after{border-color:\"{{primary-contrast-0.87}}\"}.md-checkbox-enabled.md-THEME_NAME-theme .md-indeterminate[disabled] .md-container{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-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-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]),md-tabs.md-THEME_NAME-theme.md-accent>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]) md-icon{color:\"{{accent-A100}}\"}md-tabs.md-THEME_NAME-theme.md-accent>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-active,md-tabs.md-THEME_NAME-theme.md-accent>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-active md-icon,md-tabs.md-THEME_NAME-theme.md-accent>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-focused,md-tabs.md-THEME_NAME-theme.md-accent>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-focused md-icon{color:\"{{accent-contrast}}\"}md-tabs.md-THEME_NAME-theme.md-accent>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-focused{background:\"{{accent-contrast-0.1}}\"}md-tabs.md-THEME_NAME-theme.md-accent>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-ink-bar{color:\"{{primary-600-1}}\";background:\"{{primary-600-1}}\"}md-tabs.md-THEME_NAME-theme.md-primary>md-tabs-wrapper{background-color:\"{{primary-color}}\"}md-tabs.md-THEME_NAME-theme.md-primary>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]),md-tabs.md-THEME_NAME-theme.md-primary>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]) md-icon{color:\"{{primary-100}}\"}md-tabs.md-THEME_NAME-theme.md-primary>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-active,md-tabs.md-THEME_NAME-theme.md-primary>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-active md-icon,md-tabs.md-THEME_NAME-theme.md-primary>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-focused,md-tabs.md-THEME_NAME-theme.md-primary>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-focused md-icon{color:\"{{primary-contrast}}\"}md-tabs.md-THEME_NAME-theme.md-primary>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-focused{background:\"{{primary-contrast-0.1}}\"}md-tabs.md-THEME_NAME-theme.md-warn>md-tabs-wrapper{background-color:\"{{warn-color}}\"}md-tabs.md-THEME_NAME-theme.md-warn>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]),md-tabs.md-THEME_NAME-theme.md-warn>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]) md-icon{color:\"{{warn-100}}\"}md-tabs.md-THEME_NAME-theme.md-warn>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-active,md-tabs.md-THEME_NAME-theme.md-warn>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-active md-icon,md-tabs.md-THEME_NAME-theme.md-warn>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-focused,md-tabs.md-THEME_NAME-theme.md-warn>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-focused md-icon{color:\"{{warn-contrast}}\"}md-tabs.md-THEME_NAME-theme.md-warn>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-focused{background:\"{{warn-contrast-0.1}}\"}md-toolbar>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper{background-color:\"{{primary-color}}\"}md-toolbar>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]),md-toolbar>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]) md-icon{color:\"{{primary-100}}\"}md-toolbar>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-active,md-toolbar>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-active md-icon,md-toolbar>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-focused,md-toolbar>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-focused md-icon{color:\"{{primary-contrast}}\"}md-toolbar>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-focused{background:\"{{primary-contrast-0.1}}\"}md-toolbar.md-accent>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper{background-color:\"{{accent-color}}\"}md-toolbar.md-accent>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]),md-toolbar.md-accent>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]) md-icon{color:\"{{accent-A100}}\"}md-toolbar.md-accent>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-active,md-toolbar.md-accent>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-active md-icon,md-toolbar.md-accent>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-focused,md-toolbar.md-accent>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-focused md-icon{color:\"{{accent-contrast}}\"}md-toolbar.md-accent>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-focused{background:\"{{accent-contrast-0.1}}\"}md-toolbar.md-accent>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-ink-bar{color:\"{{primary-600-1}}\";background:\"{{primary-600-1}}\"}md-toolbar.md-warn>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper{background-color:\"{{warn-color}}\"}md-toolbar.md-warn>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]),md-toolbar.md-warn>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]) md-icon{color:\"{{warn-100}}\"}md-toolbar.md-warn>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-active,md-toolbar.md-warn>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-active md-icon,md-toolbar.md-warn>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-focused,md-toolbar.md-warn>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-focused md-icon{color:\"{{warn-contrast}}\"}md-toolbar.md-warn>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-focused{background:\"{{warn-contrast-0.1}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar){background-color:\"{{primary-color}}\";color:\"{{primary-contrast}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar) md-icon{color:\"{{primary-contrast}}\";fill:\"{{primary-contrast}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar) .md-button[disabled] md-icon{color:\"{{primary-contrast-0.26}}\";fill:\"{{primary-contrast-0.26}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-accent{background-color:\"{{accent-color}}\";color:\"{{accent-contrast}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-accent .md-ink-ripple{color:\"{{accent-contrast}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-accent md-icon{color:\"{{accent-contrast}}\";fill:\"{{accent-contrast}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-accent .md-button[disabled] md-icon{color:\"{{accent-contrast-0.26}}\";fill:\"{{accent-contrast-0.26}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-warn{background-color:\"{{warn-color}}\";color:\"{{warn-contrast}}\"}.md-panel.md-tooltip.md-THEME_NAME-theme{color:\"{{background-700-contrast}}\";background-color:\"{{background-700}}\"}body.md-THEME_NAME-theme,html.md-THEME_NAME-theme{color:\"{{foreground-1}}\";background-color:\"{{background-color}}\"}");
35746})();
35747
35748
35749})(window, window.angular);;window.ngMaterial={version:{full: "1.1.3"}};