2 * Angular Material Design
3 * https://github.com/angular/material
7 (function( window, angular, undefined ){
13 angular.module('ngMaterial', ["ng","ngAnimate","ngAria","material.core","material.core.gestures","material.core.theming.palette","material.core.theming","material.components.autocomplete","material.components.backdrop","material.components.bottomSheet","material.components.button","material.components.card","material.components.checkbox","material.components.chips","material.components.content","material.components.dialog","material.components.divider","material.components.gridList","material.components.icon","material.components.input","material.components.list","material.components.progressCircular","material.components.progressLinear","material.components.radioButton","material.components.select","material.components.sidenav","material.components.slider","material.components.sticky","material.components.subheader","material.components.swipe","material.components.switch","material.components.tabs","material.components.toast","material.components.toolbar","material.components.tooltip","material.components.whiteframe"]);
20 * Initialization function that validates environment
24 .module('material.core', [ 'material.core.gestures', 'material.core.theming' ])
25 .config( MdCoreConfigure );
28 function MdCoreConfigure($provide, $mdThemingProvider) {
30 $provide.decorator('$$rAF', ["$delegate", rAFDecorator]);
32 $mdThemingProvider.theme('default')
33 .primaryPalette('indigo')
34 .accentPalette('pink')
36 .backgroundPalette('grey');
38 MdCoreConfigure.$inject = ["$provide", "$mdThemingProvider"];
40 function rAFDecorator( $delegate ) {
42 * Use this to throttle events that come in often.
43 * The throttled function will always use the *last* invocation before the
46 * For example, window resize events that fire many times a second:
47 * If we set to use an raf-throttled callback on window resize, then
48 * our callback will only be fired once per frame, with the last resize
49 * event that happened before that frame.
51 * @param {function} callback function to debounce
53 $delegate.throttle = function(cb) {
54 var queueArgs, alreadyQueued, queueCb, context;
55 return function debounced() {
56 queueArgs = arguments;
61 $delegate(function() {
62 queueCb.apply(context, queueArgs);
63 alreadyQueued = false;
75 angular.module('material.core')
76 .factory('$mdConstant', MdConstantFactory);
78 function MdConstantFactory($$rAF, $sniffer) {
80 var webkit = /webkit/i.test($sniffer.vendorPrefix);
81 function vendorProperty(name) {
82 return webkit ? ('webkit' + name.charAt(0).toUpperCase() + name.substring(1)) : name;
100 TRANSITIONEND: 'transitionend' + (webkit ? ' webkitTransitionEnd' : ''),
101 ANIMATIONEND: 'animationend' + (webkit ? ' webkitAnimationEnd' : ''),
103 TRANSFORM: vendorProperty('transform'),
104 TRANSFORM_ORIGIN: vendorProperty('transformOrigin'),
105 TRANSITION: vendorProperty('transition'),
106 TRANSITION_DURATION: vendorProperty('transitionDuration'),
107 ANIMATION_PLAY_STATE: vendorProperty('animationPlayState'),
108 ANIMATION_DURATION: vendorProperty('animationDuration'),
109 ANIMATION_NAME: vendorProperty('animationName'),
110 ANIMATION_TIMING: vendorProperty('animationTimingFunction'),
111 ANIMATION_DIRECTION: vendorProperty('animationDirection')
114 'sm': '(max-width: 600px)',
115 'gt-sm': '(min-width: 600px)',
116 'md': '(min-width: 600px) and (max-width: 960px)',
117 'gt-md': '(min-width: 960px)',
118 'lg': '(min-width: 960px) and (max-width: 1200px)',
119 'gt-lg': '(min-width: 1200px)'
131 MdConstantFactory.$inject = ["$$rAF", "$sniffer"];
138 .module('material.core')
139 .config( ["$provide", function($provide){
140 $provide.decorator('$mdUtil', ['$delegate', function ($delegate){
142 * Inject the iterator facade to easily support iteration and accessors
143 * @see iterator below
145 $delegate.iterator = MdIterator;
153 * iterator is a list facade to easily support iteration and accessors
155 * @param items Array list which this iterator will enumerate
156 * @param reloop Boolean enables iterator to consider the list as an endless reloop
158 function MdIterator(items, reloop) {
159 var trueFn = function() { return true; };
161 if (items && !angular.isArray(items)) {
162 items = Array.prototype.slice.call(items);
166 var _items = items || [ ];
185 next: angular.bind(null, findSubsequentItem, false),
186 previous: angular.bind(null, findSubsequentItem, true),
188 hasPrevious: hasPrevious,
194 * Publish copy of the enumerable set
197 function getItems() {
198 return [].concat(_items);
202 * Determine length of the list
203 * @returns {Array.length|*|number}
206 return _items.length;
210 * Is the index specified valid
212 * @returns {Array.length|*|number|boolean}
214 function inRange(index) {
215 return _items.length && ( index > -1 ) && (index < _items.length );
219 * Can the iterator proceed to the next item in the list; relative to
220 * the specified item.
223 * @returns {Array.length|*|number|boolean}
225 function hasNext(item) {
226 return item ? inRange(indexOf(item) + 1) : false;
230 * Can the iterator proceed to the previous item in the list; relative to
231 * the specified item.
234 * @returns {Array.length|*|number|boolean}
236 function hasPrevious(item) {
237 return item ? inRange(indexOf(item) - 1) : false;
241 * Get item at specified index/position
245 function itemAt(index) {
246 return inRange(index) ? _items[index] : null;
250 * Find all elements matching the key/value pair
251 * otherwise return null
258 function findBy(key, val) {
259 return _items.filter(function(item) {
260 return item[key] === val;
270 function add(item, index) {
271 if ( !item ) return -1;
273 if (!angular.isNumber(index)) {
274 index = _items.length;
277 _items.splice(index, 0, item);
279 return indexOf(item);
283 * Remove item from list...
286 function remove(item) {
287 if ( contains(item) ){
288 _items.splice(indexOf(item), 1);
293 * Get the zero-based index of the target item
297 function indexOf(item) {
298 return _items.indexOf(item);
302 * Boolean existence check
306 function contains(item) {
307 return item && (indexOf(item) > -1);
311 * Return first item in the list
315 return _items.length ? _items[0] : null;
319 * Return last item in the list...
323 return _items.length ? _items[_items.length - 1] : null;
327 * Find the next item. If reloop is true and at the end of the list, it will go back to the
328 * first item. If given, the `validate` callback will be used to determine whether the next item
329 * is valid. If not valid, it will try to find the next item again.
331 * @param {boolean} backwards Specifies the direction of searching (forwards/backwards)
332 * @param {*} item The item whose subsequent item we are looking for
333 * @param {Function=} validate The `validate` function
334 * @param {integer=} limit The recursion limit
336 * @returns {*} The subsequent item or null
338 function findSubsequentItem(backwards, item, validate, limit) {
339 validate = validate || trueFn;
341 var curIndex = indexOf(item);
343 if (!inRange(curIndex)) return null;
345 var nextIndex = curIndex + (backwards ? -1 : 1);
346 var foundItem = null;
347 if (inRange(nextIndex)) {
348 foundItem = _items[nextIndex];
350 foundItem = backwards ? last() : first();
351 nextIndex = indexOf(foundItem);
354 if ((foundItem === null) || (nextIndex === limit)) return null;
355 if (validate(foundItem)) return foundItem;
357 if (angular.isUndefined(limit)) limit = nextIndex;
359 curIndex = nextIndex;
369 angular.module('material.core')
370 .factory('$mdMedia', mdMediaFactory);
375 * @module material.core
378 * `$mdMedia` is used to evaluate whether a given media query is true or false given the
379 * current device's screen / window size. The media query will be re-evaluated on resize, allowing
380 * you to register a watch.
382 * `$mdMedia` also has pre-programmed support for media queries that match the layout breakpoints.
383 * (`sm`, `gt-sm`, `md`, `gt-md`, `lg`, `gt-lg`).
385 * @returns {boolean} a boolean representing whether or not the given media query is true or false.
389 * app.controller('MyController', function($mdMedia, $scope) {
390 * $scope.$watch(function() { return $mdMedia('lg'); }, function(big) {
391 * $scope.bigScreen = big;
394 * $scope.screenIsSmall = $mdMedia('sm');
395 * $scope.customQuery = $mdMedia('(min-width: 1234px)');
396 * $scope.anotherCustom = $mdMedia('max-width: 300px');
401 function mdMediaFactory($mdConstant, $rootScope, $window) {
405 var normalizeCache = {};
407 $mdMedia.getResponsiveAttribute = getResponsiveAttribute;
408 $mdMedia.getQuery = getQuery;
409 $mdMedia.watchResponsiveAttributes = watchResponsiveAttributes;
413 function $mdMedia(query) {
414 var validated = queries[query];
415 if (angular.isUndefined(validated)) {
416 validated = queries[query] = validate(query);
419 var result = results[validated];
420 if (angular.isUndefined(result)) {
421 result = add(validated);
427 function validate(query) {
428 return $mdConstant.MEDIA[query] ||
429 ((query.charAt(0) !== '(') ? ('(' + query + ')') : query);
432 function add(query) {
433 var result = mqls[query] = $window.matchMedia(query);
434 result.addListener(onQueryChange);
435 return (results[result.media] = !!result.matches);
438 function onQueryChange(query) {
439 $rootScope.$evalAsync(function() {
440 results[query.media] = !!query.matches;
444 function getQuery(name) {
448 function getResponsiveAttribute(attrs, attrName) {
449 for (var i = 0; i < $mdConstant.MEDIA_PRIORITY.length; i++) {
450 var mediaName = $mdConstant.MEDIA_PRIORITY[i];
451 if (!mqls[queries[mediaName]].matches) {
455 var normalizedName = getNormalizedName(attrs, attrName + '-' + mediaName);
456 if (attrs[normalizedName]) {
457 return attrs[normalizedName];
461 // fallback on unprefixed
462 return attrs[getNormalizedName(attrs, attrName)];
465 function watchResponsiveAttributes(attrNames, attrs, watchFn) {
467 attrNames.forEach(function(attrName) {
468 var normalizedName = getNormalizedName(attrs, attrName);
469 if (attrs[normalizedName]) {
471 attrs.$observe(normalizedName, angular.bind(void 0, watchFn, null)));
474 for (var mediaName in $mdConstant.MEDIA) {
475 normalizedName = getNormalizedName(attrs, attrName + '-' + mediaName);
476 if (!attrs[normalizedName]) {
480 unwatchFns.push(attrs.$observe(normalizedName, angular.bind(void 0, watchFn, mediaName)));
484 return function unwatch() {
485 unwatchFns.forEach(function(fn) { fn(); })
489 // Improves performance dramatically
490 function getNormalizedName(attrs, attrName) {
491 return normalizeCache[attrName] ||
492 (normalizeCache[attrName] = attrs.$normalize(attrName));
495 mdMediaFactory.$inject = ["$mdConstant", "$rootScope", "$window"];
502 * This var has to be outside the angular factory, otherwise when
503 * there are multiple material apps on the same page, each app
504 * will create its own instance of this array and the app's IDs
505 * will not be unique.
507 var nextUniqueId = 0;
509 angular.module('material.core')
510 .factory('$mdUtil', ["$cacheFactory", "$document", "$timeout", "$q", "$window", "$mdConstant", function($cacheFactory, $document, $timeout, $q, $window, $mdConstant) {
513 function getNode(el) {
518 now: window.performance ?
519 angular.bind(window.performance, window.performance.now) :
522 clientRect: function(element, offsetParent, isOffsetRect) {
523 var node = getNode(element);
524 offsetParent = getNode(offsetParent || node.offsetParent || document.body);
525 var nodeRect = node.getBoundingClientRect();
527 // The user can ask for an offsetRect: a rect relative to the offsetParent,
528 // or a clientRect: a rect relative to the page
529 var offsetRect = isOffsetRect ?
530 offsetParent.getBoundingClientRect() :
531 { left: 0, top: 0, width: 0, height: 0 };
533 left: nodeRect.left - offsetRect.left,
534 top: nodeRect.top - offsetRect.top,
535 width: nodeRect.width,
536 height: nodeRect.height
539 offsetRect: function(element, offsetParent) {
540 return Util.clientRect(element, offsetParent, true);
542 // Disables scroll around the passed element. Goes up the DOM to find a
543 // disableTarget (a md-content that is scrolling, or the body as a fallback)
544 // and uses CSS/JS to prevent it from scrolling
545 disableScrollAround: function(element) {
546 element = element instanceof angular.element ? element[0] : element;
547 var parentEl = element;
550 // Find the highest level scrolling md-content
551 while (parentEl = this.getClosest(parentEl, 'MD-CONTENT', true)) {
552 if (isScrolling(parentEl)) {
553 disableTarget = angular.element(parentEl)[0];
557 // Default to the body if no scrolling md-content
558 if (!disableTarget) {
559 disableTarget = $document[0].body;
560 if (!isScrolling(disableTarget)) return angular.noop;
563 if (disableTarget.nodeName == 'BODY') {
564 return disableBodyScroll();
566 return disableElementScroll();
569 // Creates a virtual scrolling mask to absorb touchmove, keyboard, scrollbar clicking, and wheel events
570 function disableElementScroll() {
571 var scrollMask = angular.element('<div class="md-scroll-mask"><div class="md-scroll-mask-bar"></div></div>');
572 var computedStyle = $window.getComputedStyle(disableTarget);
573 var disableRect = disableTarget.getBoundingClientRect();
574 var scrollWidth = disableRect.width - disableTarget.clientWidth;
575 applyStyles(scrollMask[0], {
576 zIndex: computedStyle.zIndex == 'auto' ? 2 : computedStyle.zIndex + 1,
577 width: disableRect.width + 'px',
578 height: disableRect.height + 'px',
579 top: disableRect.top + 'px',
580 left: disableRect.left + 'px'
582 scrollMask[0].firstElementChild.style.width = scrollWidth + 'px';
583 $document[0].body.appendChild(scrollMask[0]);
585 scrollMask.on('wheel', preventDefault);
586 scrollMask.on('touchmove', preventDefault);
587 $document.on('keydown', disableKeyNav);
589 return function restoreScroll() {
590 scrollMask.off('wheel');
591 scrollMask.off('touchmove');
592 scrollMask[0].parentNode.removeChild(scrollMask[0]);
593 $document.off('keydown', disableKeyNav);
596 // Prevent keypresses from elements inside the disableTarget
597 // used to stop the keypresses that could cause the page to scroll
598 // (arrow keys, spacebar, tab, etc).
599 function disableKeyNav(e) {
600 if (disableTarget.contains(e.target)) {
602 e.stopImmediatePropagation();
606 function preventDefault(e) {
611 // Converts the disableTarget (body) to a position fixed block and translate it to the propper scroll position
612 function disableBodyScroll() {
613 var restoreStyle = disableTarget.getAttribute('style') || '';
614 var scrollOffset = disableTarget.scrollTop;
616 applyStyles(disableTarget, {
620 top: -scrollOffset + 'px'
623 return function restoreScroll() {
624 disableTarget.setAttribute('style', restoreStyle);
625 disableTarget.scrollTop = scrollOffset;
629 function applyStyles (el, styles) {
630 for (var key in styles) {
631 el.style[key] = styles[key];
635 function isScrolling(el) {
636 if (el instanceof angular.element) el = el[0];
637 return el.scrollHeight > el.offsetHeight;
641 floatingScrollbars: function() {
642 if (this.floatingScrollbars.cached === undefined) {
643 var tempNode = angular.element('<div style="width: 100%; z-index: -1; position: absolute; height: 35px; overflow-y: scroll"><div style="height: 60;"></div></div>');
644 $document[0].body.appendChild(tempNode[0]);
645 this.floatingScrollbars.cached = (tempNode[0].offsetWidth == tempNode[0].childNodes[0].offsetWidth);
648 return this.floatingScrollbars.cached;
651 // Mobile safari only allows you to set focus in click event listeners...
652 forceFocus: function(element) {
653 var node = element[0] || element;
655 document.addEventListener('click', function focusOnClick(ev) {
656 if (ev.target === node && ev.$focus) {
658 ev.stopImmediatePropagation();
660 node.removeEventListener('click', focusOnClick);
664 var newEvent = document.createEvent('MouseEvents');
665 newEvent.initMouseEvent('click', false, true, window, {}, 0, 0, 0, 0,
666 false, false, false, false, 0, null);
667 newEvent.$material = true;
668 newEvent.$focus = true;
669 node.dispatchEvent(newEvent);
672 transitionEndPromise: function(element, opts) {
674 var deferred = $q.defer();
675 element.on($mdConstant.CSS.TRANSITIONEND, finished);
676 function finished(ev) {
677 // Make sure this transitionend didn't bubble up from a child
678 if (!ev || ev.target === element[0]) {
679 element.off($mdConstant.CSS.TRANSITIONEND, finished);
683 if (opts.timeout) $timeout(finished, opts.timeout);
684 return deferred.promise;
687 fakeNgModel: function() {
690 $setTouched: angular.noop,
691 $setViewValue: function(value) {
692 this.$viewValue = value;
694 this.$viewChangeListeners.forEach(function(cb) { cb(); });
696 $isEmpty: function(value) {
697 return ('' + value).length === 0;
701 $viewChangeListeners: [],
702 $render: angular.noop
706 // Returns a function, that, as long as it continues to be invoked, will not
707 // be triggered. The function will be called after it stops being called for
709 // @param wait Integer value of msecs to delay (since last debounce reset); default value 10 msecs
710 // @param invokeApply should the $timeout trigger $digest() dirty checking
711 debounce: function (func, wait, scope, invokeApply) {
714 return function debounced() {
716 args = Array.prototype.slice.call(arguments);
718 $timeout.cancel(timer);
719 timer = $timeout(function() {
722 func.apply(context, args);
724 }, wait || 10, invokeApply );
728 // Returns a function that can only be triggered every `delay` milliseconds.
729 // In other words, the function will not be called unless it has been more
730 // than `delay` milliseconds since the last call.
731 throttle: function throttle(func, delay) {
733 return function throttled() {
735 var args = arguments;
736 var now = Util.now();
738 if (!recent || (now - recent > delay)) {
739 func.apply(context, args);
746 * Measures the number of milliseconds taken to run the provided callback
747 * function. Uses a high-precision timer if available.
749 time: function time(cb) {
750 var start = Util.now();
752 return Util.now() - start;
758 * @returns {string} an unique numeric string
760 nextUid: function() {
761 return '' + nextUniqueId++;
764 // Stop watchers and events from firing on a scope without destroying it,
765 // by disconnecting it from its parent and its siblings' linked lists.
766 disconnectScope: function disconnectScope(scope) {
769 // we can't destroy the root scope or a scope that has been already destroyed
770 if (scope.$root === scope) return;
771 if (scope.$$destroyed ) return;
773 var parent = scope.$parent;
774 scope.$$disconnected = true;
776 // See Scope.$destroy
777 if (parent.$$childHead === scope) parent.$$childHead = scope.$$nextSibling;
778 if (parent.$$childTail === scope) parent.$$childTail = scope.$$prevSibling;
779 if (scope.$$prevSibling) scope.$$prevSibling.$$nextSibling = scope.$$nextSibling;
780 if (scope.$$nextSibling) scope.$$nextSibling.$$prevSibling = scope.$$prevSibling;
782 scope.$$nextSibling = scope.$$prevSibling = null;
786 // Undo the effects of disconnectScope above.
787 reconnectScope: function reconnectScope(scope) {
790 // we can't disconnect the root node or scope already disconnected
791 if (scope.$root === scope) return;
792 if (!scope.$$disconnected) return;
796 var parent = child.$parent;
797 child.$$disconnected = false;
798 // See Scope.$new for this logic...
799 child.$$prevSibling = parent.$$childTail;
800 if (parent.$$childHead) {
801 parent.$$childTail.$$nextSibling = child;
802 parent.$$childTail = child;
804 parent.$$childHead = parent.$$childTail = child;
809 * getClosest replicates jQuery.closest() to walk up the DOM tree until it finds a matching nodeName
811 * @param el Element to start walking the DOM from
812 * @param tagName Tag name to find closest to el, such as 'form'
814 getClosest: function getClosest(el, tagName, onlyParent) {
815 if (el instanceof angular.element) el = el[0];
816 tagName = tagName.toUpperCase();
817 if (onlyParent) el = el.parentNode;
818 if (!el) return null;
820 if (el.nodeName === tagName) {
823 } while (el = el.parentNode);
828 * Functional equivalent for $element.filter(‘md-bottom-sheet’)
829 * useful with interimElements where the element and its container are important...
831 extractElementByName: function (element, nodeName) {
832 for (var i = 0, len = element.length; i < len; i++) {
833 if (element[i].nodeName.toLowerCase() === nodeName){
834 return angular.element(element[i]);
841 * Give optional properties with no value a boolean true by default
843 initOptionalProperties: function (scope, attr, defaults ) {
844 defaults = defaults || { };
845 angular.forEach(scope.$$isolateBindings, function (binding, key) {
846 if (binding.optional && angular.isUndefined(scope[key])) {
847 var hasKey = attr.hasOwnProperty(attr.$normalize(binding.attrName));
849 scope[key] = angular.isDefined(defaults[key]) ? defaults[key] : hasKey;
859 * Since removing jQuery from the demos, some code that uses `element.focus()` is broken.
861 * We need to add `element.focus()`, because it's testable unlike `element[0].focus`.
863 * TODO(ajoslin): This should be added in a better place later.
866 angular.element.prototype.focus = angular.element.prototype.focus || function() {
872 angular.element.prototype.blur = angular.element.prototype.blur || function() {
884 angular.module('material.core')
885 .service('$mdAria', AriaService);
890 function AriaService($$rAF, $log, $window) {
894 expectAsync: expectAsync,
895 expectWithText: expectWithText
899 * Check if expected attribute has been specified on the target element or child
902 * @param {optional} defaultValue What to set the attr to if no value is found
904 function expect(element, attrName, defaultValue) {
905 var node = element[0] || element;
907 // if node exists and neither it nor its children have the attribute
909 ((!node.hasAttribute(attrName) ||
910 node.getAttribute(attrName).length === 0) &&
911 !childHasAttribute(node, attrName))) {
913 defaultValue = angular.isString(defaultValue) ? defaultValue.trim() : '';
914 if (defaultValue.length) {
915 element.attr(attrName, defaultValue);
917 $log.warn('ARIA: Attribute "', attrName, '", required for accessibility, is missing on node:', node);
923 function expectAsync(element, attrName, defaultValueGetter) {
924 // Problem: when retrieving the element's contents synchronously to find the label,
925 // the text may not be defined yet in the case of a binding.
926 // There is a higher chance that a binding will be defined if we wait one frame.
928 expect(element, attrName, defaultValueGetter());
932 function expectWithText(element, attrName) {
933 expectAsync(element, attrName, function() {
934 return getText(element);
938 function getText(element) {
939 return element.text().trim();
942 function childHasAttribute(node, attrName) {
943 var hasChildren = node.hasChildNodes(),
946 function isHidden(el) {
947 var style = el.currentStyle ? el.currentStyle : $window.getComputedStyle(el);
948 return (style.display === 'none');
952 var children = node.childNodes;
953 for(var i=0; i<children.length; i++){
954 var child = children[i];
955 if(child.nodeType === 1 && child.hasAttribute(attrName)) {
956 if(!isHidden(child)){
965 AriaService.$inject = ["$$rAF", "$log", "$window"];
971 angular.module('material.core')
972 .service('$mdCompiler', mdCompilerService);
974 function mdCompilerService($q, $http, $injector, $compile, $controller, $templateCache) {
975 /* jshint validthis: true */
980 * @module material.core
982 * The $mdCompiler service is an abstraction of angular's compiler, that allows the developer
983 * to easily compile an element with a templateUrl, controller, and locals.
987 * $mdCompiler.compile({
988 * templateUrl: 'modal.html',
989 * controller: 'ModalCtrl',
991 * modal: myModalInstance;
993 * }).then(function(compileData) {
994 * compileData.element; // modal.html's template in an element
995 * compileData.link(myScope); //attach controller & scope to element
1002 * @name $mdCompiler#compile
1003 * @description A helper to compile an HTML template/templateUrl with a given controller,
1004 * locals, and scope.
1005 * @param {object} options An options object, with the following properties:
1007 * - `controller` - `{(string=|function()=}` Controller fn that should be associated with
1008 * newly created scope or the name of a registered controller if passed as a string.
1009 * - `controllerAs` - `{string=}` A controller alias name. If present the controller will be
1010 * published to scope under the `controllerAs` name.
1011 * - `template` - `{string=}` An html template as a string.
1012 * - `templateUrl` - `{string=}` A path to an html template.
1013 * - `transformTemplate` - `{function(template)=}` A function which transforms the template after
1014 * it is loaded. It will be given the template string as a parameter, and should
1015 * return a a new string representing the transformed template.
1016 * - `resolve` - `{Object.<string, function>=}` - An optional map of dependencies which should
1017 * be injected into the controller. If any of these dependencies are promises, the compiler
1018 * will wait for them all to be resolved, or if one is rejected before the controller is
1019 * instantiated `compile()` will fail..
1020 * * `key` - `{string}`: a name of a dependency to be injected into the controller.
1021 * * `factory` - `{string|function}`: If `string` then it is an alias for a service.
1022 * Otherwise if function, then it is injected and the return value is treated as the
1023 * dependency. If the result is a promise, it is resolved before its value is
1024 * injected into the controller.
1026 * @returns {object=} promise A promise, which will be resolved with a `compileData` object.
1027 * `compileData` has the following properties:
1029 * - `element` - `{element}`: an uncompiled element matching the provided template.
1030 * - `link` - `{function(scope)}`: A link function, which, when called, will compile
1031 * the element and instantiate the provided controller (if given).
1032 * - `locals` - `{object}`: The locals which will be passed into the controller once `link` is
1033 * called. If `bindToController` is true, they will be coppied to the ctrl instead
1034 * - `bindToController` - `bool`: bind the locals to the controller, instead of passing them in.
1036 this.compile = function(options) {
1037 var templateUrl = options.templateUrl;
1038 var template = options.template || '';
1039 var controller = options.controller;
1040 var controllerAs = options.controllerAs;
1041 var resolve = options.resolve || {};
1042 var locals = options.locals || {};
1043 var transformTemplate = options.transformTemplate || angular.identity;
1044 var bindToController = options.bindToController;
1046 // Take resolve values and invoke them.
1047 // Resolves can either be a string (value: 'MyRegisteredAngularConst'),
1048 // or an invokable 'factory' of sorts: (value: function ValueGetter($dependency) {})
1049 angular.forEach(resolve, function(value, key) {
1050 if (angular.isString(value)) {
1051 resolve[key] = $injector.get(value);
1053 resolve[key] = $injector.invoke(value);
1056 //Add the locals, which are just straight values to inject
1057 //eg locals: { three: 3 }, will inject three into the controller
1058 angular.extend(resolve, locals);
1061 resolve.$template = $http.get(templateUrl, {cache: $templateCache})
1062 .then(function(response) {
1063 return response.data;
1066 resolve.$template = $q.when(template);
1069 // Wait for all the resolves to finish if they are promises
1070 return $q.all(resolve).then(function(locals) {
1072 var template = transformTemplate(locals.$template);
1073 var element = options.element || angular.element('<div>').html(template.trim()).contents();
1074 var linkFn = $compile(element);
1076 //Return a linking function that can be used later when the element is ready
1080 link: function link(scope) {
1081 locals.$scope = scope;
1083 //Instantiate controller if it exists, because we have scope
1085 var invokeCtrl = $controller(controller, locals, true);
1086 if (bindToController) {
1087 angular.extend(invokeCtrl.instance, locals);
1089 var ctrl = invokeCtrl();
1090 //See angular-route source for this logic
1091 element.data('$ngControllerController', ctrl);
1092 element.children().data('$ngControllerController', ctrl);
1095 scope[controllerAs] = ctrl;
1098 return linkFn(scope);
1105 mdCompilerService.$inject = ["$q", "$http", "$injector", "$compile", "$controller", "$templateCache"];
1112 /* The state of the current 'pointer'
1113 * The pointer represents the state of the current touch.
1114 * It contains normalized x and y coordinates from DOM events,
1115 * as well as other information abstracted from the DOM.
1117 var pointer, lastPointer, forceSkipClickHijack = false;
1119 // Used to attach event listeners once when multiple ng-apps are running.
1120 var isInitialized = false;
1123 .module('material.core.gestures', [ ])
1124 .provider('$mdGesture', MdGestureProvider)
1125 .factory('$$MdGestureHandler', MdGestureHandler)
1126 .run( attachToDocument );
1130 * @name $mdGestureProvider
1131 * @module material.core.gestures
1134 * In some scenarios on Mobile devices (without jQuery), the click events should NOT be hijacked.
1135 * `$mdGestureProvider` is used to configure the Gesture module to ignore or skip click hijacking on mobile
1139 * app.config(function($mdGestureProvider) {
1141 * // For mobile devices without jQuery loaded, do not
1142 * // intercept click events during the capture phase.
1143 * $mdGestureProvider.skipClickHijack();
1149 function MdGestureProvider() { }
1151 MdGestureProvider.prototype = {
1153 // Publish access to setter to configure a variable BEFORE the
1154 // $mdGesture service is instantiated...
1155 skipClickHijack: function() {
1156 return forceSkipClickHijack = true;
1160 * $get is used to build an instance of $mdGesture
1163 $get : ["$$MdGestureHandler", "$$rAF", "$timeout", function($$MdGestureHandler, $$rAF, $timeout) {
1164 return new MdGesture($$MdGestureHandler, $$rAF, $timeout);
1171 * MdGesture factory construction function
1174 function MdGesture($$MdGestureHandler, $$rAF, $timeout) {
1175 var userAgent = navigator.userAgent || navigator.vendor || window.opera;
1176 var isIos = userAgent.match(/ipad|iphone|ipod/i);
1177 var isAndroid = userAgent.match(/android/i);
1178 var hasJQuery = (typeof window.jQuery !== 'undefined') && (angular.element === window.jQuery);
1181 handler: addHandler,
1183 // On mobile w/out jQuery, we normally intercept clicks. Should we skip that?
1184 isHijackingClicks: (isIos || isAndroid) && !hasJQuery && !forceSkipClickHijack
1187 if (self.isHijackingClicks) {
1188 self.handler('click', {
1192 onEnd: function (ev, pointer) {
1193 if (pointer.distance < this.state.options.maxDistance) {
1194 this.dispatchEvent(ev, 'click');
1201 * Register an element to listen for a handler.
1202 * This allows an element to override the default options for a handler.
1203 * Additionally, some handlers like drag and hold only dispatch events if
1204 * the domEvent happens inside an element that's registered to listen for these events.
1206 * @see GestureHandler for how overriding of default options works.
1207 * @example $mdGesture.register(myElement, 'drag', { minDistance: 20, horziontal: false })
1209 function register(element, handlerName, options) {
1210 var handler = HANDLERS[handlerName.replace(/^\$md./, '')];
1212 throw new Error('Failed to register element with handler ' + handlerName + '. ' +
1213 'Available handlers: ' + Object.keys(HANDLERS).join(', '));
1215 return handler.registerElement(element, options);
1219 * add a handler to $mdGesture. see below.
1221 function addHandler(name, definition) {
1222 var handler = new $$MdGestureHandler(name);
1223 angular.extend(handler, definition);
1224 HANDLERS[name] = handler;
1230 * Register handlers. These listen to touch/start/move events, interpret them,
1231 * and dispatch gesture events depending on options & conditions. These are all
1232 * instances of GestureHandler.
1233 * @see GestureHandler
1237 * The press handler dispatches an event on touchdown/touchend.
1238 * It's a simple abstraction of touch/mouse/pointer start and end.
1241 onStart: function (ev, pointer) {
1242 this.dispatchEvent(ev, '$md.pressdown');
1244 onEnd: function (ev, pointer) {
1245 this.dispatchEvent(ev, '$md.pressup');
1250 * The hold handler dispatches an event if the user keeps their finger within
1251 * the same <maxDistance> area for <delay> ms.
1252 * The hold handler will only run if a parent of the touch target is registered
1253 * to listen for hold events through $mdGesture.register()
1260 onCancel: function () {
1261 $timeout.cancel(this.state.timeout);
1263 onStart: function (ev, pointer) {
1264 // For hold, require a parent to be registered with $mdGesture.register()
1265 // Because we prevent scroll events, this is necessary.
1266 if (!this.state.registeredParent) return this.cancel();
1268 this.state.pos = {x: pointer.x, y: pointer.y};
1269 this.state.timeout = $timeout(angular.bind(this, function holdDelayFn() {
1270 this.dispatchEvent(ev, '$md.hold');
1271 this.cancel(); //we're done!
1272 }), this.state.options.delay, false);
1274 onMove: function (ev, pointer) {
1275 // Don't scroll while waiting for hold.
1276 // If we don't preventDefault touchmove events here, Android will assume we don't
1277 // want to listen to anymore touch events. It will start scrolling and stop sending
1278 // touchmove events.
1279 ev.preventDefault();
1281 // If the user moves greater than <maxDistance> pixels, stop the hold timer
1283 var dx = this.state.pos.x - pointer.x;
1284 var dy = this.state.pos.y - pointer.y;
1285 if (Math.sqrt(dx * dx + dy * dy) > this.options.maxDistance) {
1289 onEnd: function () {
1295 * The drag handler dispatches a drag event if the user holds and moves his finger greater than
1296 * <minDistance> px in the x or y direction, depending on options.horizontal.
1297 * The drag will be cancelled if the user moves his finger greater than <minDistance>*<cancelMultiplier> in
1298 * the perpindicular direction. Eg if the drag is horizontal and the user moves his finger <minDistance>*<cancelMultiplier>
1299 * pixels vertically, this handler won't consider the move part of a drag.
1305 cancelMultiplier: 1.5
1307 onStart: function (ev) {
1308 // For drag, require a parent to be registered with $mdGesture.register()
1309 if (!this.state.registeredParent) this.cancel();
1311 onMove: function (ev, pointer) {
1312 var shouldStartDrag, shouldCancel;
1313 // Don't scroll while deciding if this touchmove qualifies as a drag event.
1314 // If we don't preventDefault touchmove events here, Android will assume we don't
1315 // want to listen to anymore touch events. It will start scrolling and stop sending
1316 // touchmove events.
1317 ev.preventDefault();
1319 if (!this.state.dragPointer) {
1320 if (this.state.options.horizontal) {
1321 shouldStartDrag = Math.abs(pointer.distanceX) > this.state.options.minDistance;
1322 shouldCancel = Math.abs(pointer.distanceY) > this.state.options.minDistance * this.state.options.cancelMultiplier;
1324 shouldStartDrag = Math.abs(pointer.distanceY) > this.state.options.minDistance;
1325 shouldCancel = Math.abs(pointer.distanceX) > this.state.options.minDistance * this.state.options.cancelMultiplier;
1328 if (shouldStartDrag) {
1329 // Create a new pointer representing this drag, starting at this point where the drag started.
1330 this.state.dragPointer = makeStartPointer(ev);
1331 updatePointerState(ev, this.state.dragPointer);
1332 this.dispatchEvent(ev, '$md.dragstart', this.state.dragPointer);
1334 } else if (shouldCancel) {
1338 this.dispatchDragMove(ev);
1341 // Only dispatch dragmove events every frame; any more is unnecessray
1342 dispatchDragMove: $$rAF.throttle(function (ev) {
1343 // Make sure the drag didn't stop while waiting for the next frame
1344 if (this.state.isRunning) {
1345 updatePointerState(ev, this.state.dragPointer);
1346 this.dispatchEvent(ev, '$md.drag', this.state.dragPointer);
1349 onEnd: function (ev, pointer) {
1350 if (this.state.dragPointer) {
1351 updatePointerState(ev, this.state.dragPointer);
1352 this.dispatchEvent(ev, '$md.dragend', this.state.dragPointer);
1358 * The swipe handler will dispatch a swipe event if, on the end of a touch,
1359 * the velocity and distance were high enough.
1360 * TODO: add vertical swiping with a `horizontal` option similar to the drag handler.
1367 onEnd: function (ev, pointer) {
1368 if (Math.abs(pointer.velocityX) > this.state.options.minVelocity &&
1369 Math.abs(pointer.distanceX) > this.state.options.minDistance) {
1370 var eventType = pointer.directionX == 'left' ? '$md.swipeleft' : '$md.swiperight';
1371 this.dispatchEvent(ev, eventType);
1377 MdGesture.$inject = ["$$MdGestureHandler", "$$rAF", "$timeout"];
1381 * A GestureHandler is an object which is able to dispatch custom dom events
1382 * based on native dom {touch,pointer,mouse}{start,move,end} events.
1384 * A gesture will manage its lifecycle through the start,move,end, and cancel
1385 * functions, which are called by native dom events.
1387 * A gesture has the concept of 'options' (eg a swipe's required velocity), which can be
1388 * overridden by elements registering through $mdGesture.register()
1390 function GestureHandler (name) {
1395 function MdGestureHandler() {
1396 var hasJQuery = (typeof window.jQuery !== 'undefined') && (angular.element === window.jQuery);
1398 GestureHandler.prototype = {
1400 // jQuery listeners don't work with custom DOMEvents, so we have to dispatch events
1401 // differently when jQuery is loaded
1402 dispatchEvent: hasJQuery ? jQueryDispatchEvent : nativeDispatchEvent,
1404 // These are overridden by the registered handler
1405 onStart: angular.noop,
1406 onMove: angular.noop,
1407 onEnd: angular.noop,
1408 onCancel: angular.noop,
1410 // onStart sets up a new state for the handler, which includes options from the
1411 // nearest registered parent element of ev.target.
1412 start: function (ev, pointer) {
1413 if (this.state.isRunning) return;
1414 var parentTarget = this.getNearestParent(ev.target);
1415 // Get the options from the nearest registered parent
1416 var parentTargetOptions = parentTarget && parentTarget.$mdGesture[this.name] || {};
1420 // Override the default options with the nearest registered parent's options
1421 options: angular.extend({}, this.options, parentTargetOptions),
1422 // Pass in the registered parent node to the state so the onStart listener can use
1423 registeredParent: parentTarget
1425 this.onStart(ev, pointer);
1427 move: function (ev, pointer) {
1428 if (!this.state.isRunning) return;
1429 this.onMove(ev, pointer);
1431 end: function (ev, pointer) {
1432 if (!this.state.isRunning) return;
1433 this.onEnd(ev, pointer);
1434 this.state.isRunning = false;
1436 cancel: function (ev, pointer) {
1437 this.onCancel(ev, pointer);
1441 // Find and return the nearest parent element that has been registered to
1442 // listen for this handler via $mdGesture.register(element, 'handlerName').
1443 getNearestParent: function (node) {
1446 if ((current.$mdGesture || {})[this.name]) {
1449 current = current.parentNode;
1454 // Called from $mdGesture.register when an element reigsters itself with a handler.
1455 // Store the options the user gave on the DOMElement itself. These options will
1456 // be retrieved with getNearestParent when the handler starts.
1457 registerElement: function (element, options) {
1459 element[0].$mdGesture = element[0].$mdGesture || {};
1460 element[0].$mdGesture[this.name] = options || {};
1461 element.on('$destroy', onDestroy);
1465 function onDestroy() {
1466 delete element[0].$mdGesture[self.name];
1467 element.off('$destroy', onDestroy);
1472 return GestureHandler;
1475 * Dispatch an event with jQuery
1476 * TODO: Make sure this sends bubbling events
1478 * @param srcEvent the original DOM touch event that started this.
1479 * @param eventType the name of the custom event to send (eg 'click' or '$md.drag')
1480 * @param eventPointer the pointer object that matches this event.
1482 function jQueryDispatchEvent(srcEvent, eventType, eventPointer) {
1483 eventPointer = eventPointer || pointer;
1484 var eventObj = new angular.element.Event(eventType);
1486 eventObj.$material = true;
1487 eventObj.pointer = eventPointer;
1488 eventObj.srcEvent = srcEvent;
1490 angular.extend(eventObj, {
1491 clientX: eventPointer.x,
1492 clientY: eventPointer.y,
1493 screenX: eventPointer.x,
1494 screenY: eventPointer.y,
1495 pageX: eventPointer.x,
1496 pageY: eventPointer.y,
1497 ctrlKey: srcEvent.ctrlKey,
1498 altKey: srcEvent.altKey,
1499 shiftKey: srcEvent.shiftKey,
1500 metaKey: srcEvent.metaKey
1502 angular.element(eventPointer.target).trigger(eventObj);
1506 * NOTE: nativeDispatchEvent is very performance sensitive.
1507 * @param srcEvent the original DOM touch event that started this.
1508 * @param eventType the name of the custom event to send (eg 'click' or '$md.drag')
1509 * @param eventPointer the pointer object that matches this event.
1511 function nativeDispatchEvent(srcEvent, eventType, eventPointer) {
1512 eventPointer = eventPointer || pointer;
1515 if (eventType === 'click') {
1516 eventObj = document.createEvent('MouseEvents');
1517 eventObj.initMouseEvent(
1518 'click', true, true, window, srcEvent.detail,
1519 eventPointer.x, eventPointer.y, eventPointer.x, eventPointer.y,
1520 srcEvent.ctrlKey, srcEvent.altKey, srcEvent.shiftKey, srcEvent.metaKey,
1521 srcEvent.button, srcEvent.relatedTarget || null
1525 eventObj = document.createEvent('CustomEvent');
1526 eventObj.initCustomEvent(eventType, true, true, {});
1528 eventObj.$material = true;
1529 eventObj.pointer = eventPointer;
1530 eventObj.srcEvent = srcEvent;
1531 eventPointer.target.dispatchEvent(eventObj);
1537 * Attach Gestures: hook document and check shouldHijack clicks
1540 function attachToDocument( $mdGesture, $$MdGestureHandler ) {
1542 // Polyfill document.contains for IE11.
1543 // TODO: move to util
1544 document.contains || (document.contains = function (node) {
1545 return document.body.contains(node);
1548 if (!isInitialized && $mdGesture.isHijackingClicks ) {
1550 * If hijack clicks is true, we preventDefault any click that wasn't
1551 * sent by ngMaterial. This is because on older Android & iOS, a false, or 'ghost',
1552 * click event will be sent ~400ms after a touchend event happens.
1553 * The only way to know if this click is real is to prevent any normal
1554 * click events, and add a flag to events sent by material so we know not to prevent those.
1556 * Two exceptions to click events that should be prevented are:
1557 * - click events sent by the keyboard (eg form submit)
1558 * - events that originate from an Ionic app
1560 document.addEventListener('click', function clickHijacker(ev) {
1561 var isKeyClick = ev.clientX === 0 && ev.clientY === 0;
1562 if (!isKeyClick && !ev.$material && !ev.isIonicTap) {
1563 ev.preventDefault();
1564 ev.stopPropagation();
1568 isInitialized = true;
1571 // Listen to all events to cover all platforms.
1572 var START_EVENTS = 'mousedown touchstart pointerdown';
1573 var MOVE_EVENTS = 'mousemove touchmove pointermove';
1574 var END_EVENTS = 'mouseup mouseleave touchend touchcancel pointerup pointercancel';
1576 angular.element(document)
1577 .on(START_EVENTS, gestureStart)
1578 .on(MOVE_EVENTS, gestureMove)
1579 .on(END_EVENTS, gestureEnd)
1581 .on('$$mdGestureReset', function gestureClearCache () {
1582 lastPointer = pointer = null;
1586 * When a DOM event happens, run all registered gesture handlers' lifecycle
1587 * methods which match the DOM event.
1588 * Eg when a 'touchstart' event happens, runHandlers('start') will call and
1589 * run `handler.cancel()` and `handler.start()` on all registered handlers.
1591 function runHandlers(handlerEvent, event) {
1593 for (var name in HANDLERS) {
1594 handler = HANDLERS[name];
1595 if( handler instanceof $$MdGestureHandler ) {
1597 if (handlerEvent === 'start') {
1598 // Run cancel to reset any handlers' state
1601 handler[handlerEvent](event, pointer);
1608 * gestureStart vets if a start event is legitimate (and not part of a 'ghost click' from iOS/Android)
1609 * If it is legitimate, we initiate the pointer state and mark the current pointer's type
1610 * For example, for a touchstart event, mark the current pointer as a 'touch' pointer, so mouse events
1613 function gestureStart(ev) {
1614 // If we're already touched down, abort
1615 if (pointer) return;
1617 var now = +Date.now();
1619 // iOS & old android bug: after a touch event, a click event is sent 350 ms later.
1620 // If <400ms have passed, don't allow an event of a different type than the previous event
1621 if (lastPointer && !typesMatch(ev, lastPointer) && (now - lastPointer.endTime < 1500)) {
1625 pointer = makeStartPointer(ev);
1627 runHandlers('start', ev);
1630 * If a move event happens of the right type, update the pointer and run all the move handlers.
1631 * "of the right type": if a mousemove happens but our pointer started with a touch event, do nothing.
1633 function gestureMove(ev) {
1634 if (!pointer || !typesMatch(ev, pointer)) return;
1636 updatePointerState(ev, pointer);
1637 runHandlers('move', ev);
1640 * If an end event happens of the right type, update the pointer, run endHandlers, and save the pointer as 'lastPointer'
1642 function gestureEnd(ev) {
1643 if (!pointer || !typesMatch(ev, pointer)) return;
1645 updatePointerState(ev, pointer);
1646 pointer.endTime = +Date.now();
1648 runHandlers('end', ev);
1650 lastPointer = pointer;
1655 attachToDocument.$inject = ["$mdGesture", "$$MdGestureHandler"];
1657 // ********************
1659 // ********************
1662 * Initiate the pointer. x, y, and the pointer's type.
1664 function makeStartPointer(ev) {
1665 var point = getEventPoint(ev);
1666 var startPointer = {
1667 startTime: +Date.now(),
1669 // 'p' for pointer events, 'm' for mouse, 't' for touch
1670 type: ev.type.charAt(0)
1672 startPointer.startX = startPointer.x = point.pageX;
1673 startPointer.startY = startPointer.y = point.pageY;
1674 return startPointer;
1678 * return whether the pointer's type matches the event's type.
1679 * Eg if a touch event happens but the pointer has a mouse type, return false.
1681 function typesMatch(ev, pointer) {
1682 return ev && pointer && ev.type.charAt(0) === pointer.type;
1686 * Update the given pointer based upon the given DOMEvent.
1687 * Distance, velocity, direction, duration, etc
1689 function updatePointerState(ev, pointer) {
1690 var point = getEventPoint(ev);
1691 var x = pointer.x = point.pageX;
1692 var y = pointer.y = point.pageY;
1694 pointer.distanceX = x - pointer.startX;
1695 pointer.distanceY = y - pointer.startY;
1696 pointer.distance = Math.sqrt(
1697 pointer.distanceX * pointer.distanceX + pointer.distanceY * pointer.distanceY
1700 pointer.directionX = pointer.distanceX > 0 ? 'right' : pointer.distanceX < 0 ? 'left' : '';
1701 pointer.directionY = pointer.distanceY > 0 ? 'up' : pointer.distanceY < 0 ? 'down' : '';
1703 pointer.duration = +Date.now() - pointer.startTime;
1704 pointer.velocityX = pointer.distanceX / pointer.duration;
1705 pointer.velocityY = pointer.distanceY / pointer.duration;
1709 * Normalize the point where the DOM event happened whether it's touch or mouse.
1710 * @returns point event obj with pageX and pageY on it.
1712 function getEventPoint(ev) {
1713 ev = ev.originalEvent || ev; // support jQuery events
1714 return (ev.touches && ev.touches[0]) ||
1715 (ev.changedTouches && ev.changedTouches[0]) ||
1723 angular.module('material.core')
1724 .provider('$$interimElement', InterimElementProvider);
1728 * @name $$interimElement
1729 * @module material.core
1733 * Factory that contructs `$$interimElement.$service` services.
1734 * Used internally in material design for elements that appear on screen temporarily.
1735 * The service provides a promise-like API for interacting with the temporary
1739 * app.service('$mdToast', function($$interimElement) {
1740 * var $mdToast = $$interimElement(toastDefaultOptions);
1744 * @param {object=} defaultOptions Options used by default for the `show` method on the service.
1746 * @returns {$$interimElement.$service}
1750 function InterimElementProvider() {
1751 createInterimElementProvider.$get = InterimElementFactory;
1752 InterimElementFactory.$inject = ["$document", "$q", "$rootScope", "$timeout", "$rootElement", "$animate", "$interpolate", "$mdCompiler", "$mdTheming"];
1753 return createInterimElementProvider;
1756 * Returns a new provider which allows configuration of a new interimElement
1757 * service. Allows configuration of default options & methods for options,
1758 * as well as configuration of 'preset' methods (eg dialog.basic(): basic is a preset method)
1760 function createInterimElementProvider(interimFactoryName) {
1761 var EXPOSED_METHODS = ['onHide', 'onShow', 'onRemove'];
1763 var customMethods = {};
1764 var providerConfig = {
1769 setDefaults: setDefaults,
1770 addPreset: addPreset,
1771 addMethod: addMethod,
1776 * all interim elements will come with the 'build' preset
1778 provider.addPreset('build', {
1779 methods: ['controller', 'controllerAs', 'resolve',
1780 'template', 'templateUrl', 'themable', 'transformTemplate', 'parent']
1783 factory.$inject = ["$$interimElement", "$animate", "$injector"];
1787 * Save the configured defaults to be used when the factory is instantiated
1789 function setDefaults(definition) {
1790 providerConfig.optionsFactory = definition.options;
1791 providerConfig.methods = (definition.methods || []).concat(EXPOSED_METHODS);
1796 * Add a method to the factory that isn't specific to any interim element operations
1799 function addMethod(name, fn) {
1800 customMethods[name] = fn;
1805 * Save the configured preset to be used when the factory is instantiated
1807 function addPreset(name, definition) {
1808 definition = definition || {};
1809 definition.methods = definition.methods || [];
1810 definition.options = definition.options || function() { return {}; };
1812 if (/^cancel|hide|show$/.test(name)) {
1813 throw new Error("Preset '" + name + "' in " + interimFactoryName + " is reserved!");
1815 if (definition.methods.indexOf('_options') > -1) {
1816 throw new Error("Method '_options' in " + interimFactoryName + " is reserved!");
1818 providerConfig.presets[name] = {
1819 methods: definition.methods.concat(EXPOSED_METHODS),
1820 optionsFactory: definition.options,
1821 argOption: definition.argOption
1827 * Create a factory that has the given methods & defaults implementing interimElement
1830 function factory($$interimElement, $animate, $injector) {
1833 var interimElementService = $$interimElement();
1836 * publicService is what the developer will be using.
1837 * It has methods hide(), cancel(), show(), build(), and any other
1838 * presets which were set during the config phase.
1840 var publicService = {
1841 hide: interimElementService.hide,
1842 cancel: interimElementService.cancel,
1843 show: showInterimElement
1846 defaultMethods = providerConfig.methods || [];
1847 // This must be invoked after the publicService is initialized
1848 defaultOptions = invokeFactory(providerConfig.optionsFactory, {});
1850 // Copy over the simple custom methods
1851 angular.forEach(customMethods, function(fn, name) {
1852 publicService[name] = fn;
1855 angular.forEach(providerConfig.presets, function(definition, name) {
1856 var presetDefaults = invokeFactory(definition.optionsFactory, {});
1857 var presetMethods = (definition.methods || []).concat(defaultMethods);
1859 // Every interimElement built with a preset has a field called `$type`,
1860 // which matches the name of the preset.
1861 // Eg in preset 'confirm', options.$type === 'confirm'
1862 angular.extend(presetDefaults, { $type: name });
1864 // This creates a preset class which has setter methods for every
1865 // method given in the `.addPreset()` function, as well as every
1866 // method given in the `.setDefaults()` function.
1870 // methods: ['hasBackdrop', 'clickOutsideToClose', 'escapeToClose', 'targetEvent'],
1871 // options: dialogDefaultOptions
1873 // .addPreset('alert', {
1874 // methods: ['title', 'ok'],
1875 // options: alertDialogOptions
1878 // Set values will be passed to the options when interimElemnt.show() is called.
1879 function Preset(opts) {
1880 this._options = angular.extend({}, presetDefaults, opts);
1882 angular.forEach(presetMethods, function(name) {
1883 Preset.prototype[name] = function(value) {
1884 this._options[name] = value;
1889 // Create shortcut method for one-linear methods
1890 if (definition.argOption) {
1891 var methodName = 'show' + name.charAt(0).toUpperCase() + name.slice(1);
1892 publicService[methodName] = function(arg) {
1893 var config = publicService[name](arg);
1894 return publicService.show(config);
1898 // eg $mdDialog.alert() will return a new alert preset
1899 publicService[name] = function(arg) {
1900 // If argOption is supplied, eg `argOption: 'content'`, then we assume
1901 // if the argument is not an options object then it is the `argOption` option.
1903 // @example `$mdToast.simple('hello')` // sets options.content to hello
1904 // // because argOption === 'content'
1905 if (arguments.length && definition.argOption && !angular.isObject(arg) &&
1906 !angular.isArray(arg)) {
1907 return (new Preset())[definition.argOption](arg);
1909 return new Preset(arg);
1915 return publicService;
1917 function showInterimElement(opts) {
1918 // opts is either a preset which stores its options on an _options field,
1919 // or just an object made up of options
1920 if (opts && opts._options) opts = opts._options;
1921 return interimElementService.show(
1922 angular.extend({}, defaultOptions, opts)
1927 * Helper to call $injector.invoke with a local of the factory name for
1929 * If an $mdDialog is providing options for a dialog and tries to inject
1930 * $mdDialog, a circular dependency error will happen.
1931 * We get around that by manually injecting $mdDialog as a local.
1933 function invokeFactory(factory, defaultVal) {
1935 locals[interimFactoryName] = publicService;
1936 return $injector.invoke(factory || function() { return defaultVal; }, {}, locals);
1944 function InterimElementFactory($document, $q, $rootScope, $timeout, $rootElement, $animate,
1945 $interpolate, $mdCompiler, $mdTheming ) {
1946 var startSymbol = $interpolate.startSymbol(),
1947 endSymbol = $interpolate.endSymbol(),
1948 usesStandardSymbols = ((startSymbol === '{{') && (endSymbol === '}}')),
1949 processTemplate = usesStandardSymbols ? angular.identity : replaceInterpolationSymbols;
1951 return function createInterimElementService() {
1954 * @name $$interimElement.$service
1957 * A service used to control inserting and removing an element into the DOM.
1970 * @name $$interimElement.$service#show
1974 * Adds the `$interimElement` to the DOM and returns a promise that will be resolved or rejected
1975 * with hide or cancel, respectively.
1977 * @param {*} options is hashMap of settings
1978 * @returns a Promise
1981 function show(options) {
1983 return service.cancel().then(function() {
1984 return show(options);
1987 var interimElement = new InterimElement(options);
1988 stack.push(interimElement);
1989 return interimElement.show().then(function() {
1990 return interimElement.deferred.promise;
1997 * @name $$interimElement.$service#hide
2001 * Removes the `$interimElement` from the DOM and resolves the promise returned from `show`
2003 * @param {*} resolveParam Data to resolve the promise with
2004 * @returns a Promise that will be resolved after the element has been removed.
2007 function hide(response) {
2008 var interimElement = stack.shift();
2009 return interimElement && interimElement.remove().then(function() {
2010 interimElement.deferred.resolve(response);
2016 * @name $$interimElement.$service#cancel
2020 * Removes the `$interimElement` from the DOM and rejects the promise returned from `show`
2022 * @param {*} reason Data to reject the promise with
2023 * @returns Promise that will be resolved after the element has been removed.
2026 function cancel(reason) {
2027 var interimElement = stack.shift();
2028 return $q.when(interimElement && interimElement.remove().then(function() {
2029 interimElement.deferred.reject(reason);
2035 * Internal Interim Element Object
2036 * Used internally to manage the DOM element and related data
2038 function InterimElement(options) {
2040 var hideTimeout, element, showDone, removeDone;
2042 options = options || {};
2043 options = angular.extend({
2044 preserveScope: false,
2045 scope: options.scope || $rootScope.$new(options.isolateScope),
2046 onShow: function(scope, element, options) {
2047 return $animate.enter(element, options.parent);
2049 onRemove: function(scope, element, options) {
2050 // Element could be undefined if a new element is shown before
2051 // the old one finishes compiling.
2052 return element && $animate.leave(element) || $q.when();
2056 if (options.template) {
2057 options.template = processTemplate(options.template);
2062 deferred: $q.defer(),
2065 if (options.skipCompile) {
2066 compilePromise = $q(function(resolve) {
2069 link: function() { return options.element; }
2073 compilePromise = $mdCompiler.compile(options);
2076 return showDone = compilePromise.then(function(compileData) {
2077 angular.extend(compileData.locals, self.options);
2079 element = compileData.link(options.scope);
2081 // Search for parent at insertion time, if not specified
2082 if (angular.isFunction(options.parent)) {
2083 options.parent = options.parent(options.scope, element, options);
2084 } else if (angular.isString(options.parent)) {
2085 options.parent = angular.element($document[0].querySelector(options.parent));
2088 // If parent querySelector/getter function fails, or it's just null,
2090 if (!(options.parent || {}).length) {
2092 if ($rootElement[0] && $rootElement[0].querySelector) {
2093 el = $rootElement[0].querySelector(':not(svg) > body');
2095 if (!el) el = $rootElement[0];
2096 if (el.nodeName == '#comment') {
2097 el = $document[0].body;
2099 options.parent = angular.element(el);
2102 if (options.themable) $mdTheming(element);
2103 var ret = options.onShow(options.scope, element, options);
2106 // Issue onComplete callback when the `show()` finishes
2107 (options.onComplete || angular.noop)(options.scope, element, options);
2111 function startHideTimeout() {
2112 if (options.hideDelay) {
2113 hideTimeout = $timeout(service.cancel, options.hideDelay) ;
2116 }, function(reason) { showDone = true; self.deferred.reject(reason); });
2118 cancelTimeout: function() {
2120 $timeout.cancel(hideTimeout);
2121 hideTimeout = undefined;
2124 remove: function() {
2125 self.cancelTimeout();
2126 return removeDone = $q.when(showDone).then(function() {
2127 var ret = element ? options.onRemove(options.scope, element, options) : true;
2128 return $q.when(ret).then(function() {
2129 if (!options.preserveScope) options.scope.$destroy();
2139 * Replace `{{` and `}}` in a string (usually a template) with the actual start-/endSymbols used
2140 * for interpolation. This allows pre-defined templates (for components such as dialog, toast etc)
2141 * to continue to work in apps that use custom interpolation start-/endSymbols.
2143 * @param {string} text The text in which to replace `{{` / `}}`
2144 * @returns {string} The modified string using the actual interpolation start-/endSymbols
2146 function replaceInterpolationSymbols(text) {
2147 if (!text || !angular.isString(text)) return text;
2148 return text.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol);
2160 * @name material.core.componentRegistry
2163 * A component instance registration service.
2164 * Note: currently this as a private service in the SideNav component.
2166 angular.module('material.core')
2167 .factory('$mdComponentRegistry', ComponentRegistry);
2172 * @name ComponentRegistry
2173 * @module material.core.componentRegistry
2176 function ComponentRegistry($log, $q) {
2179 var instances = [ ];
2184 * Used to print an error when an instance for a handle isn't found.
2186 notFoundError: function(handle) {
2187 $log.error('No instance found for handle', handle);
2190 * Return all registered instances as an array.
2192 getInstances: function() {
2197 * Get a registered instance.
2198 * @param handle the String handle to look up for a registered instance.
2200 get: function(handle) {
2201 if ( !isValidID(handle) ) return null;
2204 for(i = 0, j = instances.length; i < j; i++) {
2205 instance = instances[i];
2206 if(instance.$$mdHandle === handle) {
2214 * Register an instance.
2215 * @param instance the instance to register
2216 * @param handle the handle to identify the instance under.
2218 register: function(instance, handle) {
2219 if ( !handle ) return angular.noop;
2221 instance.$$mdHandle = handle;
2222 instances.push(instance);
2228 * Remove registration for an instance
2230 function deregister() {
2231 var index = instances.indexOf(instance);
2233 instances.splice(index, 1);
2238 * Resolve any pending promises for this instance
2240 function resolveWhen() {
2241 var dfd = pendings[handle];
2243 dfd.resolve( instance );
2244 delete pendings[handle];
2250 * Async accessor to registered component instance
2251 * If not available then a promise is created to notify
2252 * all listeners when the instance is registered.
2254 when : function(handle) {
2255 if ( isValidID(handle) ) {
2256 var deferred = $q.defer();
2257 var instance = self.get(handle);
2260 deferred.resolve( instance );
2262 pendings[handle] = deferred;
2265 return deferred.promise;
2267 return $q.reject("Invalid `md-component-id` value.");
2272 function isValidID(handle){
2273 return handle && (handle !== "");
2277 ComponentRegistry.$inject = ["$log", "$q"];
2288 * @name $mdButtonInkRipple
2289 * @module material.core
2292 * Provides ripple effects for md-button. See $mdInkRipple service for all possible configuration options.
2294 * @param {object=} scope Scope within the current context
2295 * @param {object=} element The element the ripple effect should be applied to
2296 * @param {object=} options (Optional) Configuration options to override the defaultripple configuration
2299 angular.module('material.core')
2300 .factory('$mdButtonInkRipple', MdButtonInkRipple);
2302 function MdButtonInkRipple($mdInkRipple) {
2307 function attach(scope, element, options) {
2308 var elementOptions = optionsForElement(element);
2309 return $mdInkRipple.attach(scope, element, angular.extend(elementOptions, options));
2312 function optionsForElement(element) {
2313 if (element.hasClass('md-icon-button')) {
2315 isMenuItem: element.hasClass('md-menu-item'),
2321 isMenuItem: element.hasClass('md-menu-item'),
2327 MdButtonInkRipple.$inject = ["$mdInkRipple"];;
2339 * @name $mdCheckboxInkRipple
2340 * @module material.core
2343 * Provides ripple effects for md-checkbox. See $mdInkRipple service for all possible configuration options.
2345 * @param {object=} scope Scope within the current context
2346 * @param {object=} element The element the ripple effect should be applied to
2347 * @param {object=} options (Optional) Configuration options to override the defaultripple configuration
2350 angular.module('material.core')
2351 .factory('$mdCheckboxInkRipple', MdCheckboxInkRipple);
2353 function MdCheckboxInkRipple($mdInkRipple) {
2358 function attach(scope, element, options) {
2359 return $mdInkRipple.attach(scope, element, angular.extend({
2361 dimBackground: false,
2366 MdCheckboxInkRipple.$inject = ["$mdInkRipple"];;
2378 * @name $mdListInkRipple
2379 * @module material.core
2382 * Provides ripple effects for md-list. See $mdInkRipple service for all possible configuration options.
2384 * @param {object=} scope Scope within the current context
2385 * @param {object=} element The element the ripple effect should be applied to
2386 * @param {object=} options (Optional) Configuration options to override the defaultripple configuration
2389 angular.module('material.core')
2390 .factory('$mdListInkRipple', MdListInkRipple);
2392 function MdListInkRipple($mdInkRipple) {
2397 function attach(scope, element, options) {
2398 return $mdInkRipple.attach(scope, element, angular.extend({
2400 dimBackground: true,
2406 MdListInkRipple.$inject = ["$mdInkRipple"];;
2413 angular.module('material.core')
2414 .factory('$mdInkRipple', InkRippleService)
2415 .directive('mdInkRipple', InkRippleDirective)
2416 .directive('mdNoInk', attrNoDirective())
2417 .directive('mdNoBar', attrNoDirective())
2418 .directive('mdNoStretch', attrNoDirective());
2420 function InkRippleDirective($mdButtonInkRipple, $mdCheckboxInkRipple) {
2422 controller: angular.noop,
2423 link: function (scope, element, attr) {
2424 if (attr.hasOwnProperty('mdInkRippleCheckbox')) {
2425 $mdCheckboxInkRipple.attach(scope, element);
2427 $mdButtonInkRipple.attach(scope, element);
2432 InkRippleDirective.$inject = ["$mdButtonInkRipple", "$mdCheckboxInkRipple"];
2434 function InkRippleService($window, $timeout) {
2440 function attach(scope, element, options) {
2441 if (element.controller('mdNoInk')) return angular.noop;
2443 options = angular.extend({
2444 colorElement: element,
2449 mousedownPauseTime: 150,
2450 dimBackground: false,
2458 controller = element.controller('mdInkRipple') || {},
2462 isActiveExpr = element.attr('md-highlight'),
2466 rippleSizeSetting = element.attr('md-ripple-size'),
2467 color = parseColor(element.attr('md-ink-ripple')) || parseColor(options.colorElement.length && $window.getComputedStyle(options.colorElement[0]).color || 'rgb(0, 0, 0)');
2469 switch (rippleSizeSetting) {
2471 options.fullRipple = true;
2474 options.fullRipple = false;
2478 // expose onInput for ripple testing
2479 if (options.mousedown) {
2480 element.on('$md.pressdown', onPressDown)
2481 .on('$md.pressup', onPressUp);
2484 controller.createRipple = createRipple;
2487 scope.$watch(isActiveExpr, function watchActive(newValue) {
2488 isActive = newValue;
2489 if (isActive && !ripples.length) {
2490 $timeout(function () { createRipple(0, 0); }, 0, false);
2492 angular.forEach(ripples, updateElement);
2496 // Publish self-detach method if desired...
2497 return function detach() {
2498 element.off('$md.pressdown', onPressDown)
2499 .off('$md.pressup', onPressUp);
2500 getRippleContainer().remove();
2504 * Gets the current ripple container
2505 * If there is no ripple container, it creates one and returns it
2507 * @returns {angular.element} ripple container element
2509 function getRippleContainer() {
2510 var container = element.data('$mdRippleContainer');
2511 if (container) return container;
2512 container = angular.element('<div class="md-ripple-container">');
2513 element.append(container);
2514 element.data('$mdRippleContainer', container);
2518 function parseColor(color) {
2520 if (color.indexOf('rgba') === 0) return color.replace(/\d?\.?\d*\s*\)\s*$/, '0.1)');
2521 if (color.indexOf('rgb') === 0) return rgbToRGBA(color);
2522 if (color.indexOf('#') === 0) return hexToRGBA(color);
2525 * Converts a hex value to an rgba string
2527 * @param {string} hex value (3 or 6 digits) to be converted
2529 * @returns {string} rgba color with 0.1 alpha
2531 function hexToRGBA(color) {
2532 var hex = color.charAt(0) === '#' ? color.substr(1) : color,
2533 dig = hex.length / 3,
2534 red = hex.substr(0, dig),
2535 grn = hex.substr(dig, dig),
2536 blu = hex.substr(dig * 2);
2542 return 'rgba(' + parseInt(red, 16) + ',' + parseInt(grn, 16) + ',' + parseInt(blu, 16) + ',0.1)';
2546 * Converts rgb value to rgba string
2548 * @param {string} rgb color string
2550 * @returns {string} rgba color with 0.1 alpha
2552 function rgbToRGBA(color) {
2553 return color.replace(')', ', 0.1)').replace('(', 'a(');
2558 function removeElement(elem, wait) {
2559 ripples.splice(ripples.indexOf(elem), 1);
2560 if (ripples.length === 0) {
2561 getRippleContainer().css({ backgroundColor: '' });
2563 $timeout(function () { elem.remove(); }, wait, false);
2566 function updateElement(elem) {
2567 var index = ripples.indexOf(elem),
2568 state = states[index] || {},
2569 elemIsActive = ripples.length > 1 ? false : isActive,
2570 elemIsHeld = ripples.length > 1 ? false : isHeld;
2571 if (elemIsActive || state.animating || elemIsHeld) {
2572 elem.addClass('md-ripple-visible');
2574 elem.removeClass('md-ripple-visible');
2575 if (options.outline) {
2577 width: rippleSize + 'px',
2578 height: rippleSize + 'px',
2579 marginLeft: (rippleSize * -1) + 'px',
2580 marginTop: (rippleSize * -1) + 'px'
2583 removeElement(elem, options.outline ? 450 : 650);
2588 * Creates a ripple at the provided coordinates
2590 * @param {number} left cursor position
2591 * @param {number} top cursor position
2593 * @returns {angular.element} the generated ripple element
2595 function createRipple(left, top) {
2597 color = parseColor(element.attr('md-ink-ripple')) || parseColor($window.getComputedStyle(options.colorElement[0]).color || 'rgb(0, 0, 0)');
2599 var container = getRippleContainer(),
2600 size = getRippleSize(left, top),
2601 css = getRippleCss(size, left, top),
2602 elem = getRippleElement(css),
2603 index = ripples.indexOf(elem),
2604 state = states[index] || {};
2608 state.animating = true;
2610 $timeout(function () {
2611 if (options.dimBackground) {
2612 container.css({ backgroundColor: color });
2614 elem.addClass('md-ripple-placed md-ripple-scaled');
2615 if (options.outline) {
2617 borderWidth: (size * 0.5) + 'px',
2618 marginLeft: (size * -0.5) + 'px',
2619 marginTop: (size * -0.5) + 'px'
2622 elem.css({ left: '50%', top: '50%' });
2624 updateElement(elem);
2625 $timeout(function () {
2626 state.animating = false;
2627 updateElement(elem);
2628 }, (options.outline ? 450 : 225), false);
2634 * Creates the ripple element with the provided css
2636 * @param {object} css properties to be applied
2638 * @returns {angular.element} the generated ripple element
2640 function getRippleElement(css) {
2641 var elem = angular.element('<div class="md-ripple" data-counter="' + counter++ + '">');
2642 ripples.unshift(elem);
2643 states.unshift({ animating: true });
2644 container.append(elem);
2645 css && elem.css(css);
2650 * Calculate the ripple size
2652 * @returns {number} calculated ripple diameter
2654 function getRippleSize(left, top) {
2655 var width = container.prop('offsetWidth'),
2656 height = container.prop('offsetHeight'),
2657 multiplier, size, rect;
2658 if (options.isMenuItem) {
2659 size = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2));
2660 } else if (options.outline) {
2661 rect = node.getBoundingClientRect();
2664 width = Math.max(left, width - left);
2665 height = Math.max(top, height - top);
2666 size = 2 * Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2));
2668 multiplier = options.fullRipple ? 1.1 : 0.8;
2669 size = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2)) * multiplier;
2670 if (options.fitRipple) {
2671 size = Math.min(height, width, size);
2678 * Generates the ripple css
2680 * @param {number} the diameter of the ripple
2681 * @param {number} the left cursor offset
2682 * @param {number} the top cursor offset
2684 * @returns {{backgroundColor: string, borderColor: string, width: string, height: string}}
2686 function getRippleCss(size, left, top) {
2687 var rect = node.getBoundingClientRect(),
2689 backgroundColor: rgbaToRGB(color),
2690 borderColor: rgbaToRGB(color),
2695 if (options.outline) {
2699 css.marginLeft = css.marginTop = (size * -0.5) + 'px';
2702 if (options.center) {
2703 css.left = css.top = '50%';
2705 css.left = Math.round((left - rect.left) / container.prop('offsetWidth') * 100) + '%';
2706 css.top = Math.round((top - rect.top) / container.prop('offsetHeight') * 100) + '%';
2712 * Converts rgba string to rgb, removing the alpha value
2714 * @param {string} rgba color
2716 * @returns {string} rgb color
2718 function rgbaToRGB(color) {
2719 return color.replace('rgba', 'rgb').replace(/,[^\),]+\)/, ')');
2725 * Handles user input start and stop events
2728 function onPressDown(ev) {
2729 if (!isRippleAllowed()) return;
2731 createRipple(ev.pointer.x, ev.pointer.y);
2734 function onPressUp() {
2736 var ripple = ripples[ ripples.length - 1 ];
2737 $timeout(function () { updateElement(ripple); }, 0, false);
2741 * Determines if the ripple is allowed
2743 * @returns {boolean} true if the ripple is allowed, false if not
2745 function isRippleAllowed() {
2746 var parent = node.parentNode;
2747 var grandparent = parent && parent.parentNode;
2748 var ancestor = grandparent && grandparent.parentNode;
2749 return !isDisabled(node) && !isDisabled(parent) && !isDisabled(grandparent) && !isDisabled(ancestor);
2750 function isDisabled (elem) {
2751 return elem && elem.hasAttribute && elem.hasAttribute('disabled');
2757 InkRippleService.$inject = ["$window", "$timeout"];
2760 * noink/nobar/nostretch directive: make any element that has one of
2761 * these attributes be given a controller, so that other directives can
2762 * `require:` these and see if there is a `no<xxx>` parent attribute.
2765 * <hljs lang="html">
2766 * <parent md-no-ink>
2773 * myApp.directive('detectNo', function() {
2775 * require: ['^?mdNoInk', ^?mdNoBar'],
2776 * link: function(scope, element, attr, ctrls) {
2777 * var noinkCtrl = ctrls[0];
2778 * var nobarCtrl = ctrls[1];
2780 * alert("the md-no-ink flag has been specified on an ancestor!");
2783 * alert("the md-no-bar flag has been specified on an ancestor!");
2790 function attrNoDirective() {
2793 controller: angular.noop
2807 * @name $mdTabInkRipple
2808 * @module material.core
2811 * Provides ripple effects for md-tabs. See $mdInkRipple service for all possible configuration options.
2813 * @param {object=} scope Scope within the current context
2814 * @param {object=} element The element the ripple effect should be applied to
2815 * @param {object=} options (Optional) Configuration options to override the defaultripple configuration
2818 angular.module('material.core')
2819 .factory('$mdTabInkRipple', MdTabInkRipple);
2821 function MdTabInkRipple($mdInkRipple) {
2826 function attach(scope, element, options) {
2827 return $mdInkRipple.attach(scope, element, angular.extend({
2829 dimBackground: true,
2835 MdTabInkRipple.$inject = ["$mdInkRipple"];;
2842 angular.module('material.core.theming.palette', [])
2843 .constant('$mdColorPalette', {
2859 'contrastDefaultColor': 'light',
2860 'contrastDarkColors': '50 100 200 300 400 A100',
2861 'contrastStrongLightColors': '500 600 700 A200 A400 A700'
2878 'contrastDefaultColor': 'light',
2879 'contrastDarkColors': '50 100 200 300 400 A100',
2880 'contrastStrongLightColors': '500 600 A200 A400 A700'
2897 'contrastDefaultColor': 'light',
2898 'contrastDarkColors': '50 100 200 A100',
2899 'contrastStrongLightColors': '300 400 A200 A400 A700'
2916 'contrastDefaultColor': 'light',
2917 'contrastDarkColors': '50 100 200 A100',
2918 'contrastStrongLightColors': '300 400 A200'
2935 'contrastDefaultColor': 'light',
2936 'contrastDarkColors': '50 100 200 A100',
2937 'contrastStrongLightColors': '300 400 A200 A400'
2954 'contrastDefaultColor': 'light',
2955 'contrastDarkColors': '100 200 300 400 A100',
2956 'contrastStrongLightColors': '500 600 700 A200 A400 A700'
2973 'contrastDefaultColor': 'dark',
2974 'contrastLightColors': '500 600 700 800 900 A700',
2975 'contrastStrongLightColors': '500 600 700 800 A700'
2992 'contrastDefaultColor': 'dark',
2993 'contrastLightColors': '500 600 700 800 900',
2994 'contrastStrongLightColors': '500 600 700 800'
3011 'contrastDefaultColor': 'dark',
3012 'contrastLightColors': '500 600 700 800 900',
3013 'contrastStrongLightColors': '500 600 700'
3030 'contrastDefaultColor': 'dark',
3031 'contrastLightColors': '500 600 700 800 900',
3032 'contrastStrongLightColors': '500 600 700'
3049 'contrastDefaultColor': 'dark',
3050 'contrastLightColors': '800 900',
3051 'contrastStrongLightColors': '800 900'
3068 'contrastDefaultColor': 'dark',
3069 'contrastLightColors': '900',
3070 'contrastStrongLightColors': '900'
3087 'contrastDefaultColor': 'dark'
3104 'contrastDefaultColor': 'dark'
3121 'contrastDefaultColor': 'dark',
3122 'contrastLightColors': '800 900',
3123 'contrastStrongLightColors': '800 900'
3140 'contrastDefaultColor': 'light',
3141 'contrastDarkColors': '50 100 200 300 400 A100 A200',
3142 'contrastStrongLightColors': '500 600 700 800 900 A400 A700'
3159 'contrastDefaultColor': 'light',
3160 'contrastDarkColors': '50 100 200',
3161 'contrastStrongLightColors': '300 400'
3179 'contrastDefaultColor': 'dark',
3180 'contrastLightColors': '600 700 800 900'
3197 'contrastDefaultColor': 'light',
3198 'contrastDarkColors': '50 100 200 300',
3199 'contrastStrongLightColors': '400 500'
3207 angular.module('material.core.theming', ['material.core.theming.palette'])
3208 .directive('mdTheme', ThemingDirective)
3209 .directive('mdThemable', ThemableDirective)
3210 .provider('$mdTheming', ThemingProvider)
3211 .run(generateThemes);
3215 * @name $mdThemingProvider
3216 * @module material.core
3218 * @description Provider to configure the `$mdTheming` service.
3223 * @name $mdThemingProvider#setDefaultTheme
3224 * @param {string} themeName Default theme name to be applied to elements. Default value is `default`.
3229 * @name $mdThemingProvider#alwaysWatchTheme
3230 * @param {boolean} watch Whether or not to always watch themes for changes and re-apply
3231 * classes when they change. Default is `false`. Enabling can reduce performance.
3234 /* Some Example Valid Theming Expressions
3235 * =======================================
3237 * Intention group expansion: (valid for primary, accent, warn, background)
3239 * {{primary-100}} - grab shade 100 from the primary palette
3240 * {{primary-100-0.7}} - grab shade 100, apply opacity of 0.7
3241 * {{primary-hue-1}} - grab the shade assigned to hue-1 from the primary palette
3242 * {{primary-hue-1-0.7}} - apply 0.7 opacity to primary-hue-1
3243 * {{primary-color}} - Generates .md-hue-1, .md-hue-2, .md-hue-3 with configured shades set for each hue
3244 * {{primary-color-0.7}} - Apply 0.7 opacity to each of the above rules
3245 * {{primary-contrast}} - Generates .md-hue-1, .md-hue-2, .md-hue-3 with configured contrast (ie. text) color shades set for each hue
3246 * {{primary-contrast-0.7}} - Apply 0.7 opacity to each of the above rules
3248 * Foreground expansion: Applies rgba to black/white foreground text
3250 * {{foreground-1}} - used for primary text
3251 * {{foreground-2}} - used for secondary text/divider
3252 * {{foreground-3}} - used for disabled text
3253 * {{foreground-4}} - used for dividers
3257 // In memory generated CSS rules; registered by theme.name
3258 var GENERATED = { };
3260 // In memory storage of defined themes and color palettes (both loaded by CSS, and user specified)
3264 var DARK_FOREGROUND = {
3266 '1': 'rgba(0,0,0,0.87)',
3267 '2': 'rgba(0,0,0,0.54)',
3268 '3': 'rgba(0,0,0,0.26)',
3269 '4': 'rgba(0,0,0,0.12)'
3271 var LIGHT_FOREGROUND = {
3273 '1': 'rgba(255,255,255,1.0)',
3274 '2': 'rgba(255,255,255,0.7)',
3275 '3': 'rgba(255,255,255,0.3)',
3276 '4': 'rgba(255,255,255,0.12)'
3279 var DARK_SHADOW = '1px 1px 0px rgba(0,0,0,0.4), -1px -1px 0px rgba(0,0,0,0.4)';
3280 var LIGHT_SHADOW = '';
3282 var DARK_CONTRAST_COLOR = colorToRgbaArray('rgba(0,0,0,0.87)');
3283 var LIGHT_CONTRAST_COLOR = colorToRgbaArray('rgba(255,255,255,0.87');
3284 var STRONG_LIGHT_CONTRAST_COLOR = colorToRgbaArray('rgb(255,255,255)');
3286 var THEME_COLOR_TYPES = ['primary', 'accent', 'warn', 'background'];
3287 var DEFAULT_COLOR_TYPE = 'primary';
3289 // A color in a theme will use these hues by default, if not specified by user.
3290 var LIGHT_DEFAULT_HUES = {
3305 var DARK_DEFAULT_HUES = {
3313 THEME_COLOR_TYPES.forEach(function(colorType) {
3314 // Color types with unspecified default hues will use these default hue values
3315 var defaultDefaultHues = {
3321 if (!LIGHT_DEFAULT_HUES[colorType]) LIGHT_DEFAULT_HUES[colorType] = defaultDefaultHues;
3322 if (!DARK_DEFAULT_HUES[colorType]) DARK_DEFAULT_HUES[colorType] = defaultDefaultHues;
3325 var VALID_HUE_VALUES = [
3326 '50', '100', '200', '300', '400', '500', '600',
3327 '700', '800', '900', 'A100', 'A200', 'A400', 'A700'
3330 function ThemingProvider($mdColorPalette) {
3334 var themingProvider;
3335 var defaultTheme = 'default';
3336 var alwaysWatchTheme = false;
3338 // Load JS Defined Palettes
3339 angular.extend(PALETTES, $mdColorPalette);
3341 // Default theme defined in core.js
3343 ThemingService.$inject = ["$rootScope", "$log"];
3344 return themingProvider = {
3345 definePalette: definePalette,
3346 extendPalette: extendPalette,
3347 theme: registerTheme,
3349 setDefaultTheme: function(theme) {
3350 defaultTheme = theme;
3352 alwaysWatchTheme: function(alwaysWatch) {
3353 alwaysWatchTheme = alwaysWatch;
3355 $get: ThemingService,
3356 _LIGHT_DEFAULT_HUES: LIGHT_DEFAULT_HUES,
3357 _DARK_DEFAULT_HUES: DARK_DEFAULT_HUES,
3358 _PALETTES: PALETTES,
3360 _parseRules: parseRules,
3364 // Example: $mdThemingProvider.definePalette('neonRed', { 50: '#f5fafa', ... });
3365 function definePalette(name, map) {
3367 PALETTES[name] = checkPaletteValid(name, map);
3368 return themingProvider;
3371 // Returns an new object which is a copy of a given palette `name` with variables from
3372 // `map` overwritten
3373 // Example: var neonRedMap = $mdThemingProvider.extendPalette('red', { 50: '#f5fafafa' });
3374 function extendPalette(name, map) {
3375 return checkPaletteValid(name, angular.extend({}, PALETTES[name] || {}, map) );
3378 // Make sure that palette has all required hues
3379 function checkPaletteValid(name, map) {
3380 var missingColors = VALID_HUE_VALUES.filter(function(field) {
3383 if (missingColors.length) {
3384 throw new Error("Missing colors %1 in palette %2!"
3385 .replace('%1', missingColors.join(', '))
3386 .replace('%2', name));
3392 // Register a theme (which is a collection of color palettes to use with various states
3393 // ie. warn, accent, primary )
3394 // Optionally inherit from an existing theme
3395 // $mdThemingProvider.theme('custom-theme').primaryPalette('red');
3396 function registerTheme(name, inheritFrom) {
3397 if (THEMES[name]) return THEMES[name];
3399 inheritFrom = inheritFrom || 'default';
3401 var parentTheme = typeof inheritFrom === 'string' ? THEMES[inheritFrom] : inheritFrom;
3402 var theme = new Theme(name);
3405 angular.forEach(parentTheme.colors, function(color, colorType) {
3406 theme.colors[colorType] = {
3408 // Make sure a COPY of the hues is given to the child color,
3409 // not the same reference.
3410 hues: angular.extend({}, color.hues)
3414 THEMES[name] = theme;
3419 function Theme(name) {
3424 self.dark = setDark;
3427 function setDark(isDark) {
3428 isDark = arguments.length === 0 ? true : !!isDark;
3430 // If no change, abort
3431 if (isDark === self.isDark) return;
3433 self.isDark = isDark;
3435 self.foregroundPalette = self.isDark ? LIGHT_FOREGROUND : DARK_FOREGROUND;
3436 self.foregroundShadow = self.isDark ? DARK_SHADOW : LIGHT_SHADOW;
3438 // Light and dark themes have different default hues.
3439 // Go through each existing color type for this theme, and for every
3440 // hue value that is still the default hue value from the previous light/dark setting,
3441 // set it to the default hue value from the new light/dark setting.
3442 var newDefaultHues = self.isDark ? DARK_DEFAULT_HUES : LIGHT_DEFAULT_HUES;
3443 var oldDefaultHues = self.isDark ? LIGHT_DEFAULT_HUES : DARK_DEFAULT_HUES;
3444 angular.forEach(newDefaultHues, function(newDefaults, colorType) {
3445 var color = self.colors[colorType];
3446 var oldDefaults = oldDefaultHues[colorType];
3448 for (var hueName in color.hues) {
3449 if (color.hues[hueName] === oldDefaults[hueName]) {
3450 color.hues[hueName] = newDefaults[hueName];
3459 THEME_COLOR_TYPES.forEach(function(colorType) {
3460 var defaultHues = (self.isDark ? DARK_DEFAULT_HUES : LIGHT_DEFAULT_HUES)[colorType];
3461 self[colorType + 'Palette'] = function setPaletteType(paletteName, hues) {
3462 var color = self.colors[colorType] = {
3464 hues: angular.extend({}, defaultHues, hues)
3467 Object.keys(color.hues).forEach(function(name) {
3468 if (!defaultHues[name]) {
3469 throw new Error("Invalid hue name '%1' in theme %2's %3 color %4. Available hue names: %4"
3470 .replace('%1', name)
3471 .replace('%2', self.name)
3472 .replace('%3', paletteName)
3473 .replace('%4', Object.keys(defaultHues).join(', '))
3477 Object.keys(color.hues).map(function(key) {
3478 return color.hues[key];
3479 }).forEach(function(hueValue) {
3480 if (VALID_HUE_VALUES.indexOf(hueValue) == -1) {
3481 throw new Error("Invalid hue value '%1' in theme %2's %3 color %4. Available hue values: %5"
3482 .replace('%1', hueValue)
3483 .replace('%2', self.name)
3484 .replace('%3', colorType)
3485 .replace('%4', paletteName)
3486 .replace('%5', VALID_HUE_VALUES.join(', '))
3493 self[colorType + 'Color'] = function() {
3494 var args = Array.prototype.slice.call(arguments);
3495 console.warn('$mdThemingProviderTheme.' + colorType + 'Color() has been deprecated. ' +
3496 'Use $mdThemingProviderTheme.' + colorType + 'Palette() instead.');
3497 return self[colorType + 'Palette'].apply(self, args);
3508 * Service that makes an element apply theming related classes to itself.
3511 * app.directive('myFancyDirective', function($mdTheming) {
3514 * link: function(scope, el, attrs) {
3520 * @param {el=} element to apply theming to
3523 function ThemingService($rootScope, $log) {
3525 applyTheme.inherit = function(el, parent) {
3526 var ctrl = parent.controller('mdTheme');
3528 var attrThemeValue = el.attr('md-theme-watch');
3529 if ( (alwaysWatchTheme || angular.isDefined(attrThemeValue)) && attrThemeValue != 'false') {
3530 var deregisterWatch = $rootScope.$watch(function() {
3531 return ctrl && ctrl.$mdTheme || defaultTheme;
3533 el.on('$destroy', deregisterWatch);
3535 var theme = ctrl && ctrl.$mdTheme || defaultTheme;
3539 function changeTheme(theme) {
3540 if (!registered(theme)) {
3541 $log.warn('Attempted to use unregistered theme \'' + theme + '\'. ' +
3542 'Register it with $mdThemingProvider.theme().');
3544 var oldTheme = el.data('$mdThemeName');
3545 if (oldTheme) el.removeClass('md-' + oldTheme +'-theme');
3546 el.addClass('md-' + theme + '-theme');
3547 el.data('$mdThemeName', theme);
3551 applyTheme.THEMES = angular.extend({}, THEMES);
3552 applyTheme.defaultTheme = function() { return defaultTheme; };
3553 applyTheme.registered = registered;
3557 function registered(themeName) {
3558 if (themeName === undefined || themeName === '') return true;
3559 return applyTheme.THEMES[themeName] !== undefined;
3562 function applyTheme(scope, el) {
3563 // Allow us to be invoked via a linking function signature.
3564 if (el === undefined) {
3568 if (scope === undefined) {
3571 applyTheme.inherit(el, el);
3575 ThemingProvider.$inject = ["$mdColorPalette"];
3577 function ThemingDirective($mdTheming, $interpolate, $log) {
3581 pre: function(scope, el, attrs) {
3583 $setTheme: function(theme) {
3584 if (!$mdTheming.registered(theme)) {
3585 $log.warn('attempted to use unregistered theme \'' + theme + '\'');
3587 ctrl.$mdTheme = theme;
3590 el.data('$mdThemeController', ctrl);
3591 ctrl.$setTheme($interpolate(attrs.mdTheme)(scope));
3592 attrs.$observe('mdTheme', ctrl.$setTheme);
3597 ThemingDirective.$inject = ["$mdTheming", "$interpolate", "$log"];
3599 function ThemableDirective($mdTheming) {
3602 ThemableDirective.$inject = ["$mdTheming"];
3604 function parseRules(theme, colorType, rules) {
3605 checkValidPalette(theme, colorType);
3607 rules = rules.replace(/THEME_NAME/g, theme.name);
3608 var generatedRules = [];
3609 var color = theme.colors[colorType];
3611 var themeNameRegex = new RegExp('.md-' + theme.name + '-theme', 'g');
3612 // Matches '{{ primary-color }}', etc
3613 var hueRegex = new RegExp('(\'|")?{{\\s*(' + colorType + ')-(color|contrast)-?(\\d\\.?\\d*)?\\s*}}(\"|\')?','g');
3614 var simpleVariableRegex = /'?"?\{\{\s*([a-zA-Z]+)-(A?\d+|hue\-[0-3]|shadow)-?(\d\.?\d*)?\s*\}\}'?"?/g;
3615 var palette = PALETTES[color.name];
3617 // find and replace simple variables where we use a specific hue, not an entire palette
3618 // eg. "{{primary-100}}"
3619 //\(' + THEME_COLOR_TYPES.join('\|') + '\)'
3620 rules = rules.replace(simpleVariableRegex, function(match, colorType, hue, opacity) {
3621 if (colorType === 'foreground') {
3622 if (hue == 'shadow') {
3623 return theme.foregroundShadow;
3625 return theme.foregroundPalette[hue] || theme.foregroundPalette['1'];
3628 if (hue.indexOf('hue') === 0) {
3629 hue = theme.colors[colorType].hues[hue];
3631 return rgba( (PALETTES[ theme.colors[colorType].name ][hue] || '').value, opacity );
3634 // For each type, generate rules for each hue (ie. default, md-hue-1, md-hue-2, md-hue-3)
3635 angular.forEach(color.hues, function(hueValue, hueName) {
3637 .replace(hueRegex, function(match, _, colorType, hueType, opacity) {
3638 return rgba(palette[hueValue][hueType === 'color' ? 'value' : 'contrast'], opacity);
3640 if (hueName !== 'default') {
3641 newRule = newRule.replace(themeNameRegex, '.md-' + theme.name + '-theme.md-' + hueName);
3644 // Don't apply a selector rule to the default theme, making it easier to override
3645 // styles of the base-component
3646 if (theme.name == 'default') {
3647 newRule = newRule.replace(/\.md-default-theme/g, '');
3649 generatedRules.push(newRule);
3652 return generatedRules;
3655 // Generate our themes at run time given the state of THEMES and PALETTES
3656 function generateThemes($injector) {
3658 var head = document.getElementsByTagName('head')[0];
3659 var firstChild = head ? head.firstElementChild : null;
3660 var themeCss = $injector.has('$MD_THEME_CSS') ? $injector.get('$MD_THEME_CSS') : '';
3662 if ( !firstChild ) return;
3663 if (themeCss.length === 0) return; // no rules, so no point in running this expensive task
3665 // Expose contrast colors for palettes to ensure that text is always readable
3666 angular.forEach(PALETTES, sanitizePalette);
3668 // MD_THEME_CSS is a string generated by the build process that includes all the themable
3669 // components as templates
3671 // Break the CSS into individual rules
3672 var rulesByType = {};
3673 var rules = themeCss
3674 .split(/\}(?!(\}|'|"|;))/)
3675 .filter(function(rule) { return rule && rule.length; })
3676 .map(function(rule) { return rule.trim() + '}'; });
3679 var ruleMatchRegex = new RegExp('md-(' + THEME_COLOR_TYPES.join('|') + ')', 'g');
3681 THEME_COLOR_TYPES.forEach(function(type) {
3682 rulesByType[type] = '';
3686 // Sort the rules based on type, allowing us to do color substitution on a per-type basis
3687 rules.forEach(function(rule) {
3688 var match = rule.match(ruleMatchRegex);
3689 // First: test that if the rule has '.md-accent', it goes into the accent set of rules
3690 for (var i = 0, type; type = THEME_COLOR_TYPES[i]; i++) {
3691 if (rule.indexOf('.md-' + type) > -1) {
3692 return rulesByType[type] += rule;
3696 // If no eg 'md-accent' class is found, try to just find 'accent' in the rule and guess from
3698 for (i = 0; type = THEME_COLOR_TYPES[i]; i++) {
3699 if (rule.indexOf(type) > -1) {
3700 return rulesByType[type] += rule;
3704 // Default to the primary array
3705 return rulesByType[DEFAULT_COLOR_TYPE] += rule;
3708 // For each theme, use the color palettes specified for
3709 // `primary`, `warn` and `accent` to generate CSS rules.
3711 angular.forEach(THEMES, function(theme) {
3712 if ( !GENERATED[theme.name] ) {
3715 THEME_COLOR_TYPES.forEach(function(colorType) {
3716 var styleStrings = parseRules(theme, colorType, rulesByType[colorType]);
3717 while (styleStrings.length) {
3718 var style = document.createElement('style');
3719 style.setAttribute('type', 'text/css');
3720 style.appendChild(document.createTextNode(styleStrings.shift()));
3721 head.insertBefore(style, firstChild);
3726 if (theme.colors.primary.name == theme.colors.accent.name) {
3727 console.warn("$mdThemingProvider: Using the same palette for primary and" +
3728 " accent. This violates the material design spec.");
3731 GENERATED[theme.name] = true;
3736 // *************************
3737 // Internal functions
3738 // *************************
3740 // The user specifies a 'default' contrast color as either light or dark,
3741 // then explicitly lists which hues are the opposite contrast (eg. A100 has dark, A200 has light)
3742 function sanitizePalette(palette) {
3743 var defaultContrast = palette.contrastDefaultColor;
3744 var lightColors = palette.contrastLightColors || [];
3745 var strongLightColors = palette.contrastStrongLightColors || [];
3746 var darkColors = palette.contrastDarkColors || [];
3748 // These colors are provided as space-separated lists
3749 if (typeof lightColors === 'string') lightColors = lightColors.split(' ');
3750 if (typeof strongLightColors === 'string') strongLightColors = strongLightColors.split(' ');
3751 if (typeof darkColors === 'string') darkColors = darkColors.split(' ');
3753 // Cleanup after ourselves
3754 delete palette.contrastDefaultColor;
3755 delete palette.contrastLightColors;
3756 delete palette.contrastStrongLightColors;
3757 delete palette.contrastDarkColors;
3759 // Change { 'A100': '#fffeee' } to { 'A100': { value: '#fffeee', contrast:DARK_CONTRAST_COLOR }
3760 angular.forEach(palette, function(hueValue, hueName) {
3761 if (angular.isObject(hueValue)) return; // Already converted
3762 // Map everything to rgb colors
3763 var rgbValue = colorToRgbaArray(hueValue);
3765 throw new Error("Color %1, in palette %2's hue %3, is invalid. Hex or rgb(a) color expected."
3766 .replace('%1', hueValue)
3767 .replace('%2', palette.name)
3768 .replace('%3', hueName));
3771 palette[hueName] = {
3773 contrast: getContrastColor()
3775 function getContrastColor() {
3776 if (defaultContrast === 'light') {
3777 if (darkColors.indexOf(hueName) > -1) {
3778 return DARK_CONTRAST_COLOR;
3780 return strongLightColors.indexOf(hueName) > -1 ? STRONG_LIGHT_CONTRAST_COLOR
3781 : LIGHT_CONTRAST_COLOR;
3784 if (lightColors.indexOf(hueName) > -1) {
3785 return strongLightColors.indexOf(hueName) > -1 ? STRONG_LIGHT_CONTRAST_COLOR
3786 : LIGHT_CONTRAST_COLOR;
3788 return DARK_CONTRAST_COLOR;
3797 generateThemes.$inject = ["$injector"];
3799 function checkValidPalette(theme, colorType) {
3800 // If theme attempts to use a palette that doesnt exist, throw error
3801 if (!PALETTES[ (theme.colors[colorType] || {}).name ]) {
3803 "You supplied an invalid color palette for theme %1's %2 palette. Available palettes: %3"
3804 .replace('%1', theme.name)
3805 .replace('%2', colorType)
3806 .replace('%3', Object.keys(PALETTES).join(', '))
3811 function colorToRgbaArray(clr) {
3812 if (angular.isArray(clr) && clr.length == 3) return clr;
3813 if (/^rgb/.test(clr)) {
3814 return clr.replace(/(^\s*rgba?\(|\)\s*$)/g, '').split(',').map(function(value, i) {
3815 return i == 3 ? parseFloat(value, 10) : parseInt(value, 10);
3818 if (clr.charAt(0) == '#') clr = clr.substring(1);
3819 if (!/^([a-fA-F0-9]{3}){1,2}$/g.test(clr)) return;
3821 var dig = clr.length / 3;
3822 var red = clr.substr(0, dig);
3823 var grn = clr.substr(dig, dig);
3824 var blu = clr.substr(dig * 2);
3830 return [parseInt(red, 16), parseInt(grn, 16), parseInt(blu, 16)];
3833 function rgba(rgbArray, opacity) {
3834 if ( !rgbArray ) return "rgb('0,0,0')";
3836 if (rgbArray.length == 4) {
3837 rgbArray = angular.copy(rgbArray);
3838 opacity ? rgbArray.pop() : opacity = rgbArray.pop();
3840 return opacity && (typeof opacity == 'number' || (typeof opacity == 'string' && opacity.length)) ?
3841 'rgba(' + rgbArray.join(',') + ',' + opacity + ')' :
3842 'rgb(' + rgbArray.join(',') + ')';
3852 * @name material.components.autocomplete
3855 * @see js folder for autocomplete implementation
3857 angular.module('material.components.autocomplete', [
3859 'material.components.icon'
3868 * @name material.components.backdrop
3869 * @description Backdrop
3875 * @module material.components.backdrop
3880 * `<md-backdrop>` is a backdrop element used by other components, such as dialog and bottom sheet.
3881 * Apply class `opaque` to make the backdrop use the theme backdrop color.
3885 angular.module('material.components.backdrop', [
3888 .directive('mdBackdrop', BackdropDirective);
3890 function BackdropDirective($mdTheming) {
3893 BackdropDirective.$inject = ["$mdTheming"];
3901 * @name material.components.bottomSheet
3905 angular.module('material.components.bottomSheet', [
3907 'material.components.backdrop'
3909 .directive('mdBottomSheet', MdBottomSheetDirective)
3910 .provider('$mdBottomSheet', MdBottomSheetProvider);
3912 function MdBottomSheetDirective() {
3920 * @name $mdBottomSheet
3921 * @module material.components.bottomSheet
3924 * `$mdBottomSheet` opens a bottom sheet over the app and provides a simple promise API.
3928 * - The bottom sheet's template must have an outer `<md-bottom-sheet>` element.
3929 * - Add the `md-grid` class to the bottom sheet for a grid layout.
3930 * - Add the `md-list` class to the bottom sheet for a list layout.
3933 * <hljs lang="html">
3934 * <div ng-controller="MyController">
3935 * <md-button ng-click="openBottomSheet()">
3936 * Open a Bottom Sheet!
3941 * var app = angular.module('app', ['ngMaterial']);
3942 * app.controller('MyController', function($scope, $mdBottomSheet) {
3943 * $scope.openBottomSheet = function() {
3944 * $mdBottomSheet.show({
3945 * template: '<md-bottom-sheet>Hello!</md-bottom-sheet>'
3954 * @name $mdBottomSheet#show
3957 * Show a bottom sheet with the specified options.
3959 * @param {object} options An options object, with the following properties:
3961 * - `templateUrl` - `{string=}`: The url of an html template file that will
3962 * be used as the content of the bottom sheet. Restrictions: the template must
3963 * have an outer `md-bottom-sheet` element.
3964 * - `template` - `{string=}`: Same as templateUrl, except this is an actual
3966 * - `scope` - `{object=}`: the scope to link the template / controller to. If none is specified, it will create a new child scope.
3967 * This scope will be destroyed when the bottom sheet is removed unless `preserveScope` is set to true.
3968 * - `preserveScope` - `{boolean=}`: whether to preserve the scope when the element is removed. Default is false
3969 * - `controller` - `{string=}`: The controller to associate with this bottom sheet.
3970 * - `locals` - `{string=}`: An object containing key/value pairs. The keys will
3971 * be used as names of values to inject into the controller. For example,
3972 * `locals: {three: 3}` would inject `three` into the controller with the value
3974 * - `targetEvent` - `{DOMClickEvent=}`: A click's event object. When passed in as an option,
3975 * the location of the click will be used as the starting point for the opening animation
3976 * of the the dialog.
3977 * - `resolve` - `{object=}`: Similar to locals, except it takes promises as values
3978 * and the bottom sheet will not open until the promises resolve.
3979 * - `controllerAs` - `{string=}`: An alias to assign the controller to on the scope.
3980 * - `parent` - `{element=}`: The element to append the bottom sheet to. The `parent` may be a `function`, `string`,
3981 * `object`, or null. Defaults to appending to the body of the root element (or the root element) of the application.
3982 * e.g. angular.element(document.getElementById('content')) or "#content"
3983 * - `disableParentScroll` - `{boolean=}`: Whether to disable scrolling while the bottom sheet is open.
3986 * @returns {promise} A promise that can be resolved with `$mdBottomSheet.hide()` or
3987 * rejected with `$mdBottomSheet.cancel()`.
3992 * @name $mdBottomSheet#hide
3995 * Hide the existing bottom sheet and resolve the promise returned from
3996 * `$mdBottomSheet.show()`. This call will close the most recently opened/current bottomsheet (if any).
3998 * @param {*=} response An argument for the resolved promise.
4004 * @name $mdBottomSheet#cancel
4007 * Hide the existing bottom sheet and reject the promise returned from
4008 * `$mdBottomSheet.show()`.
4010 * @param {*=} response An argument for the rejected promise.
4014 function MdBottomSheetProvider($$interimElementProvider) {
4015 // how fast we need to flick down to close the sheet, pixels/ms
4016 var CLOSING_VELOCITY = 0.5;
4017 var PADDING = 80; // same as css
4019 bottomSheetDefaults.$inject = ["$animate", "$mdConstant", "$mdUtil", "$timeout", "$compile", "$mdTheming", "$mdBottomSheet", "$rootElement", "$mdGesture"];
4020 return $$interimElementProvider('$mdBottomSheet')
4022 methods: ['disableParentScroll', 'escapeToClose', 'targetEvent'],
4023 options: bottomSheetDefaults
4027 function bottomSheetDefaults($animate, $mdConstant, $mdUtil, $timeout, $compile, $mdTheming, $mdBottomSheet, $rootElement, $mdGesture) {
4035 escapeToClose: true,
4036 disableParentScroll: true
4040 function onShow(scope, element, options) {
4042 element = $mdUtil.extractElementByName(element, 'md-bottom-sheet');
4044 // Add a backdrop that will close on click
4045 backdrop = $compile('<md-backdrop class="md-opaque md-bottom-sheet-backdrop">')(scope);
4046 backdrop.on('click', function() {
4047 $timeout($mdBottomSheet.cancel);
4049 $mdTheming.inherit(backdrop, options.parent);
4051 $animate.enter(backdrop, options.parent, null);
4053 var bottomSheet = new BottomSheet(element, options.parent);
4054 options.bottomSheet = bottomSheet;
4056 // Give up focus on calling item
4057 options.targetEvent && angular.element(options.targetEvent.target).blur();
4058 $mdTheming.inherit(bottomSheet.element, options.parent);
4060 if (options.disableParentScroll) {
4061 options.lastOverflow = options.parent.css('overflow');
4062 options.parent.css('overflow', 'hidden');
4065 return $animate.enter(bottomSheet.element, options.parent)
4067 var focusable = angular.element(
4068 element[0].querySelector('button') ||
4069 element[0].querySelector('a') ||
4070 element[0].querySelector('[ng-click]')
4074 if (options.escapeToClose) {
4075 options.rootElementKeyupCallback = function(e) {
4076 if (e.keyCode === $mdConstant.KEY_CODE.ESCAPE) {
4077 $timeout($mdBottomSheet.cancel);
4080 $rootElement.on('keyup', options.rootElementKeyupCallback);
4086 function onRemove(scope, element, options) {
4088 var bottomSheet = options.bottomSheet;
4090 $animate.leave(backdrop);
4091 return $animate.leave(bottomSheet.element).then(function() {
4092 if (options.disableParentScroll) {
4093 options.parent.css('overflow', options.lastOverflow);
4094 delete options.lastOverflow;
4097 bottomSheet.cleanup();
4100 options.targetEvent && angular.element(options.targetEvent.target).focus();
4105 * BottomSheet class to apply bottom-sheet behavior to an element
4107 function BottomSheet(element, parent) {
4108 var deregister = $mdGesture.register(parent, 'drag', { horizontal: false });
4109 parent.on('$md.dragstart', onDragStart)
4110 .on('$md.drag', onDrag)
4111 .on('$md.dragend', onDragEnd);
4115 cleanup: function cleanup() {
4117 parent.off('$md.dragstart', onDragStart)
4118 .off('$md.drag', onDrag)
4119 .off('$md.dragend', onDragEnd);
4123 function onDragStart(ev) {
4124 // Disable transitions on transform so that it feels fast
4125 element.css($mdConstant.CSS.TRANSITION_DURATION, '0ms');
4128 function onDrag(ev) {
4129 var transform = ev.pointer.distanceY;
4130 if (transform < 5) {
4131 // Slow down drag when trying to drag up, and stop after PADDING
4132 transform = Math.max(-PADDING, transform / 2);
4134 element.css($mdConstant.CSS.TRANSFORM, 'translate3d(0,' + (PADDING + transform) + 'px,0)');
4137 function onDragEnd(ev) {
4138 if (ev.pointer.distanceY > 0 &&
4139 (ev.pointer.distanceY > 20 || Math.abs(ev.pointer.velocityY) > CLOSING_VELOCITY)) {
4140 var distanceRemaining = element.prop('offsetHeight') - ev.pointer.distanceY;
4141 var transitionDuration = Math.min(distanceRemaining / ev.pointer.velocityY * 0.75, 500);
4142 element.css($mdConstant.CSS.TRANSITION_DURATION, transitionDuration + 'ms');
4143 $timeout($mdBottomSheet.cancel);
4145 element.css($mdConstant.CSS.TRANSITION_DURATION, '');
4146 element.css($mdConstant.CSS.TRANSFORM, '');
4154 MdBottomSheetProvider.$inject = ["$$interimElementProvider"];
4162 * @name material.components.button
4168 .module('material.components.button', [ 'material.core' ])
4169 .directive('mdButton', MdButtonDirective);
4174 * @module material.components.button
4179 * `<md-button>` is a button directive with optional ink ripples (default enabled).
4181 * If you supply a `href` or `ng-href` attribute, it will become an `<a>` element. Otherwise, it will
4182 * become a `<button>` element. As per the [Material Design specifications](http://www.google.com/design/spec/style/color.html#color-ui-color-application)
4183 * the FAB button background is filled with the accent color [by default]. The primary color palette may be used with
4184 * the `md-primary` class.
4186 * @param {boolean=} md-no-ink If present, disable ripple ink effects.
4187 * @param {expression=} ng-disabled En/Disable based on the expression
4188 * @param {string=} md-ripple-size Overrides the default ripple size logic. Options: `full`, `partial`, `auto`
4189 * @param {string=} aria-label Adds alternative text to button for accessibility, useful for icon buttons.
4190 * If no default text is found, a warning will be logged.
4196 * <hljs lang="html">
4197 * <md-button> Flat Button </md-button>
4198 * <md-button href="http://google.com"> Flat link </md-button>
4199 * <md-button class="md-raised"> Raised Button </md-button>
4200 * <md-button ng-disabled="true"> Disabled Button </md-button>
4202 * <md-icon md-svg-src="your/icon.svg"></md-icon>
4209 * <hljs lang="html">
4210 * <md-button class="md-fab" aria-label="FAB">
4211 * <md-icon md-svg-src="your/icon.svg"></md-icon>
4214 * <md-button class="md-fab md-mini" aria-label="Mini FAB">
4215 * <md-icon md-svg-src="your/icon.svg"></md-icon>
4217 * <!-- Button with SVG Icon -->
4218 * <md-button class="md-icon-button" aria-label="Custom Icon Button">
4219 * <md-icon md-svg-icon="path/to/your.svg"></md-icon>
4223 function MdButtonDirective($mdButtonInkRipple, $mdTheming, $mdAria, $timeout) {
4229 template: getTemplate,
4233 function isAnchor(attr) {
4234 return angular.isDefined(attr.href) || angular.isDefined(attr.ngHref) || angular.isDefined(attr.ngLink) || angular.isDefined(attr.uiSref);
4237 function getTemplate(element, attr) {
4238 return isAnchor(attr) ?
4239 '<a class="md-button" ng-transclude></a>' :
4240 '<button class="md-button" ng-transclude></button>';
4243 function postLink(scope, element, attr) {
4244 var node = element[0];
4245 $mdTheming(element);
4246 $mdButtonInkRipple.attach(scope, element);
4248 var elementHasText = node.textContent.trim();
4249 if (!elementHasText) {
4250 $mdAria.expect(element, 'aria-label');
4253 // For anchor elements, we have to set tabindex manually when the
4254 // element is disabled
4255 if (isAnchor(attr) && angular.isDefined(attr.ngDisabled) ) {
4256 scope.$watch(attr.ngDisabled, function(isDisabled) {
4257 element.attr('tabindex', isDisabled ? -1 : 0);
4261 // disabling click event when disabled is true
4262 element.on('click', function(e){
4263 if (attr.disabled === true) {
4265 e.stopImmediatePropagation();
4269 // restrict focus styles to the keyboard
4270 scope.mouseActive = false;
4271 element.on('mousedown', function() {
4272 scope.mouseActive = true;
4273 $timeout(function(){
4274 scope.mouseActive = false;
4277 .on('focus', function() {
4278 if(scope.mouseActive === false) { element.addClass('md-focused'); }
4280 .on('blur', function() { element.removeClass('md-focused'); });
4284 MdButtonDirective.$inject = ["$mdButtonInkRipple", "$mdTheming", "$mdAria", "$timeout"];
4292 * @name material.components.card
4297 angular.module('material.components.card', [
4300 .directive('mdCard', mdCardDirective);
4307 * @module material.components.card
4312 * The `<md-card>` directive is a container element used within `<md-content>` containers.
4314 * An image included as a direct descendant will fill the card's width, while the `<md-card-content>`
4315 * container will wrap text content and provide padding. An `<md-card-footer>` element can be
4316 * optionally included to put content flush against the bottom edge of the card.
4318 * Action buttons can be included in an element with the `.md-actions` class, also used in `md-dialog`.
4319 * You can then position buttons using layout attributes.
4321 * Cards have constant width and variable heights; where the maximum height is limited to what can
4322 * fit within a single view on a platform, but it can temporarily expand as needed.
4325 * ###Card with optional footer
4326 * <hljs lang="html">
4328 * <img src="card-image.png" class="md-card-image" alt="image caption">
4330 * <h2>Card headline</h2>
4331 * <p>Card content</p>
4332 * </md-card-content>
4339 * ###Card with actions
4340 * <hljs lang="html">
4342 * <img src="card-image.png" class="md-card-image" alt="image caption">
4344 * <h2>Card headline</h2>
4345 * <p>Card content</p>
4346 * </md-card-content>
4347 * <div class="md-actions" layout="row" layout-align="end center">
4348 * <md-button>Action 1</md-button>
4349 * <md-button>Action 2</md-button>
4355 function mdCardDirective($mdTheming) {
4358 link: function($scope, $element, $attr) {
4359 $mdTheming($element);
4363 mdCardDirective.$inject = ["$mdTheming"];
4371 * @name material.components.checkbox
4372 * @description Checkbox module!
4375 .module('material.components.checkbox', ['material.core'])
4376 .directive('mdCheckbox', MdCheckboxDirective);
4381 * @module material.components.checkbox
4385 * The checkbox directive is used like the normal [angular checkbox](https://docs.angularjs.org/api/ng/input/input%5Bcheckbox%5D).
4387 * As per the [material design spec](http://www.google.com/design/spec/style/color.html#color-ui-color-application)
4388 * the checkbox is in the accent color by default. The primary color palette may be used with
4389 * the `md-primary` class.
4391 * @param {string} ng-model Assignable angular expression to data-bind to.
4392 * @param {string=} name Property name of the form under which the control is published.
4393 * @param {expression=} ng-true-value The value to which the expression should be set when selected.
4394 * @param {expression=} ng-false-value The value to which the expression should be set when not selected.
4395 * @param {string=} ng-change Angular expression to be executed when input changes due to user interaction with the input element.
4396 * @param {boolean=} md-no-ink Use of attribute indicates use of ripple ink effects
4397 * @param {string=} aria-label Adds label to checkbox for accessibility.
4398 * Defaults to checkbox's text. If no default text is found, a warning will be logged.
4401 * <hljs lang="html">
4402 * <md-checkbox ng-model="isChecked" aria-label="Finished?">
4406 * <md-checkbox md-no-ink ng-model="hasInk" aria-label="No Ink Effects">
4410 * <md-checkbox ng-disabled="true" ng-model="isDisabled" aria-label="Disabled">
4417 function MdCheckboxDirective(inputDirective, $mdInkRipple, $mdAria, $mdConstant, $mdTheming, $mdUtil, $timeout) {
4418 inputDirective = inputDirective[0];
4419 var CHECKED_CSS = 'md-checked';
4424 require: '?ngModel',
4425 priority:210, // Run before ngAria
4427 '<div class="md-container" md-ink-ripple md-ink-ripple-checkbox>' +
4428 '<div class="md-icon"></div>' +
4430 '<div ng-transclude class="md-label"></div>',
4434 // **********************************************************
4436 // **********************************************************
4438 function compile (tElement, tAttrs) {
4440 tAttrs.type = 'checkbox';
4441 tAttrs.tabindex = tAttrs.tabindex || '0';
4442 tElement.attr('role', tAttrs.type);
4444 return function postLink(scope, element, attr, ngModelCtrl) {
4445 ngModelCtrl = ngModelCtrl || $mdUtil.fakeNgModel();
4446 $mdTheming(element);
4448 if (attr.ngChecked) {
4450 scope.$eval.bind(scope, attr.ngChecked),
4451 ngModelCtrl.$setViewValue.bind(ngModelCtrl)
4454 $$watchExpr('ngDisabled', 'tabindex', {
4456 false: attr.tabindex
4458 $mdAria.expectWithText(element, 'aria-label');
4460 // Reuse the original input[type=checkbox] directive from Angular core.
4461 // This is a bit hacky as we need our own event listener and own render
4463 inputDirective.link.pre(scope, {
4466 }, attr, [ngModelCtrl]);
4468 scope.mouseActive = false;
4469 element.on('click', listener)
4470 .on('keypress', keypressHandler)
4471 .on('mousedown', function() {
4472 scope.mouseActive = true;
4473 $timeout(function(){
4474 scope.mouseActive = false;
4477 .on('focus', function() {
4478 if(scope.mouseActive === false) { element.addClass('md-focused'); }
4480 .on('blur', function() { element.removeClass('md-focused'); });
4482 ngModelCtrl.$render = render;
4484 function $$watchExpr(expr, htmlAttr, valueOpts) {
4486 scope.$watch(attr[expr], function(val) {
4487 if (valueOpts[val]) {
4488 element.attr(htmlAttr, valueOpts[val]);
4494 function keypressHandler(ev) {
4495 var keyCode = ev.which || ev.keyCode;
4496 if (keyCode === $mdConstant.KEY_CODE.SPACE || keyCode === $mdConstant.KEY_CODE.ENTER) {
4497 ev.preventDefault();
4498 if (!element.hasClass('md-focused')) { element.addClass('md-focused'); }
4502 function listener(ev) {
4503 if (element[0].hasAttribute('disabled')) return;
4505 scope.$apply(function() {
4506 // Toggle the checkbox value...
4507 var viewValue = attr.ngChecked ? attr.checked : !ngModelCtrl.$viewValue;
4509 ngModelCtrl.$setViewValue( viewValue, ev && ev.type);
4510 ngModelCtrl.$render();
4515 if(ngModelCtrl.$viewValue) {
4516 element.addClass(CHECKED_CSS);
4518 element.removeClass(CHECKED_CSS);
4524 MdCheckboxDirective.$inject = ["inputDirective", "$mdInkRipple", "$mdAria", "$mdConstant", "$mdTheming", "$mdUtil", "$timeout"];
4532 * @name material.components.chips
4535 * @see js folder for chips implementation
4537 angular.module('material.components.chips', [
4539 'material.components.autocomplete'
4548 * @name material.components.content
4551 * Scrollable content
4553 angular.module('material.components.content', [
4556 .directive('mdContent', mdContentDirective);
4561 * @module material.components.content
4566 * The `<md-content>` directive is a container element useful for scrollable content
4570 * - Add the `[layout-padding]` attribute to make the content padded.
4572 * <hljs lang="html">
4573 * <md-content layout-padding>
4574 * Lorem ipsum dolor sit amet, ne quod novum mei.
4580 function mdContentDirective($mdTheming) {
4583 controller: ['$scope', '$element', ContentController],
4584 link: function(scope, element, attr) {
4585 var node = element[0];
4587 $mdTheming(element);
4588 scope.$broadcast('$mdContentLoaded', element);
4590 iosScrollFix(element[0]);
4594 function ContentController($scope, $element) {
4595 this.$scope = $scope;
4596 this.$element = $element;
4599 mdContentDirective.$inject = ["$mdTheming"];
4601 function iosScrollFix(node) {
4603 // If we scroll where there is no more room for the webview to scroll,
4604 // by default the webview itself will scroll up and down, this looks really
4605 // bad. So if we are scrolling to the very top or bottom, add/subtract one
4606 angular.element(node).on('$md.pressdown', function(ev) {
4607 // Only touch events
4608 if (ev.pointer.type !== 't') return;
4609 // Don't let a child content's touchstart ruin it for us.
4610 if (ev.$materialScrollFixed) return;
4611 ev.$materialScrollFixed = true;
4613 if (node.scrollTop === 0) {
4615 } else if (node.scrollHeight === node.scrollTop + node.offsetHeight) {
4616 node.scrollTop -= 1;
4627 * @name material.components.dialog
4629 angular.module('material.components.dialog', [
4631 'material.components.backdrop'
4633 .directive('mdDialog', MdDialogDirective)
4634 .provider('$mdDialog', MdDialogProvider);
4636 function MdDialogDirective($$rAF, $mdTheming) {
4639 link: function(scope, element, attr) {
4640 $mdTheming(element);
4642 var content = element[0].querySelector('md-dialog-content');
4643 if (content && content.scrollHeight > content.clientHeight) {
4644 element.addClass('md-content-overflow');
4650 MdDialogDirective.$inject = ["$$rAF", "$mdTheming"];
4655 * @module material.components.dialog
4658 * `$mdDialog` opens a dialog over the app to inform users about critical information or require
4659 * them to make decisions. There are two approaches for setup: a simple promise API
4660 * and regular object syntax.
4664 * - The dialog is always given an isolate scope.
4665 * - The dialog's template must have an outer `<md-dialog>` element.
4666 * Inside, use an `<md-dialog-content>` element for the dialog's content, and use
4667 * an element with class `md-actions` for the dialog's actions.
4668 * - Dialogs must cover the entire application to keep interactions inside of them.
4669 * Use the `parent` option to change where dialogs are appended.
4672 * - Complex dialogs can be sized with `flex="percentage"`, i.e. `flex="66"`.
4673 * - Default max-width is 80% of the `rootElement` or `parent`.
4676 * <hljs lang="html">
4677 * <div ng-app="demoApp" ng-controller="EmployeeController">
4679 * <md-button ng-click="showAlert()" class="md-raised md-warn">
4684 * <md-button ng-click="showDialog($event)" class="md-raised">
4689 * <md-button ng-click="closeAlert()" ng-disabled="!hasAlert()" class="md-raised">
4694 * <md-button ng-click="showGreeting($event)" class="md-raised md-primary" >
4701 * ### JavaScript: object syntax
4703 * (function(angular, undefined){
4707 * .module('demoApp', ['ngMaterial'])
4708 * .controller('AppCtrl', AppController);
4710 * function AppController($scope, $mdDialog) {
4712 * $scope.showAlert = showAlert;
4713 * $scope.showDialog = showDialog;
4714 * $scope.items = [1, 2, 3];
4716 * // Internal method
4717 * function showAlert() {
4718 * alert = $mdDialog.alert({
4719 * title: 'Attention',
4720 * content: 'This is an example of how easy dialogs can be!',
4726 * .finally(function() {
4727 * alert = undefined;
4731 * function showDialog($event) {
4732 * var parentEl = angular.element(document.body);
4735 * targetEvent: $event,
4737 * '<md-dialog aria-label="List dialog">' +
4738 * ' <md-dialog-content>'+
4740 * ' <md-list-item ng-repeat="item in items">'+
4741 * ' <p>Number {{item}}</p>' +
4744 * ' </md-dialog-content>' +
4745 * ' <div class="md-actions">' +
4746 * ' <md-button ng-click="closeDialog()" class="md-primary">' +
4752 * items: $scope.items
4754 * controller: DialogController
4756 * function DialogController($scope, $mdDialog, items) {
4757 * $scope.items = items;
4758 * $scope.closeDialog = function() {
4767 * ### JavaScript: promise API syntax, custom dialog template
4769 * (function(angular, undefined){
4773 * .module('demoApp', ['ngMaterial'])
4774 * .controller('EmployeeController', EmployeeEditor)
4775 * .controller('GreetingController', GreetingController);
4777 * // Fictitious Employee Editor to show how to use simple and complex dialogs.
4779 * function EmployeeEditor($scope, $mdDialog) {
4782 * $scope.showAlert = showAlert;
4783 * $scope.closeAlert = closeAlert;
4784 * $scope.showGreeting = showCustomGreeting;
4786 * $scope.hasAlert = function() { return !!alert };
4787 * $scope.userName = $scope.userName || 'Bobby';
4789 * // Dialog #1 - Show simple alert dialog and cache
4790 * // reference to dialog instance
4792 * function showAlert() {
4793 * alert = $mdDialog.alert()
4794 * .title('Attention, ' + $scope.userName)
4795 * .content('This is an example of how easy dialogs can be!')
4800 * .finally(function() {
4801 * alert = undefined;
4805 * // Close the specified dialog instance and resolve with 'finished' flag
4806 * // Normally this is not needed, just use '$mdDialog.hide()' to close
4807 * // the most recent dialog popup.
4809 * function closeAlert() {
4810 * $mdDialog.hide( alert, "finished" );
4811 * alert = undefined;
4814 * // Dialog #2 - Demonstrate more complex dialogs construction and popup.
4816 * function showCustomGreeting($event) {
4818 * targetEvent: $event,
4822 * ' <md-dialog-content>Hello {{ employee }}!</md-dialog-content>' +
4824 * ' <div class="md-actions">' +
4825 * ' <md-button ng-click="closeDialog()" class="md-primary">' +
4826 * ' Close Greeting' +
4830 * controller: 'GreetingController',
4831 * onComplete: afterShowAnimation,
4832 * locals: { employee: $scope.userName }
4835 * // When the 'enter' animation finishes...
4837 * function afterShowAnimation(scope, element, options) {
4838 * // post-show code here: DOM element focus, etc.
4842 * // Dialog #3 - Demonstrate use of ControllerAs and passing $scope to dialog
4843 * // Here we used ng-controller="GreetingController as vm" and
4844 * // $scope.vm === <controller instance>
4846 * function showCustomGreeting() {
4849 * clickOutsideToClose: true,
4851 * scope: $scope, // use parent scope in template
4852 * preserveScope: true, // do not forget this if use parent scope
4854 * // Since GreetingController is instantiated with ControllerAs syntax
4855 * // AND we are passing the parent '$scope' to the dialog, we MUST
4856 * // use 'vm.<xxx>' in the template markup
4858 * template: '<md-dialog>' +
4859 * ' <md-dialog-content>' +
4860 * ' Hi There {{vm.employee}}' +
4861 * ' </md-dialog-content>' +
4864 * controller: function DialogController($scope, $mdDialog) {
4865 * $scope.closeDialog = function() {
4874 * // Greeting controller used with the more complex 'showCustomGreeting()' custom dialog
4876 * function GreetingController($scope, $mdDialog, employee) {
4877 * // Assigned from construction <code>locals</code> options...
4878 * $scope.employee = employee;
4880 * $scope.closeDialog = function() {
4881 * // Easily hides most recent dialog shown...
4882 * // no specific instance reference is needed.
4893 * @name $mdDialog#alert
4896 * Builds a preconfigured dialog with the specified message.
4898 * @returns {obj} an `$mdDialogPreset` with the chainable configuration methods:
4900 * - $mdDialogPreset#title(string) - sets title to string
4901 * - $mdDialogPreset#content(string) - sets content / message to string
4902 * - $mdDialogPreset#ok(string) - sets okay button text to string
4903 * - $mdDialogPreset#theme(string) - sets the theme of the dialog
4909 * @name $mdDialog#confirm
4912 * Builds a preconfigured dialog with the specified message. You can call show and the promise returned
4913 * will be resolved only if the user clicks the confirm action on the dialog.
4915 * @returns {obj} an `$mdDialogPreset` with the chainable configuration methods:
4917 * Additionally, it supports the following methods:
4919 * - $mdDialogPreset#title(string) - sets title to string
4920 * - $mdDialogPreset#content(string) - sets content / message to string
4921 * - $mdDialogPreset#ok(string) - sets okay button text to string
4922 * - $mdDialogPreset#cancel(string) - sets cancel button text to string
4923 * - $mdDialogPreset#theme(string) - sets the theme of the dialog
4929 * @name $mdDialog#show
4932 * Show a dialog with the specified options.
4934 * @param {object} optionsOrPreset Either provide an `$mdDialogPreset` returned from `alert()`, and
4935 * `confirm()`, or an options object with the following properties:
4936 * - `templateUrl` - `{string=}`: The url of a template that will be used as the content
4938 * - `template` - `{string=}`: Same as templateUrl, except this is an actual template string.
4939 * - `targetEvent` - `{DOMClickEvent=}`: A click's event object. When passed in as an option,
4940 * the location of the click will be used as the starting point for the opening animation
4941 * of the the dialog.
4942 * - `scope` - `{object=}`: the scope to link the template / controller to. If none is specified,
4943 * it will create a new isolate scope.
4944 * This scope will be destroyed when the dialog is removed unless `preserveScope` is set to true.
4945 * - `preserveScope` - `{boolean=}`: whether to preserve the scope when the element is removed. Default is false
4946 * - `disableParentScroll` - `{boolean=}`: Whether to disable scrolling while the dialog is open.
4948 * - `hasBackdrop` - `{boolean=}`: Whether there should be an opaque backdrop behind the dialog.
4950 * - `clickOutsideToClose` - `{boolean=}`: Whether the user can click outside the dialog to
4951 * close it. Default false.
4952 * - `escapeToClose` - `{boolean=}`: Whether the user can press escape to close the dialog.
4954 * - `focusOnOpen` - `{boolean=}`: An option to override focus behavior on open. Only disable if
4955 * focusing some other way, as focus management is required for dialogs to be accessible.
4957 * - `controller` - `{string=}`: The controller to associate with the dialog. The controller
4958 * will be injected with the local `$mdDialog`, which passes along a scope for the dialog.
4959 * - `locals` - `{object=}`: An object containing key/value pairs. The keys will be used as names
4960 * of values to inject into the controller. For example, `locals: {three: 3}` would inject
4961 * `three` into the controller, with the value 3. If `bindToController` is true, they will be
4962 * copied to the controller instead.
4963 * - `bindToController` - `bool`: bind the locals to the controller, instead of passing them in.
4964 * These values will not be available until after initialization.
4965 * - `resolve` - `{object=}`: Similar to locals, except it takes promises as values, and the
4966 * dialog will not open until all of the promises resolve.
4967 * - `controllerAs` - `{string=}`: An alias to assign the controller to on the scope.
4968 * - `parent` - `{element=}`: The element to append the dialog to. Defaults to appending
4969 * to the root element of the application.
4970 * - `onComplete` `{function=}`: Callback function used to announce when the show() action is
4973 * @returns {promise} A promise that can be resolved with `$mdDialog.hide()` or
4974 * rejected with `$mdDialog.cancel()`.
4979 * @name $mdDialog#hide
4982 * Hide an existing dialog and resolve the promise returned from `$mdDialog.show()`.
4984 * @param {*=} response An argument for the resolved promise.
4986 * @returns {promise} A promise that is resolved when the dialog has been closed.
4991 * @name $mdDialog#cancel
4994 * Hide an existing dialog and reject the promise returned from `$mdDialog.show()`.
4996 * @param {*=} response An argument for the rejected promise.
4998 * @returns {promise} A promise that is resolved when the dialog has been closed.
5001 function MdDialogProvider($$interimElementProvider) {
5003 var alertDialogMethods = ['title', 'content', 'ariaLabel', 'ok'];
5005 advancedDialogOptions.$inject = ["$mdDialog", "$mdTheming"];
5006 dialogDefaultOptions.$inject = ["$mdAria", "$document", "$mdUtil", "$mdConstant", "$mdTheming", "$mdDialog", "$timeout", "$rootElement", "$animate", "$$rAF", "$q"];
5007 return $$interimElementProvider('$mdDialog')
5009 methods: ['disableParentScroll', 'hasBackdrop', 'clickOutsideToClose', 'escapeToClose', 'targetEvent', 'parent'],
5010 options: dialogDefaultOptions
5012 .addPreset('alert', {
5013 methods: ['title', 'content', 'ariaLabel', 'ok', 'theme'],
5014 options: advancedDialogOptions
5016 .addPreset('confirm', {
5017 methods: ['title', 'content', 'ariaLabel', 'ok', 'cancel', 'theme'],
5018 options: advancedDialogOptions
5022 function advancedDialogOptions($mdDialog, $mdTheming) {
5025 '<md-dialog md-theme="{{ dialog.theme }}" aria-label="{{ dialog.ariaLabel }}">',
5026 '<md-dialog-content role="document" tabIndex="-1">',
5027 '<h2 class="md-title">{{ dialog.title }}</h2>',
5028 '<p>{{ dialog.content }}</p>',
5029 '</md-dialog-content>',
5030 '<div class="md-actions">',
5031 '<md-button ng-if="dialog.$type == \'confirm\'"' +
5032 ' ng-click="dialog.abort()" class="md-primary">',
5033 '{{ dialog.cancel }}',
5035 '<md-button ng-click="dialog.hide()" class="md-primary">',
5041 controller: function mdDialogCtrl() {
5042 this.hide = function() {
5043 $mdDialog.hide(true);
5045 this.abort = function() {
5049 controllerAs: 'dialog',
5050 bindToController: true,
5051 theme: $mdTheming.defaultTheme()
5056 function dialogDefaultOptions($mdAria, $document, $mdUtil, $mdConstant, $mdTheming, $mdDialog, $timeout, $rootElement, $animate, $$rAF, $q) {
5062 clickOutsideToClose: false,
5063 escapeToClose: true,
5066 disableParentScroll: true,
5067 transformTemplate: function(template) {
5068 return '<div class="md-dialog-container">' + template + '</div>';
5072 function trapFocus(ev) {
5073 var dialog = document.querySelector('md-dialog');
5075 if (dialog && !dialog.contains(ev.target)) {
5076 ev.stopImmediatePropagation();
5081 // On show method for dialogs
5082 function onShow(scope, element, options) {
5083 angular.element($document[0].body).addClass('md-dialog-is-showing');
5084 element = $mdUtil.extractElementByName(element, 'md-dialog');
5086 // Incase the user provides a raw dom element, always wrap it in jqLite
5087 options.parent = angular.element(options.parent);
5089 options.popInTarget = angular.element((options.targetEvent || {}).target);
5090 var closeButton = findCloseButton();
5092 if (options.hasBackdrop) {
5094 var computeFrom = (options.parent[0] == $document[0].body && $document[0].documentElement
5095 && $document[0].documentElement.scrollTop) ? angular.element($document[0].documentElement) : options.parent;
5096 var parentOffset = computeFrom.prop('scrollTop');
5097 options.backdrop = angular.element('<md-backdrop class="md-dialog-backdrop md-opaque">');
5098 options.backdrop.css('top', parentOffset +'px');
5099 $mdTheming.inherit(options.backdrop, options.parent);
5100 $animate.enter(options.backdrop, options.parent);
5101 element.css('top', parentOffset +'px');
5104 var role = 'dialog',
5105 elementToFocus = closeButton;
5107 if (options.$type === 'alert') {
5108 role = 'alertdialog';
5109 elementToFocus = element.find('md-dialog-content');
5112 configureAria(element.find('md-dialog'), role, options);
5114 document.addEventListener('focus', trapFocus, true);
5116 if (options.disableParentScroll) {
5117 options.lastOverflow = options.parent.css('overflow');
5118 options.parent.css('overflow', 'hidden');
5124 options.popInTarget && options.popInTarget.length && options.popInTarget
5128 applyAriaToSiblings(element, true);
5130 if (options.escapeToClose) {
5131 options.rootElementKeyupCallback = function(e) {
5132 if (e.keyCode === $mdConstant.KEY_CODE.ESCAPE) {
5133 $timeout($mdDialog.cancel);
5136 $rootElement.on('keyup', options.rootElementKeyupCallback);
5139 if (options.clickOutsideToClose) {
5140 options.dialogClickOutsideCallback = function(ev) {
5141 // Only close if we click the flex container outside the backdrop
5142 if (ev.target === element[0]) {
5143 $timeout($mdDialog.cancel);
5146 element.on('click', options.dialogClickOutsideCallback);
5149 if (options.focusOnOpen) {
5150 elementToFocus.focus();
5155 function findCloseButton() {
5156 //If no element with class dialog-close, try to find the last
5157 //button child in md-actions and assume it is a close button
5158 var closeButton = element[0].querySelector('.dialog-close');
5160 var actionButtons = element[0].querySelectorAll('.md-actions button');
5161 closeButton = actionButtons[ actionButtons.length - 1 ];
5163 return angular.element(closeButton);
5168 // On remove function for all dialogs
5169 function onRemove(scope, element, options) {
5170 angular.element($document[0].body).removeClass('md-dialog-is-showing');
5172 if (options.backdrop) {
5173 $animate.leave(options.backdrop);
5175 if (options.disableParentScroll) {
5176 options.parent.css('overflow', options.lastOverflow);
5177 delete options.lastOverflow;
5179 if (options.escapeToClose) {
5180 $rootElement.off('keyup', options.rootElementKeyupCallback);
5182 if (options.clickOutsideToClose) {
5183 element.off('click', options.dialogClickOutsideCallback);
5186 applyAriaToSiblings(element, false);
5188 document.removeEventListener('focus', trapFocus, true);
5190 return dialogPopOut(
5193 options.popInTarget && options.popInTarget.length && options.popInTarget
5196 options.popInTarget && options.popInTarget.focus();
5202 * Inject ARIA-specific attributes appropriate for Dialogs
5204 function configureAria(element, role, options) {
5211 var dialogContent = element.find('md-dialog-content');
5212 if (dialogContent.length === 0){
5213 dialogContent = element;
5216 var dialogId = element.attr('id') || ('dialog_' + $mdUtil.nextUid());
5217 dialogContent.attr('id', dialogId);
5218 element.attr('aria-describedby', dialogId);
5220 if (options.ariaLabel) {
5221 $mdAria.expect(element, 'aria-label', options.ariaLabel);
5224 $mdAria.expectAsync(element, 'aria-label', function() {
5225 var words = dialogContent.text().split(/\s+/);
5226 if (words.length > 3) words = words.slice(0,3).concat('...');
5227 return words.join(' ');
5232 * Utility function to filter out raw DOM nodes
5234 function isNodeOneOf(elem, nodeTypeArray) {
5235 if (nodeTypeArray.indexOf(elem.nodeName) !== -1) {
5240 * Walk DOM to apply or remove aria-hidden on sibling nodes
5241 * and parent sibling nodes
5243 * Prevents screen reader interaction behind modal window
5244 * on swipe interfaces
5246 function applyAriaToSiblings(element, value) {
5247 var attribute = 'aria-hidden';
5250 element = element[0];
5252 function walkDOM(element) {
5253 while (element.parentNode) {
5254 if (element === document.body) {
5257 var children = element.parentNode.children;
5258 for (var i = 0; i < children.length; i++) {
5259 // skip over child if it is an ascendant of the dialog
5260 // or a script or style tag
5261 if (element !== children[i] && !isNodeOneOf(children[i], ['SCRIPT', 'STYLE'])) {
5262 children[i].setAttribute(attribute, value);
5266 walkDOM(element = element.parentNode);
5272 function dialogPopIn(container, parentElement, clickElement) {
5273 var dialogEl = container.find('md-dialog');
5275 parentElement.append(container);
5276 transformToClickElement(dialogEl, clickElement);
5279 dialogEl.addClass('transition-in')
5280 .css($mdConstant.CSS.TRANSFORM, '');
5283 return $mdUtil.transitionEndPromise(dialogEl);
5286 function dialogPopOut(container, parentElement, clickElement) {
5287 var dialogEl = container.find('md-dialog');
5289 dialogEl.addClass('transition-out').removeClass('transition-in');
5290 transformToClickElement(dialogEl, clickElement);
5292 return $mdUtil.transitionEndPromise(dialogEl);
5295 function transformToClickElement(dialogEl, clickElement) {
5297 var clickRect = clickElement[0].getBoundingClientRect();
5298 var dialogRect = dialogEl[0].getBoundingClientRect();
5300 var scaleX = Math.min(0.5, clickRect.width / dialogRect.width);
5301 var scaleY = Math.min(0.5, clickRect.height / dialogRect.height);
5303 dialogEl.css($mdConstant.CSS.TRANSFORM, 'translate3d(' +
5304 (-dialogRect.left + clickRect.left + clickRect.width/2 - dialogRect.width/2) + 'px,' +
5305 (-dialogRect.top + clickRect.top + clickRect.height/2 - dialogRect.height/2) + 'px,' +
5306 '0) scale(' + scaleX + ',' + scaleY + ')'
5311 function dialogTransitionEnd(dialogEl) {
5312 var deferred = $q.defer();
5313 dialogEl.on($mdConstant.CSS.TRANSITIONEND, finished);
5314 function finished(ev) {
5315 //Make sure this transitionend didn't bubble up from a child
5316 if (ev.target === dialogEl[0]) {
5317 dialogEl.off($mdConstant.CSS.TRANSITIONEND, finished);
5321 return deferred.promise;
5326 MdDialogProvider.$inject = ["$$interimElementProvider"];
5334 * @name material.components.divider
5335 * @description Divider module!
5337 angular.module('material.components.divider', [
5340 .directive('mdDivider', MdDividerDirective);
5345 * @module material.components.divider
5349 * 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.
5351 * @param {boolean=} md-inset Add this attribute to activate the inset divider style.
5353 * <hljs lang="html">
5354 * <md-divider></md-divider>
5356 * <md-divider md-inset></md-divider>
5360 function MdDividerDirective($mdTheming) {
5366 MdDividerDirective.$inject = ["$mdTheming"];
5374 * @name material.components.gridList
5376 angular.module('material.components.gridList', ['material.core'])
5377 .directive('mdGridList', GridListDirective)
5378 .directive('mdGridTile', GridTileDirective)
5379 .directive('mdGridTileFooter', GridTileCaptionDirective)
5380 .directive('mdGridTileHeader', GridTileCaptionDirective)
5381 .factory('$mdGridLayout', GridLayoutFactory);
5386 * @module material.components.gridList
5389 * Grid lists are an alternative to standard list views. Grid lists are distinct
5390 * from grids used for layouts and other visual presentations.
5392 * A grid list is best suited to presenting a homogenous data type, typically
5393 * images, and is optimized for visual comprehension and differentiating between
5396 * A grid list is a continuous element consisting of tessellated, regular
5397 * subdivisions called cells that contain tiles (`md-grid-tile`).
5399 * <img src="//material-design.storage.googleapis.com/publish/v_2/material_ext_publish/0Bx4BSt6jniD7OVlEaXZ5YmU1Xzg/components_grids_usage2.png"
5400 * style="width: 300px; height: auto; margin-right: 16px;" alt="Concept of grid explained visually">
5401 * <img src="//material-design.storage.googleapis.com/publish/v_2/material_ext_publish/0Bx4BSt6jniD7VGhsOE5idWlJWXM/components_grids_usage3.png"
5402 * style="width: 300px; height: auto;" alt="Grid concepts legend">
5404 * Cells are arrayed vertically and horizontally within the grid.
5406 * Tiles hold content and can span one or more cells vertically or horizontally.
5408 * ### Responsive Attributes
5410 * The `md-grid-list` directive supports "responsive" attributes, which allow
5411 * different `md-cols`, `md-gutter` and `md-row-height` values depending on the
5412 * currently matching media query (as defined in `$mdConstant.MEDIA`).
5414 * In order to set a responsive attribute, first define the fallback value with
5415 * the standard attribute name, then add additional attributes with the
5416 * following convention: `{base-attribute-name}-{media-query-name}="{value}"`
5417 * (ie. `md-cols-lg="8"`)
5419 * @param {number} md-cols Number of columns in the grid.
5420 * @param {string} md-row-height One of
5422 * <li>CSS length - Fixed height rows (eg. `8px` or `1rem`)</li>
5423 * <li>`{width}:{height}` - Ratio of width to height (eg.
5424 * `md-row-height="16:9"`)</li>
5425 * <li>`"fit"` - Height will be determined by subdividing the available
5426 * height by the number of rows</li>
5428 * @param {string=} md-gutter The amount of space between tiles in CSS units
5430 * @param {expression=} md-on-layout Expression to evaluate after layout. Event
5431 * object is available as `$event`, and contains performance information.
5435 * <hljs lang="html">
5436 * <md-grid-list md-cols="5" md-gutter="1em" md-row-height="4:3">
5437 * <md-grid-tile></md-grid-tile>
5441 * Fixed-height rows:
5442 * <hljs lang="html">
5443 * <md-grid-list md-cols="4" md-row-height="200px" ...>
5444 * <md-grid-tile></md-grid-tile>
5449 * <hljs lang="html">
5450 * <md-grid-list md-cols="4" md-row-height="fit" style="height: 400px;" ...>
5451 * <md-grid-tile></md-grid-tile>
5455 * Using responsive attributes:
5456 * <hljs lang="html">
5461 * md-cols-gt-lg="12"
5463 * <md-grid-tile></md-grid-tile>
5467 function GridListDirective($interpolate, $mdConstant, $mdGridLayout, $mdMedia) {
5470 controller: GridListController,
5477 function postLink(scope, element, attrs, ctrl) {
5479 element.attr('role', 'list');
5481 // Provide the controller with a way to trigger layouts.
5482 ctrl.layoutDelegate = layoutDelegate;
5484 var invalidateLayout = angular.bind(ctrl, ctrl.invalidateLayout),
5485 unwatchAttrs = watchMedia();
5486 scope.$on('$destroy', unwatchMedia);
5489 * Watches for changes in media, invalidating layout as necessary.
5491 function watchMedia() {
5492 for (var mediaName in $mdConstant.MEDIA) {
5493 $mdMedia(mediaName); // initialize
5494 $mdMedia.getQuery($mdConstant.MEDIA[mediaName])
5495 .addListener(invalidateLayout);
5497 return $mdMedia.watchResponsiveAttributes(
5498 ['md-cols', 'md-row-height'], attrs, layoutIfMediaMatch);
5501 function unwatchMedia() {
5502 ctrl.layoutDelegate = angular.noop;
5505 for (var mediaName in $mdConstant.MEDIA) {
5506 $mdMedia.getQuery($mdConstant.MEDIA[mediaName])
5507 .removeListener(invalidateLayout);
5512 * Performs grid layout if the provided mediaName matches the currently
5513 * active media type.
5515 function layoutIfMediaMatch(mediaName) {
5516 if (mediaName == null) {
5517 // TODO(shyndman): It would be nice to only layout if we have
5518 // instances of attributes using this media type
5519 ctrl.invalidateLayout();
5520 } else if ($mdMedia(mediaName)) {
5521 ctrl.invalidateLayout();
5525 var lastLayoutProps;
5528 * Invokes the layout engine, and uses its results to lay out our
5531 * @param {boolean} tilesInvalidated Whether tiles have been
5532 * added/removed/moved since the last layout. This is to avoid situations
5533 * where tiles are replaced with properties identical to their removed
5536 function layoutDelegate(tilesInvalidated) {
5537 var tiles = getTileElements();
5539 tileSpans: getTileSpans(tiles),
5540 colCount: getColumnCount(),
5541 rowMode: getRowMode(),
5542 rowHeight: getRowHeight(),
5546 if (!tilesInvalidated && angular.equals(props, lastLayoutProps)) {
5551 $mdGridLayout(props.colCount, props.tileSpans, tiles)
5552 .map(function(tilePositions, rowCount) {
5556 style: getGridStyle(props.colCount, rowCount,
5557 props.gutter, props.rowMode, props.rowHeight)
5559 tiles: tilePositions.map(function(ps, i) {
5561 element: angular.element(tiles[i]),
5562 style: getTileStyle(ps.position, ps.spans,
5563 props.colCount, props.rowCount,
5564 props.gutter, props.rowMode, props.rowHeight)
5575 performance: performance
5579 lastLayoutProps = props;
5582 // Use $interpolate to do some simple string interpolation as a convenience.
5584 var startSymbol = $interpolate.startSymbol();
5585 var endSymbol = $interpolate.endSymbol();
5587 // Returns an expression wrapped in the interpolator's start and end symbols.
5588 function expr(exprStr) {
5589 return startSymbol + exprStr + endSymbol;
5592 // The amount of space a single 1x1 tile would take up (either width or height), used as
5593 // a basis for other calculations. This consists of taking the base size percent (as would be
5594 // if evenly dividing the size between cells), and then subtracting the size of one gutter.
5595 // However, since there are no gutters on the edges, each tile only uses a fration
5596 // (gutterShare = numGutters / numCells) of the gutter size. (Imagine having one gutter per
5597 // tile, and then breaking up the extra gutter on the edge evenly among the cells).
5598 var UNIT = $interpolate(expr('share') + '% - (' + expr('gutter') + ' * ' + expr('gutterShare') + ')');
5600 // The horizontal or vertical position of a tile, e.g., the 'top' or 'left' property value.
5601 // The position comes the size of a 1x1 tile plus gutter for each previous tile in the
5602 // row/column (offset).
5603 var POSITION = $interpolate('calc((' + expr('unit') + ' + ' + expr('gutter') + ') * ' + expr('offset') + ')');
5605 // The actual size of a tile, e.g., width or height, taking rowSpan or colSpan into account.
5606 // This is computed by multiplying the base unit by the rowSpan/colSpan, and then adding back
5607 // in the space that the gutter would normally have used (which was already accounted for in
5608 // the base unit calculation).
5609 var DIMENSION = $interpolate('calc((' + expr('unit') + ') * ' + expr('span') + ' + (' + expr('span') + ' - 1) * ' + expr('gutter') + ')');
5612 * Gets the styles applied to a tile element described by the given parameters.
5613 * @param {{row: number, col: number}} position The row and column indices of the tile.
5614 * @param {{row: number, col: number}} spans The rowSpan and colSpan of the tile.
5615 * @param {number} colCount The number of columns.
5616 * @param {number} rowCount The number of rows.
5617 * @param {string} gutter The amount of space between tiles. This will be something like
5619 * @param {string} rowMode The row height mode. Can be one of:
5620 * 'fixed': all rows have a fixed size, given by rowHeight,
5621 * 'ratio': row height defined as a ratio to width, or
5622 * 'fit': fit to the grid-list element height, divinding evenly among rows.
5623 * @param {string|number} rowHeight The height of a row. This is only used for 'fixed' mode and
5624 * for 'ratio' mode. For 'ratio' mode, this is the *ratio* of width-to-height (e.g., 0.75).
5625 * @returns {Object} Map of CSS properties to be applied to the style element. Will define
5626 * values for top, left, width, height, marginTop, and paddingTop.
5628 function getTileStyle(position, spans, colCount, rowCount, gutter, rowMode, rowHeight) {
5629 // TODO(shyndman): There are style caching opportunities here.
5631 // Percent of the available horizontal space that one column takes up.
5632 var hShare = (1 / colCount) * 100;
5634 // Fraction of the gutter size that each column takes up.
5635 var hGutterShare = (colCount - 1) / colCount;
5637 // Base horizontal size of a column.
5638 var hUnit = UNIT({share: hShare, gutterShare: hGutterShare, gutter: gutter});
5640 // The width and horizontal position of each tile is always calculated the same way, but the
5641 // height and vertical position depends on the rowMode.
5643 left: POSITION({ unit: hUnit, offset: position.col, gutter: gutter }),
5644 width: DIMENSION({ unit: hUnit, span: spans.col, gutter: gutter }),
5654 // In fixed mode, simply use the given rowHeight.
5655 style.top = POSITION({ unit: rowHeight, offset: position.row, gutter: gutter });
5656 style.height = DIMENSION({ unit: rowHeight, span: spans.row, gutter: gutter });
5660 // Percent of the available vertical space that one row takes up. Here, rowHeight holds
5661 // the ratio value. For example, if the width:height ratio is 4:3, rowHeight = 1.333.
5662 var vShare = hShare / rowHeight;
5664 // Base veritcal size of a row.
5665 var vUnit = UNIT({ share: vShare, gutterShare: hGutterShare, gutter: gutter });
5667 // padidngTop and marginTop are used to maintain the given aspect ratio, as
5668 // a percentage-based value for these properties is applied to the *width* of the
5669 // containing block. See http://www.w3.org/TR/CSS2/box.html#margin-properties
5670 style.paddingTop = DIMENSION({ unit: vUnit, span: spans.row, gutter: gutter});
5671 style.marginTop = POSITION({ unit: vUnit, offset: position.row, gutter: gutter });
5675 // Fraction of the gutter size that each column takes up.
5676 var vGutterShare = (rowCount - 1) / rowCount;
5678 // Percent of the available vertical space that one row takes up.
5679 var vShare = (1 / rowCount) * 100;
5681 // Base vertical size of a row.
5682 var vUnit = UNIT({share: vShare, gutterShare: vGutterShare, gutter: gutter});
5684 style.top = POSITION({unit: vUnit, offset: position.row, gutter: gutter});
5685 style.height = DIMENSION({unit: vUnit, span: spans.row, gutter: gutter});
5692 function getGridStyle(colCount, rowCount, gutter, rowMode, rowHeight) {
5700 style.height = DIMENSION({ unit: rowHeight, span: rowCount, gutter: gutter });
5704 // rowHeight is width / height
5705 var hGutterShare = colCount === 1 ? 0 : (colCount - 1) / colCount,
5706 hShare = (1 / colCount) * 100,
5707 vShare = hShare * (1 / rowHeight),
5708 vUnit = UNIT({ share: vShare, gutterShare: hGutterShare, gutter: gutter });
5710 style.paddingBottom = DIMENSION({ unit: vUnit, span: rowCount, gutter: gutter});
5714 // noop, as the height is user set
5721 function getTileElements() {
5722 return [].filter.call(element.children(), function(ele) {
5723 return ele.tagName == 'MD-GRID-TILE';
5728 * Gets an array of objects containing the rowspan and colspan for each tile.
5729 * @returns {Array<{row: number, col: number}>}
5731 function getTileSpans(tileElements) {
5732 return [].map.call(tileElements, function(ele) {
5733 var ctrl = angular.element(ele).controller('mdGridTile');
5736 $mdMedia.getResponsiveAttribute(ctrl.$attrs, 'md-rowspan'), 10) || 1,
5738 $mdMedia.getResponsiveAttribute(ctrl.$attrs, 'md-colspan'), 10) || 1
5743 function getColumnCount() {
5744 var colCount = parseInt($mdMedia.getResponsiveAttribute(attrs, 'md-cols'), 10);
5745 if (isNaN(colCount)) {
5746 throw 'md-grid-list: md-cols attribute was not found, or contained a non-numeric value';
5751 function getGutter() {
5752 return applyDefaultUnit($mdMedia.getResponsiveAttribute(attrs, 'md-gutter') || 1);
5755 function getRowHeight() {
5756 var rowHeight = $mdMedia.getResponsiveAttribute(attrs, 'md-row-height');
5757 switch (getRowMode()) {
5759 return applyDefaultUnit(rowHeight);
5761 var whRatio = rowHeight.split(':');
5762 return parseFloat(whRatio[0]) / parseFloat(whRatio[1]);
5768 function getRowMode() {
5769 var rowHeight = $mdMedia.getResponsiveAttribute(attrs, 'md-row-height');
5770 if (rowHeight == 'fit') {
5772 } else if (rowHeight.indexOf(':') !== -1) {
5779 function applyDefaultUnit(val) {
5780 return /\D$/.test(val) ? val : val + 'px';
5784 GridListDirective.$inject = ["$interpolate", "$mdConstant", "$mdGridLayout", "$mdMedia"];
5787 function GridListController($timeout) {
5788 this.layoutInvalidated = false;
5789 this.tilesInvalidated = false;
5790 this.$timeout_ = $timeout;
5791 this.layoutDelegate = angular.noop;
5793 GridListController.$inject = ["$timeout"];
5795 GridListController.prototype = {
5796 invalidateTiles: function() {
5797 this.tilesInvalidated = true;
5798 this.invalidateLayout();
5801 invalidateLayout: function() {
5802 if (this.layoutInvalidated) {
5805 this.layoutInvalidated = true;
5806 this.$timeout_(angular.bind(this, this.layout));
5809 layout: function() {
5811 this.layoutDelegate(this.tilesInvalidated);
5813 this.layoutInvalidated = false;
5814 this.tilesInvalidated = false;
5821 function GridLayoutFactory($mdUtil) {
5822 var defaultAnimator = GridTileAnimator;
5825 * Set the reflow animator callback
5827 GridLayout.animateWith = function(customAnimator) {
5828 defaultAnimator = !angular.isFunction(customAnimator) ? GridTileAnimator : customAnimator;
5834 * Publish layout function
5836 function GridLayout(colCount, tileSpans) {
5837 var self, layoutInfo, gridStyles, layoutTime, mapTime, reflowTime;
5839 layoutTime = $mdUtil.time(function() {
5840 layoutInfo = calculateGridFor(colCount, tileSpans);
5846 * An array of objects describing each tile's position in the grid.
5848 layoutInfo: function() {
5853 * Maps grid positioning to an element and a set of styles using the
5854 * provided updateFn.
5856 map: function(updateFn) {
5857 mapTime = $mdUtil.time(function() {
5858 var info = self.layoutInfo();
5859 gridStyles = updateFn(info.positioning, info.rowCount);
5865 * Default animator simply sets the element.css( <styles> ). An alternate
5866 * animator can be provided as an argument. The function has the following
5869 * function({grid: {element: JQLite, style: Object}, tiles: Array<{element: JQLite, style: Object}>)
5871 reflow: function(animatorFn) {
5872 reflowTime = $mdUtil.time(function() {
5873 var animator = animatorFn || defaultAnimator;
5874 animator(gridStyles.grid, gridStyles.tiles);
5880 * Timing for the most recent layout run.
5882 performance: function() {
5884 tileCount: tileSpans.length,
5885 layoutTime: layoutTime,
5887 reflowTime: reflowTime,
5888 totalTime: layoutTime + mapTime + reflowTime
5895 * Default Gridlist animator simple sets the css for each element;
5896 * NOTE: any transitions effects must be manually set in the CSS.
5900 * transition: all 700ms ease-out 50ms;
5904 function GridTileAnimator(grid, tiles) {
5905 grid.element.css(grid.style);
5906 tiles.forEach(function(t) {
5907 t.element.css(t.style);
5912 * Calculates the positions of tiles.
5914 * The algorithm works as follows:
5915 * An Array<Number> with length colCount (spaceTracker) keeps track of
5916 * available tiling positions, where elements of value 0 represents an
5917 * empty position. Space for a tile is reserved by finding a sequence of
5918 * 0s with length <= than the tile's colspan. When such a space has been
5919 * found, the occupied tile positions are incremented by the tile's
5920 * rowspan value, as these positions have become unavailable for that
5923 * If the end of a row has been reached without finding space for the
5924 * tile, spaceTracker's elements are each decremented by 1 to a minimum
5925 * of 0. Rows are searched in this fashion until space is found.
5927 function calculateGridFor(colCount, tileSpans) {
5930 spaceTracker = newSpaceTracker();
5933 positioning: tileSpans.map(function(spans, i) {
5936 position: reserveSpace(spans, i)
5939 rowCount: curRow + Math.max.apply(Math, spaceTracker)
5942 function reserveSpace(spans, i) {
5943 if (spans.col > colCount) {
5944 throw 'md-grid-list: Tile at position ' + i + ' has a colspan ' +
5945 '(' + spans.col + ') that exceeds the column count ' +
5946 '(' + colCount + ')';
5952 // TODO(shyndman): This loop isn't strictly necessary if you can
5953 // determine the minimum number of rows before a space opens up. To do
5954 // this, recognize that you've iterated across an entire row looking for
5955 // space, and if so fast-forward by the minimum rowSpan count. Repeat
5956 // until the required space opens up.
5957 while (end - start < spans.col) {
5958 if (curCol >= colCount) {
5963 start = spaceTracker.indexOf(0, curCol);
5964 if (start === -1 || (end = findEnd(start + 1)) === -1) {
5973 adjustRow(start, spans.col, spans.row);
5974 curCol = start + spans.col;
5982 function nextRow() {
5985 adjustRow(0, colCount, -1); // Decrement row spans by one
5988 function adjustRow(from, cols, by) {
5989 for (var i = from; i < from + cols; i++) {
5990 spaceTracker[i] = Math.max(spaceTracker[i] + by, 0);
5994 function findEnd(start) {
5996 for (i = start; i < spaceTracker.length; i++) {
5997 if (spaceTracker[i] !== 0) {
6002 if (i === spaceTracker.length) {
6007 function newSpaceTracker() {
6009 for (var i = 0; i < colCount; i++) {
6016 GridLayoutFactory.$inject = ["$mdUtil"];
6021 * @module material.components.gridList
6024 * Tiles contain the content of an `md-grid-list`. They span one or more grid
6025 * cells vertically or horizontally, and use `md-grid-tile-{footer,header}` to
6026 * display secondary content.
6028 * ### Responsive Attributes
6030 * The `md-grid-tile` directive supports "responsive" attributes, which allow
6031 * different `md-rowspan` and `md-colspan` values depending on the currently
6032 * matching media query (as defined in `$mdConstant.MEDIA`).
6034 * In order to set a responsive attribute, first define the fallback value with
6035 * the standard attribute name, then add additional attributes with the
6036 * following convention: `{base-attribute-name}-{media-query-name}="{value}"`
6037 * (ie. `md-colspan-sm="4"`)
6039 * @param {number=} md-colspan The number of columns to span (default 1). Cannot
6040 * exceed the number of columns in the grid. Supports interpolation.
6041 * @param {number=} md-rowspan The number of rows to span (default 1). Supports
6046 * <hljs lang="html">
6048 * <md-grid-tile-header>
6049 * <h3>This is a header</h3>
6050 * </md-grid-tile-header>
6055 * <hljs lang="html">
6057 * <md-grid-tile-footer>
6058 * <h3>This is a footer</h3>
6059 * </md-grid-tile-footer>
6063 * Spanning multiple rows/columns:
6064 * <hljs lang="html">
6065 * <md-grid-tile md-colspan="2" md-rowspan="3">
6069 * Responsive attributes:
6070 * <hljs lang="html">
6071 * <md-grid-tile md-colspan="1" md-colspan-sm="3" md-colspan-md="5">
6075 function GridTileDirective($mdMedia) {
6078 require: '^mdGridList',
6079 template: '<figure ng-transclude></figure>',
6082 // Simple controller that exposes attributes to the grid directive
6083 controller: ["$attrs", function($attrs) {
6084 this.$attrs = $attrs;
6089 function postLink(scope, element, attrs, gridCtrl) {
6091 element.attr('role', 'listitem');
6093 // If our colspan or rowspan changes, trigger a layout
6094 var unwatchAttrs = $mdMedia.watchResponsiveAttributes(['md-colspan', 'md-rowspan'],
6095 attrs, angular.bind(gridCtrl, gridCtrl.invalidateLayout));
6097 // Tile registration/deregistration
6098 gridCtrl.invalidateTiles();
6099 scope.$on('$destroy', function() {
6101 gridCtrl.invalidateLayout();
6104 if (angular.isDefined(scope.$parent.$index)) {
6105 scope.$watch(function() { return scope.$parent.$index; },
6106 function indexChanged(newIdx, oldIdx) {
6107 if (newIdx === oldIdx) {
6110 gridCtrl.invalidateTiles();
6115 GridTileDirective.$inject = ["$mdMedia"];
6118 function GridTileCaptionDirective() {
6120 template: '<figcaption ng-transclude></figcaption>',
6131 * @name material.components.icon
6135 angular.module('material.components.icon', [
6138 .directive('mdIcon', mdIconDirective);
6143 * @module material.components.icon
6148 * The `<md-icon>` directive is an markup element useful for showing an icon based on a font-icon
6149 * or a SVG. Icons are view-only elements that should not be used directly as buttons; instead nest a `<md-icon>`
6150 * inside a `md-button` to add hover and click features.
6152 * When using SVGs, both external SVGs (via URLs) or sets of SVGs [from icon sets] can be
6153 * easily loaded and used.When use font-icons, developers must following three (3) simple steps:
6156 * <li>Load the font library. e.g.<br/>
6157 * <link href="https://fonts.googleapis.com/icon?family=Material+Icons"
6158 * rel="stylesheet">
6160 * <li> Use either (a) font-icon class names or (b) font ligatures to render the font glyph by using its textual name</li>
6161 * <li> Use <md-icon md-font-icon="classname" /> or <br/>
6162 * use <md-icon md-font-set="font library classname or alias"> textual_name </md-icon> or <br/>
6163 * use <md-icon md-font-set="font library classname or alias"> numerical_character_reference </md-icon>
6167 * Full details for these steps can be found:
6170 * <li>http://google.github.io/material-design-icons/</li>
6171 * <li>http://google.github.io/material-design-icons/#icon-font-for-the-web</li>
6174 * The Material Design icon style <code>.material-icons</code> and the icon font references are published in
6175 * Material Design Icons:
6178 * <li>http://www.google.com/design/icons/</li>
6179 * <li>https://www.google.com/design/icons/#ic_accessibility</li>
6182 * <h2 id="material_design_icons">Material Design Icons</h2>
6183 * Using the Material Design Icon-Selector, developers can easily and quickly search for a Material Design font-icon and
6184 * determine its textual name and character reference code. Click on any icon to see the slide-up information
6185 * panel with details regarding a SVG download or information on the font-icon usage.
6187 * <a href="https://www.google.com/design/icons/#ic_accessibility" target="_blank" style="border-bottom:none;">
6188 * <img src="https://cloud.githubusercontent.com/assets/210413/7902490/fe8dd14c-0780-11e5-98fb-c821cc6475e6.png"
6189 * aria-label="Material Design Icon-Selector" style="max-width:75%;padding-left:10%">
6192 * <span class="image_caption">
6193 * Click on the image above to link to the
6194 * <a href="https://www.google.com/design/icons/#ic_accessibility" target="_blank">Material Design Icon-Selector</a>.
6197 * @param {string} md-font-icon Name of CSS icon associated with the font-face will be used
6198 * to render the icon. Requires the fonts and the named CSS styles to be preloaded.
6199 * @param {string} md-font-set CSS style name associated with the font library; which will be assigned as
6200 * the class for the font-icon ligature. This value may also be an alias that is used to lookup the classname;
6201 * internally use `$mdIconProvider.fontSet(<alias>)` to determine the style name.
6202 * @param {string} md-svg-src URL [or expression ] used to load, cache, and display an external SVG.
6203 * @param {string} md-svg-icon Name used for lookup of the icon from the internal cache; interpolated strings or
6204 * expressions may also be used. Specific set names can be used with the syntax `<set name>:<icon name>`.<br/><br/>
6205 * To use icon sets, developers are required to pre-register the sets using the `$mdIconProvider` service.
6206 * @param {string=} aria-label Labels icon for accessibility. If an empty string is provided, icon
6207 * will be hidden from accessibility layer with `aria-hidden="true"`. If there's no aria-label on the icon
6208 * nor a label on the parent element, a warning will be logged to the console.
6212 * <hljs lang="html">
6214 * <!-- Icon ID; may contain optional icon set prefix; icons must registered using $mdIconProvider -->
6215 * <md-icon md-svg-icon="social:android" aria-label="android " ></md-icon>
6217 * <!-- Icon urls; may be preloaded in templateCache -->
6218 * <md-icon md-svg-src="/android.svg" aria-label="android " ></md-icon>
6219 * <md-icon md-svg-src="{{ getAndroid() }}" aria-label="android " ></md-icon>
6223 * Use the <code>$mdIconProvider</code> to configure your application with
6227 * angular.module('appSvgIconSets', ['ngMaterial'])
6228 * .controller('DemoCtrl', function($scope) {})
6229 * .config(function($mdIconProvider) {
6231 * .iconSet('social', 'img/icons/sets/social-icons.svg', 24)
6232 * .defaultIconSet('img/icons/sets/core-icons.svg', 24);
6237 * When using Font Icons with classnames:
6238 * <hljs lang="html">
6240 * <md-icon md-font-icon="android" aria-label="android" ></md-icon>
6241 * <md-icon class="icon_home" aria-label="Home" ></md-icon>
6245 * When using Material Font Icons with ligatures:
6246 * <hljs lang="html">
6247 * <!-- For Material Design Icons -->
6248 * <!-- The class '.material-icons' is auto-added. -->
6249 * <md-icon> face </md-icon>
6250 * <md-icon class="md-light md-48"> face </md-icon>
6251 * <md-icon md-font-set="material-icons"> face </md-icon>
6252 * <md-icon> #xE87C; </md-icon>
6255 * When using other Font-Icon libraries:
6258 * // Specify a font-icon style alias
6259 * angular.config(function($mdIconProvider) {
6260 * $mdIconProvider.fontSet('fa', 'fontawesome');
6264 * <hljs lang="html">
6265 * <md-icon md-font-set="fa">email</md-icon>
6269 function mdIconDirective($mdIcon, $mdTheming, $mdAria, $interpolate ) {
6273 fontSet : '@mdFontSet',
6274 fontIcon: '@mdFontIcon',
6275 svgIcon : '@mdSvgIcon',
6276 svgSrc : '@mdSvgSrc'
6280 template: getTemplate,
6284 function getTemplate(element, attr) {
6285 var isEmptyAttr = function(key) { return angular.isDefined(attr[key]) ? attr[key].length == 0 : false },
6286 hasAttrValue = function(key) { return attr[key] && attr[key].length > 0; },
6287 attrValue = function(key) { return hasAttrValue(key) ? attr[key] : '' };
6289 // If using the deprecated md-font-icon API
6290 // If using ligature-based font-icons, transclude the ligature or NRCs
6292 var tmplFontIcon = '<span class="md-font {{classNames}}" ng-class="fontIcon"></span>';
6293 var tmplFontSet = '<span class="{{classNames}}" ng-transclude></span>';
6295 var tmpl = hasAttrValue('mdSvgIcon') ? '' :
6296 hasAttrValue('mdSvgSrc') ? '' :
6297 isEmptyAttr('mdFontIcon') ? '' :
6298 hasAttrValue('mdFontIcon') ? tmplFontIcon : tmplFontSet;
6300 // If available, lookup the fontSet style and add to the list of classnames
6301 // NOTE: Material Icons expects classnames like `.material-icons.md-48` instead of `.material-icons .md-48`
6303 var names = (tmpl == tmplFontSet) ? $mdIcon.fontSet(attrValue('mdFontSet')) + ' ' : '';
6304 names = (names + attrValue('class')).trim();
6306 return $interpolate( tmpl )({ classNames: names });
6311 * Directive postLink
6312 * Supports embedded SVGs, font-icons, & external SVGs
6314 function postLink(scope, element, attr) {
6315 $mdTheming(element);
6317 // If using a font-icon, then the textual name of the icon itself
6318 // provides the aria-label.
6320 var label = attr.alt || scope.fontIcon || scope.svgIcon || element.text();
6321 var attrName = attr.$normalize(attr.$attr.mdSvgIcon || attr.$attr.mdSvgSrc || '');
6323 if ( !attr['aria-label'] ) {
6325 if (label != '' && !parentsHaveText() ) {
6327 $mdAria.expect(element, 'aria-label', label);
6328 $mdAria.expect(element, 'role', 'img');
6330 } else if ( !element.text() ) {
6331 // If not a font-icon with ligature, then
6332 // hide from the accessibility layer.
6334 $mdAria.expect(element, 'aria-hidden', 'true');
6339 // Use either pre-configured SVG or URL source, respectively.
6340 attr.$observe(attrName, function(attrVal) {
6344 $mdIcon(attrVal).then(function(svg) {
6345 element.append(svg);
6351 function parentsHaveText() {
6352 var parent = element.parent();
6353 if (parent.attr('aria-label') || parent.text()) {
6356 else if(parent.parent().attr('aria-label') || parent.parent().text()) {
6363 mdIconDirective.$inject = ["$mdIcon", "$mdTheming", "$mdAria", "$interpolate"];
6370 .module('material.components.icon' )
6371 .provider('$mdIcon', MdIconProvider);
6375 * @name $mdIconProvider
6376 * @module material.components.icon
6379 * `$mdIconProvider` is used only to register icon IDs with URLs. These configuration features allow
6380 * icons and icon sets to be pre-registered and associated with source URLs **before** the `<md-icon />`
6381 * directives are compiled.
6383 * If using font-icons, the developer is repsonsible for loading the fonts.
6385 * If using SVGs, loading of the actual svg files are deferred to on-demand requests and are loaded
6386 * internally by the `$mdIcon` service using the `$http` service. When an SVG is requested by name/ID,
6387 * the `$mdIcon` service searches its registry for the associated source URL;
6388 * that URL is used to on-demand load and parse the SVG dynamically.
6392 * app.config(function($mdIconProvider) {
6394 * // Configure URLs for icons specified by [set:]id.
6397 * .defaultFontSet( 'fontawesome' )
6398 * .defaultIconSet('my/app/icons.svg') // Register a default set of SVG icons
6399 * .iconSet('social', 'my/app/social.svg') // Register a named icon set of SVGs
6400 * .icon('android', 'my/app/android.svg') // Register a specific icon (by name)
6401 * .icon('work:chair', 'my/app/chair.svg'); // Register icon in a specific set
6405 * SVG icons and icon sets can be easily pre-loaded and cached using either (a) a build process or (b) a runtime
6406 * **startup** process (shown below):
6409 * app.config(function($mdIconProvider) {
6411 * // Register a default set of SVG icon definitions
6412 * $mdIconProvider.defaultIconSet('my/app/icons.svg')
6415 * .run(function($http, $templateCache){
6417 * // Pre-fetch icons sources by URL and cache in the $templateCache...
6418 * // subsequent $http calls will look there first.
6420 * var urls = [ 'imy/app/icons.svg', 'img/icons/android.svg'];
6422 * angular.forEach(urls, function(url) {
6423 * $http.get(url, {cache: $templateCache});
6430 * NOTE: the loaded SVG data is subsequently cached internally for future requests.
6436 * @name $mdIconProvider#icon
6439 * Register a source URL for a specific icon name; the name may include optional 'icon set' name prefix.
6440 * These icons will later be retrieved from the cache using `$mdIcon( <icon name> )`
6442 * @param {string} id Icon name/id used to register the icon
6443 * @param {string} url specifies the external location for the data file. Used internally by `$http` to load the
6444 * data or as part of the lookup in `$templateCache` if pre-loading was configured.
6445 * @param {string=} iconSize Number indicating the width and height of the icons in the set. All icons
6446 * in the icon set must be the same size. Default size is 24.
6448 * @returns {obj} an `$mdIconProvider` reference; used to support method call chains for the API
6452 * app.config(function($mdIconProvider) {
6454 * // Configure URLs for icons specified by [set:]id.
6457 * .icon('android', 'my/app/android.svg') // Register a specific icon (by name)
6458 * .icon('work:chair', 'my/app/chair.svg'); // Register icon in a specific set
6465 * @name $mdIconProvider#iconSet
6468 * Register a source URL for a 'named' set of icons; group of SVG definitions where each definition
6469 * has an icon id. Individual icons can be subsequently retrieved from this cached set using
6470 * `$mdIcon(<icon set name>:<icon name>)`
6472 * @param {string} id Icon name/id used to register the iconset
6473 * @param {string} url specifies the external location for the data file. Used internally by `$http` to load the
6474 * data or as part of the lookup in `$templateCache` if pre-loading was configured.
6475 * @param {string=} iconSize Number indicating the width and height of the icons in the set. All icons
6476 * in the icon set must be the same size. Default size is 24.
6478 * @returns {obj} an `$mdIconProvider` reference; used to support method call chains for the API
6483 * app.config(function($mdIconProvider) {
6485 * // Configure URLs for icons specified by [set:]id.
6488 * .iconSet('social', 'my/app/social.svg') // Register a named icon set
6495 * @name $mdIconProvider#defaultIconSet
6498 * Register a source URL for the default 'named' set of icons. Unless explicitly registered,
6499 * subsequent lookups of icons will failover to search this 'default' icon set.
6500 * Icon can be retrieved from this cached, default set using `$mdIcon(<name>)`
6502 * @param {string} url specifies the external location for the data file. Used internally by `$http` to load the
6503 * data or as part of the lookup in `$templateCache` if pre-loading was configured.
6504 * @param {string=} iconSize Number indicating the width and height of the icons in the set. All icons
6505 * in the icon set must be the same size. Default size is 24.
6507 * @returns {obj} an `$mdIconProvider` reference; used to support method call chains for the API
6511 * app.config(function($mdIconProvider) {
6513 * // Configure URLs for icons specified by [set:]id.
6516 * .defaultIconSet( 'my/app/social.svg' ) // Register a default icon set
6523 * @name $mdIconProvider#defaultFontSet
6526 * When using Font-Icons, Angular Material assumes the the Material Design icons will be used and automatically
6527 * configures the default font-set == 'material-icons'. Note that the font-set references the font-icon library
6528 * class style that should be applied to the `<md-icon>`.
6530 * Configuring the default means that the attributes
6531 * `md-font-set="material-icons"` or `class="material-icons"` do not need to be explicitly declared on the
6532 * `<md-icon>` markup. For example:
6534 * `<md-icon> face </md-icon>`
6536 * `<span class="material-icons"> face </span>`, and
6538 * `<md-icon md-font-set="fa"> face </md-icon>`
6540 * `<span class="fa"> face </span>`
6542 * @param {string} name of the font-library style that should be applied to the md-icon DOM element
6546 * app.config(function($mdIconProvider) {
6547 * $mdIconProvider.defaultFontSet( 'fontawesome' );
6555 * @name $mdIconProvider#defaultIconSize
6558 * While `<md-icon />` markup can also be style with sizing CSS, this method configures
6559 * the default width **and** height used for all icons; unless overridden by specific CSS.
6560 * The default sizing is (24px, 24px).
6562 * @param {string} iconSize Number indicating the width and height of the icons in the set. All icons
6563 * in the icon set must be the same size. Default size is 24.
6565 * @returns {obj} an `$mdIconProvider` reference; used to support method call chains for the API
6569 * app.config(function($mdIconProvider) {
6571 * // Configure URLs for icons specified by [set:]id.
6574 * .defaultIconSize(36) // Register a default icon size (width == height)
6581 defaultIconSize: 24,
6582 defaultFontSet: 'material-icons',
6586 function MdIconProvider() { }
6588 MdIconProvider.prototype = {
6590 icon : function icon(id, url, iconSize) {
6591 if ( id.indexOf(':') == -1 ) id = '$default:' + id;
6593 config[id] = new ConfigurationItem(url, iconSize );
6596 iconSet : function iconSet(id, url, iconSize) {
6597 config[id] = new ConfigurationItem(url, iconSize );
6600 defaultIconSet : function defaultIconSet(url, iconSize) {
6601 var setName = '$default';
6603 if ( !config[setName] ) {
6604 config[setName] = new ConfigurationItem(url, iconSize );
6607 config[setName].iconSize = iconSize || config.defaultIconSize;
6612 * Register an alias name associated with a font-icon library style ;
6614 fontSet : function fontSet(alias, className) {
6615 config.fontSets.push({
6617 fontSet : className || alias
6622 * Specify a default style name associated with a font-icon library
6623 * fallback to Material Icons.
6626 defaultFontSet : function defaultFontSet(className) {
6627 config.defaultFontSet = !className ? '' : className;
6631 defaultIconSize : function defaultIconSize(iconSize) {
6632 config.defaultIconSize = iconSize;
6636 preloadIcons: function ($templateCache) {
6637 var iconProvider = this;
6640 id : 'md-tabs-arrow',
6641 url: 'md-tabs-arrow.svg',
6642 svg: '<svg version="1.1" x="0px" y="0px" viewBox="0 0 24 24"><g><polygon points="15.4,7.4 14,6 8,12 14,18 15.4,16.6 10.8,12 "/></g></svg>'
6646 url: 'md-close.svg',
6647 svg: '<svg version="1.1" x="0px" y="0px" viewBox="0 0 24 24"><g><path d="M19 6.41l-1.41-1.41-5.59 5.59-5.59-5.59-1.41 1.41 5.59 5.59-5.59 5.59 1.41 1.41 5.59-5.59 5.59 5.59 1.41-1.41-5.59-5.59z"/></g></svg>'
6651 url: 'md-cancel.svg',
6652 svg: '<svg version="1.1" x="0px" y="0px" viewBox="0 0 24 24"><g><path d="M12 2c-5.53 0-10 4.47-10 10s4.47 10 10 10 10-4.47 10-10-4.47-10-10-10zm5 13.59l-1.41 1.41-3.59-3.59-3.59 3.59-1.41-1.41 3.59-3.59-3.59-3.59 1.41-1.41 3.59 3.59 3.59-3.59 1.41 1.41-3.59 3.59 3.59 3.59z"/></g></svg>'
6657 svg: '<svg version="1.1" x="0px" y="0px" viewBox="0 0 100 100"><path d="M 50 0 L 100 14 L 92 80 L 50 100 L 8 80 L 0 14 Z" fill="#b2b2b2"></path><path d="M 50 5 L 6 18 L 13.5 77 L 50 94 Z" fill="#E42939"></path><path d="M 50 5 L 94 18 L 86.5 77 L 50 94 Z" fill="#B72833"></path><path d="M 50 7 L 83 75 L 72 75 L 65 59 L 50 59 L 50 50 L 61 50 L 50 26 Z" fill="#b2b2b2"></path><path d="M 50 7 L 17 75 L 28 75 L 35 59 L 50 59 L 50 50 L 39 50 L 50 26 Z" fill="#fff"></path></svg>'
6660 id: 'md-toggle-arrow',
6661 url: 'md-toggle-arrow-svg',
6662 svg: '<svg version="1.1" x="0px" y="0px" viewBox="0 0 48 48"><path d="M24 16l-12 12 2.83 2.83 9.17-9.17 9.17 9.17 2.83-2.83z"/><path d="M0 0h48v48h-48z" fill="none"/></svg>'
6666 svgRegistry.forEach(function(asset){
6667 iconProvider.icon(asset.id, asset.url);
6668 $templateCache.put(asset.url, asset.svg);
6673 $get : ['$http', '$q', '$log', '$templateCache', function($http, $q, $log, $templateCache) {
6674 this.preloadIcons($templateCache);
6675 return MdIconService(config, $http, $q, $log, $templateCache);
6680 * Configuration item stored in the Icon registry; used for lookups
6681 * to load if not already cached in the `loaded` cache
6683 function ConfigurationItem(url, iconSize) {
6685 this.iconSize = iconSize || config.defaultIconSize;
6691 * @module material.components.icon
6694 * The `$mdIcon` service is a function used to lookup SVG icons.
6696 * @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
6697 * 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
6698 * performed. The Id may be a unique icon ID or may include an iconSet ID prefix.
6700 * For the **id** query to work properly, this means that all id-to-URL mappings must have been previously configured
6701 * using the `$mdIconProvider`.
6703 * @returns {obj} Clone of the initial SVG DOM element; which was created from the SVG markup in the SVG data file.
6707 * function SomeDirective($mdIcon) {
6709 * // See if the icon has already been loaded, if not
6710 * // then lookup the icon from the registry cache, load and cache
6711 * // it for future requests.
6712 * // NOTE: ID queries require configuration with $mdIconProvider
6714 * $mdIcon('android').then(function(iconEl) { element.append(iconEl); });
6715 * $mdIcon('work:chair').then(function(iconEl) { element.append(iconEl); });
6717 * // Load and cache the external SVG using a URL
6719 * $mdIcon('img/icons/android.svg').then(function(iconEl) {
6720 * element.append(iconEl);
6725 * NOTE: The `<md-icon /> ` directive internally uses the `$mdIcon` service to query, loaded, and instantiate
6728 function MdIconService(config, $http, $q, $log, $templateCache) {
6730 var urlRegex = /[-a-zA-Z0-9@:%_\+.~#?&//=]{2,256}\.[a-z]{2,4}\b(\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?/i;
6732 Icon.prototype = { clone : cloneSVG, prepare: prepareAndStyle };
6733 getIcon.fontSet = findRegisteredFontSet;
6735 // Publish service...
6739 * Actual $mdIcon service is essentially a lookup function
6741 function getIcon(id) {
6744 // If already loaded and cached, use a clone of the cached icon.
6745 // Otherwise either load by URL, or lookup in the registry and then load by URL, and cache.
6747 if ( iconCache[id] ) return $q.when( iconCache[id].clone() );
6748 if ( urlRegex.test(id) ) return loadByURL(id).then( cacheIcon(id) );
6749 if ( id.indexOf(':') == -1 ) id = '$default:' + id;
6752 .catch(loadFromIconSet)
6753 .catch(announceIdNotFound)
6754 .catch(announceNotFound)
6755 .then( cacheIcon(id) );
6759 * Lookup registered fontSet style using its alias...
6762 function findRegisteredFontSet(alias) {
6763 var useDefault = angular.isUndefined(alias) || !(alias && alias.length);
6764 if ( useDefault ) return config.defaultFontSet;
6767 angular.forEach(config.fontSets, function(it){
6768 if ( it.alias == alias ) result = it.fontSet || result;
6775 * Prepare and cache the loaded icon for the specified `id`
6777 function cacheIcon( id ) {
6779 return function updateCache( icon ) {
6780 iconCache[id] = isIcon(icon) ? icon : new Icon(icon, config[id]);
6782 return iconCache[id].clone();
6787 * Lookup the configuration in the registry, if !registered throw an error
6788 * otherwise load the icon [on-demand] using the registered URL.
6791 function loadByID(id) {
6792 var iconConfig = config[id];
6794 return !iconConfig ? $q.reject(id) : loadByURL(iconConfig.url).then(function(icon) {
6795 return new Icon(icon, iconConfig);
6800 * Loads the file as XML and uses querySelector( <id> ) to find
6801 * the desired node...
6803 function loadFromIconSet(id) {
6804 var setName = id.substring(0, id.lastIndexOf(':')) || '$default';
6805 var iconSetConfig = config[setName];
6807 return !iconSetConfig ? $q.reject(id) : loadByURL(iconSetConfig.url).then(extractFromSet);
6809 function extractFromSet(set) {
6810 var iconName = id.slice(id.lastIndexOf(':') + 1);
6811 var icon = set.querySelector('#' + iconName);
6812 return !icon ? $q.reject(id) : new Icon(icon, iconSetConfig);
6817 * Load the icon by URL (may use the $templateCache).
6818 * Extract the data for later conversion to Icon
6820 function loadByURL(url) {
6822 .get(url, { cache: $templateCache })
6823 .then(function(response) {
6824 return angular.element('<div>').append(response.data).find('svg')[0];
6829 * User did not specify a URL and the ID has not been registered with the $mdIcon
6832 function announceIdNotFound(id) {
6835 if (angular.isString(id)) {
6836 msg = 'icon ' + id + ' not found';
6840 return $q.reject(msg || id);
6844 * Catch HTTP or generic errors not related to incorrect icon IDs.
6846 function announceNotFound(err) {
6847 var msg = angular.isString(err) ? err : (err.message || err.data || err.statusText);
6850 return $q.reject(msg);
6854 * Check target signature to see if it is an Icon instance.
6856 function isIcon(target) {
6857 return angular.isDefined(target.element) && angular.isDefined(target.config);
6861 * Define the Icon class
6863 function Icon(el, config) {
6864 if (el.tagName != 'svg') {
6865 el = angular.element('<svg xmlns="http://www.w3.org/2000/svg">').append(el)[0];
6868 // Inject the namespace if not available...
6869 if ( !el.getAttribute('xmlns') ) {
6870 el.setAttribute('xmlns', "http://www.w3.org/2000/svg");
6874 this.config = config;
6879 * Prepare the DOM element that will be cached in the
6880 * loaded iconCache store.
6882 function prepareAndStyle() {
6883 var iconSize = this.config ? this.config.iconSize : config.defaultIconSize;
6888 'preserveAspectRatio': 'xMidYMid meet',
6889 'viewBox' : this.element.getAttribute('viewBox') || ('0 0 ' + iconSize + ' ' + iconSize)
6890 }, function(val, attr) {
6891 this.element.setAttribute(attr, val);
6895 'pointer-events' : 'none',
6897 }, function(val, style) {
6898 this.element.style[style] = val;
6903 * Clone the Icon DOM element.
6905 function cloneSVG(){
6906 return this.element.cloneNode(true);
6917 * @name material.components.input
6920 angular.module('material.components.input', [
6923 .directive('mdInputContainer', mdInputContainerDirective)
6924 .directive('label', labelDirective)
6925 .directive('input', inputTextareaDirective)
6926 .directive('textarea', inputTextareaDirective)
6927 .directive('mdMaxlength', mdMaxlengthDirective)
6928 .directive('placeholder', placeholderDirective);
6932 * @name mdInputContainer
6933 * @module material.components.input
6938 * `<md-input-container>` is the parent of any input or textarea element.
6940 * Input and textarea elements will not behave properly unless the md-input-container
6941 * parent is provided.
6943 * @param md-is-error {expression=} When the given expression evaluates to true, the input container will go into error state. Defaults to erroring if the input has been touched and is invalid.
6944 * @param md-no-float {boolean=} When present, placeholders will not be converted to floating labels
6947 * <hljs lang="html">
6949 * <md-input-container>
6950 * <label>Username</label>
6951 * <input type="text" ng-model="user.name">
6952 * </md-input-container>
6954 * <md-input-container>
6955 * <label>Description</label>
6956 * <textarea ng-model="user.description"></textarea>
6957 * </md-input-container>
6961 function mdInputContainerDirective($mdTheming, $parse) {
6962 ContainerCtrl.$inject = ["$scope", "$element", "$attrs"];
6966 controller: ContainerCtrl
6969 function postLink(scope, element, attr) {
6970 $mdTheming(element);
6972 function ContainerCtrl($scope, $element, $attrs) {
6975 self.isErrorGetter = $attrs.mdIsError && $parse($attrs.mdIsError);
6977 self.delegateClick = function() {
6980 self.element = $element;
6981 self.setFocused = function(isFocused) {
6982 $element.toggleClass('md-input-focused', !!isFocused);
6984 self.setHasValue = function(hasValue) {
6985 $element.toggleClass('md-input-has-value', !!hasValue);
6987 self.setInvalid = function(isInvalid) {
6988 $element.toggleClass('md-input-invalid', !!isInvalid);
6990 $scope.$watch(function() {
6991 return self.label && self.input;
6992 }, function(hasLabelAndInput) {
6993 if (hasLabelAndInput && !self.label.attr('for')) {
6994 self.label.attr('for', self.input.attr('id'));
6999 mdInputContainerDirective.$inject = ["$mdTheming", "$parse"];
7001 function labelDirective() {
7004 require: '^?mdInputContainer',
7005 link: function(scope, element, attr, containerCtrl) {
7006 if (!containerCtrl || attr.mdNoFloat) return;
7008 containerCtrl.label = element;
7009 scope.$on('$destroy', function() {
7010 containerCtrl.label = null;
7020 * @module material.components.input
7023 * Use the `<input>` or the `<textarea>` as a child of an `<md-input-container>`.
7025 * @param {number=} md-maxlength The maximum number of characters allowed in this input. If this is specified, a character counter will be shown underneath the input.<br/><br/>
7026 * The purpose of **`md-maxlength`** is exactly to show the max length counter text. If you don't want the counter text and only need "plain" validation, you can use the "simple" `ng-maxlength` or maxlength attributes.
7027 * @param {string=} aria-label Aria-label is required when no label is present. A warning message will be logged in the console if not present.
7028 * @param {string=} placeholder An alternative approach to using aria-label when the label is not present. The placeholder text is copied to the aria-label attribute.
7031 * <hljs lang="html">
7032 * <md-input-container>
7033 * <label>Color</label>
7034 * <input type="text" ng-model="color" required md-maxlength="10">
7035 * </md-input-container>
7037 * <h3>With Errors</h3>
7039 * <hljs lang="html">
7040 * <form name="userForm">
7041 * <md-input-container>
7042 * <label>Last Name</label>
7043 * <input name="lastName" ng-model="lastName" required md-maxlength="10" minlength="4">
7044 * <div ng-messages="userForm.lastName.$error" ng-show="userForm.lastName.$dirty">
7045 * <div ng-message="required">This is required!</div>
7046 * <div ng-message="md-maxlength">That's too long!</div>
7047 * <div ng-message="minlength">That's too short!</div>
7049 * </md-input-container>
7050 * <md-input-container>
7051 * <label>Biography</label>
7052 * <textarea name="bio" ng-model="biography" required md-maxlength="150"></textarea>
7053 * <div ng-messages="userForm.bio.$error" ng-show="userForm.bio.$dirty">
7054 * <div ng-message="required">This is required!</div>
7055 * <div ng-message="md-maxlength">That's too long!</div>
7057 * </md-input-container>
7058 * <md-input-container>
7059 * <input aria-label='title' ng-model='title'>
7060 * </md-input-container>
7061 * <md-input-container>
7062 * <input placeholder='title' ng-model='title'>
7063 * </md-input-container>
7067 * Requires [ngMessages](https://docs.angularjs.org/api/ngMessages).
7068 * Behaves like the [AngularJS input directive](https://docs.angularjs.org/api/ng/directive/input).
7072 function inputTextareaDirective($mdUtil, $window, $mdAria) {
7075 require: ['^?mdInputContainer', '?ngModel'],
7079 function postLink(scope, element, attr, ctrls) {
7081 var containerCtrl = ctrls[0];
7082 var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel();
7083 var isReadonly = angular.isDefined(attr.readonly);
7085 if ( !containerCtrl ) return;
7086 if (containerCtrl.input) {
7087 throw new Error("<md-input-container> can only have *one* <input> or <textarea> child element!");
7089 containerCtrl.input = element;
7091 if(!containerCtrl.label) {
7092 $mdAria.expect(element, 'aria-label', element.attr('placeholder'));
7095 element.addClass('md-input');
7096 if (!element.attr('id')) {
7097 element.attr('id', 'input_' + $mdUtil.nextUid());
7100 if (element[0].tagName.toLowerCase() === 'textarea') {
7104 var isErrorGetter = containerCtrl.isErrorGetter || function() {
7105 return ngModelCtrl.$invalid && ngModelCtrl.$touched;
7107 scope.$watch(isErrorGetter, containerCtrl.setInvalid);
7109 ngModelCtrl.$parsers.push(ngModelPipelineCheckValue);
7110 ngModelCtrl.$formatters.push(ngModelPipelineCheckValue);
7112 element.on('input', inputCheckValue);
7116 .on('focus', function(ev) {
7117 containerCtrl.setFocused(true);
7119 .on('blur', function(ev) {
7120 containerCtrl.setFocused(false);
7126 //ngModelCtrl.$setTouched();
7127 //if( ngModelCtrl.$invalid ) containerCtrl.setInvalid();
7129 scope.$on('$destroy', function() {
7130 containerCtrl.setFocused(false);
7131 containerCtrl.setHasValue(false);
7132 containerCtrl.input = null;
7138 function ngModelPipelineCheckValue(arg) {
7139 containerCtrl.setHasValue(!ngModelCtrl.$isEmpty(arg));
7142 function inputCheckValue() {
7143 // An input's value counts if its length > 0,
7144 // or if the input's validity state says it has bad input (eg string in a number input)
7145 containerCtrl.setHasValue(element.val().length > 0 || (element[0].validity||{}).badInput);
7148 function setupTextarea() {
7149 var node = element[0];
7150 var onChangeTextarea = $mdUtil.debounce(growTextarea, 1);
7152 function pipelineListener(value) {
7158 ngModelCtrl.$formatters.push(pipelineListener);
7159 ngModelCtrl.$viewChangeListeners.push(pipelineListener);
7163 element.on('keydown input', onChangeTextarea);
7164 element.on('scroll', onScroll);
7165 angular.element($window).on('resize', onChangeTextarea);
7167 scope.$on('$destroy', function() {
7168 angular.element($window).off('resize', onChangeTextarea);
7171 function growTextarea() {
7172 node.style.height = "auto";
7174 var height = getHeight();
7175 if (height) node.style.height = height + 'px';
7178 function getHeight () {
7179 var line = node.scrollHeight - node.offsetHeight;
7180 return node.offsetHeight + (line > 0 ? line : 0);
7183 function onScroll(e) {
7185 // for smooth new line adding
7186 var line = node.scrollHeight - node.offsetHeight;
7187 var height = node.offsetHeight + line;
7188 node.style.height = height + 'px';
7193 inputTextareaDirective.$inject = ["$mdUtil", "$window", "$mdAria"];
7195 function mdMaxlengthDirective($animate) {
7198 require: ['ngModel', '^mdInputContainer'],
7202 function postLink(scope, element, attr, ctrls) {
7204 var ngModelCtrl = ctrls[0];
7205 var containerCtrl = ctrls[1];
7206 var charCountEl = angular.element('<div class="md-char-counter">');
7208 // Stop model from trimming. This makes it so whitespace
7209 // over the maxlength still counts as invalid.
7210 attr.$set('ngTrim', 'false');
7211 containerCtrl.element.append(charCountEl);
7213 ngModelCtrl.$formatters.push(renderCharCount);
7214 ngModelCtrl.$viewChangeListeners.push(renderCharCount);
7215 element.on('input keydown', function() {
7216 renderCharCount(); //make sure it's called with no args
7219 scope.$watch(attr.mdMaxlength, function(value) {
7221 if (angular.isNumber(value) && value > 0) {
7222 if (!charCountEl.parent().length) {
7223 $animate.enter(charCountEl, containerCtrl.element,
7224 angular.element(containerCtrl.element[0].lastElementChild));
7228 $animate.leave(charCountEl);
7232 ngModelCtrl.$validators['md-maxlength'] = function(modelValue, viewValue) {
7233 if (!angular.isNumber(maxlength) || maxlength < 0) {
7236 return ( modelValue || element.val() || viewValue || '' ).length <= maxlength;
7239 function renderCharCount(value) {
7240 charCountEl.text( ( element.val() || value || '' ).length + '/' + maxlength );
7245 mdMaxlengthDirective.$inject = ["$animate"];
7247 function placeholderDirective($log) {
7248 var blackListElements = ['MD-SELECT'];
7251 require: '^^?mdInputContainer',
7256 function postLink(scope, element, attr, inputContainer) {
7257 if (!inputContainer) return;
7258 if (blackListElements.indexOf(element[0].nodeName) != -1) return;
7259 if (angular.isDefined(inputContainer.element.attr('md-no-float'))) return;
7261 var placeholderText = attr.placeholder;
7262 element.removeAttr('placeholder');
7264 if ( inputContainer.element.find('label').length == 0 ) {
7265 var placeholder = '<label ng-click="delegateClick()">' + placeholderText + '</label>';
7267 inputContainer.element.addClass('md-icon-float');
7268 inputContainer.element.prepend(placeholder);
7270 $log.warn("The placeholder='" + placeholderText + "' will be ignored since this md-input-container has a child label element.");
7275 placeholderDirective.$inject = ["$log"];
7283 * @name material.components.list
7287 angular.module('material.components.list', [
7290 .controller('MdListController', MdListController)
7291 .directive('mdList', mdListDirective)
7292 .directive('mdListItem', mdListItemDirective);
7297 * @module material.components.list
7302 * The `<md-list>` directive is a list container for 1..n `<md-list-item>` tags.
7305 * <hljs lang="html">
7307 * <md-list-item class="md-2-line" ng-repeat="item in todos">
7308 * <md-checkbox ng-model="item.done"></md-checkbox>
7309 * <div class="md-list-item-text">
7310 * <h3>{{item.title}}</h3>
7311 * <p>{{item.description}}</p>
7318 function mdListDirective($mdTheming) {
7321 compile: function(tEl) {
7322 tEl[0].setAttribute('role', 'list');
7327 mdListDirective.$inject = ["$mdTheming"];
7331 * @module material.components.list
7336 * The `<md-list-item>` directive is a container intended for row items in a `<md-list>` container.
7339 * <hljs lang="html">
7342 * Item content in list
7348 function mdListItemDirective($mdAria, $mdConstant, $timeout) {
7349 var proxiedTypes = ['md-checkbox', 'md-switch'];
7352 controller: 'MdListController',
7353 compile: function(tEl, tAttrs) {
7354 // Check for proxy controls (no ng-click on parent, and a control inside)
7355 var secondaryItem = tEl[0].querySelector('.md-secondary');
7356 var hasProxiedElement;
7359 tEl[0].setAttribute('role', 'listitem');
7361 if (!tAttrs.ngClick) {
7362 for (var i = 0, type; type = proxiedTypes[i]; ++i) {
7363 if (proxyElement = tEl[0].querySelector(type)) {
7364 hasProxiedElement = true;
7368 if (hasProxiedElement) {
7370 } else if (!tEl[0].querySelector('md-button')) {
7371 tEl.addClass('md-no-proxy');
7379 function setupToggleAria() {
7380 var toggleTypes = ['md-switch', 'md-checkbox'];
7383 for (var i = 0, toggleType; toggleType = toggleTypes[i]; ++i) {
7384 if (toggle = tEl.find(toggleType)[0]) {
7385 if (!toggle.hasAttribute('aria-label')) {
7386 var p = tEl.find('p')[0];
7388 toggle.setAttribute('aria-label', 'Toggle ' + p.textContent);
7394 function wrapIn(type) {
7396 if (type == 'div') {
7397 container = angular.element('<div class="md-no-style md-list-item-inner">');
7398 container.append(tEl.contents());
7399 tEl.addClass('md-proxy-focus');
7401 container = angular.element('<md-button class="md-no-style"><div class="md-list-item-inner"></div></md-button>');
7402 var copiedAttrs = ['ng-click', 'aria-label', 'ng-disabled'];
7403 angular.forEach(copiedAttrs, function(attr) {
7404 if (tEl[0].hasAttribute(attr)) {
7405 container[0].setAttribute(attr, tEl[0].getAttribute(attr));
7406 tEl[0].removeAttribute(attr);
7409 container.children().eq(0).append(tEl.contents());
7412 tEl[0].setAttribute('tabindex', '-1');
7413 tEl.append(container);
7415 if (secondaryItem && secondaryItem.hasAttribute('ng-click')) {
7416 $mdAria.expect(secondaryItem, 'aria-label');
7417 var buttonWrapper = angular.element('<md-button class="md-secondary-container md-icon-button">');
7418 buttonWrapper.attr('ng-click', secondaryItem.getAttribute('ng-click'));
7419 secondaryItem.removeAttribute('ng-click');
7420 secondaryItem.setAttribute('tabindex', '-1');
7421 secondaryItem.classList.remove('md-secondary');
7422 buttonWrapper.append(secondaryItem);
7423 secondaryItem = buttonWrapper[0];
7426 // Check for a secondary item and move it outside
7427 if ( secondaryItem && (
7428 secondaryItem.hasAttribute('ng-click') ||
7430 isProxiedElement(secondaryItem) )
7432 tEl.addClass('md-with-secondary');
7433 tEl.append(secondaryItem);
7437 function isProxiedElement(el) {
7438 return proxiedTypes.indexOf(el.nodeName.toLowerCase()) != -1;
7443 function postLink($scope, $element, $attr, ctrl) {
7446 firstChild = $element[0].firstElementChild,
7447 hasClick = firstChild && firstChild.hasAttribute('ng-click');
7452 if ($element.hasClass('md-proxy-focus') && proxies.length) {
7453 angular.forEach(proxies, function(proxy) {
7454 proxy = angular.element(proxy);
7456 $scope.mouseActive = false;
7457 proxy.on('mousedown', function() {
7458 $scope.mouseActive = true;
7459 $timeout(function(){
7460 $scope.mouseActive = false;
7463 .on('focus', function() {
7464 if ($scope.mouseActive === false) { $element.addClass('md-focused'); }
7465 proxy.on('blur', function proxyOnBlur() {
7466 $element.removeClass('md-focused');
7467 proxy.off('blur', proxyOnBlur);
7473 function computeProxies() {
7474 var children = $element.children();
7475 if (children.length && !children[0].hasAttribute('ng-click')) {
7476 angular.forEach(proxiedTypes, function(type) {
7477 angular.forEach(firstChild.querySelectorAll(type), function(child) {
7478 proxies.push(child);
7483 function computeClickable() {
7484 if (proxies.length || hasClick) {
7485 $element.addClass('md-clickable');
7487 ctrl.attachRipple($scope, angular.element($element[0].querySelector('.md-no-style')));
7491 if (!hasClick && !proxies.length) {
7492 firstChild && firstChild.addEventListener('keypress', function(e) {
7493 if (e.target.nodeName != 'INPUT' && e.target.nodeName != 'TEXTAREA') {
7494 var keyCode = e.which || e.keyCode;
7495 if (keyCode == $mdConstant.KEY_CODE.SPACE) {
7499 e.stopPropagation();
7506 $element.off('click');
7507 $element.off('keypress');
7509 if (proxies.length && firstChild) {
7510 $element.children().eq(0).on('click', function(e) {
7511 if (firstChild.contains(e.target)) {
7512 angular.forEach(proxies, function(proxy) {
7513 if (e.target !== proxy && !proxy.contains(e.target)) {
7514 angular.element(proxy).triggerHandler('click');
7524 mdListItemDirective.$inject = ["$mdAria", "$mdConstant", "$timeout"];
7529 * @name MdListController
7530 * @module material.components.list
7533 function MdListController($scope, $element, $mdListInkRipple) {
7535 ctrl.attachRipple = attachRipple;
7537 function attachRipple (scope, element) {
7539 $mdListInkRipple.attach(scope, element, options);
7542 MdListController.$inject = ["$scope", "$element", "$mdListInkRipple"];
7551 * @name material.components.progressCircular
7552 * @description Circular Progress module!
7554 angular.module('material.components.progressCircular', [
7557 .directive('mdProgressCircular', MdProgressCircularDirective);
7561 * @name mdProgressCircular
7562 * @module material.components.progressCircular
7566 * The circular progress directive is used to make loading content in your app as delightful and
7567 * painless as possible by minimizing the amount of visual change a user sees before they can view
7568 * and interact with content.
7570 * For operations where the percentage of the operation completed can be determined, use a
7571 * determinate indicator. They give users a quick sense of how long an operation will take.
7573 * For operations where the user is asked to wait a moment while something finishes up, and it’s
7574 * not necessary to expose what's happening behind the scenes and how long it will take, use an
7575 * indeterminate indicator.
7577 * @param {string} md-mode Select from one of two modes: determinate and indeterminate.
7578 * @param {number=} value In determinate mode, this number represents the percentage of the
7579 * circular progress. Default: 0
7580 * @param {number=} md-diameter This specifies the diamter of the circular progress. Default: 48
7583 * <hljs lang="html">
7584 * <md-progress-circular md-mode="determinate" value="..."></md-progress-circular>
7586 * <md-progress-circular md-mode="determinate" ng-value="..."></md-progress-circular>
7588 * <md-progress-circular md-mode="determinate" value="..." md-diameter="100"></md-progress-circular>
7590 * <md-progress-circular md-mode="indeterminate"></md-progress-circular>
7593 function MdProgressCircularDirective($mdConstant, $mdTheming) {
7597 // The progress 'circle' is composed of two half-circles: the left side and the right
7598 // side. Each side has CSS applied to 'fill-in' the half-circle to the appropriate progress.
7599 '<div class="md-spinner-wrapper">' +
7600 '<div class="md-inner">' +
7601 '<div class="md-gap"></div>' +
7602 '<div class="md-left">' +
7603 '<div class="md-half-circle"></div>' +
7605 '<div class="md-right">' +
7606 '<div class="md-half-circle"></div>' +
7613 function compile(tElement) {
7614 // The javascript in this file is mainly responsible for setting the correct aria attributes.
7615 // The animation of the progress spinner is done entirely with just CSS.
7616 tElement.attr('aria-valuemin', 0);
7617 tElement.attr('aria-valuemax', 100);
7618 tElement.attr('role', 'progressbar');
7623 function postLink(scope, element, attr) {
7624 $mdTheming(element);
7625 var circle = element[0];
7627 // Scale the progress circle based on the default diameter.
7628 var diameter = attr.mdDiameter || 48;
7629 var scale = diameter / 48;
7630 circle.style[$mdConstant.CSS.TRANSFORM] = 'scale(' + scale + ')';
7632 attr.$observe('value', function(value) {
7633 var percentValue = clamp(value);
7634 element.attr('aria-valuenow', percentValue);
7639 * Clamps the value to be between 0 and 100.
7640 * @param {number} value The value to clamp.
7643 function clamp(value) {
7644 return Math.max(0, Math.min(value || 0, 100));
7647 MdProgressCircularDirective.$inject = ["$mdConstant", "$mdTheming"];
7655 * @name material.components.progressLinear
7656 * @description Linear Progress module!
7658 angular.module('material.components.progressLinear', [
7661 .directive('mdProgressLinear', MdProgressLinearDirective);
7665 * @name mdProgressLinear
7666 * @module material.components.progressLinear
7670 * The linear progress directive is used to make loading content in your app as delightful and painless as possible by minimizing the amount of visual change a user sees before they can view and interact with content. Each operation should only be represented by one activity indicator—for example, one refresh operation should not display both a refresh bar and an activity circle.
7672 * For operations where the percentage of the operation completed can be determined, use a determinate indicator. They give users a quick sense of how long an operation will take.
7674 * For operations where the user is asked to wait a moment while something finishes up, and it’s not necessary to expose what's happening behind the scenes and how long it will take, use an indeterminate indicator.
7676 * @param {string} md-mode Select from one of four modes: determinate, indeterminate, buffer or query.
7677 * @param {number=} value In determinate and buffer modes, this number represents the percentage of the primary progress bar. Default: 0
7678 * @param {number=} md-buffer-value In the buffer mode, this number represents the precentage of the secondary progress bar. Default: 0
7681 * <hljs lang="html">
7682 * <md-progress-linear md-mode="determinate" value="..."></md-progress-linear>
7684 * <md-progress-linear md-mode="determinate" ng-value="..."></md-progress-linear>
7686 * <md-progress-linear md-mode="indeterminate"></md-progress-linear>
7688 * <md-progress-linear md-mode="buffer" value="..." md-buffer-value="..."></md-progress-linear>
7690 * <md-progress-linear md-mode="query"></md-progress-linear>
7693 function MdProgressLinearDirective($$rAF, $mdConstant, $mdTheming) {
7697 template: '<div class="md-container">' +
7698 '<div class="md-dashed"></div>' +
7699 '<div class="md-bar md-bar1"></div>' +
7700 '<div class="md-bar md-bar2"></div>' +
7705 function compile(tElement, tAttrs, transclude) {
7706 tElement.attr('aria-valuemin', 0);
7707 tElement.attr('aria-valuemax', 100);
7708 tElement.attr('role', 'progressbar');
7712 function postLink(scope, element, attr) {
7713 $mdTheming(element);
7714 var bar1Style = element[0].querySelector('.md-bar1').style,
7715 bar2Style = element[0].querySelector('.md-bar2').style,
7716 container = angular.element(element[0].querySelector('.md-container'));
7718 attr.$observe('value', function(value) {
7719 if (attr.mdMode == 'query') {
7723 var clamped = clamp(value);
7724 element.attr('aria-valuenow', clamped);
7725 bar2Style[$mdConstant.CSS.TRANSFORM] = transforms[clamped];
7728 attr.$observe('mdBufferValue', function(value) {
7729 bar1Style[$mdConstant.CSS.TRANSFORM] = transforms[clamp(value)];
7733 container.addClass('md-ready');
7737 function clamp(value) {
7746 return Math.ceil(value || 0);
7749 MdProgressLinearDirective.$inject = ["$$rAF", "$mdConstant", "$mdTheming"];
7752 // **********************************************************
7754 // **********************************************************
7755 var transforms = (function() {
7756 var values = new Array(101);
7757 for(var i = 0; i < 101; i++){
7758 values[i] = makeTransform(i);
7763 function makeTransform(value){
7764 var scale = value/100;
7765 var translateX = (value-100)/2;
7766 return 'translateX(' + translateX.toString() + '%) scale(' + scale.toString() + ', 1)';
7776 * @name material.components.radioButton
7777 * @description radioButton module!
7779 angular.module('material.components.radioButton', [
7782 .directive('mdRadioGroup', mdRadioGroupDirective)
7783 .directive('mdRadioButton', mdRadioButtonDirective);
7787 * @module material.components.radioButton
7788 * @name mdRadioGroup
7793 * The `<md-radio-group>` directive identifies a grouping
7794 * container for the 1..n grouped radio buttons; specified using nested
7795 * `<md-radio-button>` tags.
7797 * As per the [material design spec](http://www.google.com/design/spec/style/color.html#color-ui-color-application)
7798 * the radio button is in the accent color by default. The primary color palette may be used with
7799 * the `md-primary` class.
7801 * Note: `<md-radio-group>` and `<md-radio-button>` handle tabindex differently
7802 * than the native `<input type='radio'>` controls. Whereas the native controls
7803 * force the user to tab through all the radio buttons, `<md-radio-group>`
7804 * is focusable, and by default the `<md-radio-button>`s are not.
7806 * @param {string} ng-model Assignable angular expression to data-bind to.
7807 * @param {boolean=} md-no-ink Use of attribute indicates flag to disable ink ripple effects.
7810 * <hljs lang="html">
7811 * <md-radio-group ng-model="selected">
7814 * ng-repeat="d in colorOptions"
7815 * ng-value="d.value" aria-label="{{ d.label }}">
7819 * </md-radio-button>
7825 function mdRadioGroupDirective($mdUtil, $mdConstant, $mdTheming, $timeout) {
7826 RadioGroupController.prototype = createRadioGroupControllerProto();
7830 controller: ['$element', RadioGroupController],
7831 require: ['mdRadioGroup', '?ngModel'],
7832 link: { pre: linkRadioGroup }
7835 function linkRadioGroup(scope, element, attr, ctrls) {
7836 $mdTheming(element);
7837 var rgCtrl = ctrls[0];
7838 var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel();
7840 function setFocus() {
7841 if (!element.hasClass('md-focused')) { element.addClass('md-focused'); }
7844 function keydownListener(ev) {
7845 var keyCode = ev.which || ev.keyCode;
7847 case $mdConstant.KEY_CODE.LEFT_ARROW:
7848 case $mdConstant.KEY_CODE.UP_ARROW:
7849 ev.preventDefault();
7850 rgCtrl.selectPrevious();
7854 case $mdConstant.KEY_CODE.RIGHT_ARROW:
7855 case $mdConstant.KEY_CODE.DOWN_ARROW:
7856 ev.preventDefault();
7857 rgCtrl.selectNext();
7861 case $mdConstant.KEY_CODE.ENTER:
7862 var form = angular.element($mdUtil.getClosest(element[0], 'form'));
7863 if (form.length > 0) {
7864 form.triggerHandler('submit');
7870 rgCtrl.init(ngModelCtrl);
7872 scope.mouseActive = false;
7874 'role': 'radiogroup',
7875 'tabIndex': element.attr('tabindex') || '0'
7877 .on('keydown', keydownListener)
7878 .on('mousedown', function(event) {
7879 scope.mouseActive = true;
7880 $timeout(function() {
7881 scope.mouseActive = false;
7884 .on('focus', function() {
7885 if(scope.mouseActive === false) { rgCtrl.$element.addClass('md-focused'); }
7887 .on('blur', function() { rgCtrl.$element.removeClass('md-focused'); });
7890 function RadioGroupController($element) {
7891 this._radioButtonRenderFns = [];
7892 this.$element = $element;
7895 function createRadioGroupControllerProto() {
7897 init: function(ngModelCtrl) {
7898 this._ngModelCtrl = ngModelCtrl;
7899 this._ngModelCtrl.$render = angular.bind(this, this.render);
7901 add: function(rbRender) {
7902 this._radioButtonRenderFns.push(rbRender);
7904 remove: function(rbRender) {
7905 var index = this._radioButtonRenderFns.indexOf(rbRender);
7907 this._radioButtonRenderFns.splice(index, 1);
7910 render: function() {
7911 this._radioButtonRenderFns.forEach(function(rbRender) {
7915 setViewValue: function(value, eventType) {
7916 this._ngModelCtrl.$setViewValue(value, eventType);
7917 // update the other radio buttons as well
7920 getViewValue: function() {
7921 return this._ngModelCtrl.$viewValue;
7923 selectNext: function() {
7924 return changeSelectedButton(this.$element, 1);
7926 selectPrevious: function() {
7927 return changeSelectedButton(this.$element, -1);
7929 setActiveDescendant: function (radioId) {
7930 this.$element.attr('aria-activedescendant', radioId);
7935 * Change the radio group's selected button by a given increment.
7936 * If no button is selected, select the first button.
7938 function changeSelectedButton(parent, increment) {
7939 // Coerce all child radio buttons into an array, then wrap then in an iterator
7940 var buttons = $mdUtil.iterator(parent[0].querySelectorAll('md-radio-button'), true);
7942 if (buttons.count()) {
7943 var validate = function (button) {
7944 // If disabled, then NOT valid
7945 return !angular.element(button).attr("disabled");
7947 var selected = parent[0].querySelector('md-radio-button.md-checked');
7948 var target = buttons[increment < 0 ? 'previous' : 'next'](selected, validate) || buttons.first();
7949 // Activate radioButton's click listener (triggerHandler won't create a real click event)
7950 angular.element(target).triggerHandler('click');
7957 mdRadioGroupDirective.$inject = ["$mdUtil", "$mdConstant", "$mdTheming", "$timeout"];
7961 * @module material.components.radioButton
7962 * @name mdRadioButton
7967 * The `<md-radio-button>`directive is the child directive required to be used within `<md-radio-group>` elements.
7969 * While similar to the `<input type="radio" ng-model="" value="">` directive,
7970 * the `<md-radio-button>` directive provides ink effects, ARIA support, and
7971 * supports use within named radio groups.
7973 * @param {string} ngModel Assignable angular expression to data-bind to.
7974 * @param {string=} ngChange Angular expression to be executed when input changes due to user
7975 * interaction with the input element.
7976 * @param {string} ngValue Angular expression which sets the value to which the expression should
7977 * be set when selected.*
7978 * @param {string} value The value to which the expression should be set when selected.
7979 * @param {string=} name Property name of the form under which the control is published.
7980 * @param {string=} aria-label Adds label to radio button for accessibility.
7981 * Defaults to radio button's text. If no text content is available, a warning will be logged.
7984 * <hljs lang="html">
7986 * <md-radio-button value="1" aria-label="Label 1">
7988 * </md-radio-button>
7990 * <md-radio-button ng-model="color" ng-value="specialValue" aria-label="Green">
7992 * </md-radio-button>
7997 function mdRadioButtonDirective($mdAria, $mdUtil, $mdTheming) {
7999 var CHECKED_CSS = 'md-checked';
8003 require: '^mdRadioGroup',
8005 template: '<div class="md-container" md-ink-ripple md-ink-ripple-checkbox>' +
8006 '<div class="md-off"></div>' +
8007 '<div class="md-on"></div>' +
8009 '<div ng-transclude class="md-label"></div>',
8013 function link(scope, element, attr, rgCtrl) {
8016 $mdTheming(element);
8017 configureAria(element, scope);
8020 attr.$observe('value', render);
8023 .on('click', listener)
8024 .on('$destroy', function() {
8025 rgCtrl.remove(render);
8028 function listener(ev) {
8029 if (element[0].hasAttribute('disabled')) return;
8031 scope.$apply(function() {
8032 rgCtrl.setViewValue(attr.value, ev && ev.type);
8037 var checked = (rgCtrl.getViewValue() == attr.value);
8038 if (checked === lastChecked) {
8041 lastChecked = checked;
8042 element.attr('aria-checked', checked);
8044 element.addClass(CHECKED_CSS);
8045 rgCtrl.setActiveDescendant(element.attr('id'));
8047 element.removeClass(CHECKED_CSS);
8051 * Inject ARIA-specific attributes appropriate for each radio button
8053 function configureAria( element, scope ){
8054 scope.ariaId = buildAriaID();
8057 'id' : scope.ariaId,
8059 'aria-checked' : 'false'
8062 $mdAria.expectWithText(element, 'aria-label');
8065 * Build a unique ID for each radio button that will be used with aria-activedescendant.
8066 * Preserve existing ID if already specified.
8067 * @returns {*|string}
8069 function buildAriaID() {
8070 return attr.id || ( 'radio' + "_" + $mdUtil.nextUid() );
8075 mdRadioButtonDirective.$inject = ["$mdAria", "$mdUtil", "$mdTheming"];
8083 * @name material.components.select
8086 /***************************************************
8089 **DOCUMENTATION AND DEMOS**
8091 - [ ] ng-model with child mdOptions (basic)
8092 - [ ] ng-model="foo" ng-model-options="{ trackBy: '$value.id' }" for objects
8093 - [ ] mdOption with value
8094 - [ ] Usage with input inside
8096 ### TODO - POST RC1 ###
8097 - [ ] Abstract placement logic in $mdSelect service to $mdMenu service
8099 ***************************************************/
8101 var SELECT_EDGE_MARGIN = 8;
8102 var selectNextId = 0;
8104 angular.module('material.components.select', [
8106 'material.components.backdrop'
8108 .directive('mdSelect', SelectDirective)
8109 .directive('mdSelectMenu', SelectMenuDirective)
8110 .directive('mdOption', OptionDirective)
8111 .directive('mdOptgroup', OptgroupDirective)
8112 .provider('$mdSelect', SelectProvider);
8119 * @module material.components.select
8121 * @description Displays a select box, bound to an ng-model.
8123 * @param {expression} ng-model The model!
8124 * @param {boolean=} multiple Whether it's multiple.
8125 * @param {string=} placeholder Placeholder hint text.
8126 * @param {string=} aria-label Optional label for accessibility. Only necessary if no placeholder or
8127 * explicit label is present.
8130 * With a placeholder (label and aria-label are added dynamically)
8131 * <hljs lang="html">
8133 * ng-model="someModel"
8134 * placeholder="Select a state">
8135 * <md-option ng-value="opt" ng-repeat="opt in neighborhoods2">{{ opt }}</md-option>
8139 * With an explicit label
8140 * <hljs lang="html">
8142 * ng-model="someModel">
8143 * <md-select-label>Select a state</md-select-label>
8144 * <md-option ng-value="opt" ng-repeat="opt in neighborhoods2">{{ opt }}</md-option>
8148 function SelectDirective($mdSelect, $mdUtil, $mdTheming, $mdAria, $interpolate, $compile, $parse) {
8151 require: ['mdSelect', 'ngModel', '?^form'],
8153 controller: function() { } // empty placeholder controller to be initialized in link
8156 function compile(element, attr) {
8157 // The user is allowed to provide a label for the select as md-select-label child
8158 var labelEl = element.find('md-select-label').remove();
8160 // If not provided, we automatically make one
8161 if (!labelEl.length) {
8162 labelEl = angular.element('<md-select-label><span></span></md-select-label>');
8164 if (!labelEl[0].firstElementChild) {
8165 var spanWrapper = angular.element('<span>');
8166 spanWrapper.append(labelEl.contents());
8167 labelEl.append(spanWrapper);
8170 labelEl.append('<span class="md-select-icon" aria-hidden="true"></span>');
8171 labelEl.addClass('md-select-label');
8172 if (!labelEl[0].hasAttribute('id')) {
8173 labelEl.attr('id', 'select_label_' + $mdUtil.nextUid());
8176 // There's got to be an md-content inside. If there's not one, let's add it.
8177 if (!element.find('md-content').length) {
8178 element.append( angular.element('<md-content>').append(element.contents()) );
8181 // Add progress spinner for md-options-loading
8182 if (attr.mdOnOpen) {
8183 element.find('md-content').prepend(
8184 angular.element('<md-progress-circular>')
8185 .attr('md-mode', 'indeterminate')
8186 .attr('ng-hide', '$$loadingAsyncDone')
8193 var autofillClone = angular.element('<select class="md-visually-hidden">');
8194 autofillClone.attr({
8195 'name': '.' + attr.name,
8196 'ng-model': attr.ngModel,
8197 'aria-hidden': 'true',
8200 var opts = element.find('md-option');
8201 angular.forEach(opts, function(el) {
8202 var newEl = angular.element('<option>' + el.innerHTML + '</option>');
8203 if (el.hasAttribute('ng-value')) newEl.attr('ng-value', el.getAttribute('ng-value'));
8204 else if (el.hasAttribute('value')) newEl.attr('value', el.getAttribute('value'));
8205 autofillClone.append(newEl);
8208 element.parent().append(autofillClone);
8211 // Use everything that's left inside element.contents() as the contents of the menu
8212 var selectTemplate = '<div class="md-select-menu-container">' +
8213 '<md-select-menu ' +
8214 (angular.isDefined(attr.multiple) ? 'multiple' : '') + '>' +
8216 '</md-select-menu></div>';
8218 element.empty().append(labelEl);
8220 attr.tabindex = attr.tabindex || '0';
8222 return function postLink(scope, element, attr, ctrls) {
8226 var mdSelectCtrl = ctrls[0];
8227 var ngModel = ctrls[1];
8228 var formCtrl = ctrls[2];
8230 var labelEl = element.find('md-select-label');
8231 var customLabel = labelEl.text().length !== 0;
8232 var selectContainer, selectScope, selectMenuCtrl;
8235 $mdTheming(element);
8237 if (attr.name && formCtrl) {
8238 var selectEl = element.parent()[0].querySelector('select[name=".' + attr.name + '"]')
8239 formCtrl.$removeControl(angular.element(selectEl).controller());
8242 var originalRender = ngModel.$render;
8243 ngModel.$render = function() {
8248 mdSelectCtrl.setLabelText = function(text) {
8249 if (customLabel) return; // Assume that user is handling it on their own
8250 mdSelectCtrl.setIsPlaceholder(!text);
8251 text = text || attr.placeholder || '';
8252 var target = customLabel ? labelEl : labelEl.children().eq(0);
8256 mdSelectCtrl.setIsPlaceholder = function(val) {
8257 val ? labelEl.addClass('md-placeholder') : labelEl.removeClass('md-placeholder');
8260 scope.$$postDigest(function() {
8265 function setAriaLabel() {
8266 var labelText = element.attr('placeholder');
8268 labelText = element.find('md-select-label').text();
8270 $mdAria.expect(element, 'aria-label', labelText);
8273 function syncLabelText() {
8274 if (selectContainer) {
8275 selectMenuCtrl = selectMenuCtrl || selectContainer.find('md-select-menu').controller('mdSelectMenu');
8276 mdSelectCtrl.setLabelText(selectMenuCtrl.selectedLabels());
8280 var deregisterWatcher;
8281 attr.$observe('ngMultiple', function(val) {
8282 if (deregisterWatcher) deregisterWatcher();
8283 var parser = $parse(val);
8284 deregisterWatcher = scope.$watch(function() { return parser(scope); }, function(multiple, prevVal) {
8285 if (multiple === undefined && prevVal === undefined) return; // assume compiler did a good job
8287 element.attr('multiple', 'multiple');
8289 element.removeAttr('multiple');
8291 if (selectContainer) {
8292 selectMenuCtrl.setMultiple(multiple);
8293 originalRender = ngModel.$render;
8294 ngModel.$render = function() {
8298 selectMenuCtrl.refreshViewValue();
8304 attr.$observe('disabled', function(disabled) {
8305 if (typeof disabled == "string") {
8308 // Prevent click event being registered twice
8309 if (isDisabled !== undefined && isDisabled === disabled) {
8312 isDisabled = disabled;
8314 element.attr({'tabindex': -1, 'aria-disabled': 'true'});
8315 element.off('click', openSelect);
8316 element.off('keydown', handleKeypress);
8318 element.attr({'tabindex': attr.tabindex, 'aria-disabled': 'false'});
8319 element.on('click', openSelect);
8320 element.on('keydown', handleKeypress);
8323 if (!attr.disabled && !attr.ngDisabled) {
8324 element.attr({'tabindex': attr.tabindex, 'aria-disabled': 'false'});
8325 element.on('click', openSelect);
8326 element.on('keydown', handleKeypress);
8331 'aria-expanded': 'false'
8333 if (!element[0].hasAttribute('id')) {
8334 ariaAttrs.id = 'select_' + $mdUtil.nextUid();
8336 element.attr(ariaAttrs);
8338 scope.$on('$destroy', function() {
8340 $mdSelect.cancel().then(function() {
8341 selectContainer.remove();
8344 selectContainer.remove();
8349 // Create a fake select to find out the label value
8350 function createSelect() {
8351 selectContainer = angular.element(selectTemplate);
8352 var selectEl = selectContainer.find('md-select-menu');
8353 selectEl.data('$ngModelController', ngModel);
8354 selectEl.data('$mdSelectController', mdSelectCtrl);
8355 selectScope = scope.$new();
8356 selectContainer = $compile(selectContainer)(selectScope);
8357 selectMenuCtrl = selectContainer.find('md-select-menu').controller('mdSelectMenu');
8360 function handleKeypress(e) {
8361 var allowedCodes = [32, 13, 38, 40];
8362 if (allowedCodes.indexOf(e.keyCode) != -1 ) {
8363 // prevent page scrolling on interaction
8367 if (e.keyCode <= 90 && e.keyCode >= 31) {
8369 var node = selectMenuCtrl.optNodeForKeyboardSearch(e);
8371 var optionCtrl = angular.element(node).controller('mdOption');
8372 if (!selectMenuCtrl.isMultiple) {
8373 selectMenuCtrl.deselect( Object.keys(selectMenuCtrl.selected)[0] );
8375 selectMenuCtrl.select(optionCtrl.hashKey, optionCtrl.value);
8376 selectMenuCtrl.refreshViewValue();
8382 function openSelect() {
8383 scope.$evalAsync(function() {
8387 preserveScope: true,
8389 element: selectContainer,
8392 loadingAsync: attr.mdOnOpen ? scope.$eval(attr.mdOnOpen) || true : false,
8393 }).then(function(selectedText) {
8401 SelectDirective.$inject = ["$mdSelect", "$mdUtil", "$mdTheming", "$mdAria", "$interpolate", "$compile", "$parse"];
8403 function SelectMenuDirective($parse, $mdUtil, $mdTheming) {
8405 SelectMenuController.$inject = ["$scope", "$attrs", "$element"];
8408 require: ['mdSelectMenu', '?ngModel'],
8409 controller: SelectMenuController,
8410 link: { pre: preLink }
8413 // We use preLink instead of postLink to ensure that the select is initialized before
8414 // its child options run postLink.
8415 function preLink(scope, element, attr, ctrls) {
8416 var selectCtrl = ctrls[0];
8417 var ngModel = ctrls[1];
8419 $mdTheming(element);
8420 element.on('click', clickListener);
8421 element.on('keypress', keyListener);
8422 if (ngModel) selectCtrl.init(ngModel);
8425 function configureAria() {
8427 'id': 'select_menu_' + $mdUtil.nextUid(),
8429 'aria-multiselectable': (selectCtrl.isMultiple ? 'true' : 'false')
8433 function keyListener(e) {
8434 if (e.keyCode == 13 || e.keyCode == 32) {
8439 function clickListener(ev) {
8440 var option = $mdUtil.getClosest(ev.target, 'md-option');
8441 var optionCtrl = option && angular.element(option).data('$mdOptionController');
8442 if (!option || !optionCtrl) return;
8444 var optionHashKey = selectCtrl.hashGetter(optionCtrl.value);
8445 var isSelected = angular.isDefined(selectCtrl.selected[optionHashKey]);
8447 scope.$apply(function() {
8448 if (selectCtrl.isMultiple) {
8450 selectCtrl.deselect(optionHashKey);
8452 selectCtrl.select(optionHashKey, optionCtrl.value);
8456 selectCtrl.deselect( Object.keys(selectCtrl.selected)[0] );
8457 selectCtrl.select( optionHashKey, optionCtrl.value );
8460 selectCtrl.refreshViewValue();
8467 function SelectMenuController($scope, $attrs, $element) {
8469 self.isMultiple = angular.isDefined($attrs.multiple);
8470 // selected is an object with keys matching all of the selected options' hashed values
8472 // options is an object with keys matching every option's hash value,
8473 // and values matching every option's controller.
8476 $scope.$watch(function() { return self.options; }, function() {
8477 self.ngModel.$render();
8480 var deregisterCollectionWatch;
8481 self.setMultiple = function(isMultiple) {
8482 var ngModel = self.ngModel;
8483 self.isMultiple = isMultiple;
8484 if (deregisterCollectionWatch) deregisterCollectionWatch();
8486 if (self.isMultiple) {
8487 ngModel.$validators['md-multiple'] = validateArray;
8488 ngModel.$render = renderMultiple;
8490 // watchCollection on the model because by default ngModel only watches the model's
8491 // reference. This allowed the developer to also push and pop from their array.
8492 $scope.$watchCollection($attrs.ngModel, function(value) {
8493 if (validateArray(value)) renderMultiple(value);
8496 delete ngModel.$validators['md-multiple'];
8497 ngModel.$render = renderSingular;
8500 function validateArray(modelValue, viewValue) {
8501 // If a value is truthy but not an array, reject it.
8502 // If value is undefined/falsy, accept that it's an empty array.
8503 return angular.isArray(modelValue || viewValue || []);
8508 var clearSearchTimeout, optNodes, optText;
8509 var CLEAR_SEARCH_AFTER = 300;
8510 self.optNodeForKeyboardSearch = function(e) {
8511 clearSearchTimeout && clearTimeout(clearSearchTimeout);
8512 clearSearchTimeout = setTimeout(function() {
8513 clearSearchTimeout = undefined;
8515 optText = undefined;
8516 optNodes = undefined;
8517 }, CLEAR_SEARCH_AFTER);
8518 searchStr += String.fromCharCode(e.keyCode);
8519 var search = new RegExp('^' + searchStr, 'i');
8521 optNodes = $element.find('md-option');
8522 optText = new Array(optNodes.length);
8523 angular.forEach(optNodes, function(el, i) {
8524 optText[i] = el.textContent.trim();
8527 for (var i = 0; i < optText.length; ++i) {
8528 if (search.test(optText[i])) {
8535 self.init = function(ngModel) {
8536 self.ngModel = ngModel;
8538 // Allow users to provide `ng-model="foo" ng-model-options="{trackBy: 'foo.id'}"` so
8539 // that we can properly compare objects set on the model to the available options
8540 if (ngModel.$options && ngModel.$options.trackBy) {
8541 var trackByLocals = {};
8542 var trackByParsed = $parse(ngModel.$options.trackBy);
8543 self.hashGetter = function(value, valueScope) {
8544 trackByLocals.$value = value;
8545 return trackByParsed(valueScope || $scope, trackByLocals);
8547 // If the user doesn't provide a trackBy, we automatically generate an id for every
8550 self.hashGetter = function getHashValue(value) {
8551 if (angular.isObject(value)) {
8552 return 'object_' + (value.$$mdSelectId || (value.$$mdSelectId = ++selectNextId));
8557 self.setMultiple(self.isMultiple);
8560 self.selectedLabels = function() {
8561 var selectedOptionEls = nodesToArray($element[0].querySelectorAll('md-option[selected]'));
8562 if (selectedOptionEls.length) {
8563 return selectedOptionEls.map(function(el) { return el.textContent; }).join(', ');
8569 self.select = function(hashKey, hashedValue) {
8570 var option = self.options[hashKey];
8571 option && option.setSelected(true);
8572 self.selected[hashKey] = hashedValue;
8574 self.deselect = function(hashKey) {
8575 var option = self.options[hashKey];
8576 option && option.setSelected(false);
8577 delete self.selected[hashKey];
8580 self.addOption = function(hashKey, optionCtrl) {
8581 if (angular.isDefined(self.options[hashKey])) {
8582 throw new Error('Duplicate md-option values are not allowed in a select. ' +
8583 'Duplicate value "' + optionCtrl.value + '" found.');
8585 self.options[hashKey] = optionCtrl;
8587 // If this option's value was already in our ngModel, go ahead and select it.
8588 if (angular.isDefined(self.selected[hashKey])) {
8589 self.select(hashKey, optionCtrl.value);
8590 self.refreshViewValue();
8593 self.removeOption = function(hashKey) {
8594 delete self.options[hashKey];
8595 // Don't deselect an option when it's removed - the user's ngModel should be allowed
8596 // to have values that do not match a currently available option.
8599 self.refreshViewValue = function() {
8602 for (var hashKey in self.selected) {
8603 // If this hashKey has an associated option, push that option's value to the model.
8604 if ((option = self.options[hashKey])) {
8605 values.push(option.value);
8607 // Otherwise, the given hashKey has no associated option, and we got it
8608 // from an ngModel value at an earlier time. Push the unhashed value of
8609 // this hashKey to the model.
8610 // This allows the developer to put a value in the model that doesn't yet have
8611 // an associated option.
8612 values.push(self.selected[hashKey]);
8615 self.ngModel.$setViewValue(self.isMultiple ? values : values[0]);
8618 function renderMultiple() {
8619 var newSelectedValues = self.ngModel.$modelValue || self.ngModel.$viewValue;
8620 if (!angular.isArray(newSelectedValues)) return;
8622 var oldSelected = Object.keys(self.selected);
8624 var newSelectedHashes = newSelectedValues.map(self.hashGetter);
8625 var deselected = oldSelected.filter(function(hash) {
8626 return newSelectedHashes.indexOf(hash) === -1;
8629 deselected.forEach(self.deselect);
8630 newSelectedHashes.forEach(function(hashKey, i) {
8631 self.select(hashKey, newSelectedValues[i]);
8634 function renderSingular() {
8635 var value = self.ngModel.$viewValue || self.ngModel.$modelValue;
8636 Object.keys(self.selected).forEach(self.deselect);
8637 self.select( self.hashGetter(value), value );
8642 SelectMenuDirective.$inject = ["$parse", "$mdUtil", "$mdTheming"];
8644 function OptionDirective($mdButtonInkRipple, $mdUtil) {
8646 OptionController.$inject = ["$element"];
8649 require: ['mdOption', '^^mdSelectMenu'],
8650 controller: OptionController,
8654 function compile(element, attr) {
8655 // Manual transclusion to avoid the extra inner <span> that ng-transclude generates
8656 element.append( angular.element('<div class="md-text">').append(element.contents()) );
8658 element.attr('tabindex', attr.tabindex || '0');
8662 function postLink(scope, element, attr, ctrls) {
8663 var optionCtrl = ctrls[0];
8664 var selectCtrl = ctrls[1];
8666 if (angular.isDefined(attr.ngValue)) {
8667 scope.$watch(attr.ngValue, setOptionValue);
8668 } else if (angular.isDefined(attr.value)) {
8669 setOptionValue(attr.value);
8671 scope.$watch(function() { return element.text(); }, setOptionValue);
8674 scope.$$postDigest(function() {
8675 attr.$observe('selected', function(selected) {
8676 if (!angular.isDefined(selected)) return;
8678 if (!selectCtrl.isMultiple) {
8679 selectCtrl.deselect( Object.keys(selectCtrl.selected)[0] );
8681 selectCtrl.select(optionCtrl.hashKey, optionCtrl.value);
8683 selectCtrl.deselect(optionCtrl.hashKey);
8685 selectCtrl.refreshViewValue();
8686 selectCtrl.ngModel.$render();
8690 $mdButtonInkRipple.attach(scope, element);
8693 function setOptionValue(newValue, oldValue) {
8694 var oldHashKey = selectCtrl.hashGetter(oldValue, scope);
8695 var newHashKey = selectCtrl.hashGetter(newValue, scope);
8697 optionCtrl.hashKey = newHashKey;
8698 optionCtrl.value = newValue;
8700 selectCtrl.removeOption(oldHashKey, optionCtrl);
8701 selectCtrl.addOption(newHashKey, optionCtrl);
8704 scope.$on('$destroy', function() {
8705 selectCtrl.removeOption(optionCtrl.hashKey, optionCtrl);
8708 function configureAria() {
8711 'aria-selected': 'false'
8714 if (!element[0].hasAttribute('id')) {
8715 ariaAttrs.id = 'select_option_' + $mdUtil.nextUid();
8717 element.attr(ariaAttrs);
8721 function OptionController($element) {
8722 this.selected = false;
8723 this.setSelected = function(isSelected) {
8724 if (isSelected && !this.selected) {
8726 'selected': 'selected',
8727 'aria-selected': 'true'
8729 } else if (!isSelected && this.selected) {
8730 $element.removeAttr('selected');
8731 $element.attr('aria-selected', 'false');
8733 this.selected = isSelected;
8738 OptionDirective.$inject = ["$mdButtonInkRipple", "$mdUtil"];
8740 function OptgroupDirective() {
8745 function compile(el, attrs) {
8746 var labelElement = el.find('label');
8747 if (!labelElement.length) {
8748 labelElement = angular.element('<label>');
8749 el.prepend(labelElement);
8751 if (attrs.label) labelElement.text(attrs.label);
8755 function SelectProvider($$interimElementProvider) {
8756 selectDefaultOptions.$inject = ["$mdSelect", "$mdConstant", "$$rAF", "$mdUtil", "$mdTheming", "$timeout", "$window"];
8757 return $$interimElementProvider('$mdSelect')
8759 methods: ['target'],
8760 options: selectDefaultOptions
8764 function selectDefaultOptions($mdSelect, $mdConstant, $$rAF, $mdUtil, $mdTheming, $timeout, $window ) {
8770 disableParentScroll: true,
8774 function onShow(scope, element, opts) {
8776 throw new Error('$mdSelect.show() expected a target element in options.target but got ' +
8777 '"' + opts.target + '"!');
8780 angular.extend(opts, {
8782 target: angular.element(opts.target), //make sure it's not a naked dom node
8783 parent: angular.element(opts.parent),
8784 selectEl: element.find('md-select-menu'),
8785 contentEl: element.find('md-content'),
8786 backdrop: opts.hasBackdrop && angular.element('<md-backdrop class="md-select-backdrop md-click-catcher">')
8789 opts.resizeFn = function() {
8792 animateSelect(scope, element, opts);
8797 angular.element($window).on('resize', opts.resizeFn);
8798 angular.element($window).on('orientationchange', opts.resizeFn);
8803 element.removeClass('md-leave');
8805 var optionNodes = opts.selectEl[0].getElementsByTagName('md-option');
8807 if (opts.loadingAsync && opts.loadingAsync.then) {
8808 opts.loadingAsync.then(function() {
8809 scope.$$loadingAsyncDone = true;
8810 // Give ourselves two frames for the progress loader to clear out.
8813 // Don't go forward if the select has been removed in this time...
8814 if (opts.isRemoved) return;
8815 animateSelect(scope, element, opts);
8819 } else if (opts.loadingAsync) {
8820 scope.$$loadingAsyncDone = true;
8823 if (opts.disableParentScroll && !$mdUtil.getClosest(opts.target, 'MD-DIALOG')) {
8824 opts.restoreScroll = $mdUtil.disableScrollAround(opts.target);
8826 opts.disableParentScroll = false;
8828 // Only activate click listeners after a short time to stop accidental double taps/clicks
8829 // from clicking the wrong item
8830 $timeout(activateInteraction, 75, false);
8832 if (opts.backdrop) {
8833 $mdTheming.inherit(opts.backdrop, opts.parent);
8834 opts.parent.append(opts.backdrop);
8836 opts.parent.append(element);
8838 // Give the select a frame to 'initialize' in the DOM,
8839 // so we can read its height/width/position
8842 if (opts.isRemoved) return;
8843 animateSelect(scope, element, opts);
8847 return $mdUtil.transitionEndPromise(opts.selectEl, {timeout: 350});
8849 function configureAria() {
8850 opts.target.attr('aria-expanded', 'true');
8853 function activateInteraction() {
8854 if (opts.isRemoved) return;
8855 var selectCtrl = opts.selectEl.controller('mdSelectMenu') || {};
8856 element.addClass('md-clickable');
8858 opts.backdrop && opts.backdrop.on('click', function(e) {
8860 e.stopPropagation();
8861 opts.restoreFocus = false;
8862 scope.$apply($mdSelect.cancel);
8866 opts.selectEl.on('keydown', function(ev) {
8867 switch (ev.keyCode) {
8868 case $mdConstant.KEY_CODE.SPACE:
8869 case $mdConstant.KEY_CODE.ENTER:
8870 var option = $mdUtil.getClosest(ev.target, 'md-option');
8872 opts.selectEl.triggerHandler({
8876 ev.preventDefault();
8879 case $mdConstant.KEY_CODE.TAB:
8880 case $mdConstant.KEY_CODE.ESCAPE:
8881 ev.preventDefault();
8882 opts.restoreFocus = true;
8883 scope.$apply($mdSelect.cancel);
8887 // Cycling of options, and closing on enter
8888 opts.selectEl.on('keydown', function(ev) {
8889 switch (ev.keyCode) {
8890 case $mdConstant.KEY_CODE.UP_ARROW: return focusPrevOption();
8891 case $mdConstant.KEY_CODE.DOWN_ARROW: return focusNextOption();
8893 if (ev.keyCode >= 31 && ev.keyCode <= 90) {
8894 var optNode = opts.selectEl.controller('mdSelectMenu').optNodeForKeyboardSearch(ev);
8895 optNode && optNode.focus();
8901 function focusOption(direction) {
8902 var optionsArray = nodesToArray(optionNodes);
8903 var index = optionsArray.indexOf(opts.focusedNode);
8905 // We lost the previously focused element, reset to first option
8907 } else if (direction === 'next' && index < optionsArray.length - 1) {
8909 } else if (direction === 'prev' && index > 0) {
8912 var newOption = opts.focusedNode = optionsArray[index];
8913 newOption && newOption.focus();
8915 function focusNextOption() {
8916 focusOption('next');
8918 function focusPrevOption() {
8919 focusOption('prev');
8922 opts.selectEl.on('click', checkCloseMenu);
8923 opts.selectEl.on('keydown', function(e) {
8924 if (e.keyCode == 32 || e.keyCode == 13) {
8929 function checkCloseMenu() {
8930 if (!selectCtrl.isMultiple) {
8931 opts.restoreFocus = true;
8932 scope.$evalAsync(function() {
8933 $mdSelect.hide(selectCtrl.ngModel.$viewValue);
8941 function onRemove(scope, element, opts) {
8942 opts.isRemoved = true;
8943 element.addClass('md-leave')
8944 .removeClass('md-clickable');
8945 opts.target.attr('aria-expanded', 'false');
8948 angular.element($window).off('resize', opts.resizeFn);
8949 angular.element($window).off('orientationchange', opts.resizefn);
8950 opts.resizeFn = undefined;
8952 var mdSelect = opts.selectEl.controller('mdSelect');
8954 mdSelect.setLabelText(opts.selectEl.controller('mdSelectMenu').selectedLabels());
8957 return $mdUtil.transitionEndPromise(element, { timeout: 350 }).then(function() {
8958 element.removeClass('md-active');
8959 opts.backdrop && opts.backdrop.remove();
8960 if (element[0].parentNode === opts.parent[0]) {
8961 opts.parent[0].removeChild(element[0]); // use browser to avoid $destroy event
8963 if (opts.disableParentScroll) {
8964 opts.restoreScroll();
8966 if (opts.restoreFocus) opts.target.focus();
8970 function animateSelect(scope, element, opts) {
8971 var containerNode = element[0],
8972 targetNode = opts.target[0].firstElementChild.firstElementChild, // target the first span, functioning as the label
8973 parentNode = opts.parent[0],
8974 selectNode = opts.selectEl[0],
8975 contentNode = opts.contentEl[0],
8976 parentRect = parentNode.getBoundingClientRect(),
8977 targetRect = targetNode.getBoundingClientRect(),
8978 shouldOpenAroundTarget = false,
8980 left: parentRect.left + SELECT_EDGE_MARGIN,
8981 top: SELECT_EDGE_MARGIN,
8982 bottom: parentRect.height - SELECT_EDGE_MARGIN,
8983 right: parentRect.width - SELECT_EDGE_MARGIN - ($mdUtil.floatingScrollbars() ? 16 : 0)
8986 top: targetRect.top - bounds.top,
8987 left: targetRect.left - bounds.left,
8988 right: bounds.right - (targetRect.left + targetRect.width),
8989 bottom: bounds.bottom - (targetRect.top + targetRect.height)
8991 maxWidth = parentRect.width - SELECT_EDGE_MARGIN * 2,
8992 isScrollable = contentNode.scrollHeight > contentNode.offsetHeight,
8993 selectedNode = selectNode.querySelector('md-option[selected]'),
8994 optionNodes = selectNode.getElementsByTagName('md-option'),
8995 optgroupNodes = selectNode.getElementsByTagName('md-optgroup');
8999 // If a selected node, center around that
9001 centeredNode = selectedNode;
9002 // If there are option groups, center around the first option group
9003 } else if (optgroupNodes.length) {
9004 centeredNode = optgroupNodes[0];
9005 // Otherwise, center around the first optionNode
9006 } else if (optionNodes.length){
9007 centeredNode = optionNodes[0];
9008 // In case there are no options, center on whatever's in there... (eg progress indicator)
9010 centeredNode = contentNode.firstElementChild || contentNode;
9013 if (contentNode.offsetWidth > maxWidth) {
9014 contentNode.style['max-width'] = maxWidth + 'px';
9016 if (shouldOpenAroundTarget) {
9017 contentNode.style['min-width'] = targetRect.width + 'px';
9020 // Remove padding before we compute the position of the menu
9022 selectNode.classList.add('md-overflow');
9025 // Get the selectMenuRect *after* max-width is possibly set above
9026 var selectMenuRect = selectNode.getBoundingClientRect();
9027 var centeredRect = getOffsetRect(centeredNode);
9030 var centeredStyle = $window.getComputedStyle(centeredNode);
9031 centeredRect.paddingLeft = parseInt(centeredStyle.paddingLeft, 10) || 0;
9032 centeredRect.paddingRight = parseInt(centeredStyle.paddingRight, 10) || 0;
9035 var focusedNode = centeredNode;
9036 if ((focusedNode.tagName || '').toUpperCase() === 'MD-OPTGROUP') {
9037 focusedNode = optionNodes[0] || contentNode.firstElementChild || contentNode;
9041 var scrollBuffer = contentNode.offsetHeight / 2;
9042 contentNode.scrollTop = centeredRect.top + centeredRect.height / 2 - scrollBuffer;
9044 if (spaceAvailable.top < scrollBuffer) {
9045 contentNode.scrollTop = Math.min(
9047 contentNode.scrollTop + scrollBuffer - spaceAvailable.top
9049 } else if (spaceAvailable.bottom < scrollBuffer) {
9050 contentNode.scrollTop = Math.max(
9051 centeredRect.top + centeredRect.height - selectMenuRect.height,
9052 contentNode.scrollTop - scrollBuffer + spaceAvailable.bottom
9057 var left, top, transformOrigin;
9058 if (shouldOpenAroundTarget) {
9059 left = targetRect.left;
9060 top = targetRect.top + targetRect.height;
9061 transformOrigin = '50% 0';
9062 if (top + selectMenuRect.height > bounds.bottom) {
9063 top = targetRect.top - selectMenuRect.height;
9064 transformOrigin = '50% 100%';
9067 left = targetRect.left + centeredRect.left - centeredRect.paddingLeft;
9068 top = Math.floor(targetRect.top + targetRect.height / 2 - centeredRect.height / 2 -
9069 centeredRect.top + contentNode.scrollTop);
9072 transformOrigin = (centeredRect.left + targetRect.width / 2) + 'px ' +
9073 (centeredRect.top + centeredRect.height / 2 - contentNode.scrollTop) + 'px 0px';
9075 containerNode.style.minWidth = targetRect.width + centeredRect.paddingLeft +
9076 centeredRect.paddingRight + 'px';
9079 // Keep left and top within the window
9080 var containerRect = containerNode.getBoundingClientRect();
9081 containerNode.style.left = clamp(bounds.left, left, bounds.right - containerRect.width) + 'px';
9082 containerNode.style.top = clamp(bounds.top, top, bounds.bottom - containerRect.height) + 'px';
9083 selectNode.style[$mdConstant.CSS.TRANSFORM_ORIGIN] = transformOrigin;
9085 selectNode.style[$mdConstant.CSS.TRANSFORM] = 'scale(' +
9086 Math.min(targetRect.width / selectMenuRect.width, 1.0) + ',' +
9087 Math.min(targetRect.height / selectMenuRect.height, 1.0) +
9092 element.addClass('md-active');
9093 selectNode.style[$mdConstant.CSS.TRANSFORM] = '';
9095 opts.focusedNode = focusedNode;
9096 focusedNode.focus();
9103 function clamp(min, n, max) {
9104 return Math.max(min, Math.min(n, max));
9107 function getOffsetRect(node) {
9109 left: node.offsetLeft,
9110 top: node.offsetTop,
9111 width: node.offsetWidth,
9112 height: node.offsetHeight
9113 } : { left: 0, top: 0, width: 0, height: 0 };
9116 SelectProvider.$inject = ["$$interimElementProvider"];
9118 // Annoying method to copy nodes to an array, thanks to IE
9119 function nodesToArray(nodes) {
9121 for (var i = 0; i < nodes.length; ++i) {
9122 results.push(nodes.item(i));
9133 * @name material.components.sidenav
9136 * A Sidenav QP component.
9138 angular.module('material.components.sidenav', [
9140 'material.components.backdrop'
9142 .factory('$mdSidenav', SidenavService )
9143 .directive('mdSidenav', SidenavDirective)
9144 .directive('mdSidenavFocus', SidenavFocusDirective)
9145 .controller('$mdSidenavController', SidenavController);
9152 * @module material.components.sidenav
9155 * `$mdSidenav` makes it easy to interact with multiple sidenavs
9160 * // Async lookup for sidenav instance; will resolve when the instance is available
9161 * $mdSidenav(componentId).then(function(instance) {
9162 * $log.debug( componentId + "is now ready" );
9164 * // Async toggle the given sidenav;
9165 * // when instance is known ready and lazy lookup is not needed.
9166 * $mdSidenav(componentId)
9169 * $log.debug('toggled');
9171 * // Async open the given sidenav
9172 * $mdSidenav(componentId)
9175 * $log.debug('opened');
9177 * // Async close the given sidenav
9178 * $mdSidenav(componentId)
9181 * $log.debug('closed');
9183 * // Sync check to see if the specified sidenav is set to be open
9184 * $mdSidenav(componentId).isOpen();
9185 * // Sync check to whether given sidenav is locked open
9186 * // If this is true, the sidenav will be open regardless of close()
9187 * $mdSidenav(componentId).isLockedOpen();
9190 function SidenavService($mdComponentRegistry, $q) {
9191 return function(handle) {
9193 // Lookup the controller instance for the specified sidNav instance
9195 var errorMsg = "SideNav '" + handle + "' is not available!";
9196 var instance = $mdComponentRegistry.get(handle);
9199 $mdComponentRegistry.notFoundError(handle);
9203 // -----------------
9205 // -----------------
9206 isOpen: function() {
9207 return instance && instance.isOpen();
9209 isLockedOpen: function() {
9210 return instance && instance.isLockedOpen();
9212 // -----------------
9214 // -----------------
9215 toggle: function() {
9216 return instance ? instance.toggle() : $q.reject(errorMsg);
9219 return instance ? instance.open() : $q.reject(errorMsg);
9222 return instance ? instance.close() : $q.reject(errorMsg);
9224 then : function( callbackFn ) {
9225 var promise = instance ? $q.when(instance) : waitForInstance();
9226 return promise.then( callbackFn || angular.noop );
9231 * Deferred lookup of component instance using $component registry
9233 function waitForInstance() {
9234 return $mdComponentRegistry
9236 .then(function( it ){
9243 SidenavService.$inject = ["$mdComponentRegistry", "$q"];
9246 * @name mdSidenavFocus
9247 * @module material.components.sidenav
9252 * `$mdSidenavFocus` provides a way to specify the focused element when a sidenav opens.
9253 * This is completely optional, as the sidenav itself is focused by default.
9256 * <hljs lang="html">
9259 * <md-input-container>
9260 * <label for="testInput">Label</label>
9261 * <input id="testInput" type="text" md-sidenav-focus>
9262 * </md-input-container>
9267 function SidenavFocusDirective() {
9270 require: '^mdSidenav',
9271 link: function(scope, element, attr, sidenavCtrl) {
9272 sidenavCtrl.focusElement(element);
9279 * @module material.components.sidenav
9284 * A Sidenav component that can be opened and closed programatically.
9286 * By default, upon opening it will slide out on top of the main content area.
9288 * For keyboard and screen reader accessibility, focus is sent to the sidenav wrapper by default.
9289 * It can be overridden with the `md-sidenav-focus` directive on the child element you want focused.
9292 * <hljs lang="html">
9293 * <div layout="row" ng-controller="MyController">
9294 * <md-sidenav md-component-id="left" class="md-sidenav-left">
9300 * <md-button ng-click="openLeftMenu()">
9305 * <md-sidenav md-component-id="right"
9306 * md-is-locked-open="$mdMedia('min-width: 333px')"
9307 * class="md-sidenav-right">
9309 * <md-input-container>
9310 * <label for="testInput">Test input</label>
9311 * <input id="testInput" type="text"
9312 * ng-model="data" md-sidenav-focus>
9313 * </md-input-container>
9320 * var app = angular.module('myApp', ['ngMaterial']);
9321 * app.controller('MyController', function($scope, $mdSidenav) {
9322 * $scope.openLeftMenu = function() {
9323 * $mdSidenav('left').toggle();
9328 * @param {expression=} md-is-open A model bound to whether the sidenav is opened.
9329 * @param {string=} md-component-id componentId to use with $mdSidenav service.
9330 * @param {expression=} md-is-locked-open When this expression evalutes to true,
9331 * the sidenav 'locks open': it falls into the content's flow instead
9332 * of appearing over it. This overrides the `is-open` attribute.
9334 * The $mdMedia() service is exposed to the is-locked-open attribute, which
9335 * can be given a media query or one of the `sm`, `gt-sm`, `md`, `gt-md`, `lg` or `gt-lg` presets.
9338 * - `<md-sidenav md-is-locked-open="shouldLockOpen"></md-sidenav>`
9339 * - `<md-sidenav md-is-locked-open="$mdMedia('min-width: 1000px')"></md-sidenav>`
9340 * - `<md-sidenav md-is-locked-open="$mdMedia('sm')"></md-sidenav>` (locks open on small screens)
9342 function SidenavDirective($timeout, $animate, $parse, $log, $mdMedia, $mdConstant, $compile, $mdTheming, $q, $document) {
9346 isOpen: '=?mdIsOpen'
9348 controller: '$mdSidenavController',
9349 compile: function(element) {
9350 element.addClass('md-closed');
9351 element.attr('tabIndex', '-1');
9357 * Directive Post Link function...
9359 function postLink(scope, element, attr, sidenavCtrl) {
9360 var lastParentOverFlow;
9361 var triggeringElement = null;
9362 var promise = $q.when(true);
9364 var isLockedOpenParsed = $parse(attr.mdIsLockedOpen);
9365 var isLocked = function() {
9366 return isLockedOpenParsed(scope.$parent, {
9367 $media: function(arg) {
9368 $log.warn("$media is deprecated for is-locked-open. Use $mdMedia instead.");
9369 return $mdMedia(arg);
9374 var backdrop = $compile(
9375 '<md-backdrop class="md-sidenav-backdrop md-opaque ng-enter">'
9378 element.on('$destroy', sidenavCtrl.destroy);
9379 $mdTheming.inherit(backdrop, element);
9381 scope.$watch(isLocked, updateIsLocked);
9382 scope.$watch('isOpen', updateIsOpen);
9385 // Publish special accessor for the Controller instance
9386 sidenavCtrl.$toggleOpen = toggleOpen;
9387 sidenavCtrl.focusElement( sidenavCtrl.focusElement() || element );
9390 * Toggle the DOM classes to indicate `locked`
9393 function updateIsLocked(isLocked, oldValue) {
9394 scope.isLockedOpen = isLocked;
9395 if (isLocked === oldValue) {
9396 element.toggleClass('md-locked-open', !!isLocked);
9398 $animate[isLocked ? 'addClass' : 'removeClass'](element, 'md-locked-open');
9400 backdrop.toggleClass('md-locked-open', !!isLocked);
9404 * Toggle the SideNav view and attach/detach listeners
9407 function updateIsOpen(isOpen) {
9408 var parent = element.parent();
9410 parent[isOpen ? 'on' : 'off']('keydown', onKeyDown);
9411 backdrop[isOpen ? 'on' : 'off']('click', close);
9414 // Capture upon opening..
9415 triggeringElement = $document[0].activeElement;
9417 var focusEl = sidenavCtrl.focusElement();
9419 disableParentScroll(isOpen);
9421 return promise = $q.all([
9422 isOpen ? $animate.enter(backdrop, parent) : $animate.leave(backdrop),
9423 $animate[isOpen ? 'removeClass' : 'addClass'](element, 'md-closed')
9426 // Perform focus when animations are ALL done...
9428 focusEl && focusEl.focus();
9434 * Prevent parent scrolling (when the SideNav is open)
9436 function disableParentScroll(disabled) {
9437 var parent = element.parent();
9439 lastParentOverFlow = parent.css('overflow');
9440 parent.css('overflow', 'hidden');
9441 } else if (angular.isDefined(lastParentOverFlow)) {
9442 parent.css('overflow', lastParentOverFlow);
9443 lastParentOverFlow = undefined;
9448 * Toggle the sideNav view and publish a promise to be resolved when
9449 * the view animation finishes.
9454 function toggleOpen( isOpen ) {
9455 if (scope.isOpen == isOpen ) {
9457 return $q.when(true);
9460 var deferred = $q.defer();
9462 // Toggle value to force an async `updateIsOpen()` to run
9463 scope.isOpen = isOpen;
9465 $timeout(function() {
9467 // When the current `updateIsOpen()` animation finishes
9468 promise.then(function(result) {
9470 if ( !scope.isOpen ) {
9471 // reset focus to originating element (if available) upon close
9472 triggeringElement && triggeringElement.focus();
9473 triggeringElement = null;
9476 deferred.resolve(result);
9481 return deferred.promise;
9486 * Auto-close sideNav when the `escape` key is pressed.
9489 function onKeyDown(ev) {
9490 var isEscape = (ev.keyCode === $mdConstant.KEY_CODE.ESCAPE);
9491 return isEscape ? close(ev) : $q.when(true);
9495 * With backdrop `clicks` or `escape` key-press, immediately
9496 * apply the CSS close transition... Then notify the controller
9497 * to close() and perform its own actions.
9499 function close(ev) {
9500 ev.preventDefault();
9501 ev.stopPropagation();
9503 return sidenavCtrl.close();
9508 SidenavDirective.$inject = ["$timeout", "$animate", "$parse", "$log", "$mdMedia", "$mdConstant", "$compile", "$mdTheming", "$q", "$document"];
9513 * @name SidenavController
9514 * @module material.components.sidenav
9517 function SidenavController($scope, $element, $attrs, $mdComponentRegistry, $q) {
9522 // Use Default internal method until overridden by directive postLink
9524 // Synchronous getters
9525 self.isOpen = function() { return !!$scope.isOpen; };
9526 self.isLockedOpen = function() { return !!$scope.isLockedOpen; };
9529 self.open = function() { return self.$toggleOpen( true ); };
9530 self.close = function() { return self.$toggleOpen( false ); };
9531 self.toggle = function() { return self.$toggleOpen( !$scope.isOpen ); };
9532 self.focusElement = function(el) {
9533 if ( angular.isDefined(el) ) {
9536 return focusElement;
9539 self.$toggleOpen = function() { return $q.when($scope.isOpen); };
9541 self.destroy = $mdComponentRegistry.register(self, $attrs.mdComponentId);
9543 SidenavController.$inject = ["$scope", "$element", "$attrs", "$mdComponentRegistry", "$q"];
9551 * @name material.components.slider
9553 angular.module('material.components.slider', [
9556 .directive('mdSlider', SliderDirective);
9561 * @module material.components.slider
9564 * The `<md-slider>` component allows the user to choose from a range of
9567 * As per the [material design spec](http://www.google.com/design/spec/style/color.html#color-ui-color-application)
9568 * the slider is in the accent color by default. The primary color palette may be used with
9569 * the `md-primary` class.
9571 * It has two modes: 'normal' mode, where the user slides between a wide range
9572 * of values, and 'discrete' mode, where the user slides between only a few
9575 * To enable discrete mode, add the `md-discrete` attribute to a slider,
9576 * and use the `step` attribute to change the distance between
9577 * values the user is allowed to pick.
9580 * <h4>Normal Mode</h4>
9581 * <hljs lang="html">
9582 * <md-slider ng-model="myValue" min="5" max="500">
9585 * <h4>Discrete Mode</h4>
9586 * <hljs lang="html">
9587 * <md-slider md-discrete ng-model="myDiscreteValue" step="10" min="10" max="130">
9591 * @param {boolean=} md-discrete Whether to enable discrete mode.
9592 * @param {number=} step The distance between values the user is allowed to pick. Default 1.
9593 * @param {number=} min The minimum value the user is allowed to pick. Default 0.
9594 * @param {number=} max The maximum value the user is allowed to pick. Default 100.
9596 function SliderDirective($$rAF, $window, $mdAria, $mdUtil, $mdConstant, $mdTheming, $mdGesture, $parse) {
9599 require: '?ngModel',
9601 '<div class="md-slider-wrapper">\
9602 <div class="md-track-container">\
9603 <div class="md-track"></div>\
9604 <div class="md-track md-track-fill"></div>\
9605 <div class="md-track-ticks"></div>\
9607 <div class="md-thumb-container">\
9608 <div class="md-thumb"></div>\
9609 <div class="md-focus-thumb"></div>\
9610 <div class="md-focus-ring"></div>\
9611 <div class="md-sign">\
9612 <span class="md-thumb-text"></span>\
9614 <div class="md-disabled-thumb"></div>\
9620 // **********************************************************
9622 // **********************************************************
9624 function compile (tElement, tAttrs) {
9630 $mdAria.expect(tElement, 'aria-label');
9635 function postLink(scope, element, attr, ngModelCtrl) {
9636 $mdTheming(element);
9637 ngModelCtrl = ngModelCtrl || {
9638 // Mock ngModelController if it doesn't exist to give us
9639 // the minimum functionality needed
9640 $setViewValue: function(val) {
9641 this.$viewValue = val;
9642 this.$viewChangeListeners.forEach(function(cb) { cb(); });
9646 $viewChangeListeners: []
9649 var isDisabledParsed = attr.ngDisabled && $parse(attr.ngDisabled);
9650 var isDisabledGetter = isDisabledParsed ?
9651 function() { return isDisabledParsed(scope.$parent); } :
9653 var thumb = angular.element(element[0].querySelector('.md-thumb'));
9654 var thumbText = angular.element(element[0].querySelector('.md-thumb-text'));
9655 var thumbContainer = thumb.parent();
9656 var trackContainer = angular.element(element[0].querySelector('.md-track-container'));
9657 var activeTrack = angular.element(element[0].querySelector('.md-track-fill'));
9658 var tickContainer = angular.element(element[0].querySelector('.md-track-ticks'));
9659 var throttledRefreshDimensions = $mdUtil.throttle(refreshSliderDimensions, 5000);
9661 // Default values, overridable by attrs
9662 angular.isDefined(attr.min) ? attr.$observe('min', updateMin) : updateMin(0);
9663 angular.isDefined(attr.max) ? attr.$observe('max', updateMax) : updateMax(100);
9664 angular.isDefined(attr.step)? attr.$observe('step', updateStep) : updateStep(1);
9666 // We have to manually stop the $watch on ngDisabled because it exists
9667 // on the parent scope, and won't be automatically destroyed when
9668 // the component is destroyed.
9669 var stopDisabledWatch = angular.noop;
9670 if (attr.ngDisabled) {
9671 stopDisabledWatch = scope.$parent.$watch(attr.ngDisabled, updateAriaDisabled);
9674 $mdGesture.register(element, 'drag');
9677 .on('keydown', keydownListener)
9678 .on('$md.pressdown', onPressDown)
9679 .on('$md.pressup', onPressUp)
9680 .on('$md.dragstart', onDragStart)
9681 .on('$md.drag', onDrag)
9682 .on('$md.dragend', onDragEnd);
9684 // On resize, recalculate the slider's dimensions and re-render
9685 function updateAll() {
9686 refreshSliderDimensions();
9690 setTimeout(updateAll);
9692 var debouncedUpdateAll = $$rAF.throttle(updateAll);
9693 angular.element($window).on('resize', debouncedUpdateAll);
9695 scope.$on('$destroy', function() {
9696 angular.element($window).off('resize', debouncedUpdateAll);
9697 stopDisabledWatch();
9700 ngModelCtrl.$render = ngModelRender;
9701 ngModelCtrl.$viewChangeListeners.push(ngModelRender);
9702 ngModelCtrl.$formatters.push(minMaxValidator);
9703 ngModelCtrl.$formatters.push(stepValidator);
9711 function updateMin(value) {
9712 min = parseFloat(value);
9713 element.attr('aria-valuemin', value);
9716 function updateMax(value) {
9717 max = parseFloat(value);
9718 element.attr('aria-valuemax', value);
9721 function updateStep(value) {
9722 step = parseFloat(value);
9725 function updateAriaDisabled(isDisabled) {
9726 element.attr('aria-disabled', !!isDisabled);
9729 // Draw the ticks with canvas.
9730 // The alternative to drawing ticks with canvas is to draw one element for each tick,
9731 // which could quickly become a performance bottleneck.
9732 var tickCanvas, tickCtx;
9733 function redrawTicks() {
9734 if (!angular.isDefined(attr.mdDiscrete)) return;
9736 var numSteps = Math.floor( (max - min) / step );
9738 var trackTicksStyle = $window.getComputedStyle(tickContainer[0]);
9739 tickCanvas = angular.element('<canvas style="position:absolute;">');
9740 tickCtx = tickCanvas[0].getContext('2d');
9741 tickCtx.fillStyle = trackTicksStyle.backgroundColor || 'black';
9742 tickContainer.append(tickCanvas);
9744 var dimensions = getSliderDimensions();
9745 tickCanvas[0].width = dimensions.width;
9746 tickCanvas[0].height = dimensions.height;
9749 for (var i = 0; i <= numSteps; i++) {
9750 distance = Math.floor(dimensions.width * (i / numSteps));
9751 tickCtx.fillRect(distance - 1, 0, 2, dimensions.height);
9757 * Refreshing Dimensions
9759 var sliderDimensions = {};
9760 refreshSliderDimensions();
9761 function refreshSliderDimensions() {
9762 sliderDimensions = trackContainer[0].getBoundingClientRect();
9764 function getSliderDimensions() {
9765 throttledRefreshDimensions();
9766 return sliderDimensions;
9770 * left/right arrow listener
9772 function keydownListener(ev) {
9773 if(element[0].hasAttribute('disabled')) {
9778 if (ev.keyCode === $mdConstant.KEY_CODE.LEFT_ARROW) {
9779 changeAmount = -step;
9780 } else if (ev.keyCode === $mdConstant.KEY_CODE.RIGHT_ARROW) {
9781 changeAmount = step;
9784 if (ev.metaKey || ev.ctrlKey || ev.altKey) {
9787 ev.preventDefault();
9788 ev.stopPropagation();
9789 scope.$evalAsync(function() {
9790 setModelValue(ngModelCtrl.$viewValue + changeAmount);
9796 * ngModel setters and validators
9798 function setModelValue(value) {
9799 ngModelCtrl.$setViewValue( minMaxValidator(stepValidator(value)) );
9801 function ngModelRender() {
9802 if (isNaN(ngModelCtrl.$viewValue)) {
9803 ngModelCtrl.$viewValue = ngModelCtrl.$modelValue;
9806 var percent = (ngModelCtrl.$viewValue - min) / (max - min);
9807 scope.modelValue = ngModelCtrl.$viewValue;
9808 element.attr('aria-valuenow', ngModelCtrl.$viewValue);
9809 setSliderPercent(percent);
9810 thumbText.text( ngModelCtrl.$viewValue );
9813 function minMaxValidator(value) {
9814 if (angular.isNumber(value)) {
9815 return Math.max(min, Math.min(max, value));
9818 function stepValidator(value) {
9819 if (angular.isNumber(value)) {
9820 var formattedValue = (Math.round(value / step) * step);
9821 // Format to 3 digits after the decimal point - fixes #2015.
9822 return (Math.round(formattedValue * 1000) / 1000);
9827 * @param percent 0-1
9829 function setSliderPercent(percent) {
9830 activeTrack.css('width', (percent * 100) + '%');
9833 (percent * 100) + '%'
9835 element.toggleClass('md-min', percent === 0);
9842 var isDragging = false;
9843 var isDiscrete = angular.isDefined(attr.mdDiscrete);
9845 function onPressDown(ev) {
9846 if (isDisabledGetter()) return;
9848 element.addClass('active');
9850 refreshSliderDimensions();
9852 var exactVal = percentToValue( positionToPercent( ev.pointer.x ));
9853 var closestVal = minMaxValidator( stepValidator(exactVal) );
9854 scope.$apply(function() {
9855 setModelValue( closestVal );
9856 setSliderPercent( valueToPercent(closestVal));
9859 function onPressUp(ev) {
9860 if (isDisabledGetter()) return;
9862 element.removeClass('dragging active');
9864 var exactVal = percentToValue( positionToPercent( ev.pointer.x ));
9865 var closestVal = minMaxValidator( stepValidator(exactVal) );
9866 scope.$apply(function() {
9867 setModelValue(closestVal);
9871 function onDragStart(ev) {
9872 if (isDisabledGetter()) return;
9874 ev.stopPropagation();
9876 element.addClass('dragging');
9877 setSliderFromEvent(ev);
9879 function onDrag(ev) {
9880 if (!isDragging) return;
9881 ev.stopPropagation();
9882 setSliderFromEvent(ev);
9884 function onDragEnd(ev) {
9885 if (!isDragging) return;
9886 ev.stopPropagation();
9890 function setSliderFromEvent(ev) {
9891 // While panning discrete, update only the
9892 // visual positioning but not the model value.
9893 if ( isDiscrete ) adjustThumbPosition( ev.pointer.x );
9894 else doSlide( ev.pointer.x );
9898 * Slide the UI by changing the model value
9901 function doSlide( x ) {
9902 scope.$evalAsync( function() {
9903 setModelValue( percentToValue( positionToPercent(x) ));
9908 * Slide the UI without changing the model (while dragging/panning)
9911 function adjustThumbPosition( x ) {
9912 var exactVal = percentToValue( positionToPercent( x ));
9913 var closestVal = minMaxValidator( stepValidator(exactVal) );
9914 setSliderPercent( positionToPercent(x) );
9915 thumbText.text( closestVal );
9919 * Convert horizontal position on slider to percentage value of offset from beginning...
9923 function positionToPercent( x ) {
9924 return Math.max(0, Math.min(1, (x - sliderDimensions.left) / (sliderDimensions.width)));
9928 * Convert percentage offset on slide to equivalent model value
9932 function percentToValue( percent ) {
9933 return (min + percent * (max - min));
9936 function valueToPercent( val ) {
9937 return (val - min)/(max - min);
9941 SliderDirective.$inject = ["$$rAF", "$window", "$mdAria", "$mdUtil", "$mdConstant", "$mdTheming", "$mdGesture", "$parse"];
9949 * @name material.components.sticky
9952 * Sticky effects for md
9955 angular.module('material.components.sticky', [
9957 'material.components.content'
9959 .factory('$mdSticky', MdSticky);
9964 * @module material.components.sticky
9967 * The `$mdSticky`service provides a mixin to make elements sticky.
9969 * @returns A `$mdSticky` function that takes three arguments:
9971 * - `element`: The element that will be 'sticky'
9972 * - `elementClone`: A clone of the element, that will be shown
9973 * when the user starts scrolling past the original element.
9974 * If not provided, it will use the result of `element.clone()`.
9977 function MdSticky($document, $mdConstant, $compile, $$rAF, $mdUtil) {
9979 var browserStickySupport = checkStickySupport();
9982 * Registers an element as sticky, used internally by directives to register themselves
9984 return function registerStickyElement(scope, element, stickyClone) {
9985 var contentCtrl = element.controller('mdContent');
9986 if (!contentCtrl) return;
9988 if (browserStickySupport) {
9990 position: browserStickySupport,
9995 var $$sticky = contentCtrl.$element.data('$$sticky');
9997 $$sticky = setupSticky(contentCtrl);
9998 contentCtrl.$element.data('$$sticky', $$sticky);
10001 var deregister = $$sticky.add(element, stickyClone || element.clone());
10002 scope.$on('$destroy', deregister);
10006 function setupSticky(contentCtrl) {
10007 var contentEl = contentCtrl.$element;
10009 // Refresh elements is very expensive, so we use the debounced
10010 // version when possible.
10011 var debouncedRefreshElements = $$rAF.throttle(refreshElements);
10013 // setupAugmentedScrollEvents gives us `$scrollstart` and `$scroll`,
10014 // more reliable than `scroll` on android.
10015 setupAugmentedScrollEvents(contentEl);
10016 contentEl.on('$scrollstart', debouncedRefreshElements);
10017 contentEl.on('$scroll', onScroll);
10020 var stickyBaseoffset = contentEl.prop('offsetTop');
10023 current: null, //the currently stickied item
10027 refreshElements: refreshElements
10033 // Add an element and its sticky clone to this content's sticky collection
10034 function add(element, stickyClone) {
10035 stickyClone.addClass('md-sticky-clone');
10036 stickyClone.css('top', stickyBaseoffset + 'px');
10042 self.items.push(item);
10044 contentEl.parent().prepend(item.clone);
10046 debouncedRefreshElements();
10048 return function remove() {
10049 self.items.forEach(function(item, index) {
10050 if (item.element[0] === element[0]) {
10051 self.items.splice(index, 1);
10052 item.clone.remove();
10055 debouncedRefreshElements();
10059 function refreshElements() {
10060 // Sort our collection of elements by their current position in the DOM.
10061 // We need to do this because our elements' order of being added may not
10062 // be the same as their order of display.
10063 self.items.forEach(refreshPosition);
10064 self.items = self.items.sort(function(a, b) {
10065 return a.top < b.top ? -1 : 1;
10068 // Find which item in the list should be active,
10069 // based upon the content's current scroll position
10071 var currentScrollTop = contentEl.prop('scrollTop');
10072 for (var i = self.items.length - 1; i >= 0; i--) {
10073 if (currentScrollTop > self.items[i].top) {
10074 item = self.items[i];
10078 setCurrentItem(item);
10086 // Find the `top` of an item relative to the content element,
10087 // and also the height.
10088 function refreshPosition(item) {
10089 // Find the top of an item by adding to the offsetHeight until we reach the
10090 // content element.
10091 var current = item.element[0];
10094 while (current && current !== contentEl[0]) {
10095 item.top += current.offsetTop;
10096 item.left += current.offsetLeft;
10097 current = current.offsetParent;
10099 item.height = item.element.prop('offsetHeight');
10100 item.clone.css('margin-left', item.left + 'px');
10101 if ($mdUtil.floatingScrollbars()) {
10102 item.clone.css('margin-right', '0');
10107 // As we scroll, push in and select the correct sticky element.
10108 function onScroll() {
10109 var scrollTop = contentEl.prop('scrollTop');
10110 var isScrollingDown = scrollTop > (onScroll.prevScrollTop || 0);
10111 onScroll.prevScrollTop = scrollTop;
10114 if (scrollTop === 0) {
10115 setCurrentItem(null);
10117 // Going to next item?
10118 } else if (isScrollingDown && self.next) {
10119 if (self.next.top - scrollTop <= 0) {
10120 // Sticky the next item if we've scrolled past its position.
10121 setCurrentItem(self.next);
10122 } else if (self.current) {
10123 // Push the current item up when we're almost at the next item.
10124 if (self.next.top - scrollTop <= self.next.height) {
10125 translate(self.current, self.next.top - self.next.height - scrollTop);
10127 translate(self.current, null);
10131 // Scrolling up with a current sticky item?
10132 } else if (!isScrollingDown && self.current) {
10133 if (scrollTop < self.current.top) {
10134 // Sticky the previous item if we've scrolled up past
10135 // the original position of the currently stickied item.
10136 setCurrentItem(self.prev);
10138 // Scrolling up, and just bumping into the item above (just set to current)?
10139 // If we have a next item bumping into the current item, translate
10140 // the current item up from the top as it scrolls into view.
10141 if (self.current && self.next) {
10142 if (scrollTop >= self.next.top - self.current.height) {
10143 translate(self.current, self.next.top - scrollTop - self.current.height);
10145 translate(self.current, null);
10151 function setCurrentItem(item) {
10152 if (self.current === item) return;
10153 // Deactivate currently active item
10154 if (self.current) {
10155 translate(self.current, null);
10156 setStickyState(self.current, null);
10159 // Activate new item if given
10161 setStickyState(item, 'active');
10164 self.current = item;
10165 var index = self.items.indexOf(item);
10166 // If index === -1, index + 1 = 0. It works out.
10167 self.next = self.items[index + 1];
10168 self.prev = self.items[index - 1];
10169 setStickyState(self.next, 'next');
10170 setStickyState(self.prev, 'prev');
10173 function setStickyState(item, state) {
10174 if (!item || item.state === state) return;
10176 item.clone.attr('sticky-prev-state', item.state);
10177 item.element.attr('sticky-prev-state', item.state);
10179 item.clone.attr('sticky-state', state);
10180 item.element.attr('sticky-state', state);
10181 item.state = state;
10184 function translate(item, amount) {
10186 if (amount === null || amount === undefined) {
10187 if (item.translateY) {
10188 item.translateY = null;
10189 item.clone.css($mdConstant.CSS.TRANSFORM, '');
10192 item.translateY = amount;
10194 $mdConstant.CSS.TRANSFORM,
10195 'translate3d(' + item.left + 'px,' + amount + 'px,0)'
10201 // Function to check for browser sticky support
10202 function checkStickySupport($el) {
10204 var testEl = angular.element('<div>');
10205 $document[0].body.appendChild(testEl[0]);
10207 var stickyProps = ['sticky', '-webkit-sticky'];
10208 for (var i = 0; i < stickyProps.length; ++i) {
10209 testEl.css({position: stickyProps[i], top: 0, 'z-index': 2});
10210 if (testEl.css('position') == stickyProps[i]) {
10211 stickyProp = stickyProps[i];
10219 // Android 4.4 don't accurately give scroll events.
10220 // To fix this problem, we setup a fake scroll event. We say:
10221 // > If a scroll or touchmove event has happened in the last DELAY milliseconds,
10222 // then send a `$scroll` event every animationFrame.
10223 // Additionally, we add $scrollstart and $scrollend events.
10224 function setupAugmentedScrollEvents(element) {
10225 var SCROLL_END_DELAY = 200;
10227 var lastScrollTime;
10228 element.on('scroll touchmove', function() {
10229 if (!isScrolling) {
10230 isScrolling = true;
10231 $$rAF(loopScrollEvent);
10232 element.triggerHandler('$scrollstart');
10234 element.triggerHandler('$scroll');
10235 lastScrollTime = +$mdUtil.now();
10238 function loopScrollEvent() {
10239 if (+$mdUtil.now() - lastScrollTime > SCROLL_END_DELAY) {
10240 isScrolling = false;
10241 element.triggerHandler('$scrollend');
10243 element.triggerHandler('$scroll');
10244 $$rAF(loopScrollEvent);
10250 MdSticky.$inject = ["$document", "$mdConstant", "$compile", "$$rAF", "$mdUtil"];
10258 * @name material.components.subheader
10262 * Subheaders are special list tiles that delineate distinct sections of a
10263 * list or grid list and are typically related to the current filtering or
10264 * sorting criteria. Subheader tiles are either displayed inline with tiles or
10265 * can be associated with content, for example, in an adjacent column.
10267 * Upon scrolling, subheaders remain pinned to the top of the screen and remain
10268 * pinned until pushed on or off screen by the next subheader. @see [Material
10269 * Design Specifications](https://www.google.com/design/spec/components/subheaders.html)
10271 * > To improve the visual grouping of content, use the system color for your subheaders.
10274 angular.module('material.components.subheader', [
10276 'material.components.sticky'
10278 .directive('mdSubheader', MdSubheaderDirective);
10282 * @name mdSubheader
10283 * @module material.components.subheader
10288 * The `<md-subheader>` directive is a subheader for a section. By default it is sticky.
10289 * You can make it not sticky by applying the `md-no-sticky` class to the subheader.
10293 * <hljs lang="html">
10294 * <md-subheader>Online Friends</md-subheader>
10298 function MdSubheaderDirective($mdSticky, $compile, $mdTheming) {
10304 '<h2 class="md-subheader">' +
10305 '<div class="md-subheader-inner">' +
10306 '<span class="md-subheader-content"></span>' +
10309 compile: function(element, attr, transclude) {
10310 return function postLink(scope, element, attr) {
10311 $mdTheming(element);
10312 var outerHTML = element[0].outerHTML;
10314 function getContent(el) {
10315 return angular.element(el[0].querySelector('.md-subheader-content'));
10318 // Transclude the user-given contents of the subheader
10319 // the conventional way.
10320 transclude(scope, function(clone) {
10321 getContent(element).append(clone);
10324 // Create another clone, that uses the outer and inner contents
10325 // of the element, that will be 'stickied' as the user scrolls.
10326 if (!element.hasClass('md-no-sticky')) {
10327 transclude(scope, function(clone) {
10328 var stickyClone = $compile(angular.element(outerHTML))(scope);
10329 getContent(stickyClone).append(clone);
10330 $mdSticky(scope, element, stickyClone);
10337 MdSubheaderDirective.$inject = ["$mdSticky", "$compile", "$mdTheming"];
10345 * @name material.components.swipe
10346 * @description Swipe module!
10350 * @module material.components.swipe
10351 * @name mdSwipeLeft
10356 * The md-swipe-left directives allows you to specify custom behavior when an element is swiped
10360 * <hljs lang="html">
10361 * <div md-swipe-left="onSwipeLeft()">Swipe me left!</div>
10366 * @module material.components.swipe
10367 * @name mdSwipeRight
10372 * The md-swipe-right directives allows you to specify custom behavior when an element is swiped
10376 * <hljs lang="html">
10377 * <div md-swipe-right="onSwipeRight()">Swipe me right!</div>
10381 angular.module('material.components.swipe', ['material.core'])
10382 .directive('mdSwipeLeft', getDirective('SwipeLeft'))
10383 .directive('mdSwipeRight', getDirective('SwipeRight'));
10385 function getDirective(name) {
10386 var directiveName = 'md' + name;
10387 var eventName = '$md.' + name.toLowerCase();
10389 DirectiveFactory.$inject = ["$parse"];
10390 return DirectiveFactory;
10393 function DirectiveFactory($parse) {
10394 return { restrict: 'A', link: postLink };
10395 function postLink(scope, element, attr) {
10396 var fn = $parse(attr[directiveName]);
10397 element.on(eventName, function(ev) {
10398 scope.$apply(function() { fn(scope, { $event: ev }); });
10413 * @name material.components.switch
10416 angular.module('material.components.switch', [
10418 'material.components.checkbox'
10420 .directive('mdSwitch', MdSwitch);
10425 * @module material.components.switch
10429 * The switch directive is used very much like the normal [angular checkbox](https://docs.angularjs.org/api/ng/input/input%5Bcheckbox%5D).
10431 * As per the [material design spec](http://www.google.com/design/spec/style/color.html#color-ui-color-application)
10432 * the switch is in the accent color by default. The primary color palette may be used with
10433 * the `md-primary` class.
10435 * @param {string} ng-model Assignable angular expression to data-bind to.
10436 * @param {string=} name Property name of the form under which the control is published.
10437 * @param {expression=} ng-true-value The value to which the expression should be set when selected.
10438 * @param {expression=} ng-false-value The value to which the expression should be set when not selected.
10439 * @param {string=} ng-change Angular expression to be executed when input changes due to user interaction with the input element.
10440 * @param {boolean=} md-no-ink Use of attribute indicates use of ripple ink effects.
10441 * @param {string=} aria-label Publish the button label used by screen-readers for accessibility. Defaults to the switch's text.
10444 * <hljs lang="html">
10445 * <md-switch ng-model="isActive" aria-label="Finished?">
10449 * <md-switch md-no-ink ng-model="hasInk" aria-label="No Ink Effects">
10453 * <md-switch ng-disabled="true" ng-model="isDisabled" aria-label="Disabled">
10459 function MdSwitch(mdCheckboxDirective, $mdTheming, $mdUtil, $document, $mdConstant, $parse, $$rAF, $mdGesture) {
10460 var checkboxDirective = mdCheckboxDirective[0];
10464 priority:210, // Run before ngAria
10467 '<div class="md-container">' +
10468 '<div class="md-bar"></div>' +
10469 '<div class="md-thumb-container">' +
10470 '<div class="md-thumb" md-ink-ripple md-ink-ripple-checkbox></div>' +
10473 '<div ng-transclude class="md-label">' +
10475 require: '?ngModel',
10479 function compile(element, attr) {
10480 var checkboxLink = checkboxDirective.compile(element, attr);
10481 // no transition on initial load
10482 element.addClass('md-dragging');
10484 return function (scope, element, attr, ngModel) {
10485 ngModel = ngModel || $mdUtil.fakeNgModel();
10486 var disabledGetter = $parse(attr.ngDisabled);
10487 var thumbContainer = angular.element(element[0].querySelector('.md-thumb-container'));
10488 var switchContainer = angular.element(element[0].querySelector('.md-container'));
10490 // no transition on initial load
10492 element.removeClass('md-dragging');
10495 checkboxLink(scope, element, attr, ngModel);
10497 if (angular.isDefined(attr.ngDisabled)) {
10498 scope.$watch(disabledGetter, function(isDisabled) {
10499 element.attr('tabindex', isDisabled ? -1 : 0);
10503 // These events are triggered by setup drag
10504 $mdGesture.register(switchContainer, 'drag');
10506 .on('$md.dragstart', onDragStart)
10507 .on('$md.drag', onDrag)
10508 .on('$md.dragend', onDragEnd);
10511 function onDragStart(ev) {
10512 // Don't go if ng-disabled===true
10513 if (disabledGetter(scope)) return;
10514 ev.stopPropagation();
10516 element.addClass('md-dragging');
10518 width: thumbContainer.prop('offsetWidth')
10520 element.removeClass('transition');
10523 function onDrag(ev) {
10525 ev.stopPropagation();
10526 ev.srcEvent && ev.srcEvent.preventDefault();
10528 var percent = ev.pointer.distanceX / drag.width;
10530 //if checked, start from right. else, start from left
10531 var translate = ngModel.$viewValue ? 1 + percent : percent;
10532 // Make sure the switch stays inside its bounds, 0-1%
10533 translate = Math.max(0, Math.min(1, translate));
10535 thumbContainer.css($mdConstant.CSS.TRANSFORM, 'translate3d(' + (100*translate) + '%,0,0)');
10536 drag.translate = translate;
10539 function onDragEnd(ev) {
10541 ev.stopPropagation();
10543 element.removeClass('md-dragging');
10544 thumbContainer.css($mdConstant.CSS.TRANSFORM, '');
10546 // We changed if there is no distance (this is a click a click),
10547 // or if the drag distance is >50% of the total.
10548 var isChanged = ngModel.$viewValue ? drag.translate < 0.5 : drag.translate > 0.5;
10550 applyModelValue(!ngModel.$viewValue);
10555 function applyModelValue(newValue) {
10556 scope.$apply(function() {
10557 ngModel.$setViewValue(newValue);
10567 MdSwitch.$inject = ["mdCheckboxDirective", "$mdTheming", "$mdUtil", "$document", "$mdConstant", "$parse", "$$rAF", "$mdGesture"];
10575 * @name material.components.tabs
10578 * Tabs, created with the `<md-tabs>` directive provide *tabbed* navigation with different styles.
10579 * The Tabs component consists of clickable tabs that are aligned horizontally side-by-side.
10581 * Features include support for:
10583 * - static or dynamic tabs,
10584 * - responsive designs,
10585 * - accessibility support (ARIA),
10586 * - tab pagination,
10587 * - external or internal tab content,
10588 * - focus indicators and arrow-key navigations,
10589 * - programmatic lookup and access to tab controllers, and
10590 * - dynamic transitions through different tab contents.
10594 * @see js folder for tabs implementation
10596 angular.module('material.components.tabs', [
10598 'material.components.icon'
10607 * @name material.components.toast
10611 angular.module('material.components.toast', [
10613 'material.components.button'
10615 .directive('mdToast', MdToastDirective)
10616 .provider('$mdToast', MdToastProvider);
10618 function MdToastDirective() {
10627 * @module material.components.toast
10630 * `$mdToast` is a service to build a toast notification on any position
10631 * on the screen with an optional duration, and provides a simple promise API.
10634 * ## Restrictions on custom toasts
10635 * - The toast's template must have an outer `<md-toast>` element.
10636 * - For a toast action, use element with class `md-action`.
10637 * - Add the class `md-capsule` for curved corners.
10640 * <hljs lang="html">
10641 * <div ng-controller="MyController">
10642 * <md-button ng-click="openToast()">
10649 * var app = angular.module('app', ['ngMaterial']);
10650 * app.controller('MyController', function($scope, $mdToast) {
10651 * $scope.openToast = function($event) {
10652 * $mdToast.show($mdToast.simple().content('Hello!'));
10653 * // Could also do $mdToast.showSimple('Hello');
10661 * @name $mdToast#showSimple
10664 * Convenience method which builds and shows a simple toast.
10666 * @returns {promise} A promise that can be resolved with `$mdToast.hide()` or
10667 * rejected with `$mdToast.cancel()`.
10673 * @name $mdToast#simple
10676 * Builds a preconfigured toast.
10678 * @returns {obj} a `$mdToastPreset` with the chainable configuration methods:
10680 * - $mdToastPreset#content(string) - sets toast content to string
10681 * - $mdToastPreset#action(string) - adds an action button, which resolves the promise returned from `show()` if clicked.
10682 * - $mdToastPreset#highlightAction(boolean) - sets action button to be highlighted
10683 * - $mdToastPreset#capsule(boolean) - adds 'md-capsule' class to the toast (curved corners)
10684 * - $mdToastPreset#theme(boolean) - sets the theme on the toast to theme (default is `$mdThemingProvider`'s default theme)
10689 * @name $mdToast#updateContent
10692 * Updates the content of an existing toast. Useful for updating things like counts, etc.
10698 * @name $mdToast#build
10701 * Creates a custom `$mdToastPreset` that you can configure.
10703 * @returns {obj} a `$mdToastPreset` with the chainable configuration methods for shows' options (see below).
10708 * @name $mdToast#show
10710 * @description Shows the toast.
10712 * @param {object} optionsOrPreset Either provide an `$mdToastPreset` returned from `simple()`
10713 * and `build()`, or an options object with the following properties:
10715 * - `templateUrl` - `{string=}`: The url of an html template file that will
10716 * be used as the content of the toast. Restrictions: the template must
10717 * have an outer `md-toast` element.
10718 * - `template` - `{string=}`: Same as templateUrl, except this is an actual
10720 * - `scope` - `{object=}`: the scope to link the template / controller to. If none is specified, it will create a new child scope.
10721 * This scope will be destroyed when the toast is removed unless `preserveScope` is set to true.
10722 * - `preserveScope` - `{boolean=}`: whether to preserve the scope when the element is removed. Default is false
10723 * - `hideDelay` - `{number=}`: How many milliseconds the toast should stay
10724 * active before automatically closing. Set to 0 or false to have the toast stay open until
10725 * closed manually. Default: 3000.
10726 * - `position` - `{string=}`: Where to place the toast. Available: any combination
10727 * of 'bottom', 'left', 'top', 'right', 'fit'. Default: 'bottom left'.
10728 * - `controller` - `{string=}`: The controller to associate with this toast.
10729 * The controller will be injected the local `$hideToast`, which is a function
10730 * used to hide the toast.
10731 * - `locals` - `{string=}`: An object containing key/value pairs. The keys will
10732 * be used as names of values to inject into the controller. For example,
10733 * `locals: {three: 3}` would inject `three` into the controller with the value
10735 * - `bindToController` - `bool`: bind the locals to the controller, instead of passing them in. These values will not be available until after initialization.
10736 * - `resolve` - `{object=}`: Similar to locals, except it takes promises as values
10737 * and the toast will not open until the promises resolve.
10738 * - `controllerAs` - `{string=}`: An alias to assign the controller to on the scope.
10739 * - `parent` - `{element=}`: The element to append the toast to. Defaults to appending
10740 * to the root element of the application.
10742 * @returns {promise} A promise that can be resolved with `$mdToast.hide()` or
10743 * rejected with `$mdToast.cancel()`.
10748 * @name $mdToast#hide
10751 * Hide an existing toast and resolve the promise returned from `$mdToast.show()`.
10753 * @param {*=} response An argument for the resolved promise.
10755 * @returns {promise} a promise that is called when the existing element is removed from the DOM
10761 * @name $mdToast#cancel
10764 * Hide the existing toast and reject the promise returned from
10765 * `$mdToast.show()`.
10767 * @param {*=} response An argument for the rejected promise.
10769 * @returns {promise} a promise that is called when the existing element is removed from the DOM
10773 function MdToastProvider($$interimElementProvider) {
10774 var activeToastContent;
10775 var $mdToast = $$interimElementProvider('$mdToast')
10777 methods: ['position', 'hideDelay', 'capsule' ],
10778 options: toastDefaultOptions
10780 .addPreset('simple', {
10781 argOption: 'content',
10782 methods: ['content', 'action', 'highlightAction', 'theme', 'parent'],
10783 options: /* @ngInject */ ["$mdToast", "$mdTheming", function($mdToast, $mdTheming) {
10786 '<md-toast md-theme="{{ toast.theme }}" ng-class="{\'md-capsule\': toast.capsule}">',
10787 '<span flex>{{ toast.content }}</span>',
10788 '<md-button class="md-action" ng-if="toast.action" ng-click="toast.resolve()" ng-class="{\'md-highlight\': toast.highlightAction}">',
10789 '{{ toast.action }}',
10793 controller: /* @ngInject */ ["$scope", function mdToastCtrl($scope) {
10795 $scope.$watch(function() { return activeToastContent; }, function() {
10796 self.content = activeToastContent;
10798 this.resolve = function() {
10802 theme: $mdTheming.defaultTheme(),
10803 controllerAs: 'toast',
10804 bindToController: true
10809 .addMethod('updateContent', function(newContent) {
10810 activeToastContent = newContent;
10813 toastDefaultOptions.$inject = ["$timeout", "$animate", "$mdToast", "$mdUtil"];
10817 function toastDefaultOptions($timeout, $animate, $mdToast, $mdUtil) {
10820 onRemove: onRemove,
10821 position: 'bottom left',
10826 function onShow(scope, element, options) {
10827 element = $mdUtil.extractElementByName(element, 'md-toast');
10829 // 'top left' -> 'md-top md-left'
10830 activeToastContent = options.content;
10831 element.addClass(options.position.split(' ').map(function(pos) {
10832 return 'md-' + pos;
10834 options.parent.addClass(toastOpenClass(options.position));
10836 options.onSwipe = function(ev, gesture) {
10837 //Add swipeleft/swiperight class to element so it can animate correctly
10838 element.addClass('md-' + ev.type.replace('$md.',''));
10839 $timeout($mdToast.cancel);
10841 element.on('$md.swipeleft $md.swiperight', options.onSwipe);
10842 return $animate.enter(element, options.parent);
10845 function onRemove(scope, element, options) {
10846 element.off('$md.swipeleft $md.swiperight', options.onSwipe);
10847 options.parent.removeClass(toastOpenClass(options.position));
10848 return $animate.leave(element);
10851 function toastOpenClass(position) {
10852 return 'md-toast-open-' +
10853 (position.indexOf('top') > -1 ? 'top' : 'bottom');
10858 MdToastProvider.$inject = ["$$interimElementProvider"];
10866 * @name material.components.toolbar
10868 angular.module('material.components.toolbar', [
10870 'material.components.content'
10872 .directive('mdToolbar', mdToolbarDirective);
10877 * @module material.components.toolbar
10880 * `md-toolbar` is used to place a toolbar in your app.
10882 * Toolbars are usually used above a content area to display the title of the
10883 * current page, and show relevant action buttons for that page.
10885 * You can change the height of the toolbar by adding either the
10886 * `md-medium-tall` or `md-tall` class to the toolbar.
10889 * <hljs lang="html">
10890 * <div layout="column" layout-fill>
10893 * <div class="md-toolbar-tools">
10894 * <span>My App's Title</span>
10896 * <!-- fill up the space between left and right area -->
10897 * <span flex></span>
10911 * @param {boolean=} md-scroll-shrink Whether the header should shrink away as
10912 * the user scrolls down, and reveal itself as the user scrolls up.
10913 * Note: for scrollShrink to work, the toolbar must be a sibling of a
10914 * `md-content` element, placed before it. See the scroll shrink demo.
10917 * @param {number=} md-shrink-speed-factor How much to change the speed of the toolbar's
10918 * shrinking by. For example, if 0.25 is given then the toolbar will shrink
10919 * at one fourth the rate at which the user scrolls down. Default 0.5.
10921 function mdToolbarDirective($$rAF, $mdConstant, $mdUtil, $mdTheming, $animate, $timeout) {
10925 controller: angular.noop,
10926 link: function(scope, element, attr) {
10927 $mdTheming(element);
10929 if (angular.isDefined(attr.mdScrollShrink)) {
10930 setupScrollShrink();
10933 function setupScrollShrink() {
10934 // Current "y" position of scroll
10936 // Store the last scroll top position
10937 var prevScrollTop = 0;
10939 var shrinkSpeedFactor = attr.mdShrinkSpeedFactor || 0.5;
10942 var contentElement;
10944 var debouncedContentScroll = $$rAF.throttle(onContentScroll);
10945 var debouncedUpdateHeight = $mdUtil.debounce(updateToolbarHeight, 5 * 1000);
10947 // Wait for $mdContentLoaded event from mdContent directive.
10948 // If the mdContent element is a sibling of our toolbar, hook it up
10949 // to scroll events.
10950 scope.$on('$mdContentLoaded', onMdContentLoad);
10952 function onMdContentLoad($event, newContentEl) {
10953 // Toolbar and content must be siblings
10954 if (element.parent()[0] === newContentEl.parent()[0]) {
10955 // unhook old content event listener if exists
10956 if (contentElement) {
10957 contentElement.off('scroll', debouncedContentScroll);
10960 newContentEl.on('scroll', debouncedContentScroll);
10961 newContentEl.attr('scroll-shrink', 'true');
10963 contentElement = newContentEl;
10964 $$rAF(updateToolbarHeight);
10968 function updateToolbarHeight() {
10969 toolbarHeight = element.prop('offsetHeight');
10970 // Add a negative margin-top the size of the toolbar to the content el.
10971 // The content will start transformed down the toolbarHeight amount,
10972 // so everything looks normal.
10974 // As the user scrolls down, the content will be transformed up slowly
10975 // to put the content underneath where the toolbar was.
10976 var margin = (-toolbarHeight * shrinkSpeedFactor) + 'px';
10977 contentElement.css('margin-top', margin);
10978 contentElement.css('margin-bottom', margin);
10983 function onContentScroll(e) {
10984 var scrollTop = e ? e.target.scrollTop : prevScrollTop;
10986 debouncedUpdateHeight();
10989 toolbarHeight / shrinkSpeedFactor,
10990 Math.max(0, y + scrollTop - prevScrollTop)
10994 $mdConstant.CSS.TRANSFORM,
10995 'translate3d(0,' + (-y * shrinkSpeedFactor) + 'px,0)'
10997 contentElement.css(
10998 $mdConstant.CSS.TRANSFORM,
10999 'translate3d(0,' + ((toolbarHeight - y) * shrinkSpeedFactor) + 'px,0)'
11002 prevScrollTop = scrollTop;
11004 if (element.hasClass('md-whiteframe-z1')) {
11006 $timeout(function () { $animate.removeClass(element, 'md-whiteframe-z1'); });
11010 $timeout(function () { $animate.addClass(element, 'md-whiteframe-z1'); });
11021 mdToolbarDirective.$inject = ["$$rAF", "$mdConstant", "$mdUtil", "$mdTheming", "$animate", "$timeout"];
11029 * @name material.components.tooltip
11032 .module('material.components.tooltip', [ 'material.core' ])
11033 .directive('mdTooltip', MdTooltipDirective);
11038 * @module material.components.tooltip
11040 * Tooltips are used to describe elements that are interactive and primarily graphical (not textual).
11042 * Place a `<md-tooltip>` as a child of the element it describes.
11044 * A tooltip will activate when the user focuses, hovers over, or touches the parent.
11047 * <hljs lang="html">
11048 * <md-button class="md-fab md-accent" aria-label="Play">
11052 * <md-icon icon="img/icons/ic_play_arrow_24px.svg"></md-icon>
11056 * @param {expression=} md-visible Boolean bound to whether the tooltip is
11057 * currently visible.
11058 * @param {number=} md-delay How many milliseconds to wait to show the tooltip after the user focuses, hovers, or touches the parent. Defaults to 400ms.
11059 * @param {string=} md-direction Which direction would you like the tooltip to go? Supports left, right, top, and bottom. Defaults to bottom.
11060 * @param {boolean=} md-autohide If present or provided with a boolean value, the tooltip will hide on mouse leave, regardless of focus
11062 function MdTooltipDirective($timeout, $window, $$rAF, $document, $mdUtil, $mdTheming, $rootElement,
11065 var TOOLTIP_SHOW_DELAY = 300;
11066 var TOOLTIP_WINDOW_EDGE_SPACE = 8;
11071 priority:210, // Before ngAria
11073 <div class="md-background"></div>\
11074 <div class="md-content" ng-transclude></div>',
11076 visible: '=?mdVisible',
11077 delay: '=?mdDelay',
11078 autohide: '=?mdAutohide'
11083 function postLink(scope, element, attr) {
11085 $mdTheming(element);
11087 var parent = getParentWithPointerEvents(),
11088 background = angular.element(element[0].getElementsByClassName('md-background')[0]),
11089 content = angular.element(element[0].getElementsByClassName('md-content')[0]),
11090 direction = attr.mdDirection,
11091 current = getNearestContentElement(),
11092 tooltipParent = angular.element(current || document.body),
11093 debouncedOnResize = $$rAF.throttle(function () { if (scope.visible) positionTooltip(); });
11099 manipulateElement();
11101 configureWatchers();
11105 function setDefaults () {
11106 if (!angular.isDefined(attr.mdDelay)) scope.delay = TOOLTIP_SHOW_DELAY;
11109 function configureWatchers () {
11110 scope.$on('$destroy', function() {
11111 scope.visible = false;
11113 angular.element($window).off('resize', debouncedOnResize);
11115 scope.$watch('visible', function (isVisible) {
11116 if (isVisible) showTooltip();
11117 else hideTooltip();
11121 function addAriaLabel () {
11122 if (!parent.attr('aria-label') && !parent.text().trim()) {
11123 parent.attr('aria-label', element.text().trim());
11127 function manipulateElement () {
11129 element.attr('role', 'tooltip');
11132 function getParentWithPointerEvents () {
11133 var parent = element.parent();
11134 while (parent && $window.getComputedStyle(parent[0])['pointer-events'] == 'none') {
11135 parent = parent.parent();
11140 function getNearestContentElement () {
11141 var current = element.parent()[0];
11142 // Look for the nearest parent md-content, stopping at the rootElement.
11143 while (current && current !== $rootElement[0] && current !== document.body) {
11144 current = current.parentNode;
11149 function hasComputedStyleValue(key, value) {
11150 // Check if we should show it or not...
11151 var computedStyles = $window.getComputedStyle(element[0]);
11152 return angular.isDefined(computedStyles[key]) && (computedStyles[key] == value);
11155 function bindEvents () {
11156 var mouseActive = false;
11157 var enterHandler = function() {
11158 if (!hasComputedStyleValue('pointer-events','none')) {
11162 var leaveHandler = function () {
11163 var autohide = scope.hasOwnProperty('autohide') ? scope.autohide : attr.hasOwnProperty('mdAutohide');
11164 if (autohide || mouseActive || ($document[0].activeElement !== parent[0]) ) {
11167 mouseActive = false;
11170 // to avoid `synthetic clicks` we listen to mousedown instead of `click`
11171 parent.on('mousedown', function() { mouseActive = true; });
11172 parent.on('focus mouseenter touchstart', enterHandler );
11173 parent.on('blur mouseleave touchend touchcancel', leaveHandler );
11176 angular.element($window).on('resize', debouncedOnResize);
11179 function setVisible (value) {
11180 setVisible.value = !!value;
11181 if (!setVisible.queued) {
11183 setVisible.queued = true;
11184 $timeout(function() {
11185 scope.visible = setVisible.value;
11186 setVisible.queued = false;
11189 $timeout(function() { scope.visible = false; });
11194 function showTooltip() {
11195 // Insert the element before positioning it, so we can get the position
11196 // and check if we should display it
11197 tooltipParent.append(element);
11199 // Check if we should display it or not.
11200 // This handles hide-* and show-* along with any user defined css
11201 if ( hasComputedStyleValue('display','none') ) {
11202 scope.visible = false;
11208 angular.forEach([element, background, content], function (element) {
11209 $animate.addClass(element, 'md-show');
11213 function hideTooltip() {
11215 $animate.removeClass(content, 'md-show'),
11216 $animate.removeClass(background, 'md-show'),
11217 $animate.removeClass(element, 'md-show')
11218 ]).then(function () {
11219 if (!scope.visible) element.detach();
11223 function positionTooltip() {
11224 var tipRect = $mdUtil.offsetRect(element, tooltipParent);
11225 var parentRect = $mdUtil.offsetRect(parent, tooltipParent);
11226 var newPosition = getPosition(direction);
11228 // If the user provided a direction, just nudge the tooltip onto the screen
11229 // Otherwise, recalculate based on 'top' since default is 'bottom'
11231 newPosition = fitInParent(newPosition);
11232 } else if (newPosition.top > element.prop('offsetParent').scrollHeight - tipRect.height - TOOLTIP_WINDOW_EDGE_SPACE) {
11233 newPosition = fitInParent(getPosition('top'));
11236 element.css({top: newPosition.top + 'px', left: newPosition.left + 'px'});
11238 positionBackground();
11240 function positionBackground () {
11241 var size = direction === 'left' || direction === 'right'
11242 ? Math.sqrt(Math.pow(tipRect.width, 2) + Math.pow(tipRect.height / 2, 2)) * 2
11243 : Math.sqrt(Math.pow(tipRect.width / 2, 2) + Math.pow(tipRect.height, 2)) * 2,
11244 position = direction === 'left' ? { left: 100, top: 50 }
11245 : direction === 'right' ? { left: 0, top: 50 }
11246 : direction === 'top' ? { left: 50, top: 100 }
11247 : { left: 50, top: 0 };
11249 width: size + 'px',
11250 height: size + 'px',
11251 left: position.left + '%',
11252 top: position.top + '%'
11256 function fitInParent (pos) {
11257 var newPosition = { left: pos.left, top: pos.top };
11258 newPosition.left = Math.min( newPosition.left, tooltipParent.prop('scrollWidth') - tipRect.width - TOOLTIP_WINDOW_EDGE_SPACE );
11259 newPosition.left = Math.max( newPosition.left, TOOLTIP_WINDOW_EDGE_SPACE );
11260 newPosition.top = Math.min( newPosition.top, tooltipParent.prop('scrollHeight') - tipRect.height - TOOLTIP_WINDOW_EDGE_SPACE );
11261 newPosition.top = Math.max( newPosition.top, TOOLTIP_WINDOW_EDGE_SPACE );
11262 return newPosition;
11265 function getPosition (dir) {
11266 return dir === 'left'
11267 ? { left: parentRect.left - tipRect.width - TOOLTIP_WINDOW_EDGE_SPACE,
11268 top: parentRect.top + parentRect.height / 2 - tipRect.height / 2 }
11270 ? { left: parentRect.left + parentRect.width + TOOLTIP_WINDOW_EDGE_SPACE,
11271 top: parentRect.top + parentRect.height / 2 - tipRect.height / 2 }
11273 ? { left: parentRect.left + parentRect.width / 2 - tipRect.width / 2,
11274 top: parentRect.top - tipRect.height - TOOLTIP_WINDOW_EDGE_SPACE }
11275 : { left: parentRect.left + parentRect.width / 2 - tipRect.width / 2,
11276 top: parentRect.top + parentRect.height + TOOLTIP_WINDOW_EDGE_SPACE };
11283 MdTooltipDirective.$inject = ["$timeout", "$window", "$$rAF", "$document", "$mdUtil", "$mdTheming", "$rootElement", "$animate", "$q"];
11291 * @name material.components.whiteframe
11293 angular.module('material.components.whiteframe', []);
11300 .module('material.components.autocomplete')
11301 .controller('MdAutocompleteCtrl', MdAutocompleteCtrl);
11303 var ITEM_HEIGHT = 41,
11304 MAX_HEIGHT = 5.5 * ITEM_HEIGHT,
11307 function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $timeout, $mdTheming, $window, $animate, $rootElement) {
11309 //-- private variables
11312 itemParts = $scope.itemsExpr.split(/ in /i),
11313 itemExpr = itemParts[1],
11318 selectedItemWatchers = [],
11322 //-- public variables
11324 self.scope = $scope;
11325 self.parent = $scope.$parent;
11326 self.itemName = itemParts[0];
11328 self.loading = false;
11329 self.hidden = true;
11331 self.messages = [];
11332 self.id = $mdUtil.nextUid();
11334 //-- public methods
11336 self.keydown = keydown;
11338 self.focus = focus;
11339 self.clear = clearValue;
11340 self.select = select;
11341 self.getCurrentDisplayValue = getCurrentDisplayValue;
11342 self.registerSelectedItemWatcher = registerSelectedItemWatcher;
11343 self.unregisterSelectedItemWatcher = unregisterSelectedItemWatcher;
11345 self.listEnter = function () { noBlur = true; };
11346 self.listLeave = function () {
11348 if (!hasFocus) self.hidden = true;
11350 self.mouseUp = function () { elements.input.focus(); };
11354 //-- initialization methods
11357 configureWatchers();
11358 $timeout(function () {
11365 function positionDropdown () {
11366 if (!elements) return $timeout(positionDropdown, 0, false);
11367 var hrect = elements.wrap.getBoundingClientRect(),
11368 vrect = elements.snap.getBoundingClientRect(),
11369 root = elements.root.getBoundingClientRect(),
11370 top = vrect.bottom - root.top,
11371 bot = root.bottom - vrect.top,
11372 left = hrect.left - root.left,
11373 width = hrect.width,
11376 minWidth: width + 'px',
11377 maxWidth: Math.max(hrect.right - root.left, root.right - hrect.left) - MENU_PADDING + 'px'
11379 if (top > bot && root.height - hrect.bottom - MENU_PADDING < MAX_HEIGHT) {
11380 styles.top = 'auto';
11381 styles.bottom = bot + 'px';
11382 styles.maxHeight = Math.min(MAX_HEIGHT, hrect.top - root.top - MENU_PADDING) + 'px';
11384 styles.top = top + 'px';
11385 styles.bottom = 'auto';
11386 styles.maxHeight = Math.min(MAX_HEIGHT, root.bottom - hrect.bottom - MENU_PADDING) + 'px';
11388 elements.$.ul.css(styles);
11389 $timeout(correctHorizontalAlignment, 0, false);
11391 function correctHorizontalAlignment () {
11392 var dropdown = elements.ul.getBoundingClientRect(),
11394 if (dropdown.right > root.right - MENU_PADDING) {
11395 styles.left = (hrect.right - dropdown.width) + 'px';
11397 elements.$.ul.css(styles);
11401 function moveDropdown () {
11402 if (!elements.$.root.length) return;
11403 $mdTheming(elements.$.ul);
11404 elements.$.ul.detach();
11405 elements.$.root.append(elements.$.ul);
11406 if ($animate.pin) $animate.pin(elements.$.ul, $rootElement);
11409 function focusElement () {
11410 if ($scope.autofocus) elements.input.focus();
11413 function configureWatchers () {
11414 var wait = parseInt($scope.delay, 10) || 0;
11415 $scope.$watch('searchText', wait
11416 ? $mdUtil.debounce(handleSearchText, wait)
11417 : handleSearchText);
11418 registerSelectedItemWatcher(selectedItemChange);
11419 $scope.$watch('selectedItem', handleSelectedItemChange);
11420 $scope.$watch('$mdAutocompleteCtrl.hidden', function (hidden, oldHidden) {
11421 if (!hidden && oldHidden) positionDropdown();
11423 angular.element($window).on('resize', positionDropdown);
11424 $scope.$on('$destroy', cleanup);
11427 function cleanup () {
11428 elements.$.ul.remove();
11431 function gatherElements () {
11434 ul: $element.find('ul')[0],
11435 input: $element.find('input')[0],
11436 wrap: $element.find('md-autocomplete-wrap')[0],
11437 root: document.body
11439 elements.li = elements.ul.getElementsByTagName('li');
11440 elements.snap = getSnapTarget();
11441 elements.$ = getAngularElements(elements);
11444 function getSnapTarget () {
11445 for (var element = $element; element.length; element = element.parent()) {
11446 if (angular.isDefined(element.attr('md-autocomplete-snap'))) return element[0];
11448 return elements.wrap;
11451 function getAngularElements (elements) {
11453 for (var key in elements) {
11454 obj[key] = angular.element(elements[key]);
11459 //-- event/change handlers
11461 function selectedItemChange (selectedItem, previousSelectedItem) {
11462 if (selectedItem) {
11463 $scope.searchText = getDisplayValue(selectedItem);
11465 if ($scope.itemChange && selectedItem !== previousSelectedItem)
11466 $scope.itemChange(getItemScope(selectedItem));
11469 function handleSelectedItemChange(selectedItem, previousSelectedItem) {
11470 for (var i = 0; i < selectedItemWatchers.length; ++i) {
11471 selectedItemWatchers[i](selectedItem, previousSelectedItem);
11476 * Register a function to be called when the selected item changes.
11479 function registerSelectedItemWatcher(cb) {
11480 if (selectedItemWatchers.indexOf(cb) == -1) {
11481 selectedItemWatchers.push(cb);
11486 * Unregister a function previously registered for selected item changes.
11489 function unregisterSelectedItemWatcher(cb) {
11490 var i = selectedItemWatchers.indexOf(cb);
11492 selectedItemWatchers.splice(i, 1);
11496 function handleSearchText (searchText, previousSearchText) {
11497 self.index = getDefaultIndex();
11498 //-- do nothing on init
11499 if (searchText === previousSearchText) return;
11500 //-- clear selected item if search text no longer matches it
11501 if (searchText !== getDisplayValue($scope.selectedItem)) $scope.selectedItem = null;
11503 //-- trigger change event if available
11504 if ($scope.textChange && searchText !== previousSearchText)
11505 $scope.textChange(getItemScope($scope.selectedItem));
11506 //-- cancel results if search text is not long enough
11507 if (!isMinLengthMet()) {
11508 self.loading = false;
11510 self.hidden = shouldHide();
11519 if (!noBlur) self.hidden = true;
11522 function focus () {
11524 //-- if searchText is null, let's force it to be a string
11525 if (!angular.isString($scope.searchText)) $scope.searchText = '';
11526 if ($scope.minLength > 0) return;
11527 self.hidden = shouldHide();
11528 if (!self.hidden) handleQuery();
11531 function keydown (event) {
11532 switch (event.keyCode) {
11533 case $mdConstant.KEY_CODE.DOWN_ARROW:
11534 if (self.loading) return;
11535 event.preventDefault();
11536 self.index = Math.min(self.index + 1, self.matches.length - 1);
11540 case $mdConstant.KEY_CODE.UP_ARROW:
11541 if (self.loading) return;
11542 event.preventDefault();
11543 self.index = self.index < 0 ? self.matches.length - 1 : Math.max(0, self.index - 1);
11547 case $mdConstant.KEY_CODE.TAB:
11548 case $mdConstant.KEY_CODE.ENTER:
11549 if (self.hidden || self.loading || self.index < 0 || self.matches.length < 1) return;
11550 event.preventDefault();
11551 select(self.index);
11553 case $mdConstant.KEY_CODE.ESCAPE:
11555 self.hidden = true;
11556 self.index = getDefaultIndex();
11564 function getMinLength () {
11565 return angular.isNumber($scope.minLength) ? $scope.minLength : 1;
11568 function getDisplayValue (item) {
11569 return (item && $scope.itemText) ? $scope.itemText(getItemScope(item)) : item;
11572 function getItemScope (item) {
11575 if (self.itemName) locals[self.itemName] = item;
11579 function getDefaultIndex () {
11580 return $scope.autoselect ? 0 : -1;
11583 function shouldHide () {
11584 if (!isMinLengthMet()) return true;
11587 function getCurrentDisplayValue () {
11588 return getDisplayValue(self.matches[self.index]);
11591 function isMinLengthMet () {
11592 return $scope.searchText && $scope.searchText.length >= getMinLength();
11597 function select (index) {
11598 $scope.selectedItem = self.matches[index];
11599 self.hidden = true;
11602 //-- force form to update state for validation
11603 $timeout(function () {
11604 elements.$.input.controller('ngModel').$setViewValue(getDisplayValue($scope.selectedItem) || $scope.searchText);
11605 self.hidden = true;
11609 function clearValue () {
11610 $scope.searchText = '';
11613 // Per http://www.w3schools.com/jsref/event_oninput.asp
11614 var eventObj = document.createEvent('CustomEvent');
11615 eventObj.initCustomEvent('input', true, true, {value: $scope.searchText});
11616 elements.input.dispatchEvent(eventObj);
11618 elements.input.focus();
11621 function fetchResults (searchText) {
11622 var items = $scope.$parent.$eval(itemExpr),
11623 term = searchText.toLowerCase();
11624 if (angular.isArray(items)) {
11625 handleResults(items);
11627 self.loading = true;
11628 if (items.success) items.success(handleResults);
11629 if (items.then) items.then(handleResults);
11630 if (items.error) items.error(function () { self.loading = false; });
11632 function handleResults (matches) {
11633 cache[term] = matches;
11634 if (searchText !== $scope.searchText) return; //-- just cache the results if old request
11635 self.loading = false;
11637 self.matches = matches;
11638 self.hidden = shouldHide();
11640 positionDropdown();
11644 function updateMessages () {
11645 self.messages = [ getCountMessage(), getCurrentDisplayValue() ];
11648 function getCountMessage () {
11649 if (lastCount === self.matches.length) return '';
11650 lastCount = self.matches.length;
11651 switch (self.matches.length) {
11652 case 0: return 'There are no matches available.';
11653 case 1: return 'There is 1 match available.';
11654 default: return 'There are ' + self.matches.length + ' matches available.';
11658 function updateScroll () {
11659 if (!elements.li[self.index]) return;
11660 var li = elements.li[self.index],
11661 top = li.offsetTop,
11662 bot = top + li.offsetHeight,
11663 hgt = elements.ul.clientHeight;
11664 if (top < elements.ul.scrollTop) {
11665 elements.ul.scrollTop = top;
11666 } else if (bot > elements.ul.scrollTop + hgt) {
11667 elements.ul.scrollTop = bot - hgt;
11671 function handleQuery () {
11672 var searchText = $scope.searchText,
11673 term = searchText.toLowerCase();
11674 //-- cancel promise if a promise is in progress
11675 if (promise && promise.cancel) {
11679 //-- if results are cached, pull in cached results
11680 if (!$scope.noCache && cache[term]) {
11681 self.matches = cache[term];
11684 fetchResults(searchText);
11686 if (hasFocus) self.hidden = shouldHide();
11690 MdAutocompleteCtrl.$inject = ["$scope", "$element", "$mdUtil", "$mdConstant", "$timeout", "$mdTheming", "$window", "$animate", "$rootElement"];
11697 .module('material.components.autocomplete')
11698 .directive('mdAutocomplete', MdAutocomplete);
11702 * @name mdAutocomplete
11703 * @module material.components.autocomplete
11706 * `<md-autocomplete>` is a special input component with a drop-down of all possible matches to a custom query.
11707 * This component allows you to provide real-time suggestions as the user types in the input area.
11709 * To start, you will need to specify the required parameters and provide a template for your results.
11710 * The content inside `md-autocomplete` will be treated as a template.
11712 * In more complex cases, you may want to include other content such as a message to display when
11713 * no matches were found. You can do this by wrapping your template in `md-item-template` and adding
11714 * a tag for `md-not-found`. An example of this is shown below.
11717 * You can use `ng-messages` to include validation the same way that you would normally validate;
11718 * however, if you want to replicate a standard input with a floating label, you will have to do the
11721 * - Make sure that your template is wrapped in `md-item-template`
11722 * - Add your `ng-messages` code inside of `md-autocomplete`
11723 * - Add your validation properties to `md-autocomplete` (ie. `required`)
11724 * - Add a `name` to `md-autocomplete` (to be used on the generated `input`)
11726 * There is an example below of how this should look.
11729 * @param {expression} md-items An expression in the format of `item in items` to iterate over matches for your search.
11730 * @param {expression=} md-selected-item-change An expression to be run each time a new item is selected
11731 * @param {expression=} md-search-text-change An expression to be run each time the search text updates
11732 * @param {string=} md-search-text A model to bind the search query text to
11733 * @param {object=} md-selected-item A model to bind the selected item to
11734 * @param {string=} md-item-text An expression that will convert your object to a single string.
11735 * @param {string=} placeholder Placeholder text that will be forwarded to the input.
11736 * @param {boolean=} md-no-cache Disables the internal caching that happens in autocomplete
11737 * @param {boolean=} ng-disabled Determines whether or not to disable the input field
11738 * @param {number=} md-min-length Specifies the minimum length of text before autocomplete will make suggestions
11739 * @param {number=} md-delay Specifies the amount of time (in milliseconds) to wait before looking for results
11740 * @param {boolean=} md-autofocus If true, will immediately focus the input element
11741 * @param {boolean=} md-autoselect If true, the first item will be selected by default
11742 * @param {string=} md-menu-class This will be applied to the dropdown menu for styling
11743 * @param {string=} md-floating-label This will add a floating label to autocomplete and wrap it in `md-input-container`
11747 * <hljs lang="html">
11749 * md-selected-item="selectedItem"
11750 * md-search-text="searchText"
11751 * md-items="item in getMatches(searchText)"
11752 * md-item-text="item.display">
11753 * <span md-highlight-text="searchText">{{item.display}}</span>
11754 * </md-autocomplete>
11757 * ###Example with "not found" message
11758 * <hljs lang="html">
11760 * md-selected-item="selectedItem"
11761 * md-search-text="searchText"
11762 * md-items="item in getMatches(searchText)"
11763 * md-item-text="item.display">
11764 * <md-item-template>
11765 * <span md-highlight-text="searchText">{{item.display}}</span>
11766 * </md-item-template>
11768 * No matches found.
11770 * </md-autocomplete>
11773 * In this example, our code utilizes `md-item-template` and `md-not-found` to specify the different
11774 * parts that make up our component.
11776 * ### Example with validation
11777 * <hljs lang="html">
11778 * <form name="autocompleteForm">
11781 * input-name="autocomplete"
11782 * md-selected-item="selectedItem"
11783 * md-search-text="searchText"
11784 * md-items="item in getMatches(searchText)"
11785 * md-item-text="item.display">
11786 * <md-item-template>
11787 * <span md-highlight-text="searchText">{{item.display}}</span>
11788 * </md-item-template>
11789 * <div ng-messages="autocompleteForm.autocomplete.$error">
11790 * <div ng-message="required">This field is required</div>
11792 * </md-autocomplete>
11796 * In this example, our code utilizes `md-item-template` and `md-not-found` to specify the different
11797 * parts that make up our component.
11800 function MdAutocomplete ($mdTheming, $mdUtil) {
11802 controller: 'MdAutocompleteCtrl',
11803 controllerAs: '$mdAutocompleteCtrl',
11806 inputName: '@mdInputName',
11807 inputMinlength: '@mdInputMinlength',
11808 inputMaxlength: '@mdInputMaxlength',
11809 searchText: '=?mdSearchText',
11810 selectedItem: '=?mdSelectedItem',
11811 itemsExpr: '@mdItems',
11812 itemText: '&mdItemText',
11813 placeholder: '@placeholder',
11814 noCache: '=?mdNoCache',
11815 itemChange: '&?mdSelectedItemChange',
11816 textChange: '&?mdSearchTextChange',
11817 minLength: '=?mdMinLength',
11818 delay: '=?mdDelay',
11819 autofocus: '=?mdAutofocus',
11820 floatingLabel: '@?mdFloatingLabel',
11821 autoselect: '=?mdAutoselect',
11822 menuClass: '@?mdMenuClass'
11824 template: function (element, attr) {
11825 var noItemsTemplate = getNoItemsTemplate(),
11826 itemTemplate = getItemTemplate(),
11827 leftover = element.html();
11829 <md-autocomplete-wrap\
11831 ng-class="{ \'md-whiteframe-z1\': !floatingLabel }"\
11833 ' + getInputElement() + '\
11834 <md-progress-linear\
11835 ng-if="$mdAutocompleteCtrl.loading"\
11836 md-mode="indeterminate"></md-progress-linear>\
11837 <ul role="presentation"\
11838 class="md-autocomplete-suggestions md-whiteframe-z1 {{menuClass || \'\'}}"\
11839 id="ul-{{$mdAutocompleteCtrl.id}}"\
11840 ng-mouseenter="$mdAutocompleteCtrl.listEnter()"\
11841 ng-mouseleave="$mdAutocompleteCtrl.listLeave()"\
11842 ng-mouseup="$mdAutocompleteCtrl.mouseUp()">\
11843 <li ng-repeat="(index, item) in $mdAutocompleteCtrl.matches"\
11844 ng-class="{ selected: index === $mdAutocompleteCtrl.index }"\
11845 ng-hide="$mdAutocompleteCtrl.hidden"\
11846 ng-click="$mdAutocompleteCtrl.select(index)"\
11847 md-autocomplete-list-item="$mdAutocompleteCtrl.itemName">\
11848 ' + itemTemplate + '\
11850 ' + noItemsTemplate + '\
11852 </md-autocomplete-wrap>\
11854 class="md-visually-hidden"\
11856 aria-live="assertive">\
11857 <p ng-repeat="message in $mdAutocompleteCtrl.messages" ng-if="message">{{message}}</p>\
11860 function getItemTemplate() {
11861 var templateTag = element.find('md-item-template').remove(),
11862 html = templateTag.length ? templateTag.html() : element.html();
11863 if (!templateTag.length) element.empty();
11867 function getNoItemsTemplate() {
11868 var templateTag = element.find('md-not-found').remove(),
11869 template = templateTag.length ? templateTag.html() : '';
11871 ? '<li ng-if="!$mdAutocompleteCtrl.matches.length && !$mdAutocompleteCtrl.loading\
11872 && !$mdAutocompleteCtrl.hidden"\
11873 ng-hide="$mdAutocompleteCtrl.hidden"\
11874 md-autocomplete-parent-scope>' + template + '</li>'
11879 function getInputElement() {
11880 if (attr.mdFloatingLabel) {
11882 <md-input-container flex ng-if="floatingLabel">\
11883 <label>{{floatingLabel}}</label>\
11884 <input type="search"\
11885 id="fl-input-{{$mdAutocompleteCtrl.id}}"\
11886 name="{{inputName}}"\
11887 autocomplete="off"\
11888 ng-required="isRequired"\
11889 ng-minlength="inputMinlength"\
11890 ng-maxlength="inputMaxlength"\
11891 ng-disabled="isDisabled"\
11892 ng-model="$mdAutocompleteCtrl.scope.searchText"\
11893 ng-keydown="$mdAutocompleteCtrl.keydown($event)"\
11894 ng-blur="$mdAutocompleteCtrl.blur()"\
11895 ng-focus="$mdAutocompleteCtrl.focus()"\
11896 aria-owns="ul-{{$mdAutocompleteCtrl.id}}"\
11897 aria-label="{{floatingLabel}}"\
11898 aria-autocomplete="list"\
11899 aria-haspopup="true"\
11900 aria-activedescendant=""\
11901 aria-expanded="{{!$mdAutocompleteCtrl.hidden}}"/>\
11902 <div md-autocomplete-parent-scope md-autocomplete-replace>' + leftover + '</div>\
11903 </md-input-container>';
11906 <input flex type="search"\
11907 id="input-{{$mdAutocompleteCtrl.id}}"\
11908 name="{{inputName}}"\
11909 ng-if="!floatingLabel"\
11910 autocomplete="off"\
11911 ng-required="isRequired"\
11912 ng-disabled="isDisabled"\
11913 ng-model="$mdAutocompleteCtrl.scope.searchText"\
11914 ng-keydown="$mdAutocompleteCtrl.keydown($event)"\
11915 ng-blur="$mdAutocompleteCtrl.blur()"\
11916 ng-focus="$mdAutocompleteCtrl.focus()"\
11917 placeholder="{{placeholder}}"\
11918 aria-owns="ul-{{$mdAutocompleteCtrl.id}}"\
11919 aria-label="{{placeholder}}"\
11920 aria-autocomplete="list"\
11921 aria-haspopup="true"\
11922 aria-activedescendant=""\
11923 aria-expanded="{{!$mdAutocompleteCtrl.hidden}}"/>\
11927 ng-if="$mdAutocompleteCtrl.scope.searchText && !isDisabled"\
11928 ng-click="$mdAutocompleteCtrl.clear()">\
11929 <md-icon md-svg-icon="md-close"></md-icon>\
11930 <span class="md-visually-hidden">Clear</span>\
11938 function link (scope, element, attr) {
11939 attr.$observe('disabled', function (value) { scope.isDisabled = value; });
11940 attr.$observe('required', function (value) { scope.isRequired = value !== null; });
11942 $mdUtil.initOptionalProperties(scope, attr, {searchText:null, selectedItem:null} );
11944 $mdTheming(element);
11947 MdAutocomplete.$inject = ["$mdTheming", "$mdUtil"];
11954 .module('material.components.autocomplete')
11955 .controller('MdHighlightCtrl', MdHighlightCtrl);
11957 function MdHighlightCtrl ($scope, $element, $interpolate) {
11962 function init (term) {
11963 var unsafeText = $interpolate($element.html())($scope),
11964 text = angular.element('<div>').text(unsafeText).html(),
11965 flags = $element.attr('md-highlight-flags') || '',
11966 watcher = $scope.$watch(term, function (term) {
11967 var regex = getRegExp(term, flags),
11968 html = text.replace(regex, '<span class="highlight">$&</span>');
11969 $element.html(html);
11971 $element.on('$destroy', function () { watcher(); });
11974 function sanitize (term) {
11975 if (!term) return term;
11976 return term.replace(/[\\\^\$\*\+\?\.\(\)\|\{\}\[\]]/g, '\\$&');
11979 function getRegExp (text, flags) {
11981 if (flags.indexOf('^') >= 1) str += '^';
11983 if (flags.indexOf('$') >= 1) str += '$';
11984 return new RegExp(sanitize(str), flags.replace(/[\$\^]/g, ''));
11987 MdHighlightCtrl.$inject = ["$scope", "$element", "$interpolate"];
11994 .module('material.components.autocomplete')
11995 .directive('mdHighlightText', MdHighlight);
11999 * @name mdHighlightText
12000 * @module material.components.autocomplete
12003 * The `md-highlight-text` directive allows you to specify text that should be highlighted within
12004 * an element. Highlighted text will be wrapped in `<span class="highlight"></span>` which can
12005 * be styled through CSS. Please note that child elements may not be used with this directive.
12007 * @param {string} md-highlight-text A model to be searched for
12008 * @param {string=} md-highlight-flags A list of flags (loosely based on JavaScript RexExp flags).
12009 * #### **Supported flags**:
12010 * - `g`: Find all matches within the provided text
12011 * - `i`: Ignore case when searching for matches
12012 * - `$`: Only match if the text ends with the search term
12013 * - `^`: Only match if the text begins with the search term
12016 * <hljs lang="html">
12017 * <input placeholder="Enter a search term..." ng-model="searchTerm" type="text" />
12019 * <li ng-repeat="result in results" md-highlight-text="searchTerm">
12026 function MdHighlight () {
12030 controller: 'MdHighlightCtrl',
12031 link: function (scope, element, attr, ctrl) {
12032 ctrl.init(attr.mdHighlightText);
12042 .module('material.components.autocomplete')
12043 .directive('mdAutocompleteListItem', MdAutocompleteListItem);
12045 function MdAutocompleteListItem ($compile, $mdUtil) {
12051 function postLink (scope, element, attr) {
12052 var ctrl = scope.$parent.$mdAutocompleteCtrl,
12053 newScope = ctrl.parent.$new(false, ctrl.parent),
12054 itemName = ctrl.scope.$eval(attr.mdAutocompleteListItem);
12055 newScope[itemName] = scope.item;
12056 $compile(element.contents())(newScope);
12059 id: 'item_' + $mdUtil.nextUid()
12063 MdAutocompleteListItem.$inject = ["$compile", "$mdUtil"];
12070 .module('material.components.autocomplete')
12071 .directive('mdAutocompleteParentScope', MdAutocompleteParentScope);
12073 function MdAutocompleteParentScope ($compile, $mdUtil) {
12080 function postLink (scope, element, attr) {
12081 var ctrl = scope.$parent.$mdAutocompleteCtrl;
12082 $compile(element.contents())(ctrl.parent);
12083 if (attr.hasOwnProperty('mdAutocompleteReplace')) {
12084 element.after(element.contents());
12089 MdAutocompleteParentScope.$inject = ["$compile", "$mdUtil"];
12096 .module('material.components.chips')
12097 .directive('mdChip', MdChip);
12102 * @module material.components.chips
12105 * `<md-chip>` is a component used within `<md-chips>` and is responsible for rendering individual
12110 * <hljs lang="html">
12111 * <md-chip>{{$chip}}</md-chip>
12116 // This hint text is hidden within a chip but used by screen readers to
12117 // inform the user how they can interact with a chip.
12118 var DELETE_HINT_TEMPLATE = '\
12119 <span ng-if="!$mdChipsCtrl.readonly" class="md-visually-hidden">\
12120 {{$mdChipsCtrl.deleteHint}}\
12124 * MDChip Directive Definition
12126 * @param $mdTheming
12127 * @param $mdInkRipple
12130 function MdChip($mdTheming) {
12133 require: '^?mdChips',
12137 function compile(element, attr) {
12138 element.append(DELETE_HINT_TEMPLATE);
12139 return function postLink(scope, element, attr, ctrl) {
12140 element.addClass('md-chip');
12141 $mdTheming(element);
12143 if (ctrl) angular.element(element[0].querySelector('.md-chip-content'))
12144 .on('blur', function () {
12145 ctrl.selectedChip = -1;
12150 MdChip.$inject = ["$mdTheming"];
12157 .module('material.components.chips')
12158 .directive('mdChipRemove', MdChipRemove);
12162 * @name mdChipRemove
12163 * @module material.components.chips
12166 * `<md-chip-remove>`
12167 * Designates an element to be used as the delete button for a chip. This
12168 * element is passed as a child of the `md-chips` element.
12171 * <hljs lang="html">
12172 * <md-chips><button md-chip-remove>DEL</button></md-chips>
12178 * MdChipRemove Directive Definition.
12182 * @returns {{restrict: string, require: string[], link: Function, scope: boolean}}
12185 function MdChipRemove ($timeout) {
12188 require: '^mdChips',
12193 function postLink(scope, element, attr, ctrl) {
12194 element.on('click', function(event) {
12195 scope.$apply(function() {
12196 ctrl.removeChip(scope.$$replacedScope.$index);
12200 // Child elements aren't available until after a $timeout tick as they are hidden by an
12201 // `ng-if`. see http://goo.gl/zIWfuw
12202 $timeout(function() {
12203 element.attr({ tabindex: -1, ariaHidden: true });
12204 element.find('button').attr('tabindex', '-1');
12208 MdChipRemove.$inject = ["$timeout"];
12215 .module('material.components.chips')
12216 .directive('mdChipTransclude', MdChipTransclude);
12218 function MdChipTransclude ($compile, $mdUtil) {
12225 function link (scope, element, attr) {
12226 var ctrl = scope.$parent.$mdChipsCtrl,
12227 newScope = ctrl.parent.$new(false, ctrl.parent);
12228 newScope.$$replacedScope = scope;
12229 newScope.$chip = scope.$chip;
12230 newScope.$mdChipsCtrl = ctrl;
12231 element.html(ctrl.$scope.$eval(attr.mdChipTransclude));
12232 $compile(element.contents())(newScope);
12235 MdChipTransclude.$inject = ["$compile", "$mdUtil"];
12242 .module('material.components.chips')
12243 .controller('MdChipsCtrl', MdChipsCtrl);
12246 * Controller for the MdChips component. Responsible for adding to and
12247 * removing from the list of chips, marking chips as selected, and binding to
12248 * the models of various input components.
12251 * @param $mdConstant
12256 function MdChipsCtrl ($scope, $mdConstant, $log, $element, $timeout) {
12257 /** @type {$timeout} **/
12258 this.$timeout = $timeout;
12260 /** @type {Object} */
12261 this.$mdConstant = $mdConstant;
12263 /** @type {angular.$scope} */
12264 this.$scope = $scope;
12266 /** @type {angular.$scope} */
12267 this.parent = $scope.$parent;
12269 /** @type {$log} */
12272 /** @type {$element} */
12273 this.$element = $element;
12275 /** @type {angular.NgModelController} */
12276 this.ngModelCtrl = null;
12278 /** @type {angular.NgModelController} */
12279 this.userInputNgModelCtrl = null;
12281 /** @type {Element} */
12282 this.userInputElement = null;
12284 /** @type {Array.<Object>} */
12287 /** @type {number} */
12288 this.selectedChip = -1;
12292 * Hidden hint text for how to delete a chip. Used to give context to screen readers.
12295 this.deleteHint = 'Press delete to remove this chip.';
12298 * Hidden label for the delete button. Used to give context to screen readers.
12301 this.deleteButtonLabel = 'Remove';
12304 * Model used by the input element.
12307 this.chipBuffer = '';
12310 * Whether to use the mdOnAppend expression to transform the chip buffer
12311 * before appending it to the list.
12314 this.useMdOnAppend = false;
12316 MdChipsCtrl.$inject = ["$scope", "$mdConstant", "$log", "$element", "$timeout"];
12319 * Handles the keydown event on the input element: <enter> appends the
12320 * buffer to the chip list, while backspace removes the last chip in the list
12321 * if the current buffer is empty.
12324 MdChipsCtrl.prototype.inputKeydown = function(event) {
12325 var chipBuffer = this.getChipBuffer();
12326 switch (event.keyCode) {
12327 case this.$mdConstant.KEY_CODE.ENTER:
12328 if (this.$scope.requireMatch || !chipBuffer) break;
12329 event.preventDefault();
12330 this.appendChip(chipBuffer);
12331 this.resetChipBuffer();
12333 case this.$mdConstant.KEY_CODE.BACKSPACE:
12334 if (chipBuffer) break;
12335 event.stopPropagation();
12336 if (this.items.length) this.selectAndFocusChipSafe(this.items.length - 1);
12342 * Handles the keydown event on the chip elements: backspace removes the selected chip, arrow
12343 * keys switch which chips is active
12346 MdChipsCtrl.prototype.chipKeydown = function (event) {
12347 if (this.getChipBuffer()) return;
12348 switch (event.keyCode) {
12349 case this.$mdConstant.KEY_CODE.BACKSPACE:
12350 case this.$mdConstant.KEY_CODE.DELETE:
12351 if (this.selectedChip < 0) return;
12352 event.preventDefault();
12353 this.removeAndSelectAdjacentChip(this.selectedChip);
12355 case this.$mdConstant.KEY_CODE.LEFT_ARROW:
12356 event.preventDefault();
12357 if (this.selectedChip < 0) this.selectedChip = this.items.length;
12358 if (this.items.length) this.selectAndFocusChipSafe(this.selectedChip - 1);
12360 case this.$mdConstant.KEY_CODE.RIGHT_ARROW:
12361 event.preventDefault();
12362 this.selectAndFocusChipSafe(this.selectedChip + 1);
12364 case this.$mdConstant.KEY_CODE.ESCAPE:
12365 case this.$mdConstant.KEY_CODE.TAB:
12366 if (this.selectedChip < 0) return;
12367 event.preventDefault();
12374 * Get the input's placeholder - uses `placeholder` when list is empty and `secondary-placeholder`
12375 * when the list is non-empty. If `secondary-placeholder` is not provided, `placeholder` is used
12378 MdChipsCtrl.prototype.getPlaceholder = function() {
12379 // Allow `secondary-placeholder` to be blank.
12380 var useSecondary = (this.items.length &&
12381 (this.secondaryPlaceholder == '' || this.secondaryPlaceholder));
12382 return useSecondary ? this.placeholder : this.secondaryPlaceholder;
12386 * Removes chip at {@code index} and selects the adjacent chip.
12389 MdChipsCtrl.prototype.removeAndSelectAdjacentChip = function(index) {
12390 var selIndex = this.getAdjacentChipIndex(index);
12391 this.removeChip(index);
12392 this.$timeout(angular.bind(this, function () {
12393 this.selectAndFocusChipSafe(selIndex);
12398 * Sets the selected chip index to -1.
12400 MdChipsCtrl.prototype.resetSelectedChip = function() {
12401 this.selectedChip = -1;
12405 * Gets the index of an adjacent chip to select after deletion. Adjacency is
12406 * determined as the next chip in the list, unless the target chip is the
12407 * last in the list, then it is the chip immediately preceding the target. If
12408 * there is only one item in the list, -1 is returned (select none).
12409 * The number returned is the index to select AFTER the target has been
12411 * If the current chip is not selected, then -1 is returned to select none.
12413 MdChipsCtrl.prototype.getAdjacentChipIndex = function(index) {
12414 var len = this.items.length - 1;
12415 return (len == 0) ? -1 :
12416 (index == len) ? index -1 : index;
12420 * Append the contents of the buffer to the chip list. This method will first
12421 * call out to the md-on-append method, if provided
12424 MdChipsCtrl.prototype.appendChip = function(newChip) {
12425 if (this.items.indexOf(newChip) + 1) return;
12426 if (this.useMdOnAppend && this.mdOnAppend) {
12427 newChip = this.mdOnAppend({'$chip': newChip});
12429 this.items.push(newChip);
12433 * Sets whether to use the md-on-append expression. This expression is
12434 * bound to scope and controller in {@code MdChipsDirective} as
12435 * {@code mdOnAppend}. Due to the nature of directive scope bindings, the
12436 * controller cannot know on its own/from the scope whether an expression was
12437 * actually provided.
12439 MdChipsCtrl.prototype.useMdOnAppendExpression = function() {
12440 this.useMdOnAppend = true;
12444 * Gets the input buffer. The input buffer can be the model bound to the
12445 * default input item {@code this.chipBuffer}, the {@code selectedItem}
12446 * model of an {@code md-autocomplete}, or, through some magic, the model
12447 * bound to any inpput or text area element found within a
12448 * {@code md-input-container} element.
12449 * @return {Object|string}
12451 MdChipsCtrl.prototype.getChipBuffer = function() {
12452 return !this.userInputElement ? this.chipBuffer :
12453 this.userInputNgModelCtrl ? this.userInputNgModelCtrl.$viewValue :
12454 this.userInputElement[0].value;
12458 * Resets the input buffer for either the internal input or user provided input element.
12460 MdChipsCtrl.prototype.resetChipBuffer = function() {
12461 if (this.userInputElement) {
12462 if (this.userInputNgModelCtrl) {
12463 this.userInputNgModelCtrl.$setViewValue('');
12464 this.userInputNgModelCtrl.$render();
12466 this.userInputElement[0].value = '';
12469 this.chipBuffer = '';
12474 * Removes the chip at the given index.
12477 MdChipsCtrl.prototype.removeChip = function(index) {
12478 this.items.splice(index, 1);
12481 MdChipsCtrl.prototype.removeChipAndFocusInput = function (index) {
12482 this.removeChip(index);
12486 * Selects the chip at `index`,
12489 MdChipsCtrl.prototype.selectAndFocusChipSafe = function(index) {
12490 if (!this.items.length) {
12491 this.selectChip(-1);
12495 if (index === this.items.length) return this.onFocus();
12496 index = Math.max(index, 0);
12497 index = Math.min(index, this.items.length - 1);
12498 this.selectChip(index);
12499 this.focusChip(index);
12503 * Marks the chip at the given index as selected.
12506 MdChipsCtrl.prototype.selectChip = function(index) {
12507 if (index >= -1 && index <= this.items.length) {
12508 this.selectedChip = index;
12510 this.$log.warn('Selected Chip index out of bounds; ignoring.');
12515 * Selects the chip at `index` and gives it focus.
12518 MdChipsCtrl.prototype.selectAndFocusChip = function(index) {
12519 this.selectChip(index);
12521 this.focusChip(index);
12526 * Call `focus()` on the chip at `index`
12528 MdChipsCtrl.prototype.focusChip = function(index) {
12529 this.$element[0].querySelector('md-chip[index="' + index + '"] .md-chip-content').focus();
12533 * Configures the required interactions with the ngModel Controller.
12534 * Specifically, set {@code this.items} to the {@code NgModelCtrl#$viewVale}.
12535 * @param ngModelCtrl
12537 MdChipsCtrl.prototype.configureNgModel = function(ngModelCtrl) {
12538 this.ngModelCtrl = ngModelCtrl;
12541 ngModelCtrl.$render = function() {
12542 // model is updated. do something.
12543 self.items = self.ngModelCtrl.$viewValue;
12547 MdChipsCtrl.prototype.onFocus = function () {
12548 var input = this.$element[0].querySelector('input');
12549 input && input.focus();
12550 this.resetSelectedChip();
12553 MdChipsCtrl.prototype.onInputFocus = function () {
12554 this.inputHasFocus = true;
12555 this.resetSelectedChip();
12558 MdChipsCtrl.prototype.onInputBlur = function () {
12559 this.inputHasFocus = false;
12563 * Configure event bindings on a user-provided input element.
12564 * @param inputElement
12566 MdChipsCtrl.prototype.configureUserInput = function(inputElement) {
12567 this.userInputElement = inputElement;
12569 // Find the NgModelCtrl for the input element
12570 var ngModelCtrl = inputElement.controller('ngModel');
12571 // `.controller` will look in the parent as well.
12572 if (ngModelCtrl != this.ngModelCtrl) {
12573 this.userInputNgModelCtrl = ngModelCtrl;
12576 // Bind to keydown and focus events of input
12577 var scope = this.$scope;
12580 .attr({ tabindex: 0 })
12581 .on('keydown', function(event) { scope.$apply( angular.bind(ctrl, function() { ctrl.inputKeydown(event); })) })
12582 .on('focus', angular.bind(ctrl, ctrl.onInputFocus))
12583 .on('blur', angular.bind(ctrl, ctrl.onInputBlur));
12586 MdChipsCtrl.prototype.configureAutocomplete = function(ctrl) {
12588 ctrl.registerSelectedItemWatcher(angular.bind(this, function (item) {
12590 this.appendChip(item);
12591 this.resetChipBuffer();
12595 this.$element.find('input')
12596 .on('focus',angular.bind(this, this.onInputFocus) )
12597 .on('blur', angular.bind(this, this.onInputBlur) );
12600 MdChipsCtrl.prototype.hasFocus = function () {
12601 return this.inputHasFocus || this.selectedChip >= 0;
12609 .module('material.components.chips')
12610 .directive('mdChips', MdChips);
12615 * @module material.components.chips
12618 * `<md-chips>` is an input component for building lists of strings or objects. The list items are
12619 * displayed as 'chips'. This component can make use of an `<input>` element or an
12620 * `<md-autocomplete>` element.
12622 * <strong>Custom `<md-chip-template>` template</strong>
12623 * A custom template may be provided to render the content of each chip. This is achieved by
12624 * specifying an `<md-chip-template>` element as a child of `<md-chips>`. Note: Any attributes on
12625 * `<md-chip-template>` will be dropped as only the innerHTML is used for the chip template. The
12626 * variables `$chip` and `$index` are available in the scope of `<md-chip-template>`, representing
12627 * the chip object and its index in the list of chips, respectively.
12628 * To override the chip delete control, include an element (ideally a button) with the attribute
12629 * `md-chip-remove`. A click listener to remove the chip will be added automatically. The element
12630 * is also placed as a sibling to the chip content (on which there are also click listeners) to
12631 * avoid a nested ng-click situation.
12633 * <h3> Pending Features </h3>
12634 * <ul style="padding-left:20px;">
12637 * <li>Colours for hover, press states (ripple?).</li>
12640 * <ul>List Manipulation
12641 * <li>delete item via DEL or backspace keys when selected</li>
12645 * <li>de-dupe values (or support duplicates, but fix the ng-repeat duplicate key issue)</li>
12646 * <li>allow a validation callback</li>
12647 * <li>hilighting style for invalid chips</li>
12650 * <ul>Item mutation
12652 * <md-chip-edit>` template, show/hide the edit element on tap/click? double tap/double
12657 * <ul>Truncation and Disambiguation (?)
12658 * <li>Truncate chip text where possible, but do not truncate entries such that two are
12659 * indistinguishable.</li>
12662 * <ul>Drag and Drop
12663 * <li>Drag and drop chips between related `<md-chips>` elements.
12668 * <span style="font-size:.8em;text-align:center">
12669 * Warning: This component is a WORK IN PROGRESS. If you use it now,
12670 * it will probably break on you in the future.
12673 * @param {string=|object=} ng-model A model to bind the list of items to
12674 * @param {string=} placeholder Placeholder text that will be forwarded to the input.
12675 * @param {string=} secondary-placeholder Placeholder text that will be forwarded to the input,
12676 * displayed when there is at least on item in the list
12677 * @param {boolean=} readonly Disables list manipulation (deleting or adding list items), hiding
12678 * the input and delete buttons
12679 * @param {expression} md-on-append An expression expected to convert the input string into an
12680 * object when adding a chip.
12681 * @param {string=} delete-hint A string read by screen readers instructing users that pressing
12682 * the delete key will remove the chip.
12683 * @param {string=} delete-button-label A label for the delete button. Also hidden and read by
12687 * <hljs lang="html">
12689 * ng-model="myItems"
12690 * placeholder="Add an item"
12691 * readonly="isReadOnly">
12698 var MD_CHIPS_TEMPLATE = '\
12700 ng-if="!$mdChipsCtrl.readonly || $mdChipsCtrl.items.length > 0"\
12701 ng-keydown="$mdChipsCtrl.chipKeydown($event)"\
12702 ng-class="{ \'md-focused\': $mdChipsCtrl.hasFocus() }"\
12704 <md-chip ng-repeat="$chip in $mdChipsCtrl.items"\
12705 index="{{$index}}"\
12706 ng-class="{\'md-focused\': $mdChipsCtrl.selectedChip == $index}">\
12707 <div class="md-chip-content"\
12709 aria-hidden="true"\
12710 ng-focus="!$mdChipsCtrl.readonly && $mdChipsCtrl.selectChip($index)"\
12711 md-chip-transclude="$mdChipsCtrl.chipContentsTemplate"></div>\
12712 <div class="md-chip-remove-container"\
12713 md-chip-transclude="$mdChipsCtrl.chipRemoveTemplate"></div>\
12715 <div ng-if="!$mdChipsCtrl.readonly && $mdChipsCtrl.ngModelCtrl"\
12716 class="md-chip-input-container"\
12717 md-chip-transclude="$mdChipsCtrl.chipInputTemplate"></div>\
12721 var CHIP_INPUT_TEMPLATE = '\
12724 placeholder="{{$mdChipsCtrl.getPlaceholder()}}"\
12725 aria-label="{{$mdChipsCtrl.getPlaceholder()}}"\
12726 ng-model="$mdChipsCtrl.chipBuffer"\
12727 ng-focus="$mdChipsCtrl.onInputFocus()"\
12728 ng-blur="$mdChipsCtrl.onInputBlur()"\
12729 ng-keydown="$mdChipsCtrl.inputKeydown($event)">';
12731 var CHIP_DEFAULT_TEMPLATE = '\
12732 <span>{{$chip}}</span>';
12734 var CHIP_REMOVE_TEMPLATE = '\
12736 class="md-chip-remove"\
12737 ng-if="!$mdChipsCtrl.readonly"\
12738 ng-click="$mdChipsCtrl.removeChipAndFocusInput($$replacedScope.$index)"\
12740 aria-hidden="true"\
12742 <md-icon md-svg-icon="md-close"></md-icon>\
12743 <span class="md-visually-hidden">\
12744 {{$mdChipsCtrl.deleteButtonLabel}}\
12749 * MDChips Directive Definition
12751 function MdChips ($mdTheming, $mdUtil, $compile, $log, $timeout) {
12753 template: function(element, attrs) {
12754 // Clone the element into an attribute. By prepending the attribute
12755 // name with '$', Angular won't write it into the DOM. The cloned
12756 // element propagates to the link function via the attrs argument,
12757 // where various contained-elements can be consumed.
12758 var content = attrs['$mdUserTemplate'] = element.clone();
12759 return MD_CHIPS_TEMPLATE;
12761 require: ['mdChips'],
12763 controller: 'MdChipsCtrl',
12764 controllerAs: '$mdChipsCtrl',
12765 bindToController: true,
12768 readonly: '=readonly',
12770 secondaryPlaceholder: '@',
12773 deleteButtonLabel: '@',
12774 requireMatch: '=?mdRequireMatch'
12779 * Builds the final template for `md-chips` and returns the postLink function.
12781 * Building the template involves 3 key components:
12786 * If no `ng-model` is provided, only the static chip work needs to be done.
12788 * If no user-passed `md-chip-template` exists, the default template is used. This resulting
12789 * template is appended to the chip content element.
12791 * The remove button may be overridden by passing an element with an md-chip-remove attribute.
12793 * If an `input` or `md-autocomplete` element is provided by the caller, it is set aside for
12794 * transclusion later. The transclusion happens in `postLink` as the parent scope is required.
12795 * If no user input is provided, a default one is appended to the input container node in the
12798 * Static Chips (i.e. `md-chip` elements passed from the caller) are gathered and set aside for
12799 * transclusion in the `postLink` function.
12804 * @returns {Function}
12806 function compile(element, attr) {
12807 // Grab the user template from attr and reset the attribute to null.
12808 var userTemplate = attr['$mdUserTemplate'];
12809 attr['$mdUserTemplate'] = null;
12811 // Set the chip remove, chip contents and chip input templates. The link function will put
12812 // them on the scope for transclusion later.
12813 var chipRemoveTemplate = getTemplateByQuery('md-chips>*[md-chip-remove]') || CHIP_REMOVE_TEMPLATE,
12814 chipContentsTemplate = getTemplateByQuery('md-chips>md-chip-template') || CHIP_DEFAULT_TEMPLATE,
12815 chipInputTemplate = getTemplateByQuery('md-chips>md-autocomplete')
12816 || getTemplateByQuery('md-chips>input')
12817 || CHIP_INPUT_TEMPLATE,
12818 staticChips = userTemplate.find('md-chip');
12820 // Warn of malformed template. See #2545
12821 if (userTemplate[0].querySelector('md-chip-template>*[md-chip-remove]')) {
12822 $log.warn('invalid placement of md-chip-remove within md-chip-template.');
12825 function getTemplateByQuery (query) {
12826 if (!attr.ngModel) return;
12827 var element = userTemplate[0].querySelector(query);
12828 return element && element.outerHTML;
12832 * Configures controller and transcludes.
12834 return function postLink(scope, element, attrs, controllers) {
12836 $mdUtil.initOptionalProperties(scope, attr);
12838 $mdTheming(element);
12839 var mdChipsCtrl = controllers[0];
12840 mdChipsCtrl.chipContentsTemplate = chipContentsTemplate;
12841 mdChipsCtrl.chipRemoveTemplate = chipRemoveTemplate;
12842 mdChipsCtrl.chipInputTemplate = chipInputTemplate;
12845 .attr({ ariaHidden: true, tabindex: -1 })
12846 .on('focus', function () { mdChipsCtrl.onFocus(); });
12848 if (attr.ngModel) {
12849 mdChipsCtrl.configureNgModel(element.controller('ngModel'));
12851 // If an `md-on-append` attribute was set, tell the controller to use the expression
12852 // when appending chips.
12853 if (attrs.mdOnAppend) mdChipsCtrl.useMdOnAppendExpression();
12855 // The md-autocomplete and input elements won't be compiled until after this directive
12856 // is complete (due to their nested nature). Wait a tick before looking for them to
12857 // configure the controller.
12858 if (chipInputTemplate != CHIP_INPUT_TEMPLATE) {
12859 $timeout(function() {
12860 if (chipInputTemplate.indexOf('<md-autocomplete') === 0)
12862 .configureAutocomplete(element.find('md-autocomplete')
12863 .controller('mdAutocomplete'));
12864 mdChipsCtrl.configureUserInput(element.find('input'));
12869 // Compile with the parent's scope and prepend any static chips to the wrapper.
12870 if (staticChips.length > 0) {
12871 var compiledStaticChips = $compile(staticChips)(scope.$parent);
12872 $timeout(function() { element.find('md-chips-wrap').prepend(compiledStaticChips); });
12877 MdChips.$inject = ["$mdTheming", "$mdUtil", "$compile", "$log", "$timeout"];
12884 .module('material.components.chips')
12885 .controller('MdContactChipsCtrl', MdContactChipsCtrl);
12890 * Controller for the MdContactChips component
12893 function MdContactChipsCtrl () {
12894 /** @type {Object} */
12895 this.selectedItem = null;
12897 /** @type {string} */
12898 this.searchText = '';
12902 MdContactChipsCtrl.prototype.queryContact = function(searchText) {
12903 var results = this.contactQuery({'$query': searchText});
12904 return this.filterSelected ?
12905 results.filter(angular.bind(this, this.filterSelectedContacts)) : results;
12909 MdContactChipsCtrl.prototype.filterSelectedContacts = function(contact) {
12910 return this.contacts.indexOf(contact) == -1;
12918 .module('material.components.chips')
12919 .directive('mdContactChips', MdContactChips);
12923 * @name mdContactChips
12924 * @module material.components.chips
12927 * `<md-contact-chips>` is an input component based on `md-chips` and makes use of an
12928 * `md-autocomplete` element. The component allows the caller to supply a query expression
12929 * which returns a list of possible contacts. The user can select one of these and add it to
12930 * the list of chips.
12932 * @param {string=|object=} ng-model A model to bind the list of items to
12933 * @param {string=} placeholder Placeholder text that will be forwarded to the input.
12934 * @param {string=} secondary-placeholder Placeholder text that will be forwarded to the input,
12935 * displayed when there is at least on item in the list
12936 * @param {expression} md-contacts An expression expected to return contacts matching the search
12938 * @param {string} md-contact-name The field name of the contact object representing the
12940 * @param {string} md-contact-email The field name of the contact object representing the
12941 * contact's email address.
12942 * @param {string} md-contact-image The field name of the contact object representing the
12946 * // The following attribute has been removed but may come back.
12947 * @param {expression=} filter-selected Whether to filter selected contacts from the list of
12948 * suggestions shown in the autocomplete.
12953 * <hljs lang="html">
12954 * <md-contact-chips
12955 * ng-model="ctrl.contacts"
12956 * md-contacts="ctrl.querySearch($query)"
12957 * md-contact-name="name"
12958 * md-contact-image="image"
12959 * md-contact-email="email"
12960 * placeholder="To">
12961 * </md-contact-chips>
12967 var MD_CONTACT_CHIPS_TEMPLATE = '\
12968 <md-chips class="md-contact-chips"\
12969 ng-model="$mdContactChipsCtrl.contacts"\
12970 md-require-match="$mdContactChipsCtrl.requireMatch"\
12971 md-autocomplete-snap>\
12973 md-menu-class="md-contact-chips-suggestions"\
12974 md-selected-item="$mdContactChipsCtrl.selectedItem"\
12975 md-search-text="$mdContactChipsCtrl.searchText"\
12976 md-items="item in $mdContactChipsCtrl.queryContact($mdContactChipsCtrl.searchText)"\
12977 md-item-text="$mdContactChipsCtrl.mdContactName"\
12978 md-no-cache="true"\
12980 placeholder="{{$mdContactChipsCtrl.contacts.length == 0 ?\
12981 $mdContactChipsCtrl.placeholder : $mdContactChipsCtrl.secondaryPlaceholder}}">\
12982 <div class="md-contact-suggestion">\
12984 ng-src="{{item[$mdContactChipsCtrl.contactImage]}}"\
12985 alt="{{item[$mdContactChipsCtrl.contactName]}}" />\
12986 <span class="md-contact-name" md-highlight-text="$mdContactChipsCtrl.searchText">\
12987 {{item[$mdContactChipsCtrl.contactName]}}\
12989 <span class="md-contact-email" >{{item[$mdContactChipsCtrl.contactEmail]}}</span>\
12991 </md-autocomplete>\
12992 <md-chip-template>\
12993 <div class="md-contact-avatar">\
12995 ng-src="{{$chip[$mdContactChipsCtrl.contactImage]}}"\
12996 alt="{{$chip[$mdContactChipsCtrl.contactName]}}" />\
12998 <div class="md-contact-name">\
12999 {{$chip[$mdContactChipsCtrl.contactName]}}\
13001 </md-chip-template>\
13006 * MDContactChips Directive Definition
13008 * @param $mdTheming
13012 function MdContactChips ($mdTheming, $mdUtil) {
13014 template: function(element, attrs) {
13015 return MD_CONTACT_CHIPS_TEMPLATE;
13018 controller: 'MdContactChipsCtrl',
13019 controllerAs: '$mdContactChipsCtrl',
13020 bindToController: true,
13023 contactQuery: '&mdContacts',
13025 secondaryPlaceholder: '@',
13026 contactName: '@mdContactName',
13027 contactImage: '@mdContactImage',
13028 contactEmail: '@mdContactEmail',
13029 contacts: '=ngModel',
13030 requireMatch: '=?mdRequireMatch'
13034 function compile(element, attr) {
13035 return function postLink(scope, element, attrs, controllers) {
13037 $mdUtil.initOptionalProperties(scope, attr);
13038 $mdTheming(element);
13040 element.attr('tabindex', '-1');
13044 MdContactChips.$inject = ["$mdTheming", "$mdUtil"];
13053 * @module material.components.tabs
13058 * Use the `<md-tab>` a nested directive used within `<md-tabs>` to specify a tab with a **label** and optional *view content*.
13060 * If the `label` attribute is not specified, then an optional `<md-tab-label>` tag can be used to specify more
13061 * complex tab header markup. If neither the **label** nor the **md-tab-label** are specified, then the nested
13062 * markup of the `<md-tab>` is used as the tab header markup.
13064 * Please note that if you use `<md-tab-label>`, your content **MUST** be wrapped in the `<md-tab-body>` tag. This
13065 * is to define a clear separation between the tab content and the tab label.
13067 * If a tab **label** has been identified, then any **non-**`<md-tab-label>` markup
13068 * will be considered tab content and will be transcluded to the internal `<div class="md-tabs-content">` container.
13070 * This container is used by the TabsController to show/hide the active tab's content view. This synchronization is
13071 * automatically managed by the internal TabsController whenever the tab selection changes. Selection changes can
13072 * be initiated via data binding changes, programmatic invocation, or user gestures.
13074 * @param {string=} label Optional attribute to specify a simple string as the tab label
13075 * @param {boolean=} disabled If present, disabled tab selection.
13076 * @param {expression=} md-on-deselect Expression to be evaluated after the tab has been de-selected.
13077 * @param {expression=} md-on-select Expression to be evaluated after the tab has been selected.
13082 * <hljs lang="html">
13083 * <md-tab label="" disabled="" md-on-select="" md-on-deselect="" >
13084 * <h3>My Tab content</h3>
13089 * <h3>My Tab content</h3>
13093 * Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium,
13094 * totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae
13095 * dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit,
13096 * sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.
13104 .module('material.components.tabs')
13105 .directive('mdTab', MdTab);
13107 function MdTab () {
13109 require: '^?mdTabs',
13111 template: function (element, attr) {
13112 var label = getLabel(),
13113 body = getTemplate();
13115 '<md-tab-label>' + label + '</md-tab-label>' +
13116 '<md-tab-body>' + body + '</md-tab-body>';
13117 function getLabel () {
13118 return getLabelElement() || getLabelAttribute() || getElementContents();
13119 function getLabelAttribute () { return attr.label; }
13120 function getLabelElement () {
13121 var label = element.find('md-tab-label');
13122 if (label.length) return label.remove().html();
13124 function getElementContents () {
13125 var html = element.html();
13130 function getTemplate () {
13131 var content = element.find('md-tab-body'),
13132 template = content.length ? content.html() : attr.label ? element.html() : '';
13133 if (content.length) content.remove();
13134 else if (attr.label) element.empty();
13139 active: '=?mdActive',
13140 disabled: '=?ngDisabled',
13141 select: '&?mdOnSelect',
13142 deselect: '&?mdOnDeselect'
13147 function postLink (scope, element, attr, ctrl) {
13149 var tabs = element.parent()[0].getElementsByTagName('md-tab'),
13150 index = Array.prototype.indexOf.call(tabs, element[0]),
13151 body = element.find('md-tab-body').remove(),
13152 label = element.find('md-tab-label').remove(),
13153 data = ctrl.insertTab({
13155 parent: scope.$parent,
13158 template: body.html(),
13159 label: label.html()
13162 scope.select = scope.select || angular.noop;
13163 scope.deselect = scope.deselect || angular.noop;
13165 scope.$watch('active', function (active) { if (active) ctrl.select(data.getIndex()); });
13166 scope.$watch('disabled', function () { ctrl.refreshIndex(); });
13169 return Array.prototype.indexOf.call(tabs, element[0]);
13171 function (newIndex) {
13172 data.index = newIndex;
13173 ctrl.updateTabOrder();
13176 scope.$on('$destroy', function () { ctrl.removeTab(data); });
13186 .module('material.components.tabs')
13187 .directive('mdTabItem', MdTabItem);
13189 function MdTabItem () {
13191 require: '^?mdTabs',
13192 link: function link (scope, element, attr, ctrl) {
13194 ctrl.attachRipple(scope, element);
13204 .module('material.components.tabs')
13205 .directive('mdTabLabel', MdTabLabel);
13207 function MdTabLabel () {
13208 return { terminal: true };
13216 angular.module('material.components.tabs')
13217 .directive('mdTabScroll', MdTabScroll);
13219 function MdTabScroll ($parse) {
13222 compile: function ($element, attr) {
13223 var fn = $parse(attr.mdTabScroll, null, true);
13224 return function ngEventHandler (scope, element) {
13225 element.on('mousewheel', function (event) {
13226 scope.$apply(function () { fn(scope, { $event: event }); });
13232 MdTabScroll.$inject = ["$parse"];
13239 .module('material.components.tabs')
13240 .controller('MdTabsController', MdTabsController);
13245 function MdTabsController ($scope, $element, $window, $timeout, $mdConstant, $mdTabInkRipple,
13246 $mdUtil, $animate) {
13249 elements = getElements(),
13253 ctrl.scope = $scope;
13254 ctrl.parent = $scope.$parent;
13256 ctrl.lastSelectedIndex = null;
13257 ctrl.focusIndex = $scope.selectedIndex || 0;
13258 ctrl.offsetLeft = 0;
13259 ctrl.hasContent = false;
13260 ctrl.hasFocus = false;
13261 ctrl.lastClick = true;
13263 ctrl.redirectFocus = redirectFocus;
13264 ctrl.attachRipple = attachRipple;
13265 ctrl.shouldStretchTabs = shouldStretchTabs;
13266 ctrl.shouldPaginate = shouldPaginate;
13267 ctrl.shouldCenterTabs = shouldCenterTabs;
13268 ctrl.insertTab = insertTab;
13269 ctrl.removeTab = removeTab;
13270 ctrl.select = select;
13271 ctrl.scroll = scroll;
13272 ctrl.nextPage = nextPage;
13273 ctrl.previousPage = previousPage;
13274 ctrl.keydown = keydown;
13275 ctrl.canPageForward = canPageForward;
13276 ctrl.canPageBack = canPageBack;
13277 ctrl.refreshIndex = refreshIndex;
13278 ctrl.incrementSelectedIndex = incrementSelectedIndex;
13279 ctrl.updateInkBarStyles = $mdUtil.debounce(updateInkBarStyles, 100);
13280 ctrl.updateTabOrder = $mdUtil.debounce(updateTabOrder, 100);
13285 $scope.$watch('selectedIndex', handleSelectedIndexChange);
13286 $scope.$watch('$mdTabsCtrl.focusIndex', handleFocusIndexChange);
13287 $scope.$watch('$mdTabsCtrl.offsetLeft', handleOffsetChange);
13288 $scope.$watch('$mdTabsCtrl.hasContent', handleHasContent);
13289 angular.element($window).on('resize', handleWindowResize);
13290 angular.element(elements.paging).on('DOMSubtreeModified', ctrl.updateInkBarStyles);
13291 $timeout(updateHeightFromContent, 0, false);
13292 $timeout(adjustOffset);
13293 $scope.$on('$destroy', cleanup);
13296 function cleanup () {
13298 angular.element($window).off('resize', handleWindowResize);
13299 angular.element(elements.paging).off('DOMSubtreeModified', ctrl.updateInkBarStyles);
13302 //-- Change handlers
13304 function handleHasContent (hasContent) {
13305 $element[hasContent ? 'removeClass' : 'addClass']('md-no-tab-content');
13308 function handleOffsetChange (left) {
13309 var newValue = shouldCenterTabs() ? '' : '-' + left + 'px';
13310 angular.element(elements.paging).css('transform', 'translate3d(' + newValue + ', 0, 0)');
13311 $scope.$broadcast('$mdTabsPaginationChanged');
13314 function handleFocusIndexChange (newIndex, oldIndex) {
13315 if (newIndex === oldIndex) return;
13316 if (!elements.tabs[newIndex]) return;
13321 function handleSelectedIndexChange (newValue, oldValue) {
13322 if (newValue === oldValue) return;
13324 $scope.selectedIndex = getNearestSafeIndex(newValue);
13325 ctrl.lastSelectedIndex = oldValue;
13326 ctrl.updateInkBarStyles();
13327 updateHeightFromContent();
13328 $scope.$broadcast('$mdTabsChanged');
13329 ctrl.tabs[oldValue] && ctrl.tabs[oldValue].scope.deselect();
13330 ctrl.tabs[newValue] && ctrl.tabs[newValue].scope.select();
13333 function handleResizeWhenVisible () {
13334 //-- if there is already a watcher waiting for resize, do nothing
13335 if (handleResizeWhenVisible.watcher) return;
13336 //-- otherwise, we will abuse the $watch function to check for visible
13337 handleResizeWhenVisible.watcher = $scope.$watch(function () {
13338 //-- since we are checking for DOM size, we use $timeout to wait for after the DOM updates
13339 $timeout(function () {
13340 //-- if the watcher has already run (ie. multiple digests in one cycle), do nothing
13341 if (!handleResizeWhenVisible.watcher) return;
13343 if ($element.prop('offsetParent')) {
13344 handleResizeWhenVisible.watcher();
13345 handleResizeWhenVisible.watcher = null;
13347 //-- we have to trigger our own $apply so that the DOM bindings will update
13348 handleWindowResize();
13354 //-- Event handlers / actions
13356 function keydown (event) {
13357 switch (event.keyCode) {
13358 case $mdConstant.KEY_CODE.LEFT_ARROW:
13359 event.preventDefault();
13360 incrementSelectedIndex(-1, true);
13362 case $mdConstant.KEY_CODE.RIGHT_ARROW:
13363 event.preventDefault();
13364 incrementSelectedIndex(1, true);
13366 case $mdConstant.KEY_CODE.SPACE:
13367 case $mdConstant.KEY_CODE.ENTER:
13368 event.preventDefault();
13369 if (!locked) $scope.selectedIndex = ctrl.focusIndex;
13372 ctrl.lastClick = false;
13375 function select (index) {
13376 if (!locked) ctrl.focusIndex = $scope.selectedIndex = index;
13377 ctrl.lastClick = true;
13378 ctrl.tabs[index].element.triggerHandler('click');
13381 function scroll (event) {
13382 if (!shouldPaginate()) return;
13383 event.preventDefault();
13384 ctrl.offsetLeft = fixOffset(ctrl.offsetLeft - event.wheelDelta);
13387 function nextPage () {
13388 var viewportWidth = elements.canvas.clientWidth,
13389 totalWidth = viewportWidth + ctrl.offsetLeft,
13391 for (i = 0; i < elements.tabs.length; i++) {
13392 tab = elements.tabs[i];
13393 if (tab.offsetLeft + tab.offsetWidth > totalWidth) break;
13395 ctrl.offsetLeft = fixOffset(tab.offsetLeft);
13398 function previousPage () {
13400 for (i = 0; i < elements.tabs.length; i++) {
13401 tab = elements.tabs[i];
13402 if (tab.offsetLeft + tab.offsetWidth >= ctrl.offsetLeft) break;
13404 ctrl.offsetLeft = fixOffset(tab.offsetLeft + tab.offsetWidth - elements.canvas.clientWidth);
13407 function handleWindowResize () {
13408 $scope.$apply(function () {
13409 ctrl.lastSelectedIndex = $scope.selectedIndex;
13410 ctrl.offsetLeft = fixOffset(ctrl.offsetLeft);
13411 $timeout(ctrl.updateInkBarStyles, 0, false);
13415 function removeTab (tabData) {
13416 var selectedIndex = $scope.selectedIndex,
13417 tab = ctrl.tabs.splice(tabData.getIndex(), 1)[0];
13419 //-- when removing a tab, if the selected index did not change, we have to manually trigger the
13420 // tab select/deselect events
13421 if ($scope.selectedIndex === selectedIndex && !destroyed) {
13422 tab.scope.deselect();
13423 ctrl.tabs[$scope.selectedIndex] && ctrl.tabs[$scope.selectedIndex].scope.select();
13425 $timeout(function () {
13426 ctrl.offsetLeft = fixOffset(ctrl.offsetLeft);
13430 function insertTab (tabData, index) {
13432 getIndex: function () { return ctrl.tabs.indexOf(tab); },
13433 isActive: function () { return this.getIndex() === $scope.selectedIndex; },
13434 isLeft: function () { return this.getIndex() < $scope.selectedIndex; },
13435 isRight: function () { return this.getIndex() > $scope.selectedIndex; },
13436 shouldRender: function () { return !$scope.noDisconnect || this.isActive(); },
13437 hasFocus: function () { return !ctrl.lastClick && ctrl.hasFocus && this.getIndex() === ctrl.focusIndex; },
13438 id: $mdUtil.nextUid()
13440 tab = angular.extend(proto, tabData);
13441 if (angular.isDefined(index)) {
13442 ctrl.tabs.splice(index, 0, tab);
13444 ctrl.tabs.push(tab);
13447 updateHasContent();
13451 //-- Getter methods
13453 function getElements () {
13456 //-- gather tab bar elements
13457 elements.wrapper = $element[0].getElementsByTagName('md-tabs-wrapper')[0];
13458 elements.canvas = elements.wrapper.getElementsByTagName('md-tabs-canvas')[0];
13459 elements.paging = elements.canvas.getElementsByTagName('md-pagination-wrapper')[0];
13460 elements.tabs = elements.paging.getElementsByTagName('md-tab-item');
13461 elements.dummies = elements.canvas.getElementsByTagName('md-dummy-tab');
13462 elements.inkBar = elements.paging.getElementsByTagName('md-ink-bar')[0];
13464 //-- gather tab content elements
13465 elements.contentsWrapper = $element[0].getElementsByTagName('md-tabs-content-wrapper')[0];
13466 elements.contents = elements.contentsWrapper.getElementsByTagName('md-tab-content');
13471 function canPageBack () {
13472 return ctrl.offsetLeft > 0;
13475 function canPageForward () {
13476 var lastTab = elements.tabs[elements.tabs.length - 1];
13477 return lastTab && lastTab.offsetLeft + lastTab.offsetWidth > elements.canvas.clientWidth + ctrl.offsetLeft;
13480 function shouldStretchTabs () {
13481 switch ($scope.stretchTabs) {
13482 case 'always': return true;
13483 case 'never': return false;
13484 default: return !shouldPaginate() && $window.matchMedia('(max-width: 600px)').matches;
13488 function shouldCenterTabs () {
13489 return $scope.centerTabs && !shouldPaginate();
13492 function shouldPaginate () {
13493 if ($scope.noPagination) return false;
13494 var canvasWidth = $element.prop('clientWidth');
13495 angular.forEach(elements.tabs, function (tab) { canvasWidth -= tab.offsetWidth; });
13496 return canvasWidth < 0;
13499 function getNearestSafeIndex(newIndex) {
13500 var maxOffset = Math.max(ctrl.tabs.length - newIndex, newIndex),
13502 for (i = 0; i <= maxOffset; i++) {
13503 tab = ctrl.tabs[newIndex + i];
13504 if (tab && (tab.scope.disabled !== true)) return tab.getIndex();
13505 tab = ctrl.tabs[newIndex - i];
13506 if (tab && (tab.scope.disabled !== true)) return tab.getIndex();
13511 //-- Utility methods
13513 function updateTabOrder () {
13514 var selectedItem = ctrl.tabs[$scope.selectedIndex],
13515 focusItem = ctrl.tabs[ctrl.focusIndex];
13516 ctrl.tabs = ctrl.tabs.sort(function (a, b) {
13517 return a.index - b.index;
13519 $scope.selectedIndex = ctrl.tabs.indexOf(selectedItem);
13520 ctrl.focusIndex = ctrl.tabs.indexOf(focusItem);
13523 function incrementSelectedIndex (inc, focus) {
13525 index = focus ? ctrl.focusIndex : $scope.selectedIndex;
13526 for (newIndex = index + inc;
13527 ctrl.tabs[newIndex] && ctrl.tabs[newIndex].scope.disabled;
13528 newIndex += inc) {}
13529 if (ctrl.tabs[newIndex]) {
13530 if (focus) ctrl.focusIndex = newIndex;
13531 else $scope.selectedIndex = newIndex;
13535 function redirectFocus () {
13536 elements.dummies[ctrl.focusIndex].focus();
13539 function adjustOffset () {
13540 if (shouldCenterTabs()) return;
13541 var tab = elements.tabs[ctrl.focusIndex],
13542 left = tab.offsetLeft,
13543 right = tab.offsetWidth + left;
13544 ctrl.offsetLeft = Math.max(ctrl.offsetLeft, fixOffset(right - elements.canvas.clientWidth));
13545 ctrl.offsetLeft = Math.min(ctrl.offsetLeft, fixOffset(left));
13548 function processQueue () {
13549 queue.forEach(function (func) { $timeout(func); });
13553 function updateHasContent () {
13554 var hasContent = false;
13555 angular.forEach(ctrl.tabs, function (tab) {
13556 if (tab.template) hasContent = true;
13558 ctrl.hasContent = hasContent;
13561 function refreshIndex () {
13562 $scope.selectedIndex = getNearestSafeIndex($scope.selectedIndex);
13563 ctrl.focusIndex = getNearestSafeIndex(ctrl.focusIndex);
13566 function updateHeightFromContent () {
13567 if (!$scope.dynamicHeight) return $element.css('height', '');
13568 if (!ctrl.tabs.length) return queue.push(updateHeightFromContent);
13569 var tabContent = elements.contents[$scope.selectedIndex],
13570 contentHeight = tabContent ? tabContent.offsetHeight : 0,
13571 tabsHeight = elements.wrapper.offsetHeight,
13572 newHeight = contentHeight + tabsHeight,
13573 currentHeight = $element.prop('clientHeight');
13574 if (currentHeight === newHeight) return;
13579 { height: currentHeight + 'px' },
13580 { height: newHeight + 'px'}
13582 .then(function () {
13583 $element.css('height', '');
13588 function updateInkBarStyles () {
13589 if (!elements.tabs[$scope.selectedIndex]) return;
13590 if (!ctrl.tabs.length) return queue.push(ctrl.updateInkBarStyles);
13591 //-- if the element is not visible, we will not be able to calculate sizes until it is
13592 //-- we should treat that as a resize event rather than just updating the ink bar
13593 if (!$element.prop('offsetParent')) return handleResizeWhenVisible();
13594 var index = $scope.selectedIndex,
13595 totalWidth = elements.paging.offsetWidth,
13596 tab = elements.tabs[index],
13597 left = tab.offsetLeft,
13598 right = totalWidth - left - tab.offsetWidth;
13599 updateInkBarClassName();
13600 angular.element(elements.inkBar).css({ left: left + 'px', right: right + 'px' });
13603 function updateInkBarClassName () {
13604 var newIndex = $scope.selectedIndex,
13605 oldIndex = ctrl.lastSelectedIndex,
13606 ink = angular.element(elements.inkBar);
13607 ink.removeClass('md-left md-right');
13608 if (!angular.isNumber(oldIndex)) return;
13609 if (newIndex < oldIndex) {
13610 ink.addClass('md-left');
13611 } else if (newIndex > oldIndex) {
13612 ink.addClass('md-right');
13616 function fixOffset (value) {
13617 if (!elements.tabs.length || !shouldPaginate()) return 0;
13618 var lastTab = elements.tabs[elements.tabs.length - 1],
13619 totalWidth = lastTab.offsetLeft + lastTab.offsetWidth;
13620 value = Math.max(0, value);
13621 value = Math.min(totalWidth - elements.canvas.clientWidth, value);
13625 function attachRipple (scope, element) {
13626 var options = { colorElement: angular.element(elements.inkBar) };
13627 $mdTabInkRipple.attach(scope, element, options);
13630 MdTabsController.$inject = ["$scope", "$element", "$window", "$timeout", "$mdConstant", "$mdTabInkRipple", "$mdUtil", "$animate"];
13639 * @module material.components.tabs
13644 * The `<md-tabs>` directive serves as the container for 1..n `<md-tab>` child directives to produces a Tabs components.
13645 * In turn, the nested `<md-tab>` directive is used to specify a tab label for the **header button** and a [optional] tab view
13646 * content that will be associated with each tab button.
13648 * Below is the markup for its simplest usage:
13650 * <hljs lang="html">
13652 * <md-tab label="Tab #1"></md-tab>
13653 * <md-tab label="Tab #2"></md-tab>
13654 * <md-tab label="Tab #3"></md-tab>
13658 * Tabs supports three (3) usage scenarios:
13660 * 1. Tabs (buttons only)
13661 * 2. Tabs with internal view content
13662 * 3. Tabs with external view content
13664 * **Tab-only** support is useful when tab buttons are used for custom navigation regardless of any other components, content, or views.
13665 * **Tabs with internal views** are the traditional usages where each tab has associated view content and the view switching is managed internally by the Tabs component.
13666 * **Tabs with external view content** is often useful when content associated with each tab is independently managed and data-binding notifications announce tab selection changes.
13668 * Additional features also include:
13670 * * Content can include any markup.
13671 * * If a tab is disabled while active/selected, then the next tab will be auto-selected.
13673 * ### Explanation of tab stretching
13675 * 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.
13677 * On mobile devices, tabs will be expanded to fill the available horizontal space. When this happens, all tabs will become the same size.
13679 * On desktops, by default, stretching will never occur.
13681 * This default behavior can be overridden through the `md-stretch-tabs` attribute. Here is a table showing when stretching will occur:
13683 * `md-stretch-tabs` | mobile | desktop
13684 * ------------------|-----------|--------
13685 * `auto` | stretched | ---
13686 * `always` | stretched | stretched
13687 * `never` | --- | ---
13689 * @param {integer=} md-selected Index of the active/selected tab
13690 * @param {boolean=} md-no-ink If present, disables ink ripple effects.
13691 * @param {boolean=} md-no-bar If present, disables the selection ink bar.
13692 * @param {string=} md-align-tabs Attribute to indicate position of tab buttons: `bottom` or `top`; default is `top`
13693 * @param {string=} md-stretch-tabs Attribute to indicate whether or not to stretch tabs: `auto`, `always`, or `never`; default is `auto`
13694 * @param {boolean=} md-dynamic-height When enabled, the tab wrapper will resize based on the contents of the selected tab
13695 * @param {boolean=} md-center-tabs When enabled, tabs will be centered provided there is no need for pagination
13696 * @param {boolean=} md-no-pagination When enabled, pagination will remain off
13697 * @param {boolean=} md-swipe-content When enabled, swipe gestures will be enabled for the content area to jump between tabs
13698 * @param {boolean=} md-no-disconnect If your tab content has background tasks (ie. event listeners), you will want to include this to prevent the scope from being disconnected
13701 * <hljs lang="html">
13702 * <md-tabs md-selected="selectedIndex" >
13703 * <img ng-src="img/angular.png" class="centered">
13705 * ng-repeat="tab in tabs | orderBy:predicate:reversed"
13706 * md-on-select="onTabSelected(tab)"
13707 * md-on-deselect="announceDeselected(tab)"
13708 * ng-disabled="tab.disabled">
13711 * <img src="img/removeTab.png" ng-click="removeTab(tab)" class="delete">
13722 .module('material.components.tabs')
13723 .directive('mdTabs', MdTabs);
13725 function MdTabs ($mdTheming, $mdUtil, $compile) {
13728 noPagination: '=?mdNoPagination',
13729 dynamicHeight: '=?mdDynamicHeight',
13730 centerTabs: '=?mdCenterTabs',
13731 selectedIndex: '=?mdSelected',
13732 stretchTabs: '@?mdStretchTabs',
13733 swipeContent: '=?mdSwipeContent',
13734 noDisconnect: '=?mdNoDisconnect'
13736 template: function (element, attr) {
13737 attr["$mdTabsTemplate"] = element.html();
13739 <md-tabs-wrapper ng-class="{ \'md-stretch-tabs\': $mdTabsCtrl.shouldStretchTabs() }">\
13740 <md-tab-data></md-tab-data>\
13744 aria-label="Previous Page"\
13745 aria-disabled="{{!$mdTabsCtrl.canPageBack()}}"\
13746 ng-class="{ \'md-disabled\': !$mdTabsCtrl.canPageBack() }"\
13747 ng-if="$mdTabsCtrl.shouldPaginate()"\
13748 ng-click="$mdTabsCtrl.previousPage()">\
13749 <md-icon md-svg-icon="md-tabs-arrow"></md-icon>\
13754 aria-label="Next Page"\
13755 aria-disabled="{{!$mdTabsCtrl.canPageForward()}}"\
13756 ng-class="{ \'md-disabled\': !$mdTabsCtrl.canPageForward() }"\
13757 ng-if="$mdTabsCtrl.shouldPaginate()"\
13758 ng-click="$mdTabsCtrl.nextPage()">\
13759 <md-icon md-svg-icon="md-tabs-arrow"></md-icon>\
13763 aria-activedescendant="tab-item-{{$mdTabsCtrl.tabs[$mdTabsCtrl.focusIndex].id}}"\
13764 ng-focus="$mdTabsCtrl.redirectFocus()"\
13766 \'md-paginated\': $mdTabsCtrl.shouldPaginate(),\
13767 \'md-center-tabs\': $mdTabsCtrl.shouldCenterTabs()\
13769 ng-keydown="$mdTabsCtrl.keydown($event)"\
13771 <md-pagination-wrapper\
13772 ng-class="{ \'md-center-tabs\': $mdTabsCtrl.shouldCenterTabs() }"\
13773 md-tab-scroll="$mdTabsCtrl.scroll($event)">\
13777 style="max-width: {{ tabWidth ? tabWidth + \'px\' : \'none\' }}"\
13778 ng-repeat="tab in $mdTabsCtrl.tabs"\
13780 aria-controls="tab-content-{{tab.id}}"\
13781 aria-selected="{{tab.isActive()}}"\
13782 aria-disabled="{{tab.scope.disabled || \'false\'}}"\
13783 ng-click="$mdTabsCtrl.select(tab.getIndex())"\
13785 \'md-active\': tab.isActive(),\
13786 \'md-focused\': tab.hasFocus(),\
13787 \'md-disabled\': tab.scope.disabled\
13789 ng-disabled="tab.scope.disabled"\
13790 md-swipe-left="$mdTabsCtrl.nextPage()"\
13791 md-swipe-right="$mdTabsCtrl.previousPage()"\
13792 md-template="tab.label"\
13793 md-scope="tab.parent"></md-tab-item>\
13794 <md-ink-bar ng-hide="noInkBar"></md-ink-bar>\
13795 </md-pagination-wrapper>\
13796 <div class="md-visually-hidden md-dummy-wrapper">\
13799 id="tab-item-{{tab.id}}"\
13801 aria-controls="tab-content-{{tab.id}}"\
13802 aria-selected="{{tab.isActive()}}"\
13803 aria-disabled="{{tab.scope.disabled || \'false\'}}"\
13804 ng-focus="$mdTabsCtrl.hasFocus = true"\
13805 ng-blur="$mdTabsCtrl.hasFocus = false"\
13806 ng-repeat="tab in $mdTabsCtrl.tabs"\
13807 md-template="tab.label"\
13808 md-scope="tab.parent"></md-dummy-tab>\
13811 </md-tabs-wrapper>\
13812 <md-tabs-content-wrapper ng-show="$mdTabsCtrl.hasContent">\
13814 id="tab-content-{{tab.id}}"\
13816 aria-labelledby="tab-item-{{tab.id}}"\
13817 md-swipe-left="swipeContent && $mdTabsCtrl.incrementSelectedIndex(1)"\
13818 md-swipe-right="swipeContent && $mdTabsCtrl.incrementSelectedIndex(-1)"\
13819 ng-if="$mdTabsCtrl.hasContent"\
13820 ng-repeat="(index, tab) in $mdTabsCtrl.tabs"\
13821 md-connected-if="tab.isActive()"\
13823 \'md-no-transition\': $mdTabsCtrl.lastSelectedIndex == null,\
13824 \'md-active\': tab.isActive(),\
13825 \'md-left\': tab.isLeft(),\
13826 \'md-right\': tab.isRight(),\
13827 \'md-no-scroll\': dynamicHeight\
13830 md-template="tab.template"\
13831 md-scope="tab.parent"\
13832 ng-if="tab.shouldRender()"></div>\
13834 </md-tabs-content-wrapper>\
13837 controller: 'MdTabsController',
13838 controllerAs: '$mdTabsCtrl',
13839 link: function (scope, element, attr) {
13840 compileTabData(attr.$mdTabsTemplate);
13841 delete attr.$mdTabsTemplate;
13843 $mdUtil.initOptionalProperties(scope, attr);
13845 //-- watch attributes
13846 attr.$observe('mdNoBar', function (value) { scope.noInkBar = angular.isDefined(value); });
13847 //-- set default value for selectedIndex
13848 scope.selectedIndex = angular.isNumber(scope.selectedIndex) ? scope.selectedIndex : 0;
13850 $mdTheming(element);
13852 function compileTabData (template) {
13853 var dataElement = element.find('md-tab-data');
13854 dataElement.html(template);
13855 $compile(dataElement.contents())(scope.$parent);
13860 MdTabs.$inject = ["$mdTheming", "$mdUtil", "$compile"];
13867 .module('material.components.tabs')
13868 .directive('mdTemplate', MdTemplate);
13870 function MdTemplate ($compile, $mdUtil, $timeout) {
13875 template: '=mdTemplate',
13876 compileScope: '=mdScope',
13877 connected: '=?mdConnectedIf'
13879 require: '^?mdTabs'
13881 function link (scope, element, attr, ctrl) {
13883 var compileScope = scope.compileScope.$new();
13884 element.html(scope.template);
13885 $compile(element.contents())(compileScope);
13886 return $timeout(handleScope);
13887 function handleScope () {
13888 scope.$watch('connected', function (value) { value === false ? disconnect() : reconnect(); });
13889 scope.$on('$destroy', reconnect);
13891 function disconnect () {
13892 if (ctrl.scope.noDisconnect) return;
13893 $mdUtil.disconnectScope(compileScope);
13895 function reconnect () {
13896 if (ctrl.scope.noDisconnect) return;
13897 $mdUtil.reconnectScope(compileScope);
13901 MdTemplate.$inject = ["$compile", "$mdUtil", "$timeout"];
13905 angular.module("material.core").constant("$MD_THEME_CSS", "/* mixin definition ; sets LTR and RTL within the same style call */md-autocomplete.md-THEME_NAME-theme { background: '{{background-50}}'; } md-autocomplete.md-THEME_NAME-theme[disabled] { background: '{{background-100}}'; } md-autocomplete.md-THEME_NAME-theme button md-icon path { fill: '{{background-600}}'; } md-autocomplete.md-THEME_NAME-theme button:after { background: '{{background-600-0.3}}'; }.md-autocomplete-suggestions.md-THEME_NAME-theme { background: '{{background-50}}'; } .md-autocomplete-suggestions.md-THEME_NAME-theme li { color: '{{background-900}}'; } .md-autocomplete-suggestions.md-THEME_NAME-theme li .highlight { color: '{{background-600}}'; } .md-autocomplete-suggestions.md-THEME_NAME-theme li:hover, .md-autocomplete-suggestions.md-THEME_NAME-theme li.selected { background: '{{background-200}}'; }md-backdrop.md-opaque.md-THEME_NAME-theme { background-color: '{{foreground-4-0.5}}'; }md-bottom-sheet.md-THEME_NAME-theme { background-color: '{{background-50}}'; border-top-color: '{{background-300}}'; } md-bottom-sheet.md-THEME_NAME-theme.md-list md-list-item { color: '{{foreground-1}}'; } md-bottom-sheet.md-THEME_NAME-theme .md-subheader { background-color: '{{background-50}}'; } md-bottom-sheet.md-THEME_NAME-theme .md-subheader { color: '{{foreground-1}}'; }a.md-button.md-THEME_NAME-theme, .md-button.md-THEME_NAME-theme { border-radius: 3px; } a.md-button.md-THEME_NAME-theme:not([disabled]):hover, .md-button.md-THEME_NAME-theme:not([disabled]):hover { background-color: '{{background-500-0.2}}'; } a.md-button.md-THEME_NAME-theme:not([disabled]).md-focused, .md-button.md-THEME_NAME-theme:not([disabled]).md-focused { background-color: '{{background-500-0.2}}'; } a.md-button.md-THEME_NAME-theme:not([disabled]).md-icon-button:hover, .md-button.md-THEME_NAME-theme:not([disabled]).md-icon-button:hover { background-color: transparent; } a.md-button.md-THEME_NAME-theme.md-fab, .md-button.md-THEME_NAME-theme.md-fab { border-radius: 50%; background-color: '{{accent-color}}'; color: '{{accent-contrast}}'; } a.md-button.md-THEME_NAME-theme.md-fab md-icon, .md-button.md-THEME_NAME-theme.md-fab md-icon { color: '{{accent-contrast}}'; } a.md-button.md-THEME_NAME-theme.md-fab:not([disabled]):hover, .md-button.md-THEME_NAME-theme.md-fab:not([disabled]):hover { background-color: '{{accent-color}}'; } a.md-button.md-THEME_NAME-theme.md-fab:not([disabled]).md-focused, .md-button.md-THEME_NAME-theme.md-fab:not([disabled]).md-focused { background-color: '{{accent-A700}}'; } a.md-button.md-THEME_NAME-theme.md-icon-button, .md-button.md-THEME_NAME-theme.md-icon-button { border-radius: 50%; } a.md-button.md-THEME_NAME-theme.md-primary, .md-button.md-THEME_NAME-theme.md-primary { color: '{{primary-color}}'; } a.md-button.md-THEME_NAME-theme.md-primary.md-raised, a.md-button.md-THEME_NAME-theme.md-primary.md-fab, .md-button.md-THEME_NAME-theme.md-primary.md-raised, .md-button.md-THEME_NAME-theme.md-primary.md-fab { color: '{{primary-contrast}}'; background-color: '{{primary-color}}'; } a.md-button.md-THEME_NAME-theme.md-primary.md-raised:not([disabled]) md-icon, a.md-button.md-THEME_NAME-theme.md-primary.md-fab:not([disabled]) md-icon, .md-button.md-THEME_NAME-theme.md-primary.md-raised:not([disabled]) md-icon, .md-button.md-THEME_NAME-theme.md-primary.md-fab:not([disabled]) md-icon { color: '{{primary-contrast}}'; } a.md-button.md-THEME_NAME-theme.md-primary.md-raised:not([disabled]):hover, a.md-button.md-THEME_NAME-theme.md-primary.md-fab:not([disabled]):hover, .md-button.md-THEME_NAME-theme.md-primary.md-raised:not([disabled]):hover, .md-button.md-THEME_NAME-theme.md-primary.md-fab:not([disabled]):hover { background-color: '{{primary-color}}'; } a.md-button.md-THEME_NAME-theme.md-primary.md-raised:not([disabled]).md-focused, a.md-button.md-THEME_NAME-theme.md-primary.md-fab:not([disabled]).md-focused, .md-button.md-THEME_NAME-theme.md-primary.md-raised:not([disabled]).md-focused, .md-button.md-THEME_NAME-theme.md-primary.md-fab:not([disabled]).md-focused { background-color: '{{primary-600}}'; } a.md-button.md-THEME_NAME-theme.md-primary:not([disabled]) md-icon, .md-button.md-THEME_NAME-theme.md-primary:not([disabled]) md-icon { color: '{{primary-color}}'; } a.md-button.md-THEME_NAME-theme.md-fab, .md-button.md-THEME_NAME-theme.md-fab { border-radius: 50%; background-color: '{{accent-color}}'; color: '{{accent-contrast}}'; } a.md-button.md-THEME_NAME-theme.md-fab:not([disabled]) .md-icon, .md-button.md-THEME_NAME-theme.md-fab:not([disabled]) .md-icon { color: '{{accent-contrast}}'; } a.md-button.md-THEME_NAME-theme.md-fab:not([disabled]):hover, .md-button.md-THEME_NAME-theme.md-fab:not([disabled]):hover { background-color: '{{accent-color}}'; } a.md-button.md-THEME_NAME-theme.md-fab:not([disabled]).md-focused, .md-button.md-THEME_NAME-theme.md-fab:not([disabled]).md-focused { background-color: '{{accent-A700}}'; } a.md-button.md-THEME_NAME-theme.md-raised, .md-button.md-THEME_NAME-theme.md-raised { color: '{{background-contrast}}'; background-color: '{{background-50}}'; } a.md-button.md-THEME_NAME-theme.md-raised:not([disabled]) .md-icon, .md-button.md-THEME_NAME-theme.md-raised:not([disabled]) .md-icon { color: '{{background-contrast}}'; } a.md-button.md-THEME_NAME-theme.md-raised:not([disabled]):hover, .md-button.md-THEME_NAME-theme.md-raised:not([disabled]):hover { background-color: '{{background-50}}'; } a.md-button.md-THEME_NAME-theme.md-raised:not([disabled]).md-focused, .md-button.md-THEME_NAME-theme.md-raised:not([disabled]).md-focused { background-color: '{{background-200}}'; } a.md-button.md-THEME_NAME-theme.md-warn, .md-button.md-THEME_NAME-theme.md-warn { color: '{{warn-color}}'; } a.md-button.md-THEME_NAME-theme.md-warn.md-raised, a.md-button.md-THEME_NAME-theme.md-warn.md-fab, .md-button.md-THEME_NAME-theme.md-warn.md-raised, .md-button.md-THEME_NAME-theme.md-warn.md-fab { color: '{{warn-contrast}}'; background-color: '{{warn-color}}'; } a.md-button.md-THEME_NAME-theme.md-warn.md-raised:not([disabled]) md-icon, a.md-button.md-THEME_NAME-theme.md-warn.md-fab:not([disabled]) md-icon, .md-button.md-THEME_NAME-theme.md-warn.md-raised:not([disabled]) md-icon, .md-button.md-THEME_NAME-theme.md-warn.md-fab:not([disabled]) md-icon { color: '{{warn-contrast}}'; } a.md-button.md-THEME_NAME-theme.md-warn.md-raised:not([disabled]):hover, a.md-button.md-THEME_NAME-theme.md-warn.md-fab:not([disabled]):hover, .md-button.md-THEME_NAME-theme.md-warn.md-raised:not([disabled]):hover, .md-button.md-THEME_NAME-theme.md-warn.md-fab:not([disabled]):hover { background-color: '{{warn-color}}'; } a.md-button.md-THEME_NAME-theme.md-warn.md-raised:not([disabled]).md-focused, a.md-button.md-THEME_NAME-theme.md-warn.md-fab:not([disabled]).md-focused, .md-button.md-THEME_NAME-theme.md-warn.md-raised:not([disabled]).md-focused, .md-button.md-THEME_NAME-theme.md-warn.md-fab:not([disabled]).md-focused { background-color: '{{warn-700}}'; } a.md-button.md-THEME_NAME-theme.md-warn:not([disabled]) md-icon, .md-button.md-THEME_NAME-theme.md-warn:not([disabled]) md-icon { color: '{{warn-color}}'; } a.md-button.md-THEME_NAME-theme.md-accent, .md-button.md-THEME_NAME-theme.md-accent { color: '{{accent-color}}'; } a.md-button.md-THEME_NAME-theme.md-accent.md-raised, a.md-button.md-THEME_NAME-theme.md-accent.md-fab, .md-button.md-THEME_NAME-theme.md-accent.md-raised, .md-button.md-THEME_NAME-theme.md-accent.md-fab { color: '{{accent-contrast}}'; background-color: '{{accent-color}}'; } a.md-button.md-THEME_NAME-theme.md-accent.md-raised:not([disabled]) md-icon, a.md-button.md-THEME_NAME-theme.md-accent.md-fab:not([disabled]) md-icon, .md-button.md-THEME_NAME-theme.md-accent.md-raised:not([disabled]) md-icon, .md-button.md-THEME_NAME-theme.md-accent.md-fab:not([disabled]) md-icon { color: '{{accent-contrast}}'; } a.md-button.md-THEME_NAME-theme.md-accent.md-raised:not([disabled]):hover, a.md-button.md-THEME_NAME-theme.md-accent.md-fab:not([disabled]):hover, .md-button.md-THEME_NAME-theme.md-accent.md-raised:not([disabled]):hover, .md-button.md-THEME_NAME-theme.md-accent.md-fab:not([disabled]):hover { background-color: '{{accent-color}}'; } a.md-button.md-THEME_NAME-theme.md-accent.md-raised:not([disabled]).md-focused, a.md-button.md-THEME_NAME-theme.md-accent.md-fab:not([disabled]).md-focused, .md-button.md-THEME_NAME-theme.md-accent.md-raised:not([disabled]).md-focused, .md-button.md-THEME_NAME-theme.md-accent.md-fab:not([disabled]).md-focused { background-color: '{{accent-700}}'; } a.md-button.md-THEME_NAME-theme.md-accent:not([disabled]) md-icon, .md-button.md-THEME_NAME-theme.md-accent:not([disabled]) md-icon { color: '{{accent-color}}'; } a.md-button.md-THEME_NAME-theme[disabled], a.md-button.md-THEME_NAME-theme.md-raised[disabled], a.md-button.md-THEME_NAME-theme.md-fab[disabled], a.md-button.md-THEME_NAME-theme.md-accent[disabled], a.md-button.md-THEME_NAME-theme.md-warn[disabled], .md-button.md-THEME_NAME-theme[disabled], .md-button.md-THEME_NAME-theme.md-raised[disabled], .md-button.md-THEME_NAME-theme.md-fab[disabled], .md-button.md-THEME_NAME-theme.md-accent[disabled], .md-button.md-THEME_NAME-theme.md-warn[disabled] { color: '{{foreground-3}}'; cursor: not-allowed; } a.md-button.md-THEME_NAME-theme[disabled] md-icon, a.md-button.md-THEME_NAME-theme.md-raised[disabled] md-icon, a.md-button.md-THEME_NAME-theme.md-fab[disabled] md-icon, a.md-button.md-THEME_NAME-theme.md-accent[disabled] md-icon, a.md-button.md-THEME_NAME-theme.md-warn[disabled] md-icon, .md-button.md-THEME_NAME-theme[disabled] md-icon, .md-button.md-THEME_NAME-theme.md-raised[disabled] md-icon, .md-button.md-THEME_NAME-theme.md-fab[disabled] md-icon, .md-button.md-THEME_NAME-theme.md-accent[disabled] md-icon, .md-button.md-THEME_NAME-theme.md-warn[disabled] md-icon { color: '{{foreground-3}}'; } a.md-button.md-THEME_NAME-theme.md-raised[disabled], a.md-button.md-THEME_NAME-theme.md-fab[disabled], .md-button.md-THEME_NAME-theme.md-raised[disabled], .md-button.md-THEME_NAME-theme.md-fab[disabled] { background-color: '{{foreground-4}}'; } a.md-button.md-THEME_NAME-theme[disabled], .md-button.md-THEME_NAME-theme[disabled] { background-color: transparent; }md-card.md-THEME_NAME-theme { background-color: '{{background-color}}'; border-radius: 2px; } md-card.md-THEME_NAME-theme .md-card-image { border-radius: 2px 2px 0 0; }md-checkbox.md-THEME_NAME-theme .md-ripple { color: '{{accent-600}}'; }md-checkbox.md-THEME_NAME-theme.md-checked .md-ripple { color: '{{background-600}}'; }md-checkbox.md-THEME_NAME-theme.md-checked.md-focused .md-container:before { background-color: '{{accent-color-0.26}}'; }md-checkbox.md-THEME_NAME-theme .md-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: '{{background-200}}'; }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-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: '{{background-200}}'; }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-icon { border-color: '{{foreground-2}}'; }md-checkbox.md-THEME_NAME-theme:not([disabled]).md-warn.md-checked .md-icon { background-color: '{{warn-color-0.87}}'; }md-checkbox.md-THEME_NAME-theme:not([disabled]).md-warn.md-checked.md-focused:not([disabled]) .md-container:before { background-color: '{{warn-color-0.26}}'; }md-checkbox.md-THEME_NAME-theme:not([disabled]).md-warn.md-checked .md-icon:after { border-color: '{{background-200}}'; }md-checkbox.md-THEME_NAME-theme[disabled] .md-icon { border-color: '{{foreground-3}}'; }md-checkbox.md-THEME_NAME-theme[disabled].md-checked .md-icon { background-color: '{{foreground-3}}'; }md-checkbox.md-THEME_NAME-theme[disabled] .md-label { color: '{{foreground-3}}'; }md-chips.md-THEME_NAME-theme .md-chips { box-shadow: 0 1px '{{background-300}}'; } md-chips.md-THEME_NAME-theme .md-chips.md-focused { box-shadow: 0 2px '{{primary-color}}'; }md-chips.md-THEME_NAME-theme .md-chip { background: '{{background-300}}'; color: '{{background-800}}'; } md-chips.md-THEME_NAME-theme .md-chip.md-focused { background: '{{primary-color}}'; color: '{{primary-contrast}}'; } md-chips.md-THEME_NAME-theme .md-chip.md-focused md-icon { color: '{{primary-contrast}}'; }md-chips.md-THEME_NAME-theme md-chip-remove .md-button md-icon path { fill: '{{background-500}}'; }.md-contact-suggestion span.md-contact-email { color: '{{background-400}}'; }md-content.md-THEME_NAME-theme { background-color: '{{background-color}}'; }md-dialog.md-THEME_NAME-theme { border-radius: 4px; background-color: '{{background-color}}'; } md-dialog.md-THEME_NAME-theme.md-content-overflow .md-actions { border-top-color: '{{foreground-4}}'; }md-divider.md-THEME_NAME-theme { border-top-color: '{{foreground-4}}'; }md-icon.md-THEME_NAME-theme { color: '{{foreground-2}}'; } md-icon.md-THEME_NAME-theme.md-primary { color: '{{primary-color}}'; } md-icon.md-THEME_NAME-theme.md-accent { color: '{{accent-color}}'; } md-icon.md-THEME_NAME-theme.md-warn { color: '{{warn-color}}'; }md-input-container.md-THEME_NAME-theme .md-input { color: '{{foreground-1}}'; border-color: '{{foreground-4}}'; text-shadow: '{{foreground-shadow}}'; } md-input-container.md-THEME_NAME-theme .md-input::-webkit-input-placeholder, md-input-container.md-THEME_NAME-theme .md-input::-moz-placeholder, md-input-container.md-THEME_NAME-theme .md-input:-moz-placeholder, md-input-container.md-THEME_NAME-theme .md-input:-ms-input-placeholder { color: '{{foreground-3}}'; }md-input-container.md-THEME_NAME-theme > md-icon { color: '{{foreground-1}}'; }md-input-container.md-THEME_NAME-theme label, md-input-container.md-THEME_NAME-theme .md-placeholder { text-shadow: '{{foreground-shadow}}'; color: '{{foreground-3}}'; }md-input-container.md-THEME_NAME-theme ng-messages, md-input-container.md-THEME_NAME-theme [ng-message], md-input-container.md-THEME_NAME-theme [data-ng-message], md-input-container.md-THEME_NAME-theme [x-ng-message] { color: '{{warn-500}}'; }md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-has-value label { color: '{{foreground-2}}'; }md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused .md-input { border-color: '{{primary-500}}'; }md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused label { color: '{{primary-500}}'; }md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused md-icon { color: '{{primary-500}}'; }md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused.md-accent .md-input { border-color: '{{accent-500}}'; }md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused.md-accent label { color: '{{accent-500}}'; }md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused.md-warn .md-input { border-color: '{{warn-500}}'; }md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused.md-warn label { color: '{{warn-500}}'; }md-input-container.md-THEME_NAME-theme.md-input-invalid .md-input { border-color: '{{warn-500}}'; }md-input-container.md-THEME_NAME-theme.md-input-invalid.md-input-focused label { color: '{{warn-500}}'; }md-input-container.md-THEME_NAME-theme.md-input-invalid ng-message, md-input-container.md-THEME_NAME-theme.md-input-invalid data-ng-message, md-input-container.md-THEME_NAME-theme.md-input-invalid x-ng-message, md-input-container.md-THEME_NAME-theme.md-input-invalid [ng-message], md-input-container.md-THEME_NAME-theme.md-input-invalid [data-ng-message], md-input-container.md-THEME_NAME-theme.md-input-invalid [x-ng-message], md-input-container.md-THEME_NAME-theme.md-input-invalid .md-char-counter { color: '{{warn-500}}'; }md-input-container.md-THEME_NAME-theme .md-input[disabled], [disabled] md-input-container.md-THEME_NAME-theme .md-input { border-bottom-color: transparent; color: '{{foreground-3}}'; background-image: linear-gradient(to right, '{{foreground-3}}' 0%, '{{foreground-3}}' 33%, transparent 0%); background-image: -ms-linear-gradient(left, transparent 0%, '{{foreground-3}}' 100%); }md-list.md-THEME_NAME-theme md-list-item.md-2-line .md-list-item-text h3, md-list.md-THEME_NAME-theme md-list-item.md-2-line .md-list-item-text h4, md-list.md-THEME_NAME-theme md-list-item.md-3-line .md-list-item-text h3, md-list.md-THEME_NAME-theme md-list-item.md-3-line .md-list-item-text h4 { color: '{{foreground-1}}'; }md-list.md-THEME_NAME-theme md-list-item.md-2-line .md-list-item-text p, md-list.md-THEME_NAME-theme md-list-item.md-3-line .md-list-item-text p { color: '{{foreground-2}}'; }md-list.md-THEME_NAME-theme .md-proxy-focus.md-focused div.md-no-style { background-color: '{{background-100}}'; }md-list.md-THEME_NAME-theme md-list-item > md-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-list.md-THEME_NAME-theme md-list-item button { background-color: '{{background-color}}'; } md-list.md-THEME_NAME-theme md-list-item button.md-button:not([disabled]):hover { background-color: '{{background-color}}'; }md-progress-circular.md-THEME_NAME-theme { background-color: transparent; } md-progress-circular.md-THEME_NAME-theme .md-inner .md-gap { border-top-color: '{{primary-color}}'; border-bottom-color: '{{primary-color}}'; } md-progress-circular.md-THEME_NAME-theme .md-inner .md-left .md-half-circle, md-progress-circular.md-THEME_NAME-theme .md-inner .md-right .md-half-circle { border-top-color: '{{primary-color}}'; } md-progress-circular.md-THEME_NAME-theme .md-inner .md-right .md-half-circle { border-right-color: '{{primary-color}}'; } md-progress-circular.md-THEME_NAME-theme .md-inner .md-left .md-half-circle { border-left-color: '{{primary-color}}'; } md-progress-circular.md-THEME_NAME-theme.md-warn .md-inner .md-gap { border-top-color: '{{warn-color}}'; border-bottom-color: '{{warn-color}}'; } md-progress-circular.md-THEME_NAME-theme.md-warn .md-inner .md-left .md-half-circle, md-progress-circular.md-THEME_NAME-theme.md-warn .md-inner .md-right .md-half-circle { border-top-color: '{{warn-color}}'; } md-progress-circular.md-THEME_NAME-theme.md-warn .md-inner .md-right .md-half-circle { border-right-color: '{{warn-color}}'; } md-progress-circular.md-THEME_NAME-theme.md-warn .md-inner .md-left .md-half-circle { border-left-color: '{{warn-color}}'; } md-progress-circular.md-THEME_NAME-theme.md-accent .md-inner .md-gap { border-top-color: '{{accent-color}}'; border-bottom-color: '{{accent-color}}'; } md-progress-circular.md-THEME_NAME-theme.md-accent .md-inner .md-left .md-half-circle, md-progress-circular.md-THEME_NAME-theme.md-accent .md-inner .md-right .md-half-circle { border-top-color: '{{accent-color}}'; } md-progress-circular.md-THEME_NAME-theme.md-accent .md-inner .md-right .md-half-circle { border-right-color: '{{accent-color}}'; } md-progress-circular.md-THEME_NAME-theme.md-accent .md-inner .md-left .md-half-circle { border-left-color: '{{accent-color}}'; }md-progress-linear.md-THEME_NAME-theme .md-container { background-color: '{{primary-100}}'; }md-progress-linear.md-THEME_NAME-theme .md-bar { background-color: '{{primary-color}}'; }md-progress-linear.md-THEME_NAME-theme.md-warn .md-container { background-color: '{{warn-100}}'; }md-progress-linear.md-THEME_NAME-theme.md-warn .md-bar { background-color: '{{warn-color}}'; }md-progress-linear.md-THEME_NAME-theme.md-accent .md-container { background-color: '{{accent-100}}'; }md-progress-linear.md-THEME_NAME-theme.md-accent .md-bar { background-color: '{{accent-color}}'; }md-progress-linear.md-THEME_NAME-theme[md-mode=buffer].md-warn .md-bar1 { background-color: '{{warn-100}}'; }md-progress-linear.md-THEME_NAME-theme[md-mode=buffer].md-warn .md-dashed:before { background: radial-gradient('{{warn-100}}' 0%, '{{warn-100}}' 16%, transparent 42%); }md-progress-linear.md-THEME_NAME-theme[md-mode=buffer].md-accent .md-bar1 { background-color: '{{accent-100}}'; }md-progress-linear.md-THEME_NAME-theme[md-mode=buffer].md-accent .md-dashed:before { background: radial-gradient('{{accent-100}}' 0%, '{{accent-100}}' 16%, transparent 42%); }md-radio-button.md-THEME_NAME-theme .md-off { border-color: '{{foreground-2}}'; }md-radio-button.md-THEME_NAME-theme .md-on { background-color: '{{accent-color-0.87}}'; }md-radio-button.md-THEME_NAME-theme.md-checked .md-off { border-color: '{{accent-color-0.87}}'; }md-radio-button.md-THEME_NAME-theme.md-checked .md-ink-ripple { color: '{{accent-color-0.87}}'; }md-radio-button.md-THEME_NAME-theme .md-container .md-ripple { color: '{{accent-600}}'; }md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-primary .md-on, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-primary .md-on, md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-primary .md-on, md-radio-button.md-THEME_NAME-theme:not([disabled]).md-primary .md-on { background-color: '{{primary-color-0.87}}'; }md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-primary .md-checked .md-off, md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-primary.md-checked .md-off, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-primary .md-checked .md-off, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-primary.md-checked .md-off, md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-primary .md-checked .md-off, md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-primary.md-checked .md-off, md-radio-button.md-THEME_NAME-theme:not([disabled]).md-primary .md-checked .md-off, md-radio-button.md-THEME_NAME-theme:not([disabled]).md-primary.md-checked .md-off { border-color: '{{primary-color-0.87}}'; }md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-primary .md-checked .md-ink-ripple, md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-primary.md-checked .md-ink-ripple, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-primary .md-checked .md-ink-ripple, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-primary.md-checked .md-ink-ripple, md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-primary .md-checked .md-ink-ripple, md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-primary.md-checked .md-ink-ripple, md-radio-button.md-THEME_NAME-theme:not([disabled]).md-primary .md-checked .md-ink-ripple, md-radio-button.md-THEME_NAME-theme:not([disabled]).md-primary.md-checked .md-ink-ripple { color: '{{primary-color-0.87}}'; }md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-primary .md-container .md-ripple, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-primary .md-container .md-ripple, md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-primary .md-container .md-ripple, md-radio-button.md-THEME_NAME-theme:not([disabled]).md-primary .md-container .md-ripple { color: '{{primary-600}}'; }md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-warn .md-on, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-warn .md-on, md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-warn .md-on, md-radio-button.md-THEME_NAME-theme:not([disabled]).md-warn .md-on { background-color: '{{warn-color-0.87}}'; }md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-warn .md-checked .md-off, md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-warn.md-checked .md-off, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-warn .md-checked .md-off, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-warn.md-checked .md-off, md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-warn .md-checked .md-off, md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-warn.md-checked .md-off, md-radio-button.md-THEME_NAME-theme:not([disabled]).md-warn .md-checked .md-off, md-radio-button.md-THEME_NAME-theme:not([disabled]).md-warn.md-checked .md-off { border-color: '{{warn-color-0.87}}'; }md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-warn .md-checked .md-ink-ripple, md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-warn.md-checked .md-ink-ripple, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-warn .md-checked .md-ink-ripple, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-warn.md-checked .md-ink-ripple, md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-warn .md-checked .md-ink-ripple, md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-warn.md-checked .md-ink-ripple, md-radio-button.md-THEME_NAME-theme:not([disabled]).md-warn .md-checked .md-ink-ripple, md-radio-button.md-THEME_NAME-theme:not([disabled]).md-warn.md-checked .md-ink-ripple { color: '{{warn-color-0.87}}'; }md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-warn .md-container .md-ripple, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-warn .md-container .md-ripple, md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-warn .md-container .md-ripple, md-radio-button.md-THEME_NAME-theme:not([disabled]).md-warn .md-container .md-ripple { color: '{{warn-600}}'; }md-radio-group.md-THEME_NAME-theme[disabled], md-radio-button.md-THEME_NAME-theme[disabled] { color: '{{foreground-3}}'; } md-radio-group.md-THEME_NAME-theme[disabled] .md-container .md-off, md-radio-button.md-THEME_NAME-theme[disabled] .md-container .md-off { border-color: '{{foreground-3}}'; } md-radio-group.md-THEME_NAME-theme[disabled] .md-container .md-on, md-radio-button.md-THEME_NAME-theme[disabled] .md-container .md-on { border-color: '{{foreground-3}}'; }md-radio-group.md-THEME_NAME-theme.md-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:not([disabled]).md-primary .md-container:before { background-color: '{{primary-color-0.26}}'; }md-radio-group.md-THEME_NAME-theme.md-focused:not(:empty) .md-checked.md-primary .md-container:before { background-color: '{{warn-color-0.26}}'; }md-select.md-THEME_NAME-theme.ng-invalid.ng-dirty .md-select-label { color: '{{warn-500}}' !important; border-bottom-color: '{{warn-500}}' !important; }md-select.md-THEME_NAME-theme:not([disabled]):focus .md-select-label { border-bottom-color: '{{primary-color}}'; color: '{{ foreground-1 }}'; } md-select.md-THEME_NAME-theme:not([disabled]):focus .md-select-label.md-placeholder { color: '{{ foreground-1 }}'; }md-select.md-THEME_NAME-theme:not([disabled]):focus.md-accent .md-select-label { border-bottom-color: '{{accent-color}}'; }md-select.md-THEME_NAME-theme:not([disabled]):focus.md-warn .md-select-label { border-bottom-color: '{{warn-color}}'; }md-select.md-THEME_NAME-theme[disabled] .md-select-label { color: '{{foreground-3}}'; } md-select.md-THEME_NAME-theme[disabled] .md-select-label.md-placeholder { color: '{{foreground-3}}'; }md-select.md-THEME_NAME-theme .md-select-label { border-bottom-color: '{{foreground-4}}'; } md-select.md-THEME_NAME-theme .md-select-label.md-placeholder { color: '{{foreground-2}}'; }md-select-menu.md-THEME_NAME-theme md-optgroup { color: '{{foreground-2}}'; } md-select-menu.md-THEME_NAME-theme md-optgroup md-option { color: '{{foreground-1}}'; }md-select-menu.md-THEME_NAME-theme md-option[selected] { color: '{{primary-500}}'; } md-select-menu.md-THEME_NAME-theme md-option[selected]:focus { color: '{{primary-600}}'; } md-select-menu.md-THEME_NAME-theme md-option[selected].md-accent { color: '{{accent-500}}'; } md-select-menu.md-THEME_NAME-theme md-option[selected].md-accent:focus { color: '{{accent-600}}'; }md-select-menu.md-THEME_NAME-theme md-option:focus:not([selected]) { background: '{{background-200}}'; }md-sidenav.md-THEME_NAME-theme { background-color: '{{background-color}}'; }md-slider.md-THEME_NAME-theme .md-track { background-color: '{{foreground-3}}'; }md-slider.md-THEME_NAME-theme .md-track-ticks { background-color: '{{foreground-4}}'; }md-slider.md-THEME_NAME-theme .md-focus-thumb { background-color: '{{foreground-2}}'; }md-slider.md-THEME_NAME-theme .md-focus-ring { border-color: '{{foreground-4}}'; }md-slider.md-THEME_NAME-theme .md-disabled-thumb { border-color: '{{background-color}}'; }md-slider.md-THEME_NAME-theme.md-min .md-thumb:after { background-color: '{{background-color}}'; }md-slider.md-THEME_NAME-theme .md-track.md-track-fill { background-color: '{{accent-color}}'; }md-slider.md-THEME_NAME-theme .md-thumb:after { border-color: '{{accent-color}}'; background-color: '{{accent-color}}'; }md-slider.md-THEME_NAME-theme .md-sign { background-color: '{{accent-color}}'; } md-slider.md-THEME_NAME-theme .md-sign:after { border-top-color: '{{accent-color}}'; }md-slider.md-THEME_NAME-theme .md-thumb-text { color: '{{accent-contrast}}'; }md-slider.md-THEME_NAME-theme.md-warn .md-track.md-track-fill { background-color: '{{warn-color}}'; }md-slider.md-THEME_NAME-theme.md-warn .md-thumb:after { border-color: '{{warn-color}}'; background-color: '{{warn-color}}'; }md-slider.md-THEME_NAME-theme.md-warn .md-sign { background-color: '{{warn-color}}'; } md-slider.md-THEME_NAME-theme.md-warn .md-sign:after { border-top-color: '{{warn-color}}'; }md-slider.md-THEME_NAME-theme.md-warn .md-thumb-text { color: '{{warn-contrast}}'; }md-slider.md-THEME_NAME-theme.md-primary .md-track.md-track-fill { background-color: '{{primary-color}}'; }md-slider.md-THEME_NAME-theme.md-primary .md-thumb:after { border-color: '{{primary-color}}'; background-color: '{{primary-color}}'; }md-slider.md-THEME_NAME-theme.md-primary .md-sign { background-color: '{{primary-color}}'; } md-slider.md-THEME_NAME-theme.md-primary .md-sign:after { border-top-color: '{{primary-color}}'; }md-slider.md-THEME_NAME-theme.md-primary .md-thumb-text { color: '{{primary-contrast}}'; }md-slider.md-THEME_NAME-theme[disabled] .md-thumb:after { border-color: '{{foreground-3}}'; }md-slider.md-THEME_NAME-theme[disabled]:not(.md-min) .md-thumb:after { background-color: '{{foreground-3}}'; }.md-subheader.md-THEME_NAME-theme { color: '{{ foreground-2-0.23 }}'; background-color: '{{background-color}}'; } .md-subheader.md-THEME_NAME-theme.md-primary { color: '{{primary-color}}'; } .md-subheader.md-THEME_NAME-theme.md-accent { color: '{{accent-color}}'; } .md-subheader.md-THEME_NAME-theme.md-warn { color: '{{warn-color}}'; }md-switch.md-THEME_NAME-theme .md-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-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-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-thumb { background-color: '{{warn-color}}'; }md-switch.md-THEME_NAME-theme.md-checked.md-warn .md-bar { background-color: '{{warn-color-0.5}}'; }md-switch.md-THEME_NAME-theme.md-checked.md-warn.md-focused .md-thumb:before { background-color: '{{warn-color-0.26}}'; }md-switch.md-THEME_NAME-theme[disabled] .md-thumb { background-color: '{{background-400}}'; }md-switch.md-THEME_NAME-theme[disabled] .md-bar { background-color: '{{foreground-4}}'; }md-tabs.md-THEME_NAME-theme md-tabs-wrapper { background-color: transparent; border-color: '{{foreground-4}}'; }md-tabs.md-THEME_NAME-theme .md-paginator md-icon { color: '{{primary-color}}'; }md-tabs.md-THEME_NAME-theme md-ink-bar { color: '{{accent-color}}'; background: '{{accent-color}}'; }md-tabs.md-THEME_NAME-theme .md-tab { color: '{{foreground-2}}'; } md-tabs.md-THEME_NAME-theme .md-tab[disabled] { color: '{{foreground-3}}'; } md-tabs.md-THEME_NAME-theme .md-tab.md-active, md-tabs.md-THEME_NAME-theme .md-tab.md-focused { color: '{{primary-color}}'; } md-tabs.md-THEME_NAME-theme .md-tab.md-focused { background: '{{primary-color-0.1}}'; } md-tabs.md-THEME_NAME-theme .md-tab .md-ripple-container { color: '{{accent-100}}'; }md-tabs.md-THEME_NAME-theme.md-accent md-tabs-wrapper { background-color: '{{accent-color}}'; }md-tabs.md-THEME_NAME-theme.md-accent md-tab-item:not([disabled]) { color: '{{accent-100}}'; } md-tabs.md-THEME_NAME-theme.md-accent md-tab-item:not([disabled]).md-active, md-tabs.md-THEME_NAME-theme.md-accent md-tab-item:not([disabled]).md-focused { color: '{{accent-contrast}}'; } md-tabs.md-THEME_NAME-theme.md-accent md-tab-item:not([disabled]).md-focused { background: '{{accent-contrast-0.1}}'; }md-tabs.md-THEME_NAME-theme.md-accent 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-tab-item:not([disabled]) { color: '{{primary-100}}'; } md-tabs.md-THEME_NAME-theme.md-primary md-tab-item:not([disabled]).md-active, md-tabs.md-THEME_NAME-theme.md-primary md-tab-item:not([disabled]).md-focused { color: '{{primary-contrast}}'; } md-tabs.md-THEME_NAME-theme.md-primary 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-tab-item:not([disabled]) { color: '{{warn-100}}'; } md-tabs.md-THEME_NAME-theme.md-warn md-tab-item:not([disabled]).md-active, md-tabs.md-THEME_NAME-theme.md-warn md-tab-item:not([disabled]).md-focused { color: '{{warn-contrast}}'; } md-tabs.md-THEME_NAME-theme.md-warn 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-tab-item:not([disabled]) { color: '{{primary-100}}'; } md-toolbar > md-tabs.md-THEME_NAME-theme md-tab-item:not([disabled]).md-active, md-toolbar > md-tabs.md-THEME_NAME-theme md-tab-item:not([disabled]).md-focused { color: '{{primary-contrast}}'; } md-toolbar > md-tabs.md-THEME_NAME-theme 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-tab-item:not([disabled]) { color: '{{accent-100}}'; } md-toolbar.md-accent > md-tabs.md-THEME_NAME-theme md-tab-item:not([disabled]).md-active, md-toolbar.md-accent > md-tabs.md-THEME_NAME-theme md-tab-item:not([disabled]).md-focused { color: '{{accent-contrast}}'; } md-toolbar.md-accent > md-tabs.md-THEME_NAME-theme md-tab-item:not([disabled]).md-focused { background: '{{accent-contrast-0.1}}'; }md-toolbar.md-accent > md-tabs.md-THEME_NAME-theme 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-tab-item:not([disabled]) { color: '{{warn-100}}'; } md-toolbar.md-warn > md-tabs.md-THEME_NAME-theme md-tab-item:not([disabled]).md-active, md-toolbar.md-warn > md-tabs.md-THEME_NAME-theme md-tab-item:not([disabled]).md-focused { color: '{{warn-contrast}}'; } md-toolbar.md-warn > md-tabs.md-THEME_NAME-theme md-tab-item:not([disabled]).md-focused { background: '{{warn-contrast-0.1}}'; }md-toast.md-THEME_NAME-theme { background-color: #323232; color: '{{background-50}}'; } md-toast.md-THEME_NAME-theme .md-button { color: '{{background-50}}'; } md-toast.md-THEME_NAME-theme .md-button.md-highlight { color: '{{primary-A200}}'; } md-toast.md-THEME_NAME-theme .md-button.md-highlight.md-accent { color: '{{accent-A200}}'; } md-toast.md-THEME_NAME-theme .md-button.md-highlight.md-warn { color: '{{warn-A200}}'; }md-toolbar.md-THEME_NAME-theme { background-color: '{{primary-color}}'; color: '{{primary-contrast}}'; } md-toolbar.md-THEME_NAME-theme md-icon { color: '{{primary-contrast}}'; } md-toolbar.md-THEME_NAME-theme .md-button { color: '{{primary-contrast}}'; } md-toolbar.md-THEME_NAME-theme.md-accent { background-color: '{{accent-color}}'; color: '{{accent-contrast}}'; } md-toolbar.md-THEME_NAME-theme.md-warn { background-color: '{{warn-color}}'; color: '{{warn-contrast}}'; }md-tooltip.md-THEME_NAME-theme { color: '{{background-A100}}'; } md-tooltip.md-THEME_NAME-theme .md-background { background-color: '{{foreground-2}}'; }");
13909 })(window, window.angular);