From 663394f188c4460ad889b70a82557db0f9754032 Mon Sep 17 00:00:00 2001 From: seshukm Date: Mon, 6 Mar 2017 10:23:16 +0530 Subject: thirdparty files updated for the vnf market place IssueId : CLIENT-4 Change-Id: Id58c2d11985bda35dc482b122dc404aea2e477da Signed-off-by: seshukm --- .../angular-material/angular-material.js | 35749 +++++++++++++++++++ 1 file changed, 35749 insertions(+) create mode 100644 vnfmarket/common/thirdparty/angular-material/angular-material.js (limited to 'vnfmarket/common/thirdparty/angular-material/angular-material.js') diff --git a/vnfmarket/common/thirdparty/angular-material/angular-material.js b/vnfmarket/common/thirdparty/angular-material/angular-material.js new file mode 100644 index 00000000..59362522 --- /dev/null +++ b/vnfmarket/common/thirdparty/angular-material/angular-material.js @@ -0,0 +1,35749 @@ +/*! + * Angular Material Design + * https://github.com/angular/material + * @license MIT + * v1.1.3 + */ +(function( window, angular, undefined ){ +"use strict"; + +(function(){ +"use strict"; + +angular.module('ngMaterial', ["ng","ngAnimate","ngAria","material.core","material.core.gestures","material.core.interaction","material.core.layout","material.core.meta","material.core.theming.palette","material.core.theming","material.core.animate","material.components.autocomplete","material.components.backdrop","material.components.bottomSheet","material.components.button","material.components.card","material.components.checkbox","material.components.chips","material.components.colors","material.components.content","material.components.datepicker","material.components.dialog","material.components.divider","material.components.fabActions","material.components.fabShared","material.components.fabSpeedDial","material.components.fabToolbar","material.components.gridList","material.components.icon","material.components.input","material.components.list","material.components.menu","material.components.menuBar","material.components.panel","material.components.navBar","material.components.progressCircular","material.components.progressLinear","material.components.radioButton","material.components.select","material.components.showHide","material.components.sidenav","material.components.slider","material.components.sticky","material.components.subheader","material.components.swipe","material.components.switch","material.components.toast","material.components.tabs","material.components.toolbar","material.components.tooltip","material.components.truncate","material.components.virtualRepeat","material.components.whiteframe"]); +})(); +(function(){ +"use strict"; + +/** + * Initialization function that validates environment + * requirements. + */ +DetectNgTouch.$inject = ["$log", "$injector"]; +MdCoreConfigure.$inject = ["$provide", "$mdThemingProvider"]; +rAFDecorator.$inject = ["$delegate"]; +qDecorator.$inject = ["$delegate"]; +angular + .module('material.core', [ + 'ngAnimate', + 'material.core.animate', + 'material.core.layout', + 'material.core.interaction', + 'material.core.gestures', + 'material.core.theming' + ]) + .config(MdCoreConfigure) + .run(DetectNgTouch); + + +/** + * Detect if the ng-Touch module is also being used. + * Warn if detected. + * @ngInject + */ +function DetectNgTouch($log, $injector) { + if ( $injector.has('$swipe') ) { + var msg = "" + + "You are using the ngTouch module. \n" + + "Angular Material already has mobile click, tap, and swipe support... \n" + + "ngTouch is not supported with Angular Material!"; + $log.warn(msg); + } +} + +/** + * @ngInject + */ +function MdCoreConfigure($provide, $mdThemingProvider) { + + $provide.decorator('$$rAF', ['$delegate', rAFDecorator]); + $provide.decorator('$q', ['$delegate', qDecorator]); + + $mdThemingProvider.theme('default') + .primaryPalette('indigo') + .accentPalette('pink') + .warnPalette('deep-orange') + .backgroundPalette('grey'); +} + +/** + * @ngInject + */ +function rAFDecorator($delegate) { + /** + * Use this to throttle events that come in often. + * The throttled function will always use the *last* invocation before the + * coming frame. + * + * For example, window resize events that fire many times a second: + * If we set to use an raf-throttled callback on window resize, then + * our callback will only be fired once per frame, with the last resize + * event that happened before that frame. + * + * @param {function} callback function to debounce + */ + $delegate.throttle = function(cb) { + var queuedArgs, alreadyQueued, queueCb, context; + return function debounced() { + queuedArgs = arguments; + context = this; + queueCb = cb; + if (!alreadyQueued) { + alreadyQueued = true; + $delegate(function() { + queueCb.apply(context, Array.prototype.slice.call(queuedArgs)); + alreadyQueued = false; + }); + } + }; + }; + return $delegate; +} + +/** + * @ngInject + */ +function qDecorator($delegate) { + /** + * Adds a shim for $q.resolve for Angular version that don't have it, + * so we don't have to think about it. + * + * via https://github.com/angular/angular.js/pull/11987 + */ + + // TODO(crisbeto): this won't be necessary once we drop Angular 1.3 + if (!$delegate.resolve) { + $delegate.resolve = $delegate.when; + } + return $delegate; +} + +})(); +(function(){ +"use strict"; + + +MdAutofocusDirective.$inject = ["$parse"];angular.module('material.core') + .directive('mdAutofocus', MdAutofocusDirective) + + // Support the deprecated md-auto-focus and md-sidenav-focus as well + .directive('mdAutoFocus', MdAutofocusDirective) + .directive('mdSidenavFocus', MdAutofocusDirective); + +/** + * @ngdoc directive + * @name mdAutofocus + * @module material.core.util + * + * @description + * + * `[md-autofocus]` provides an optional way to identify the focused element when a `$mdDialog`, + * `$mdBottomSheet`, `$mdMenu` or `$mdSidenav` opens or upon page load for input-like elements. + * + * When one of these opens, it will find the first nested element with the `[md-autofocus]` + * attribute directive and optional expression. An expression may be specified as the directive + * value to enable conditional activation of the autofocus. + * + * @usage + * + * ### Dialog + * + * + *
+ * + * + * + * + *
+ *
+ *
+ * + * ### Bottomsheet + * + * + * Comment Actions + * + * + * + * + * + * {{ item.name }} + * + * + * + * + * + * + * + * ### Autocomplete + * + * + * {{item.display}} + * + * + * + * ### Sidenav + * + *
+ * + * Left Nav! + * + * + * + * Center Content + * + * Open Left Menu + * + * + * + * + *
+ * + * + * + * + *
+ *
+ *
+ *
+ **/ +function MdAutofocusDirective($parse) { + return { + restrict: 'A', + link: { + pre: preLink + } + }; + + function preLink(scope, element, attr) { + var attrExp = attr.mdAutoFocus || attr.mdAutofocus || attr.mdSidenavFocus; + + // Initially update the expression by manually parsing the expression as per $watch source. + updateExpression($parse(attrExp)(scope)); + + // Only watch the expression if it is not empty. + if (attrExp) { + scope.$watch(attrExp, updateExpression); + } + + /** + * Updates the autofocus class which is used to determine whether the attribute + * expression evaluates to true or false. + * @param {string|boolean} value Attribute Value + */ + function updateExpression(value) { + + // Rather than passing undefined to the jqLite toggle class function we explicitly set the + // value to true. Otherwise the class will be just toggled instead of being forced. + if (angular.isUndefined(value)) { + value = true; + } + + element.toggleClass('md-autofocus', !!value); + } + } + +} + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.core.colorUtil + * @description + * Color Util + */ +angular + .module('material.core') + .factory('$mdColorUtil', ColorUtilFactory); + +function ColorUtilFactory() { + /** + * Converts hex value to RGBA string + * @param color {string} + * @returns {string} + */ + function hexToRgba (color) { + var hex = color[ 0 ] === '#' ? color.substr(1) : color, + dig = hex.length / 3, + red = hex.substr(0, dig), + green = hex.substr(dig, dig), + blue = hex.substr(dig * 2); + if (dig === 1) { + red += red; + green += green; + blue += blue; + } + return 'rgba(' + parseInt(red, 16) + ',' + parseInt(green, 16) + ',' + parseInt(blue, 16) + ',0.1)'; + } + + /** + * Converts rgba value to hex string + * @param color {string} + * @returns {string} + */ + function rgbaToHex(color) { + color = color.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i); + + var hex = (color && color.length === 4) ? "#" + + ("0" + parseInt(color[1],10).toString(16)).slice(-2) + + ("0" + parseInt(color[2],10).toString(16)).slice(-2) + + ("0" + parseInt(color[3],10).toString(16)).slice(-2) : ''; + + return hex.toUpperCase(); + } + + /** + * Converts an RGB color to RGBA + * @param color {string} + * @returns {string} + */ + function rgbToRgba (color) { + return color.replace(')', ', 0.1)').replace('(', 'a('); + } + + /** + * Converts an RGBA color to RGB + * @param color {string} + * @returns {string} + */ + function rgbaToRgb (color) { + return color + ? color.replace('rgba', 'rgb').replace(/,[^\),]+\)/, ')') + : 'rgb(0,0,0)'; + } + + return { + rgbaToHex: rgbaToHex, + hexToRgba: hexToRgba, + rgbToRgba: rgbToRgba, + rgbaToRgb: rgbaToRgb + }; +} + +})(); +(function(){ +"use strict"; + +angular.module('material.core') +.factory('$mdConstant', MdConstantFactory); + +/** + * Factory function that creates the grab-bag $mdConstant service. + * @ngInject + */ +function MdConstantFactory() { + + var prefixTestEl = document.createElement('div'); + var vendorPrefix = getVendorPrefix(prefixTestEl); + var isWebkit = /webkit/i.test(vendorPrefix); + var SPECIAL_CHARS_REGEXP = /([:\-_]+(.))/g; + + function vendorProperty(name) { + // Add a dash between the prefix and name, to be able to transform the string into camelcase. + var prefixedName = vendorPrefix + '-' + name; + var ucPrefix = camelCase(prefixedName); + var lcPrefix = ucPrefix.charAt(0).toLowerCase() + ucPrefix.substring(1); + + return hasStyleProperty(prefixTestEl, name) ? name : // The current browser supports the un-prefixed property + hasStyleProperty(prefixTestEl, ucPrefix) ? ucPrefix : // The current browser only supports the prefixed property. + hasStyleProperty(prefixTestEl, lcPrefix) ? lcPrefix : name; // Some browsers are only supporting the prefix in lowercase. + } + + function hasStyleProperty(testElement, property) { + return angular.isDefined(testElement.style[property]); + } + + function camelCase(input) { + return input.replace(SPECIAL_CHARS_REGEXP, function(matches, separator, letter, offset) { + return offset ? letter.toUpperCase() : letter; + }); + } + + function getVendorPrefix(testElement) { + var prop, match; + var vendorRegex = /^(Moz|webkit|ms)(?=[A-Z])/; + + for (prop in testElement.style) { + if (match = vendorRegex.exec(prop)) { + return match[0]; + } + } + } + + var self = { + isInputKey : function(e) { return (e.keyCode >= 31 && e.keyCode <= 90); }, + isNumPadKey : function (e){ return (3 === e.location && e.keyCode >= 97 && e.keyCode <= 105); }, + isNavigationKey : function(e) { + var kc = self.KEY_CODE, NAVIGATION_KEYS = [kc.SPACE, kc.ENTER, kc.UP_ARROW, kc.DOWN_ARROW]; + return (NAVIGATION_KEYS.indexOf(e.keyCode) != -1); + }, + + /** + * Maximum size, in pixels, that can be explicitly set to an element. The actual value varies + * between browsers, but IE11 has the very lowest size at a mere 1,533,917px. Ideally we could + * compute this value, but Firefox always reports an element to have a size of zero if it + * goes over the max, meaning that we'd have to binary search for the value. + */ + ELEMENT_MAX_PIXELS: 1533917, + + /** + * Priority for a directive that should run before the directives from ngAria. + */ + BEFORE_NG_ARIA: 210, + + /** + * Common Keyboard actions and their associated keycode. + */ + KEY_CODE: { + COMMA: 188, + SEMICOLON : 186, + ENTER: 13, + ESCAPE: 27, + SPACE: 32, + PAGE_UP: 33, + PAGE_DOWN: 34, + END: 35, + HOME: 36, + LEFT_ARROW : 37, + UP_ARROW : 38, + RIGHT_ARROW : 39, + DOWN_ARROW : 40, + TAB : 9, + BACKSPACE: 8, + DELETE: 46 + }, + + /** + * Vendor prefixed CSS properties to be used to support the given functionality in older browsers + * as well. + */ + CSS: { + /* Constants */ + TRANSITIONEND: 'transitionend' + (isWebkit ? ' webkitTransitionEnd' : ''), + ANIMATIONEND: 'animationend' + (isWebkit ? ' webkitAnimationEnd' : ''), + + TRANSFORM: vendorProperty('transform'), + TRANSFORM_ORIGIN: vendorProperty('transformOrigin'), + TRANSITION: vendorProperty('transition'), + TRANSITION_DURATION: vendorProperty('transitionDuration'), + ANIMATION_PLAY_STATE: vendorProperty('animationPlayState'), + ANIMATION_DURATION: vendorProperty('animationDuration'), + ANIMATION_NAME: vendorProperty('animationName'), + ANIMATION_TIMING: vendorProperty('animationTimingFunction'), + ANIMATION_DIRECTION: vendorProperty('animationDirection') + }, + + /** + * As defined in core/style/variables.scss + * + * $layout-breakpoint-xs: 600px !default; + * $layout-breakpoint-sm: 960px !default; + * $layout-breakpoint-md: 1280px !default; + * $layout-breakpoint-lg: 1920px !default; + * + */ + MEDIA: { + 'xs' : '(max-width: 599px)' , + 'gt-xs' : '(min-width: 600px)' , + 'sm' : '(min-width: 600px) and (max-width: 959px)' , + 'gt-sm' : '(min-width: 960px)' , + 'md' : '(min-width: 960px) and (max-width: 1279px)' , + 'gt-md' : '(min-width: 1280px)' , + 'lg' : '(min-width: 1280px) and (max-width: 1919px)', + 'gt-lg' : '(min-width: 1920px)' , + 'xl' : '(min-width: 1920px)' , + 'landscape' : '(orientation: landscape)' , + 'portrait' : '(orientation: portrait)' , + 'print' : 'print' + }, + + MEDIA_PRIORITY: [ + 'xl', + 'gt-lg', + 'lg', + 'gt-md', + 'md', + 'gt-sm', + 'sm', + 'gt-xs', + 'xs', + 'landscape', + 'portrait', + 'print' + ] + }; + + return self; +} + +})(); +(function(){ +"use strict"; + + angular + .module('material.core') + .config( ["$provide", function($provide){ + $provide.decorator('$mdUtil', ['$delegate', function ($delegate){ + /** + * Inject the iterator facade to easily support iteration and accessors + * @see iterator below + */ + $delegate.iterator = MdIterator; + + return $delegate; + } + ]); + }]); + + /** + * iterator is a list facade to easily support iteration and accessors + * + * @param items Array list which this iterator will enumerate + * @param reloop Boolean enables iterator to consider the list as an endless reloop + */ + function MdIterator(items, reloop) { + var trueFn = function() { return true; }; + + if (items && !angular.isArray(items)) { + items = Array.prototype.slice.call(items); + } + + reloop = !!reloop; + var _items = items || [ ]; + + // Published API + return { + items: getItems, + count: count, + + inRange: inRange, + contains: contains, + indexOf: indexOf, + itemAt: itemAt, + + findBy: findBy, + + add: add, + remove: remove, + + first: first, + last: last, + next: angular.bind(null, findSubsequentItem, false), + previous: angular.bind(null, findSubsequentItem, true), + + hasPrevious: hasPrevious, + hasNext: hasNext + + }; + + /** + * Publish copy of the enumerable set + * @returns {Array|*} + */ + function getItems() { + return [].concat(_items); + } + + /** + * Determine length of the list + * @returns {Array.length|*|number} + */ + function count() { + return _items.length; + } + + /** + * Is the index specified valid + * @param index + * @returns {Array.length|*|number|boolean} + */ + function inRange(index) { + return _items.length && ( index > -1 ) && (index < _items.length ); + } + + /** + * Can the iterator proceed to the next item in the list; relative to + * the specified item. + * + * @param item + * @returns {Array.length|*|number|boolean} + */ + function hasNext(item) { + return item ? inRange(indexOf(item) + 1) : false; + } + + /** + * Can the iterator proceed to the previous item in the list; relative to + * the specified item. + * + * @param item + * @returns {Array.length|*|number|boolean} + */ + function hasPrevious(item) { + return item ? inRange(indexOf(item) - 1) : false; + } + + /** + * Get item at specified index/position + * @param index + * @returns {*} + */ + function itemAt(index) { + return inRange(index) ? _items[index] : null; + } + + /** + * Find all elements matching the key/value pair + * otherwise return null + * + * @param val + * @param key + * + * @return array + */ + function findBy(key, val) { + return _items.filter(function(item) { + return item[key] === val; + }); + } + + /** + * Add item to list + * @param item + * @param index + * @returns {*} + */ + function add(item, index) { + if ( !item ) return -1; + + if (!angular.isNumber(index)) { + index = _items.length; + } + + _items.splice(index, 0, item); + + return indexOf(item); + } + + /** + * Remove item from list... + * @param item + */ + function remove(item) { + if ( contains(item) ){ + _items.splice(indexOf(item), 1); + } + } + + /** + * Get the zero-based index of the target item + * @param item + * @returns {*} + */ + function indexOf(item) { + return _items.indexOf(item); + } + + /** + * Boolean existence check + * @param item + * @returns {boolean} + */ + function contains(item) { + return item && (indexOf(item) > -1); + } + + /** + * Return first item in the list + * @returns {*} + */ + function first() { + return _items.length ? _items[0] : null; + } + + /** + * Return last item in the list... + * @returns {*} + */ + function last() { + return _items.length ? _items[_items.length - 1] : null; + } + + /** + * Find the next item. If reloop is true and at the end of the list, it will go back to the + * first item. If given, the `validate` callback will be used to determine whether the next item + * is valid. If not valid, it will try to find the next item again. + * + * @param {boolean} backwards Specifies the direction of searching (forwards/backwards) + * @param {*} item The item whose subsequent item we are looking for + * @param {Function=} validate The `validate` function + * @param {integer=} limit The recursion limit + * + * @returns {*} The subsequent item or null + */ + function findSubsequentItem(backwards, item, validate, limit) { + validate = validate || trueFn; + + var curIndex = indexOf(item); + while (true) { + if (!inRange(curIndex)) return null; + + var nextIndex = curIndex + (backwards ? -1 : 1); + var foundItem = null; + if (inRange(nextIndex)) { + foundItem = _items[nextIndex]; + } else if (reloop) { + foundItem = backwards ? last() : first(); + nextIndex = indexOf(foundItem); + } + + if ((foundItem === null) || (nextIndex === limit)) return null; + if (validate(foundItem)) return foundItem; + + if (angular.isUndefined(limit)) limit = nextIndex; + + curIndex = nextIndex; + } + } + } + + +})(); +(function(){ +"use strict"; + + +mdMediaFactory.$inject = ["$mdConstant", "$rootScope", "$window"];angular.module('material.core') +.factory('$mdMedia', mdMediaFactory); + +/** + * @ngdoc service + * @name $mdMedia + * @module material.core + * + * @description + * `$mdMedia` is used to evaluate whether a given media query is true or false given the + * current device's screen / window size. The media query will be re-evaluated on resize, allowing + * you to register a watch. + * + * `$mdMedia` also has pre-programmed support for media queries that match the layout breakpoints: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
BreakpointmediaQuery
xs(max-width: 599px)
gt-xs(min-width: 600px)
sm(min-width: 600px) and (max-width: 959px)
gt-sm(min-width: 960px)
md(min-width: 960px) and (max-width: 1279px)
gt-md(min-width: 1280px)
lg(min-width: 1280px) and (max-width: 1919px)
gt-lg(min-width: 1920px)
xl(min-width: 1920px)
landscapelandscape
portraitportrait
printprint
+ * + * See Material Design's Layout - Adaptive UI for more details. + * + * + * + * + * + * @returns {boolean} a boolean representing whether or not the given media query is true or false. + * + * @usage + * + * app.controller('MyController', function($mdMedia, $scope) { + * $scope.$watch(function() { return $mdMedia('lg'); }, function(big) { + * $scope.bigScreen = big; + * }); + * + * $scope.screenIsSmall = $mdMedia('sm'); + * $scope.customQuery = $mdMedia('(min-width: 1234px)'); + * $scope.anotherCustom = $mdMedia('max-width: 300px'); + * }); + * + */ + +/* @ngInject */ +function mdMediaFactory($mdConstant, $rootScope, $window) { + var queries = {}; + var mqls = {}; + var results = {}; + var normalizeCache = {}; + + $mdMedia.getResponsiveAttribute = getResponsiveAttribute; + $mdMedia.getQuery = getQuery; + $mdMedia.watchResponsiveAttributes = watchResponsiveAttributes; + + return $mdMedia; + + function $mdMedia(query) { + var validated = queries[query]; + if (angular.isUndefined(validated)) { + validated = queries[query] = validate(query); + } + + var result = results[validated]; + if (angular.isUndefined(result)) { + result = add(validated); + } + + return result; + } + + function validate(query) { + return $mdConstant.MEDIA[query] || + ((query.charAt(0) !== '(') ? ('(' + query + ')') : query); + } + + function add(query) { + var result = mqls[query]; + if ( !result ) { + result = mqls[query] = $window.matchMedia(query); + } + + result.addListener(onQueryChange); + return (results[result.media] = !!result.matches); + } + + function onQueryChange(query) { + $rootScope.$evalAsync(function() { + results[query.media] = !!query.matches; + }); + } + + function getQuery(name) { + return mqls[name]; + } + + function getResponsiveAttribute(attrs, attrName) { + for (var i = 0; i < $mdConstant.MEDIA_PRIORITY.length; i++) { + var mediaName = $mdConstant.MEDIA_PRIORITY[i]; + if (!mqls[queries[mediaName]].matches) { + continue; + } + + var normalizedName = getNormalizedName(attrs, attrName + '-' + mediaName); + if (attrs[normalizedName]) { + return attrs[normalizedName]; + } + } + + // fallback on unprefixed + return attrs[getNormalizedName(attrs, attrName)]; + } + + function watchResponsiveAttributes(attrNames, attrs, watchFn) { + var unwatchFns = []; + attrNames.forEach(function(attrName) { + var normalizedName = getNormalizedName(attrs, attrName); + if (angular.isDefined(attrs[normalizedName])) { + unwatchFns.push( + attrs.$observe(normalizedName, angular.bind(void 0, watchFn, null))); + } + + for (var mediaName in $mdConstant.MEDIA) { + normalizedName = getNormalizedName(attrs, attrName + '-' + mediaName); + if (angular.isDefined(attrs[normalizedName])) { + unwatchFns.push( + attrs.$observe(normalizedName, angular.bind(void 0, watchFn, mediaName))); + } + } + }); + + return function unwatch() { + unwatchFns.forEach(function(fn) { fn(); }); + }; + } + + // Improves performance dramatically + function getNormalizedName(attrs, attrName) { + return normalizeCache[attrName] || + (normalizeCache[attrName] = attrs.$normalize(attrName)); + } +} + +})(); +(function(){ +"use strict"; + +angular + .module('material.core') + .config( ["$provide", function($provide) { + $provide.decorator('$mdUtil', ['$delegate', function ($delegate) { + + // Inject the prefixer into our original $mdUtil service. + $delegate.prefixer = MdPrefixer; + + return $delegate; + }]); + }]); + +function MdPrefixer(initialAttributes, buildSelector) { + var PREFIXES = ['data', 'x']; + + if (initialAttributes) { + // The prefixer also accepts attributes as a parameter, and immediately builds a list or selector for + // the specified attributes. + return buildSelector ? _buildSelector(initialAttributes) : _buildList(initialAttributes); + } + + return { + buildList: _buildList, + buildSelector: _buildSelector, + hasAttribute: _hasAttribute, + removeAttribute: _removeAttribute + }; + + function _buildList(attributes) { + attributes = angular.isArray(attributes) ? attributes : [attributes]; + + attributes.forEach(function(item) { + PREFIXES.forEach(function(prefix) { + attributes.push(prefix + '-' + item); + }); + }); + + return attributes; + } + + function _buildSelector(attributes) { + attributes = angular.isArray(attributes) ? attributes : [attributes]; + + return _buildList(attributes) + .map(function(item) { + return '[' + item + ']'; + }) + .join(','); + } + + function _hasAttribute(element, attribute) { + element = _getNativeElement(element); + + if (!element) { + return false; + } + + var prefixedAttrs = _buildList(attribute); + + for (var i = 0; i < prefixedAttrs.length; i++) { + if (element.hasAttribute(prefixedAttrs[i])) { + return true; + } + } + + return false; + } + + function _removeAttribute(element, attribute) { + element = _getNativeElement(element); + + if (!element) { + return; + } + + _buildList(attribute).forEach(function(prefixedAttribute) { + element.removeAttribute(prefixedAttribute); + }); + } + + /** + * Transforms a jqLite or DOM element into a HTML element. + * This is useful when supporting jqLite elements and DOM elements at + * same time. + * @param element {JQLite|Element} Element to be parsed + * @returns {HTMLElement} Parsed HTMLElement + */ + function _getNativeElement(element) { + element = element[0] || element; + + if (element.nodeType) { + return element; + } + } + +} + +})(); +(function(){ +"use strict"; + +/* + * This var has to be outside the angular factory, otherwise when + * there are multiple material apps on the same page, each app + * will create its own instance of this array and the app's IDs + * will not be unique. + */ +UtilFactory.$inject = ["$document", "$timeout", "$compile", "$rootScope", "$$mdAnimate", "$interpolate", "$log", "$rootElement", "$window", "$$rAF"]; +var nextUniqueId = 0; + +/** + * @ngdoc module + * @name material.core.util + * @description + * Util + */ +angular + .module('material.core') + .factory('$mdUtil', UtilFactory); + +/** + * @ngInject + */ +function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $interpolate, $log, $rootElement, $window, $$rAF) { + // Setup some core variables for the processTemplate method + var startSymbol = $interpolate.startSymbol(), + endSymbol = $interpolate.endSymbol(), + usesStandardSymbols = ((startSymbol === '{{') && (endSymbol === '}}')); + + /** + * Checks if the target element has the requested style by key + * @param {DOMElement|JQLite} target Target element + * @param {string} key Style key + * @param {string=} expectedVal Optional expected value + * @returns {boolean} Whether the target element has the style or not + */ + var hasComputedStyle = function (target, key, expectedVal) { + var hasValue = false; + + if ( target && target.length ) { + var computedStyles = $window.getComputedStyle(target[0]); + hasValue = angular.isDefined(computedStyles[key]) && (expectedVal ? computedStyles[key] == expectedVal : true); + } + + return hasValue; + }; + + function validateCssValue(value) { + return !value ? '0' : + hasPx(value) || hasPercent(value) ? value : value + 'px'; + } + + function hasPx(value) { + return String(value).indexOf('px') > -1; + } + + function hasPercent(value) { + return String(value).indexOf('%') > -1; + + } + + var $mdUtil = { + dom: {}, + now: window.performance && window.performance.now ? + angular.bind(window.performance, window.performance.now) : Date.now || function() { + return new Date().getTime(); + }, + + /** + * Cross-version compatibility method to retrieve an option of a ngModel controller, + * which supports the breaking changes in the AngularJS snapshot (SHA 87a2ff76af5d0a9268d8eb84db5755077d27c84c). + * @param {!angular.ngModelCtrl} ngModelCtrl + * @param {!string} optionName + * @returns {Object|undefined} + */ + getModelOption: function (ngModelCtrl, optionName) { + if (!ngModelCtrl.$options) { + return; + } + + var $options = ngModelCtrl.$options; + + // The newer versions of Angular introduced a `getOption function and made the option values no longer + // visible on the $options object. + return $options.getOption ? $options.getOption(optionName) : $options[optionName] + }, + + /** + * Bi-directional accessor/mutator used to easily update an element's + * property based on the current 'dir'ectional value. + */ + bidi : function(element, property, lValue, rValue) { + var ltr = !($document[0].dir == 'rtl' || $document[0].body.dir == 'rtl'); + + // If accessor + if ( arguments.length == 0 ) return ltr ? 'ltr' : 'rtl'; + + // If mutator + var elem = angular.element(element); + + if ( ltr && angular.isDefined(lValue)) { + elem.css(property, validateCssValue(lValue)); + } + else if ( !ltr && angular.isDefined(rValue)) { + elem.css(property, validateCssValue(rValue) ); + } + }, + + bidiProperty: function (element, lProperty, rProperty, value) { + var ltr = !($document[0].dir == 'rtl' || $document[0].body.dir == 'rtl'); + + var elem = angular.element(element); + + if ( ltr && angular.isDefined(lProperty)) { + elem.css(lProperty, validateCssValue(value)); + elem.css(rProperty, ''); + } + else if ( !ltr && angular.isDefined(rProperty)) { + elem.css(rProperty, validateCssValue(value) ); + elem.css(lProperty, ''); + } + }, + + clientRect: function(element, offsetParent, isOffsetRect) { + var node = getNode(element); + offsetParent = getNode(offsetParent || node.offsetParent || document.body); + var nodeRect = node.getBoundingClientRect(); + + // The user can ask for an offsetRect: a rect relative to the offsetParent, + // or a clientRect: a rect relative to the page + var offsetRect = isOffsetRect ? + offsetParent.getBoundingClientRect() : + {left: 0, top: 0, width: 0, height: 0}; + return { + left: nodeRect.left - offsetRect.left, + top: nodeRect.top - offsetRect.top, + width: nodeRect.width, + height: nodeRect.height + }; + }, + offsetRect: function(element, offsetParent) { + return $mdUtil.clientRect(element, offsetParent, true); + }, + + // Annoying method to copy nodes to an array, thanks to IE + nodesToArray: function(nodes) { + nodes = nodes || []; + + var results = []; + for (var i = 0; i < nodes.length; ++i) { + results.push(nodes.item(i)); + } + return results; + }, + + /** + * Determines the absolute position of the viewport. + * Useful when making client rectangles absolute. + * @returns {number} + */ + getViewportTop: function() { + return window.scrollY || window.pageYOffset || 0; + }, + + /** + * Finds the proper focus target by searching the DOM. + * + * @param containerEl + * @param attributeVal + * @returns {*} + */ + findFocusTarget: function(containerEl, attributeVal) { + var AUTO_FOCUS = this.prefixer('md-autofocus', true); + var elToFocus; + + elToFocus = scanForFocusable(containerEl, attributeVal || AUTO_FOCUS); + + if ( !elToFocus && attributeVal != AUTO_FOCUS) { + // Scan for deprecated attribute + elToFocus = scanForFocusable(containerEl, this.prefixer('md-auto-focus', true)); + + if ( !elToFocus ) { + // Scan for fallback to 'universal' API + elToFocus = scanForFocusable(containerEl, AUTO_FOCUS); + } + } + + return elToFocus; + + /** + * Can target and nested children for specified Selector (attribute) + * whose value may be an expression that evaluates to True/False. + */ + function scanForFocusable(target, selector) { + var elFound, items = target[0].querySelectorAll(selector); + + // Find the last child element with the focus attribute + if ( items && items.length ){ + items.length && angular.forEach(items, function(it) { + it = angular.element(it); + + // Check the element for the md-autofocus class to ensure any associated expression + // evaluated to true. + var isFocusable = it.hasClass('md-autofocus'); + if (isFocusable) elFound = it; + }); + } + return elFound; + } + }, + + /** + * Disables scroll around the passed parent element. + * @param element Unused + * @param {!Element|!angular.JQLite} parent Element to disable scrolling within. + * Defaults to body if none supplied. + * @param options Object of options to modify functionality + * - disableScrollMask Boolean of whether or not to create a scroll mask element or + * use the passed parent element. + */ + disableScrollAround: function(element, parent, options) { + options = options || {}; + + $mdUtil.disableScrollAround._count = Math.max(0, $mdUtil.disableScrollAround._count || 0); + $mdUtil.disableScrollAround._count++; + + if ($mdUtil.disableScrollAround._restoreScroll) { + return $mdUtil.disableScrollAround._restoreScroll; + } + + var body = $document[0].body; + var restoreBody = disableBodyScroll(); + var restoreElement = disableElementScroll(parent); + + return $mdUtil.disableScrollAround._restoreScroll = function() { + if (--$mdUtil.disableScrollAround._count <= 0) { + restoreBody(); + restoreElement(); + delete $mdUtil.disableScrollAround._restoreScroll; + } + }; + + /** + * Creates a virtual scrolling mask to prevent touchmove, keyboard, scrollbar clicking, + * and wheel events + */ + function disableElementScroll(element) { + element = angular.element(element || body); + + var scrollMask; + + if (options.disableScrollMask) { + scrollMask = element; + } else { + scrollMask = angular.element( + '
' + + '
' + + '
'); + element.append(scrollMask); + } + + scrollMask.on('wheel', preventDefault); + scrollMask.on('touchmove', preventDefault); + + return function restoreElementScroll() { + scrollMask.off('wheel'); + scrollMask.off('touchmove'); + + if (!options.disableScrollMask) { + scrollMask[0].parentNode.removeChild(scrollMask[0]); + } + }; + + function preventDefault(e) { + e.preventDefault(); + } + } + + // Converts the body to a position fixed block and translate it to the proper scroll position + function disableBodyScroll() { + var documentElement = $document[0].documentElement; + + var prevDocumentStyle = documentElement.style.cssText || ''; + var prevBodyStyle = body.style.cssText || ''; + + var viewportTop = $mdUtil.getViewportTop(); + var clientWidth = body.clientWidth; + var hasVerticalScrollbar = body.scrollHeight > body.clientHeight + 1; + + if (hasVerticalScrollbar) { + angular.element(body).css({ + position: 'fixed', + width: '100%', + top: -viewportTop + 'px' + }); + } + + if (body.clientWidth < clientWidth) { + body.style.overflow = 'hidden'; + } + + // This should be applied after the manipulation to the body, because + // adding a scrollbar can potentially resize it, causing the measurement + // to change. + if (hasVerticalScrollbar) { + documentElement.style.overflowY = 'scroll'; + } + + return function restoreScroll() { + // Reset the inline style CSS to the previous. + body.style.cssText = prevBodyStyle; + documentElement.style.cssText = prevDocumentStyle; + + // The body loses its scroll position while being fixed. + body.scrollTop = viewportTop; + }; + } + + }, + + enableScrolling: function() { + var restoreFn = this.disableScrollAround._restoreScroll; + restoreFn && restoreFn(); + }, + + floatingScrollbars: function() { + if (this.floatingScrollbars.cached === undefined) { + var tempNode = angular.element('
').css({ + width: '100%', + 'z-index': -1, + position: 'absolute', + height: '35px', + 'overflow-y': 'scroll' + }); + tempNode.children().css('height', '60px'); + + $document[0].body.appendChild(tempNode[0]); + this.floatingScrollbars.cached = (tempNode[0].offsetWidth == tempNode[0].childNodes[0].offsetWidth); + tempNode.remove(); + } + return this.floatingScrollbars.cached; + }, + + // Mobile safari only allows you to set focus in click event listeners... + forceFocus: function(element) { + var node = element[0] || element; + + document.addEventListener('click', function focusOnClick(ev) { + if (ev.target === node && ev.$focus) { + node.focus(); + ev.stopImmediatePropagation(); + ev.preventDefault(); + node.removeEventListener('click', focusOnClick); + } + }, true); + + var newEvent = document.createEvent('MouseEvents'); + newEvent.initMouseEvent('click', false, true, window, {}, 0, 0, 0, 0, + false, false, false, false, 0, null); + newEvent.$material = true; + newEvent.$focus = true; + node.dispatchEvent(newEvent); + }, + + /** + * facade to build md-backdrop element with desired styles + * NOTE: Use $compile to trigger backdrop postLink function + */ + createBackdrop: function(scope, addClass) { + return $compile($mdUtil.supplant('', [addClass]))(scope); + }, + + /** + * supplant() method from Crockford's `Remedial Javascript` + * Equivalent to use of $interpolate; without dependency on + * interpolation symbols and scope. Note: the '{}' can + * be property names, property chains, or array indices. + */ + supplant: function(template, values, pattern) { + pattern = pattern || /\{([^\{\}]*)\}/g; + return template.replace(pattern, function(a, b) { + var p = b.split('.'), + r = values; + try { + for (var s in p) { + if (p.hasOwnProperty(s) ) { + r = r[p[s]]; + } + } + } catch (e) { + r = a; + } + return (typeof r === 'string' || typeof r === 'number') ? r : a; + }); + }, + + fakeNgModel: function() { + return { + $fake: true, + $setTouched: angular.noop, + $setViewValue: function(value) { + this.$viewValue = value; + this.$render(value); + this.$viewChangeListeners.forEach(function(cb) { + cb(); + }); + }, + $isEmpty: function(value) { + return ('' + value).length === 0; + }, + $parsers: [], + $formatters: [], + $viewChangeListeners: [], + $render: angular.noop + }; + }, + + // Returns a function, that, as long as it continues to be invoked, will not + // be triggered. The function will be called after it stops being called for + // N milliseconds. + // @param wait Integer value of msecs to delay (since last debounce reset); default value 10 msecs + // @param invokeApply should the $timeout trigger $digest() dirty checking + debounce: function(func, wait, scope, invokeApply) { + var timer; + + return function debounced() { + var context = scope, + args = Array.prototype.slice.call(arguments); + + $timeout.cancel(timer); + timer = $timeout(function() { + + timer = undefined; + func.apply(context, args); + + }, wait || 10, invokeApply); + }; + }, + + // Returns a function that can only be triggered every `delay` milliseconds. + // In other words, the function will not be called unless it has been more + // than `delay` milliseconds since the last call. + throttle: function throttle(func, delay) { + var recent; + return function throttled() { + var context = this; + var args = arguments; + var now = $mdUtil.now(); + + if (!recent || (now - recent > delay)) { + func.apply(context, args); + recent = now; + } + }; + }, + + /** + * Measures the number of milliseconds taken to run the provided callback + * function. Uses a high-precision timer if available. + */ + time: function time(cb) { + var start = $mdUtil.now(); + cb(); + return $mdUtil.now() - start; + }, + + /** + * Create an implicit getter that caches its `getter()` + * lookup value + */ + valueOnUse : function (scope, key, getter) { + var value = null, args = Array.prototype.slice.call(arguments); + var params = (args.length > 3) ? args.slice(3) : [ ]; + + Object.defineProperty(scope, key, { + get: function () { + if (value === null) value = getter.apply(scope, params); + return value; + } + }); + }, + + /** + * Get a unique ID. + * + * @returns {string} an unique numeric string + */ + nextUid: function() { + return '' + nextUniqueId++; + }, + + // Stop watchers and events from firing on a scope without destroying it, + // by disconnecting it from its parent and its siblings' linked lists. + disconnectScope: function disconnectScope(scope) { + if (!scope) return; + + // we can't destroy the root scope or a scope that has been already destroyed + if (scope.$root === scope) return; + if (scope.$$destroyed) return; + + var parent = scope.$parent; + scope.$$disconnected = true; + + // See Scope.$destroy + if (parent.$$childHead === scope) parent.$$childHead = scope.$$nextSibling; + if (parent.$$childTail === scope) parent.$$childTail = scope.$$prevSibling; + if (scope.$$prevSibling) scope.$$prevSibling.$$nextSibling = scope.$$nextSibling; + if (scope.$$nextSibling) scope.$$nextSibling.$$prevSibling = scope.$$prevSibling; + + scope.$$nextSibling = scope.$$prevSibling = null; + + }, + + // Undo the effects of disconnectScope above. + reconnectScope: function reconnectScope(scope) { + if (!scope) return; + + // we can't disconnect the root node or scope already disconnected + if (scope.$root === scope) return; + if (!scope.$$disconnected) return; + + var child = scope; + + var parent = child.$parent; + child.$$disconnected = false; + // See Scope.$new for this logic... + child.$$prevSibling = parent.$$childTail; + if (parent.$$childHead) { + parent.$$childTail.$$nextSibling = child; + parent.$$childTail = child; + } else { + parent.$$childHead = parent.$$childTail = child; + } + }, + + /* + * getClosest replicates jQuery.closest() to walk up the DOM tree until it finds a matching nodeName + * + * @param el Element to start walking the DOM from + * @param check Either a string or a function. If a string is passed, it will be evaluated against + * each of the parent nodes' tag name. If a function is passed, the loop will call it with each of + * the parents and will use the return value to determine whether the node is a match. + * @param onlyParent Only start checking from the parent element, not `el`. + */ + getClosest: function getClosest(el, validateWith, onlyParent) { + if ( angular.isString(validateWith) ) { + var tagName = validateWith.toUpperCase(); + validateWith = function(el) { + return el.nodeName.toUpperCase() === tagName; + }; + } + + if (el instanceof angular.element) el = el[0]; + if (onlyParent) el = el.parentNode; + if (!el) return null; + + do { + if (validateWith(el)) { + return el; + } + } while (el = el.parentNode); + + return null; + }, + + /** + * Build polyfill for the Node.contains feature (if needed) + */ + elementContains: function(node, child) { + var hasContains = (window.Node && window.Node.prototype && Node.prototype.contains); + var findFn = hasContains ? angular.bind(node, node.contains) : angular.bind(node, function(arg) { + // compares the positions of two nodes and returns a bitmask + return (node === child) || !!(this.compareDocumentPosition(arg) & 16) + }); + + return findFn(child); + }, + + /** + * Functional equivalent for $element.filter(‘md-bottom-sheet’) + * useful with interimElements where the element and its container are important... + * + * @param {[]} elements to scan + * @param {string} name of node to find (e.g. 'md-dialog') + * @param {boolean=} optional flag to allow deep scans; defaults to 'false'. + * @param {boolean=} optional flag to enable log warnings; defaults to false + */ + extractElementByName: function(element, nodeName, scanDeep, warnNotFound) { + var found = scanTree(element); + if (!found && !!warnNotFound) { + $log.warn( $mdUtil.supplant("Unable to find node '{0}' in element '{1}'.",[nodeName, element[0].outerHTML]) ); + } + + return angular.element(found || element); + + /** + * Breadth-First tree scan for element with matching `nodeName` + */ + function scanTree(element) { + return scanLevel(element) || (!!scanDeep ? scanChildren(element) : null); + } + + /** + * Case-insensitive scan of current elements only (do not descend). + */ + function scanLevel(element) { + if ( element ) { + for (var i = 0, len = element.length; i < len; i++) { + if (element[i].nodeName.toLowerCase() === nodeName) { + return element[i]; + } + } + } + return null; + } + + /** + * Scan children of specified node + */ + function scanChildren(element) { + var found; + if ( element ) { + for (var i = 0, len = element.length; i < len; i++) { + var target = element[i]; + if ( !found ) { + for (var j = 0, numChild = target.childNodes.length; j < numChild; j++) { + found = found || scanTree([target.childNodes[j]]); + } + } + } + } + return found; + } + + }, + + /** + * Give optional properties with no value a boolean true if attr provided or false otherwise + */ + initOptionalProperties: function(scope, attr, defaults) { + defaults = defaults || {}; + angular.forEach(scope.$$isolateBindings, function(binding, key) { + if (binding.optional && angular.isUndefined(scope[key])) { + var attrIsDefined = angular.isDefined(attr[binding.attrName]); + scope[key] = angular.isDefined(defaults[key]) ? defaults[key] : attrIsDefined; + } + }); + }, + + /** + * Alternative to $timeout calls with 0 delay. + * nextTick() coalesces all calls within a single frame + * to minimize $digest thrashing + * + * @param callback + * @param digest + * @returns {*} + */ + nextTick: function(callback, digest, scope) { + //-- grab function reference for storing state details + var nextTick = $mdUtil.nextTick; + var timeout = nextTick.timeout; + var queue = nextTick.queue || []; + + //-- add callback to the queue + queue.push({scope: scope, callback: callback}); + + //-- set default value for digest + if (digest == null) digest = true; + + //-- store updated digest/queue values + nextTick.digest = nextTick.digest || digest; + nextTick.queue = queue; + + //-- either return existing timeout or create a new one + return timeout || (nextTick.timeout = $timeout(processQueue, 0, false)); + + /** + * Grab a copy of the current queue + * Clear the queue for future use + * Process the existing queue + * Trigger digest if necessary + */ + function processQueue() { + var queue = nextTick.queue; + var digest = nextTick.digest; + + nextTick.queue = []; + nextTick.timeout = null; + nextTick.digest = false; + + queue.forEach(function(queueItem) { + var skip = queueItem.scope && queueItem.scope.$$destroyed; + if (!skip) { + queueItem.callback(); + } + }); + + if (digest) $rootScope.$digest(); + } + }, + + /** + * Processes a template and replaces the start/end symbols if the application has + * overriden them. + * + * @param template The template to process whose start/end tags may be replaced. + * @returns {*} + */ + processTemplate: function(template) { + if (usesStandardSymbols) { + return template; + } else { + if (!template || !angular.isString(template)) return template; + return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol); + } + }, + + /** + * Scan up dom hierarchy for enabled parent; + */ + getParentWithPointerEvents: function (element) { + var parent = element.parent(); + + // jqLite might return a non-null, but still empty, parent; so check for parent and length + while (hasComputedStyle(parent, 'pointer-events', 'none')) { + parent = parent.parent(); + } + + return parent; + }, + + getNearestContentElement: function (element) { + var current = element.parent()[0]; + // Look for the nearest parent md-content, stopping at the rootElement. + while (current && current !== $rootElement[0] && current !== document.body && current.nodeName.toUpperCase() !== 'MD-CONTENT') { + current = current.parentNode; + } + return current; + }, + + /** + * Checks if the current browser is natively supporting the `sticky` position. + * @returns {string} supported sticky property name + */ + checkStickySupport: function() { + var stickyProp; + var testEl = angular.element('
'); + $document[0].body.appendChild(testEl[0]); + + var stickyProps = ['sticky', '-webkit-sticky']; + for (var i = 0; i < stickyProps.length; ++i) { + testEl.css({ + position: stickyProps[i], + top: 0, + 'z-index': 2 + }); + + if (testEl.css('position') == stickyProps[i]) { + stickyProp = stickyProps[i]; + break; + } + } + + testEl.remove(); + + return stickyProp; + }, + + /** + * Parses an attribute value, mostly a string. + * By default checks for negated values and returns `false´ if present. + * Negated values are: (native falsy) and negative strings like: + * `false` or `0`. + * @param value Attribute value which should be parsed. + * @param negatedCheck When set to false, won't check for negated values. + * @returns {boolean} + */ + parseAttributeBoolean: function(value, negatedCheck) { + return value === '' || !!value && (negatedCheck === false || value !== 'false' && value !== '0'); + }, + + hasComputedStyle: hasComputedStyle, + + /** + * Returns true if the parent form of the element has been submitted. + * + * @param element An Angular or HTML5 element. + * + * @returns {boolean} + */ + isParentFormSubmitted: function(element) { + var parent = $mdUtil.getClosest(element, 'form'); + var form = parent ? angular.element(parent).controller('form') : null; + + return form ? form.$submitted : false; + }, + + /** + * Animate the requested element's scrollTop to the requested scrollPosition with basic easing. + * + * @param {!HTMLElement} element The element to scroll. + * @param {number} scrollEnd The new/final scroll position. + * @param {number=} duration Duration of the scroll. Default is 1000ms. + */ + animateScrollTo: function(element, scrollEnd, duration) { + var scrollStart = element.scrollTop; + var scrollChange = scrollEnd - scrollStart; + var scrollingDown = scrollStart < scrollEnd; + var startTime = $mdUtil.now(); + + $$rAF(scrollChunk); + + function scrollChunk() { + var newPosition = calculateNewPosition(); + + element.scrollTop = newPosition; + + if (scrollingDown ? newPosition < scrollEnd : newPosition > scrollEnd) { + $$rAF(scrollChunk); + } + } + + function calculateNewPosition() { + var easeDuration = duration || 1000; + var currentTime = $mdUtil.now() - startTime; + + return ease(currentTime, scrollStart, scrollChange, easeDuration); + } + + function ease(currentTime, start, change, duration) { + // If the duration has passed (which can occur if our app loses focus due to $$rAF), jump + // straight to the proper position + if (currentTime > duration) { + return start + change; + } + + var ts = (currentTime /= duration) * currentTime; + var tc = ts * currentTime; + + return start + change * (-2 * tc + 3 * ts); + } + }, + + /** + * Provides an easy mechanism for removing duplicates from an array. + * + * var myArray = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4]; + * + * $mdUtil.uniq(myArray) => [1, 2, 3, 4] + * + * @param {array} array The array whose unique values should be returned. + * + * @returns {array} A copy of the array containing only unique values. + */ + uniq: function(array) { + if (!array) { return; } + + return array.filter(function(value, index, self) { + return self.indexOf(value) === index; + }); + } + }; + + +// Instantiate other namespace utility methods + + $mdUtil.dom.animator = $$mdAnimate($mdUtil); + + return $mdUtil; + + function getNode(el) { + return el[0] || el; + } + +} + +/* + * Since removing jQuery from the demos, some code that uses `element.focus()` is broken. + * We need to add `element.focus()`, because it's testable unlike `element[0].focus`. + */ + +angular.element.prototype.focus = angular.element.prototype.focus || function() { + if (this.length) { + this[0].focus(); + } + return this; + }; +angular.element.prototype.blur = angular.element.prototype.blur || function() { + if (this.length) { + this[0].blur(); + } + return this; + }; + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.core.compiler + * @description + * Angular Material template and element compiler. + */ +MdCompilerService.$inject = ["$q", "$templateRequest", "$injector", "$compile", "$controller"]; +angular + .module('material.core') + .service('$mdCompiler', MdCompilerService); + +/** + * @ngdoc service + * @name $mdCompiler + * @module material.core.compiler + * @description + * The $mdCompiler service is an abstraction of Angular's compiler, that allows developers + * to easily compile an element with options like in a Directive Definition Object. + * + * > The compiler powers a lot of components inside of Angular Material. + * > Like the `$mdPanel` or `$mdDialog`. + * + * @usage + * + * Basic Usage with a template + * + * + * $mdCompiler.compile({ + * templateUrl: 'modal.html', + * controller: 'ModalCtrl', + * locals: { + * modal: myModalInstance; + * } + * }).then(function (compileData) { + * compileData.element; // Compiled DOM element + * compileData.link(myScope); // Instantiate controller and link element to scope. + * }); + * + * + * Example with a content element + * + * + * + * // Create a virtual element and link it manually. + * // The compiler doesn't need to recompile the element each time. + * var myElement = $compile('Test')(myScope); + * + * $mdCompiler.compile({ + * contentElement: myElement + * }).then(function (compileData) { + * compileData.element // Content Element (same as above) + * compileData.link // This does nothing when using a contentElement. + * }); + * + * + * > Content Element is a significant performance improvement when the developer already knows that the + * > compiled element will be always the same and the scope will not change either. + * + * The `contentElement` option also supports DOM elements which will be temporary removed and restored + * at its old position. + * + * + * var domElement = document.querySelector('#myElement'); + * + * $mdCompiler.compile({ + * contentElement: myElement + * }).then(function (compileData) { + * compileData.element // Content Element (same as above) + * compileData.link // This does nothing when using a contentElement. + * }); + * + * + * The `$mdCompiler` can also query for the element in the DOM itself. + * + * + * $mdCompiler.compile({ + * contentElement: '#myElement' + * }).then(function (compileData) { + * compileData.element // Content Element (same as above) + * compileData.link // This does nothing when using a contentElement. + * }); + * + * + */ +function MdCompilerService($q, $templateRequest, $injector, $compile, $controller) { + /** @private @const {!angular.$q} */ + this.$q = $q; + + /** @private @const {!angular.$templateRequest} */ + this.$templateRequest = $templateRequest; + + /** @private @const {!angular.$injector} */ + this.$injector = $injector; + + /** @private @const {!angular.$compile} */ + this.$compile = $compile; + + /** @private @const {!angular.$controller} */ + this.$controller = $controller; +} + +/** + * @ngdoc method + * @name $mdCompiler#compile + * @description + * + * A method to compile a HTML template with the Angular compiler. + * The `$mdCompiler` is wrapper around the Angular compiler and provides extra functionality + * like controller instantiation or async resolves. + * + * @param {!Object} options An options object, with the following properties: + * + * - `controller` - `{string|Function}` Controller fn that should be associated with + * newly created scope or the name of a registered controller if passed as a string. + * - `controllerAs` - `{string=}` A controller alias name. If present the controller will be + * published to scope under the `controllerAs` name. + * - `contentElement` - `{string|Element}`: Instead of using a template, which will be + * compiled each time, you can also use a DOM element.
+ * - `template` - `{string=}` An html template as a string. + * - `templateUrl` - `{string=}` A path to an html template. + * - `transformTemplate` - `{function(template)=}` A function which transforms the template after + * it is loaded. It will be given the template string as a parameter, and should + * return a a new string representing the transformed template. + * - `resolve` - `{Object.=}` - An optional map of dependencies which should + * be injected into the controller. If any of these dependencies are promises, the compiler + * will wait for them all to be resolved, or if one is rejected before the controller is + * instantiated `compile()` will fail.. + * * `key` - `{string}`: a name of a dependency to be injected into the controller. + * * `factory` - `{string|function}`: If `string` then it is an alias for a service. + * Otherwise if function, then it is injected and the return value is treated as the + * dependency. If the result is a promise, it is resolved before its value is + * injected into the controller. + * + * @returns {Object} promise A promise, which will be resolved with a `compileData` object. + * `compileData` has the following properties: + * + * - `element` - `{element}`: an uncompiled element matching the provided template. + * - `link` - `{function(scope)}`: A link function, which, when called, will compile + * the element and instantiate the provided controller (if given). + * - `locals` - `{object}`: The locals which will be passed into the controller once `link` is + * called. If `bindToController` is true, they will be coppied to the ctrl instead + * + */ +MdCompilerService.prototype.compile = function(options) { + + if (options.contentElement) { + return this._prepareContentElement(options); + } else { + return this._compileTemplate(options); + } + +}; + +/** + * Instead of compiling any template, the compiler just fetches an existing HTML element from the DOM and + * provides a restore function to put the element back it old DOM position. + * @param {!Object} options Options to be used for the compiler. + * @private + */ +MdCompilerService.prototype._prepareContentElement = function(options) { + + var contentElement = this._fetchContentElement(options); + + return this.$q.resolve({ + element: contentElement.element, + cleanup: contentElement.restore, + locals: {}, + link: function() { + return contentElement.element; + } + }); + +}; + +/** + * Compiles a template by considering all options and waiting for all resolves to be ready. + * @param {!Object} options Compile options + * @returns {!Object} Compile data with link function. + * @private + */ +MdCompilerService.prototype._compileTemplate = function(options) { + + var self = this; + var templateUrl = options.templateUrl; + var template = options.template || ''; + var resolve = angular.extend({}, options.resolve); + var locals = angular.extend({}, options.locals); + var transformTemplate = options.transformTemplate || angular.identity; + + // Take resolve values and invoke them. + // Resolves can either be a string (value: 'MyRegisteredAngularConst'), + // or an invokable 'factory' of sorts: (value: function ValueGetter($dependency) {}) + angular.forEach(resolve, function(value, key) { + if (angular.isString(value)) { + resolve[key] = self.$injector.get(value); + } else { + resolve[key] = self.$injector.invoke(value); + } + }); + + // Add the locals, which are just straight values to inject + // eg locals: { three: 3 }, will inject three into the controller + angular.extend(resolve, locals); + + if (templateUrl) { + resolve.$$ngTemplate = this.$templateRequest(templateUrl); + } else { + resolve.$$ngTemplate = this.$q.when(template); + } + + + // Wait for all the resolves to finish if they are promises + return this.$q.all(resolve).then(function(locals) { + + var template = transformTemplate(locals.$$ngTemplate, options); + var element = options.element || angular.element('
').html(template.trim()).contents(); + + return self._compileElement(locals, element, options); + }); + +}; + +/** + * Method to compile an element with the given options. + * @param {!Object} locals Locals to be injected to the controller if present + * @param {!JQLite} element Element to be compiled and linked + * @param {!Object} options Options to be used for linking. + * @returns {!Object} Compile data with link function. + * @private + */ +MdCompilerService.prototype._compileElement = function(locals, element, options) { + var self = this; + var ngLinkFn = this.$compile(element); + + var compileData = { + element: element, + cleanup: element.remove.bind(element), + locals: locals, + link: linkFn + }; + + function linkFn(scope) { + locals.$scope = scope; + + // Instantiate controller if the developer provided one. + if (options.controller) { + + var injectLocals = angular.extend(locals, { + $element: element + }); + + var invokeCtrl = self.$controller(options.controller, injectLocals, true, options.controllerAs); + + if (options.bindToController) { + angular.extend(invokeCtrl.instance, locals); + } + + var ctrl = invokeCtrl(); + + // Unique identifier for Angular Route ngView controllers. + element.data('$ngControllerController', ctrl); + element.children().data('$ngControllerController', ctrl); + + // Expose the instantiated controller to the compile data + compileData.controller = ctrl; + } + + // Invoke the Angular $compile link function. + return ngLinkFn(scope); + } + + return compileData; + +}; + +/** + * Fetches an element removing it from the DOM and using it temporary for the compiler. + * Elements which were fetched will be restored after use. + * @param {!Object} options Options to be used for the compilation. + * @returns {{element: !JQLite, restore: !Function}} + * @private + */ +MdCompilerService.prototype._fetchContentElement = function(options) { + + var contentEl = options.contentElement; + var restoreFn = null; + + if (angular.isString(contentEl)) { + contentEl = document.querySelector(contentEl); + restoreFn = createRestoreFn(contentEl); + } else { + contentEl = contentEl[0] || contentEl; + + // When the element is visible in the DOM, then we restore it at close of the dialog. + // Otherwise it will be removed from the DOM after close. + if (document.contains(contentEl)) { + restoreFn = createRestoreFn(contentEl); + } else { + restoreFn = function() { + if (contentEl.parentNode) { + contentEl.parentNode.removeChild(contentEl); + } + } + } + } + + return { + element: angular.element(contentEl), + restore: restoreFn + }; + + function createRestoreFn(element) { + var parent = element.parentNode; + var nextSibling = element.nextElementSibling; + + return function() { + if (!nextSibling) { + // When the element didn't had any sibling, then it can be simply appended to the + // parent, because it plays no role, which index it had before. + parent.appendChild(element); + } else { + // When the element had a sibling, which marks the previous position of the element + // in the DOM, we insert it correctly before the sibling, to have the same index as + // before. + parent.insertBefore(element, nextSibling); + } + } + } +}; + + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.core.aria + * @description + * Aria Expectations for ngMaterial components. + */ +MdAriaService.$inject = ["$$rAF", "$log", "$window", "$interpolate"]; +angular + .module('material.core') + .provider('$mdAria', MdAriaProvider); + +/** + * @ngdoc service + * @name $mdAriaProvider + * @module material.core.aria + * + * @description + * + * Modify options of the `$mdAria` service, which will be used by most of the Angular Material + * components. + * + * You are able to disable `$mdAria` warnings, by using the following markup. + * + * + * app.config(function($mdAriaProvider) { + * // Globally disables all ARIA warnings. + * $mdAriaProvider.disableWarnings(); + * }); + * + * + */ +function MdAriaProvider() { + + var config = { + /** Whether we should show ARIA warnings in the console if labels are missing on the element */ + showWarnings: true + }; + + return { + disableWarnings: disableWarnings, + $get: ["$$rAF", "$log", "$window", "$interpolate", function($$rAF, $log, $window, $interpolate) { + return MdAriaService.apply(config, arguments); + }] + }; + + /** + * @ngdoc method + * @name $mdAriaProvider#disableWarnings + * @description Disables all ARIA warnings generated by Angular Material. + */ + function disableWarnings() { + config.showWarnings = false; + } +} + +/* + * @ngInject + */ +function MdAriaService($$rAF, $log, $window, $interpolate) { + + // Load the showWarnings option from the current context and store it inside of a scope variable, + // because the context will be probably lost in some function calls. + var showWarnings = this.showWarnings; + + return { + expect: expect, + expectAsync: expectAsync, + expectWithText: expectWithText, + expectWithoutText: expectWithoutText, + getText: getText + }; + + /** + * Check if expected attribute has been specified on the target element or child + * @param element + * @param attrName + * @param {optional} defaultValue What to set the attr to if no value is found + */ + function expect(element, attrName, defaultValue) { + + var node = angular.element(element)[0] || element; + + // if node exists and neither it nor its children have the attribute + if (node && + ((!node.hasAttribute(attrName) || + node.getAttribute(attrName).length === 0) && + !childHasAttribute(node, attrName))) { + + defaultValue = angular.isString(defaultValue) ? defaultValue.trim() : ''; + if (defaultValue.length) { + element.attr(attrName, defaultValue); + } else if (showWarnings) { + $log.warn('ARIA: Attribute "', attrName, '", required for accessibility, is missing on node:', node); + } + + } + } + + function expectAsync(element, attrName, defaultValueGetter) { + // Problem: when retrieving the element's contents synchronously to find the label, + // the text may not be defined yet in the case of a binding. + // There is a higher chance that a binding will be defined if we wait one frame. + $$rAF(function() { + expect(element, attrName, defaultValueGetter()); + }); + } + + function expectWithText(element, attrName) { + var content = getText(element) || ""; + var hasBinding = content.indexOf($interpolate.startSymbol()) > -1; + + if (hasBinding) { + expectAsync(element, attrName, function() { + return getText(element); + }); + } else { + expect(element, attrName, content); + } + } + + function expectWithoutText(element, attrName) { + var content = getText(element); + var hasBinding = content.indexOf($interpolate.startSymbol()) > -1; + + if ( !hasBinding && !content) { + expect(element, attrName, content); + } + } + + function getText(element) { + element = element[0] || element; + var walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false); + var text = ''; + + var node; + while (node = walker.nextNode()) { + if (!isAriaHiddenNode(node)) { + text += node.textContent; + } + } + + return text.trim() || ''; + + function isAriaHiddenNode(node) { + while (node.parentNode && (node = node.parentNode) !== element) { + if (node.getAttribute && node.getAttribute('aria-hidden') === 'true') { + return true; + } + } + } + } + + function childHasAttribute(node, attrName) { + var hasChildren = node.hasChildNodes(), + hasAttr = false; + + function isHidden(el) { + var style = el.currentStyle ? el.currentStyle : $window.getComputedStyle(el); + return (style.display === 'none'); + } + + if (hasChildren) { + var children = node.childNodes; + for (var i=0; i < children.length; i++) { + var child = children[i]; + if (child.nodeType === 1 && child.hasAttribute(attrName)) { + if (!isHidden(child)) { + hasAttr = true; + } + } + } + } + + return hasAttr; + } +} + +})(); +(function(){ +"use strict"; + + +MdGesture.$inject = ["$$MdGestureHandler", "$$rAF", "$timeout"]; +attachToDocument.$inject = ["$mdGesture", "$$MdGestureHandler"];var HANDLERS = {}; + +/* The state of the current 'pointer' + * The pointer represents the state of the current touch. + * It contains normalized x and y coordinates from DOM events, + * as well as other information abstracted from the DOM. + */ + +var pointer, lastPointer, forceSkipClickHijack = false; + +/** + * The position of the most recent click if that click was on a label element. + * @type {{x: number, y: number}?} + */ +var lastLabelClickPos = null; + +// Used to attach event listeners once when multiple ng-apps are running. +var isInitialized = false; + +angular + .module('material.core.gestures', [ ]) + .provider('$mdGesture', MdGestureProvider) + .factory('$$MdGestureHandler', MdGestureHandler) + .run( attachToDocument ); + +/** + * @ngdoc service + * @name $mdGestureProvider + * @module material.core.gestures + * + * @description + * In some scenarios on Mobile devices (without jQuery), the click events should NOT be hijacked. + * `$mdGestureProvider` is used to configure the Gesture module to ignore or skip click hijacking on mobile + * devices. + * + * + * app.config(function($mdGestureProvider) { + * + * // For mobile devices without jQuery loaded, do not + * // intercept click events during the capture phase. + * $mdGestureProvider.skipClickHijack(); + * + * }); + * + * + */ +function MdGestureProvider() { } + +MdGestureProvider.prototype = { + + // Publish access to setter to configure a variable BEFORE the + // $mdGesture service is instantiated... + skipClickHijack: function() { + return forceSkipClickHijack = true; + }, + + /** + * $get is used to build an instance of $mdGesture + * @ngInject + */ + $get : ["$$MdGestureHandler", "$$rAF", "$timeout", function($$MdGestureHandler, $$rAF, $timeout) { + return new MdGesture($$MdGestureHandler, $$rAF, $timeout); + }] +}; + + + +/** + * MdGesture factory construction function + * @ngInject + */ +function MdGesture($$MdGestureHandler, $$rAF, $timeout) { + var userAgent = navigator.userAgent || navigator.vendor || window.opera; + var isIos = userAgent.match(/ipad|iphone|ipod/i); + var isAndroid = userAgent.match(/android/i); + var touchActionProperty = getTouchAction(); + var hasJQuery = (typeof window.jQuery !== 'undefined') && (angular.element === window.jQuery); + + var self = { + handler: addHandler, + register: register, + // On mobile w/out jQuery, we normally intercept clicks. Should we skip that? + isHijackingClicks: (isIos || isAndroid) && !hasJQuery && !forceSkipClickHijack + }; + + if (self.isHijackingClicks) { + var maxClickDistance = 6; + self.handler('click', { + options: { + maxDistance: maxClickDistance + }, + onEnd: checkDistanceAndEmit('click') + }); + + self.handler('focus', { + options: { + maxDistance: maxClickDistance + }, + onEnd: function(ev, pointer) { + if (pointer.distance < this.state.options.maxDistance) { + if (canFocus(ev.target)) { + this.dispatchEvent(ev, 'focus', pointer); + ev.target.focus(); + } + } + + function canFocus(element) { + var focusableElements = ['INPUT', 'SELECT', 'BUTTON', 'TEXTAREA', 'VIDEO', 'AUDIO']; + + return (element.getAttribute('tabindex') != '-1') && + !element.hasAttribute('DISABLED') && + (element.hasAttribute('tabindex') || element.hasAttribute('href') || element.isContentEditable || + (focusableElements.indexOf(element.nodeName) != -1)); + } + } + }); + + self.handler('mouseup', { + options: { + maxDistance: maxClickDistance + }, + onEnd: checkDistanceAndEmit('mouseup') + }); + + self.handler('mousedown', { + onStart: function(ev) { + this.dispatchEvent(ev, 'mousedown'); + } + }); + } + + function checkDistanceAndEmit(eventName) { + return function(ev, pointer) { + if (pointer.distance < this.state.options.maxDistance) { + this.dispatchEvent(ev, eventName, pointer); + } + }; + } + + /* + * Register an element to listen for a handler. + * This allows an element to override the default options for a handler. + * Additionally, some handlers like drag and hold only dispatch events if + * the domEvent happens inside an element that's registered to listen for these events. + * + * @see GestureHandler for how overriding of default options works. + * @example $mdGesture.register(myElement, 'drag', { minDistance: 20, horziontal: false }) + */ + function register(element, handlerName, options) { + var handler = HANDLERS[handlerName.replace(/^\$md./, '')]; + if (!handler) { + throw new Error('Failed to register element with handler ' + handlerName + '. ' + + 'Available handlers: ' + Object.keys(HANDLERS).join(', ')); + } + return handler.registerElement(element, options); + } + + /* + * add a handler to $mdGesture. see below. + */ + function addHandler(name, definition) { + var handler = new $$MdGestureHandler(name); + angular.extend(handler, definition); + HANDLERS[name] = handler; + + return self; + } + + /* + * Register handlers. These listen to touch/start/move events, interpret them, + * and dispatch gesture events depending on options & conditions. These are all + * instances of GestureHandler. + * @see GestureHandler + */ + return self + /* + * The press handler dispatches an event on touchdown/touchend. + * It's a simple abstraction of touch/mouse/pointer start and end. + */ + .handler('press', { + onStart: function (ev, pointer) { + this.dispatchEvent(ev, '$md.pressdown'); + }, + onEnd: function (ev, pointer) { + this.dispatchEvent(ev, '$md.pressup'); + } + }) + + /* + * The hold handler dispatches an event if the user keeps their finger within + * the same area for ms. + * The hold handler will only run if a parent of the touch target is registered + * to listen for hold events through $mdGesture.register() + */ + .handler('hold', { + options: { + maxDistance: 6, + delay: 500 + }, + onCancel: function () { + $timeout.cancel(this.state.timeout); + }, + onStart: function (ev, pointer) { + // For hold, require a parent to be registered with $mdGesture.register() + // Because we prevent scroll events, this is necessary. + if (!this.state.registeredParent) return this.cancel(); + + this.state.pos = {x: pointer.x, y: pointer.y}; + this.state.timeout = $timeout(angular.bind(this, function holdDelayFn() { + this.dispatchEvent(ev, '$md.hold'); + this.cancel(); //we're done! + }), this.state.options.delay, false); + }, + onMove: function (ev, pointer) { + // Don't scroll while waiting for hold. + // If we don't preventDefault touchmove events here, Android will assume we don't + // want to listen to anymore touch events. It will start scrolling and stop sending + // touchmove events. + if (!touchActionProperty && ev.type === 'touchmove') ev.preventDefault(); + + // If the user moves greater than pixels, stop the hold timer + // set in onStart + var dx = this.state.pos.x - pointer.x; + var dy = this.state.pos.y - pointer.y; + if (Math.sqrt(dx * dx + dy * dy) > this.options.maxDistance) { + this.cancel(); + } + }, + onEnd: function () { + this.onCancel(); + } + }) + + /* + * The drag handler dispatches a drag event if the user holds and moves his finger greater than + * px in the x or y direction, depending on options.horizontal. + * The drag will be cancelled if the user moves his finger greater than * in + * the perpendicular direction. Eg if the drag is horizontal and the user moves his finger * + * pixels vertically, this handler won't consider the move part of a drag. + */ + .handler('drag', { + options: { + minDistance: 6, + horizontal: true, + cancelMultiplier: 1.5 + }, + onSetup: function(element, options) { + if (touchActionProperty) { + // We check for horizontal to be false, because otherwise we would overwrite the default opts. + this.oldTouchAction = element[0].style[touchActionProperty]; + element[0].style[touchActionProperty] = options.horizontal ? 'pan-y' : 'pan-x'; + } + }, + onCleanup: function(element) { + if (this.oldTouchAction) { + element[0].style[touchActionProperty] = this.oldTouchAction; + } + }, + onStart: function (ev) { + // For drag, require a parent to be registered with $mdGesture.register() + if (!this.state.registeredParent) this.cancel(); + }, + onMove: function (ev, pointer) { + var shouldStartDrag, shouldCancel; + // Don't scroll while deciding if this touchmove qualifies as a drag event. + // If we don't preventDefault touchmove events here, Android will assume we don't + // want to listen to anymore touch events. It will start scrolling and stop sending + // touchmove events. + if (!touchActionProperty && ev.type === 'touchmove') ev.preventDefault(); + + if (!this.state.dragPointer) { + if (this.state.options.horizontal) { + shouldStartDrag = Math.abs(pointer.distanceX) > this.state.options.minDistance; + shouldCancel = Math.abs(pointer.distanceY) > this.state.options.minDistance * this.state.options.cancelMultiplier; + } else { + shouldStartDrag = Math.abs(pointer.distanceY) > this.state.options.minDistance; + shouldCancel = Math.abs(pointer.distanceX) > this.state.options.minDistance * this.state.options.cancelMultiplier; + } + + if (shouldStartDrag) { + // Create a new pointer representing this drag, starting at this point where the drag started. + this.state.dragPointer = makeStartPointer(ev); + updatePointerState(ev, this.state.dragPointer); + this.dispatchEvent(ev, '$md.dragstart', this.state.dragPointer); + + } else if (shouldCancel) { + this.cancel(); + } + } else { + this.dispatchDragMove(ev); + } + }, + // Only dispatch dragmove events every frame; any more is unnecessary + dispatchDragMove: $$rAF.throttle(function (ev) { + // Make sure the drag didn't stop while waiting for the next frame + if (this.state.isRunning) { + updatePointerState(ev, this.state.dragPointer); + this.dispatchEvent(ev, '$md.drag', this.state.dragPointer); + } + }), + onEnd: function (ev, pointer) { + if (this.state.dragPointer) { + updatePointerState(ev, this.state.dragPointer); + this.dispatchEvent(ev, '$md.dragend', this.state.dragPointer); + } + } + }) + + /* + * The swipe handler will dispatch a swipe event if, on the end of a touch, + * the velocity and distance were high enough. + */ + .handler('swipe', { + options: { + minVelocity: 0.65, + minDistance: 10 + }, + onEnd: function (ev, pointer) { + var eventType; + + if (Math.abs(pointer.velocityX) > this.state.options.minVelocity && + Math.abs(pointer.distanceX) > this.state.options.minDistance) { + eventType = pointer.directionX == 'left' ? '$md.swipeleft' : '$md.swiperight'; + this.dispatchEvent(ev, eventType); + } + else if (Math.abs(pointer.velocityY) > this.state.options.minVelocity && + Math.abs(pointer.distanceY) > this.state.options.minDistance) { + eventType = pointer.directionY == 'up' ? '$md.swipeup' : '$md.swipedown'; + this.dispatchEvent(ev, eventType); + } + } + }); + + function getTouchAction() { + var testEl = document.createElement('div'); + var vendorPrefixes = ['', 'webkit', 'Moz', 'MS', 'ms', 'o']; + + for (var i = 0; i < vendorPrefixes.length; i++) { + var prefix = vendorPrefixes[i]; + var property = prefix ? prefix + 'TouchAction' : 'touchAction'; + if (angular.isDefined(testEl.style[property])) { + return property; + } + } + } + +} + +/** + * MdGestureHandler + * A GestureHandler is an object which is able to dispatch custom dom events + * based on native dom {touch,pointer,mouse}{start,move,end} events. + * + * A gesture will manage its lifecycle through the start,move,end, and cancel + * functions, which are called by native dom events. + * + * A gesture has the concept of 'options' (eg a swipe's required velocity), which can be + * overridden by elements registering through $mdGesture.register() + */ +function GestureHandler (name) { + this.name = name; + this.state = {}; +} + +function MdGestureHandler() { + var hasJQuery = (typeof window.jQuery !== 'undefined') && (angular.element === window.jQuery); + + GestureHandler.prototype = { + options: {}, + // jQuery listeners don't work with custom DOMEvents, so we have to dispatch events + // differently when jQuery is loaded + dispatchEvent: hasJQuery ? jQueryDispatchEvent : nativeDispatchEvent, + + // These are overridden by the registered handler + onSetup: angular.noop, + onCleanup: angular.noop, + onStart: angular.noop, + onMove: angular.noop, + onEnd: angular.noop, + onCancel: angular.noop, + + // onStart sets up a new state for the handler, which includes options from the + // nearest registered parent element of ev.target. + start: function (ev, pointer) { + if (this.state.isRunning) return; + var parentTarget = this.getNearestParent(ev.target); + // Get the options from the nearest registered parent + var parentTargetOptions = parentTarget && parentTarget.$mdGesture[this.name] || {}; + + this.state = { + isRunning: true, + // Override the default options with the nearest registered parent's options + options: angular.extend({}, this.options, parentTargetOptions), + // Pass in the registered parent node to the state so the onStart listener can use + registeredParent: parentTarget + }; + this.onStart(ev, pointer); + }, + move: function (ev, pointer) { + if (!this.state.isRunning) return; + this.onMove(ev, pointer); + }, + end: function (ev, pointer) { + if (!this.state.isRunning) return; + this.onEnd(ev, pointer); + this.state.isRunning = false; + }, + cancel: function (ev, pointer) { + this.onCancel(ev, pointer); + this.state = {}; + }, + + // Find and return the nearest parent element that has been registered to + // listen for this handler via $mdGesture.register(element, 'handlerName'). + getNearestParent: function (node) { + var current = node; + while (current) { + if ((current.$mdGesture || {})[this.name]) { + return current; + } + current = current.parentNode; + } + return null; + }, + + // Called from $mdGesture.register when an element registers itself with a handler. + // Store the options the user gave on the DOMElement itself. These options will + // be retrieved with getNearestParent when the handler starts. + registerElement: function (element, options) { + var self = this; + element[0].$mdGesture = element[0].$mdGesture || {}; + element[0].$mdGesture[this.name] = options || {}; + element.on('$destroy', onDestroy); + + self.onSetup(element, options || {}); + + return onDestroy; + + function onDestroy() { + delete element[0].$mdGesture[self.name]; + element.off('$destroy', onDestroy); + + self.onCleanup(element, options || {}); + } + } + }; + + return GestureHandler; + + /* + * Dispatch an event with jQuery + * TODO: Make sure this sends bubbling events + * + * @param srcEvent the original DOM touch event that started this. + * @param eventType the name of the custom event to send (eg 'click' or '$md.drag') + * @param eventPointer the pointer object that matches this event. + */ + function jQueryDispatchEvent(srcEvent, eventType, eventPointer) { + eventPointer = eventPointer || pointer; + var eventObj = new angular.element.Event(eventType); + + eventObj.$material = true; + eventObj.pointer = eventPointer; + eventObj.srcEvent = srcEvent; + + angular.extend(eventObj, { + clientX: eventPointer.x, + clientY: eventPointer.y, + screenX: eventPointer.x, + screenY: eventPointer.y, + pageX: eventPointer.x, + pageY: eventPointer.y, + ctrlKey: srcEvent.ctrlKey, + altKey: srcEvent.altKey, + shiftKey: srcEvent.shiftKey, + metaKey: srcEvent.metaKey + }); + angular.element(eventPointer.target).trigger(eventObj); + } + + /* + * NOTE: nativeDispatchEvent is very performance sensitive. + * @param srcEvent the original DOM touch event that started this. + * @param eventType the name of the custom event to send (eg 'click' or '$md.drag') + * @param eventPointer the pointer object that matches this event. + */ + function nativeDispatchEvent(srcEvent, eventType, eventPointer) { + eventPointer = eventPointer || pointer; + var eventObj; + + if (eventType === 'click' || eventType == 'mouseup' || eventType == 'mousedown' ) { + eventObj = document.createEvent('MouseEvents'); + eventObj.initMouseEvent( + eventType, true, true, window, srcEvent.detail, + eventPointer.x, eventPointer.y, eventPointer.x, eventPointer.y, + srcEvent.ctrlKey, srcEvent.altKey, srcEvent.shiftKey, srcEvent.metaKey, + srcEvent.button, srcEvent.relatedTarget || null + ); + + } else { + eventObj = document.createEvent('CustomEvent'); + eventObj.initCustomEvent(eventType, true, true, {}); + } + eventObj.$material = true; + eventObj.pointer = eventPointer; + eventObj.srcEvent = srcEvent; + eventPointer.target.dispatchEvent(eventObj); + } + +} + +/** + * Attach Gestures: hook document and check shouldHijack clicks + * @ngInject + */ +function attachToDocument( $mdGesture, $$MdGestureHandler ) { + + // Polyfill document.contains for IE11. + // TODO: move to util + document.contains || (document.contains = function (node) { + return document.body.contains(node); + }); + + if (!isInitialized && $mdGesture.isHijackingClicks ) { + /* + * If hijack clicks is true, we preventDefault any click that wasn't + * sent by ngMaterial. This is because on older Android & iOS, a false, or 'ghost', + * click event will be sent ~400ms after a touchend event happens. + * The only way to know if this click is real is to prevent any normal + * click events, and add a flag to events sent by material so we know not to prevent those. + * + * Two exceptions to click events that should be prevented are: + * - click events sent by the keyboard (eg form submit) + * - events that originate from an Ionic app + */ + document.addEventListener('click' , clickHijacker , true); + document.addEventListener('mouseup' , mouseInputHijacker, true); + document.addEventListener('mousedown', mouseInputHijacker, true); + document.addEventListener('focus' , mouseInputHijacker, true); + + isInitialized = true; + } + + function mouseInputHijacker(ev) { + var isKeyClick = !ev.clientX && !ev.clientY; + if (!isKeyClick && !ev.$material && !ev.isIonicTap + && !isInputEventFromLabelClick(ev)) { + ev.preventDefault(); + ev.stopPropagation(); + } + } + + function clickHijacker(ev) { + var isKeyClick = ev.clientX === 0 && ev.clientY === 0; + if (!isKeyClick && !ev.$material && !ev.isIonicTap + && !isInputEventFromLabelClick(ev)) { + ev.preventDefault(); + ev.stopPropagation(); + lastLabelClickPos = null; + } else { + lastLabelClickPos = null; + if (ev.target.tagName.toLowerCase() == 'label') { + lastLabelClickPos = {x: ev.x, y: ev.y}; + } + } + } + + + // Listen to all events to cover all platforms. + var START_EVENTS = 'mousedown touchstart pointerdown'; + var MOVE_EVENTS = 'mousemove touchmove pointermove'; + var END_EVENTS = 'mouseup mouseleave touchend touchcancel pointerup pointercancel'; + + angular.element(document) + .on(START_EVENTS, gestureStart) + .on(MOVE_EVENTS, gestureMove) + .on(END_EVENTS, gestureEnd) + // For testing + .on('$$mdGestureReset', function gestureClearCache () { + lastPointer = pointer = null; + }); + + /* + * When a DOM event happens, run all registered gesture handlers' lifecycle + * methods which match the DOM event. + * Eg when a 'touchstart' event happens, runHandlers('start') will call and + * run `handler.cancel()` and `handler.start()` on all registered handlers. + */ + function runHandlers(handlerEvent, event) { + var handler; + for (var name in HANDLERS) { + handler = HANDLERS[name]; + if( handler instanceof $$MdGestureHandler ) { + + if (handlerEvent === 'start') { + // Run cancel to reset any handlers' state + handler.cancel(); + } + handler[handlerEvent](event, pointer); + + } + } + } + + /* + * gestureStart vets if a start event is legitimate (and not part of a 'ghost click' from iOS/Android) + * If it is legitimate, we initiate the pointer state and mark the current pointer's type + * For example, for a touchstart event, mark the current pointer as a 'touch' pointer, so mouse events + * won't effect it. + */ + function gestureStart(ev) { + // If we're already touched down, abort + if (pointer) return; + + var now = +Date.now(); + + // iOS & old android bug: after a touch event, a click event is sent 350 ms later. + // If <400ms have passed, don't allow an event of a different type than the previous event + if (lastPointer && !typesMatch(ev, lastPointer) && (now - lastPointer.endTime < 1500)) { + return; + } + + pointer = makeStartPointer(ev); + + runHandlers('start', ev); + } + /* + * If a move event happens of the right type, update the pointer and run all the move handlers. + * "of the right type": if a mousemove happens but our pointer started with a touch event, do nothing. + */ + function gestureMove(ev) { + if (!pointer || !typesMatch(ev, pointer)) return; + + updatePointerState(ev, pointer); + runHandlers('move', ev); + } + /* + * If an end event happens of the right type, update the pointer, run endHandlers, and save the pointer as 'lastPointer' + */ + function gestureEnd(ev) { + if (!pointer || !typesMatch(ev, pointer)) return; + + updatePointerState(ev, pointer); + pointer.endTime = +Date.now(); + + runHandlers('end', ev); + + lastPointer = pointer; + pointer = null; + } + +} + +// ******************** +// Module Functions +// ******************** + +/* + * Initiate the pointer. x, y, and the pointer's type. + */ +function makeStartPointer(ev) { + var point = getEventPoint(ev); + var startPointer = { + startTime: +Date.now(), + target: ev.target, + // 'p' for pointer events, 'm' for mouse, 't' for touch + type: ev.type.charAt(0) + }; + startPointer.startX = startPointer.x = point.pageX; + startPointer.startY = startPointer.y = point.pageY; + return startPointer; +} + +/* + * return whether the pointer's type matches the event's type. + * Eg if a touch event happens but the pointer has a mouse type, return false. + */ +function typesMatch(ev, pointer) { + return ev && pointer && ev.type.charAt(0) === pointer.type; +} + +/** + * Gets whether the given event is an input event that was caused by clicking on an + * associated label element. + * + * This is necessary because the browser will, upon clicking on a label element, fire an + * *extra* click event on its associated input (if any). mdGesture is able to flag the label + * click as with `$material` correctly, but not the second input click. + * + * In order to determine whether an input event is from a label click, we compare the (x, y) for + * the event to the (x, y) for the most recent label click (which is cleared whenever a non-label + * click occurs). Unfortunately, there are no event properties that tie the input and the label + * together (such as relatedTarget). + * + * @param {MouseEvent} event + * @returns {boolean} + */ +function isInputEventFromLabelClick(event) { + return lastLabelClickPos + && lastLabelClickPos.x == event.x + && lastLabelClickPos.y == event.y; +} + +/* + * Update the given pointer based upon the given DOMEvent. + * Distance, velocity, direction, duration, etc + */ +function updatePointerState(ev, pointer) { + var point = getEventPoint(ev); + var x = pointer.x = point.pageX; + var y = pointer.y = point.pageY; + + pointer.distanceX = x - pointer.startX; + pointer.distanceY = y - pointer.startY; + pointer.distance = Math.sqrt( + pointer.distanceX * pointer.distanceX + pointer.distanceY * pointer.distanceY + ); + + pointer.directionX = pointer.distanceX > 0 ? 'right' : pointer.distanceX < 0 ? 'left' : ''; + pointer.directionY = pointer.distanceY > 0 ? 'down' : pointer.distanceY < 0 ? 'up' : ''; + + pointer.duration = +Date.now() - pointer.startTime; + pointer.velocityX = pointer.distanceX / pointer.duration; + pointer.velocityY = pointer.distanceY / pointer.duration; +} + +/* + * Normalize the point where the DOM event happened whether it's touch or mouse. + * @returns point event obj with pageX and pageY on it. + */ +function getEventPoint(ev) { + ev = ev.originalEvent || ev; // support jQuery events + return (ev.touches && ev.touches[0]) || + (ev.changedTouches && ev.changedTouches[0]) || + ev; +} + +})(); +(function(){ +"use strict"; + +angular.module('material.core') + .provider('$$interimElement', InterimElementProvider); + +/* + * @ngdoc service + * @name $$interimElement + * @module material.core + * + * @description + * + * Factory that contructs `$$interimElement.$service` services. + * Used internally in material design for elements that appear on screen temporarily. + * The service provides a promise-like API for interacting with the temporary + * elements. + * + * ```js + * app.service('$mdToast', function($$interimElement) { + * var $mdToast = $$interimElement(toastDefaultOptions); + * return $mdToast; + * }); + * ``` + * @param {object=} defaultOptions Options used by default for the `show` method on the service. + * + * @returns {$$interimElement.$service} + * + */ + +function InterimElementProvider() { + InterimElementFactory.$inject = ["$document", "$q", "$rootScope", "$timeout", "$rootElement", "$animate", "$mdUtil", "$mdCompiler", "$mdTheming", "$injector", "$exceptionHandler"]; + createInterimElementProvider.$get = InterimElementFactory; + return createInterimElementProvider; + + /** + * Returns a new provider which allows configuration of a new interimElement + * service. Allows configuration of default options & methods for options, + * as well as configuration of 'preset' methods (eg dialog.basic(): basic is a preset method) + */ + function createInterimElementProvider(interimFactoryName) { + factory.$inject = ["$$interimElement", "$injector"]; + var EXPOSED_METHODS = ['onHide', 'onShow', 'onRemove']; + + var customMethods = {}; + var providerConfig = { + presets: {} + }; + + var provider = { + setDefaults: setDefaults, + addPreset: addPreset, + addMethod: addMethod, + $get: factory + }; + + /** + * all interim elements will come with the 'build' preset + */ + provider.addPreset('build', { + methods: ['controller', 'controllerAs', 'resolve', 'multiple', + 'template', 'templateUrl', 'themable', 'transformTemplate', 'parent', 'contentElement'] + }); + + return provider; + + /** + * Save the configured defaults to be used when the factory is instantiated + */ + function setDefaults(definition) { + providerConfig.optionsFactory = definition.options; + providerConfig.methods = (definition.methods || []).concat(EXPOSED_METHODS); + return provider; + } + + /** + * Add a method to the factory that isn't specific to any interim element operations + */ + + function addMethod(name, fn) { + customMethods[name] = fn; + return provider; + } + + /** + * Save the configured preset to be used when the factory is instantiated + */ + function addPreset(name, definition) { + definition = definition || {}; + definition.methods = definition.methods || []; + definition.options = definition.options || function() { return {}; }; + + if (/^cancel|hide|show$/.test(name)) { + throw new Error("Preset '" + name + "' in " + interimFactoryName + " is reserved!"); + } + if (definition.methods.indexOf('_options') > -1) { + throw new Error("Method '_options' in " + interimFactoryName + " is reserved!"); + } + providerConfig.presets[name] = { + methods: definition.methods.concat(EXPOSED_METHODS), + optionsFactory: definition.options, + argOption: definition.argOption + }; + return provider; + } + + function addPresetMethod(presetName, methodName, method) { + providerConfig.presets[presetName][methodName] = method; + } + + /** + * Create a factory that has the given methods & defaults implementing interimElement + */ + /* @ngInject */ + function factory($$interimElement, $injector) { + var defaultMethods; + var defaultOptions; + var interimElementService = $$interimElement(); + + /* + * publicService is what the developer will be using. + * It has methods hide(), cancel(), show(), build(), and any other + * presets which were set during the config phase. + */ + var publicService = { + hide: interimElementService.hide, + cancel: interimElementService.cancel, + show: showInterimElement, + + // Special internal method to destroy an interim element without animations + // used when navigation changes causes a $scope.$destroy() action + destroy : destroyInterimElement + }; + + + defaultMethods = providerConfig.methods || []; + // This must be invoked after the publicService is initialized + defaultOptions = invokeFactory(providerConfig.optionsFactory, {}); + + // Copy over the simple custom methods + angular.forEach(customMethods, function(fn, name) { + publicService[name] = fn; + }); + + angular.forEach(providerConfig.presets, function(definition, name) { + var presetDefaults = invokeFactory(definition.optionsFactory, {}); + var presetMethods = (definition.methods || []).concat(defaultMethods); + + // Every interimElement built with a preset has a field called `$type`, + // which matches the name of the preset. + // Eg in preset 'confirm', options.$type === 'confirm' + angular.extend(presetDefaults, { $type: name }); + + // This creates a preset class which has setter methods for every + // method given in the `.addPreset()` function, as well as every + // method given in the `.setDefaults()` function. + // + // @example + // .setDefaults({ + // methods: ['hasBackdrop', 'clickOutsideToClose', 'escapeToClose', 'targetEvent'], + // options: dialogDefaultOptions + // }) + // .addPreset('alert', { + // methods: ['title', 'ok'], + // options: alertDialogOptions + // }) + // + // Set values will be passed to the options when interimElement.show() is called. + function Preset(opts) { + this._options = angular.extend({}, presetDefaults, opts); + } + angular.forEach(presetMethods, function(name) { + Preset.prototype[name] = function(value) { + this._options[name] = value; + return this; + }; + }); + + // Create shortcut method for one-linear methods + if (definition.argOption) { + var methodName = 'show' + name.charAt(0).toUpperCase() + name.slice(1); + publicService[methodName] = function(arg) { + var config = publicService[name](arg); + return publicService.show(config); + }; + } + + // eg $mdDialog.alert() will return a new alert preset + publicService[name] = function(arg) { + // If argOption is supplied, eg `argOption: 'content'`, then we assume + // if the argument is not an options object then it is the `argOption` option. + // + // @example `$mdToast.simple('hello')` // sets options.content to hello + // // because argOption === 'content' + if (arguments.length && definition.argOption && + !angular.isObject(arg) && !angular.isArray(arg)) { + + return (new Preset())[definition.argOption](arg); + + } else { + return new Preset(arg); + } + + }; + }); + + return publicService; + + /** + * + */ + function showInterimElement(opts) { + // opts is either a preset which stores its options on an _options field, + // or just an object made up of options + opts = opts || { }; + if (opts._options) opts = opts._options; + + return interimElementService.show( + angular.extend({}, defaultOptions, opts) + ); + } + + /** + * Special method to hide and destroy an interimElement WITHOUT + * any 'leave` or hide animations ( an immediate force hide/remove ) + * + * NOTE: This calls the onRemove() subclass method for each component... + * which must have code to respond to `options.$destroy == true` + */ + function destroyInterimElement(opts) { + return interimElementService.destroy(opts); + } + + /** + * Helper to call $injector.invoke with a local of the factory name for + * this provider. + * If an $mdDialog is providing options for a dialog and tries to inject + * $mdDialog, a circular dependency error will happen. + * We get around that by manually injecting $mdDialog as a local. + */ + function invokeFactory(factory, defaultVal) { + var locals = {}; + locals[interimFactoryName] = publicService; + return $injector.invoke(factory || function() { return defaultVal; }, {}, locals); + } + + } + + } + + /* @ngInject */ + function InterimElementFactory($document, $q, $rootScope, $timeout, $rootElement, $animate, + $mdUtil, $mdCompiler, $mdTheming, $injector, $exceptionHandler) { + return function createInterimElementService() { + var SHOW_CANCELLED = false; + + /* + * @ngdoc service + * @name $$interimElement.$service + * + * @description + * A service used to control inserting and removing an element into the DOM. + * + */ + + var service; + + var showPromises = []; // Promises for the interim's which are currently opening. + var hidePromises = []; // Promises for the interim's which are currently hiding. + var showingInterims = []; // Interim elements which are currently showing up. + + // Publish instance $$interimElement service; + // ... used as $mdDialog, $mdToast, $mdMenu, and $mdSelect + + return service = { + show: show, + hide: waitForInterim(hide), + cancel: waitForInterim(cancel), + destroy : destroy, + $injector_: $injector + }; + + /* + * @ngdoc method + * @name $$interimElement.$service#show + * @kind function + * + * @description + * Adds the `$interimElement` to the DOM and returns a special promise that will be resolved or rejected + * with hide or cancel, respectively. To external cancel/hide, developers should use the + * + * @param {*} options is hashMap of settings + * @returns a Promise + * + */ + function show(options) { + options = options || {}; + var interimElement = new InterimElement(options || {}); + + // When an interim element is currently showing, we have to cancel it. + // Just hiding it, will resolve the InterimElement's promise, the promise should be + // rejected instead. + var hideAction = options.multiple ? $q.resolve() : $q.all(showPromises); + + if (!options.multiple) { + // Wait for all opening interim's to finish their transition. + hideAction = hideAction.then(function() { + // Wait for all closing and showing interim's to be completely closed. + var promiseArray = hidePromises.concat(showingInterims.map(service.cancel)); + return $q.all(promiseArray); + }); + } + + var showAction = hideAction.then(function() { + + return interimElement + .show() + .catch(function(reason) { return reason; }) + .finally(function() { + showPromises.splice(showPromises.indexOf(showAction), 1); + showingInterims.push(interimElement); + }); + + }); + + showPromises.push(showAction); + + // In Angular 1.6+, exceptions inside promises will cause a rejection. We need to handle + // the rejection and only log it if it's an error. + interimElement.deferred.promise.catch(function(fault) { + if (fault instanceof Error) { + $exceptionHandler(fault); + } + + return fault; + }); + + // Return a promise that will be resolved when the interim + // element is hidden or cancelled... + return interimElement.deferred.promise; + } + + /* + * @ngdoc method + * @name $$interimElement.$service#hide + * @kind function + * + * @description + * Removes the `$interimElement` from the DOM and resolves the promise returned from `show` + * + * @param {*} resolveParam Data to resolve the promise with + * @returns a Promise that will be resolved after the element has been removed. + * + */ + function hide(reason, options) { + options = options || {}; + + if (options.closeAll) { + // We have to make a shallow copy of the array, because otherwise the map will break. + return $q.all(showingInterims.slice().reverse().map(closeElement)); + } else if (options.closeTo !== undefined) { + return $q.all(showingInterims.slice(options.closeTo).map(closeElement)); + } + + // Hide the latest showing interim element. + return closeElement(showingInterims[showingInterims.length - 1]); + + function closeElement(interim) { + + var hideAction = interim + .remove(reason, false, options || { }) + .catch(function(reason) { return reason; }) + .finally(function() { + hidePromises.splice(hidePromises.indexOf(hideAction), 1); + }); + + showingInterims.splice(showingInterims.indexOf(interim), 1); + hidePromises.push(hideAction); + + return interim.deferred.promise; + } + } + + /* + * @ngdoc method + * @name $$interimElement.$service#cancel + * @kind function + * + * @description + * Removes the `$interimElement` from the DOM and rejects the promise returned from `show` + * + * @param {*} reason Data to reject the promise with + * @returns Promise that will be resolved after the element has been removed. + * + */ + function cancel(reason, options) { + var interim = showingInterims.pop(); + if (!interim) { + return $q.when(reason); + } + + var cancelAction = interim + .remove(reason, true, options || {}) + .catch(function(reason) { return reason; }) + .finally(function() { + hidePromises.splice(hidePromises.indexOf(cancelAction), 1); + }); + + hidePromises.push(cancelAction); + + // Since Angular 1.6.7, promises will be logged to $exceptionHandler when the promise + // is not handling the rejection. We create a pseudo catch handler, which will prevent the + // promise from being logged to the $exceptionHandler. + return interim.deferred.promise.catch(angular.noop); + } + + /** + * Creates a function to wait for at least one interim element to be available. + * @param callbackFn Function to be used as callback + * @returns {Function} + */ + function waitForInterim(callbackFn) { + return function() { + var fnArguments = arguments; + + if (!showingInterims.length) { + // When there are still interim's opening, then wait for the first interim element to + // finish its open animation. + if (showPromises.length) { + return showPromises[0].finally(function () { + return callbackFn.apply(service, fnArguments); + }); + } + + return $q.when("No interim elements currently showing up."); + } + + return callbackFn.apply(service, fnArguments); + }; + } + + /* + * Special method to quick-remove the interim element without animations + * Note: interim elements are in "interim containers" + */ + function destroy(targetEl) { + var interim = !targetEl ? showingInterims.shift() : null; + + var parentEl = angular.element(targetEl).length && angular.element(targetEl)[0].parentNode; + + if (parentEl) { + // Try to find the interim in the stack which corresponds to the supplied DOM element. + var filtered = showingInterims.filter(function(entry) { + return entry.options.element[0] === parentEl; + }); + + // Note: This function might be called when the element already has been removed, + // in which case we won't find any matches. + if (filtered.length) { + interim = filtered[0]; + showingInterims.splice(showingInterims.indexOf(interim), 1); + } + } + + return interim ? interim.remove(SHOW_CANCELLED, false, { '$destroy': true }) : + $q.when(SHOW_CANCELLED); + } + + /* + * Internal Interim Element Object + * Used internally to manage the DOM element and related data + */ + function InterimElement(options) { + var self, element, showAction = $q.when(true); + + options = configureScopeAndTransitions(options); + + return self = { + options : options, + deferred: $q.defer(), + show : createAndTransitionIn, + remove : transitionOutAndRemove + }; + + /** + * Compile, link, and show this interim element + * Use optional autoHided and transition-in effects + */ + function createAndTransitionIn() { + return $q(function(resolve, reject) { + + // Trigger onCompiling callback before the compilation starts. + // This is useful, when modifying options, which can be influenced by developers. + options.onCompiling && options.onCompiling(options); + + compileElement(options) + .then(function( compiledData ) { + element = linkElement( compiledData, options ); + + // Expose the cleanup function from the compiler. + options.cleanupElement = compiledData.cleanup; + + showAction = showElement(element, options, compiledData.controller) + .then(resolve, rejectAll); + }).catch(rejectAll); + + function rejectAll(fault) { + // Force the '$md.show()' promise to reject + self.deferred.reject(fault); + + // Continue rejection propagation + reject(fault); + } + }); + } + + /** + * After the show process has finished/rejected: + * - announce 'removing', + * - perform the transition-out, and + * - perform optional clean up scope. + */ + function transitionOutAndRemove(response, isCancelled, opts) { + + // abort if the show() and compile failed + if ( !element ) return $q.when(false); + + options = angular.extend(options || {}, opts || {}); + options.cancelAutoHide && options.cancelAutoHide(); + options.element.triggerHandler('$mdInterimElementRemove'); + + if ( options.$destroy === true ) { + + return hideElement(options.element, options).then(function(){ + (isCancelled && rejectAll(response)) || resolveAll(response); + }); + + } else { + $q.when(showAction).finally(function() { + hideElement(options.element, options).then(function() { + isCancelled ? rejectAll(response) : resolveAll(response); + }, rejectAll); + }); + + return self.deferred.promise; + } + + + /** + * The `show()` returns a promise that will be resolved when the interim + * element is hidden or cancelled... + */ + function resolveAll(response) { + self.deferred.resolve(response); + } + + /** + * Force the '$md.show()' promise to reject + */ + function rejectAll(fault) { + self.deferred.reject(fault); + } + } + + /** + * Prepare optional isolated scope and prepare $animate with default enter and leave + * transitions for the new element instance. + */ + function configureScopeAndTransitions(options) { + options = options || { }; + if ( options.template ) { + options.template = $mdUtil.processTemplate(options.template); + } + + return angular.extend({ + preserveScope: false, + cancelAutoHide : angular.noop, + scope: options.scope || $rootScope.$new(options.isolateScope), + + /** + * Default usage to enable $animate to transition-in; can be easily overridden via 'options' + */ + onShow: function transitionIn(scope, element, options) { + return $animate.enter(element, options.parent); + }, + + /** + * Default usage to enable $animate to transition-out; can be easily overridden via 'options' + */ + onRemove: function transitionOut(scope, element) { + // Element could be undefined if a new element is shown before + // the old one finishes compiling. + return element && $animate.leave(element) || $q.when(); + } + }, options ); + + } + + /** + * Compile an element with a templateUrl, controller, and locals + */ + function compileElement(options) { + + var compiled = !options.skipCompile ? $mdCompiler.compile(options) : null; + + return compiled || $q(function (resolve) { + resolve({ + locals: {}, + link: function () { + return options.element; + } + }); + }); + } + + /** + * Link an element with compiled configuration + */ + function linkElement(compileData, options){ + angular.extend(compileData.locals, options); + + var element = compileData.link(options.scope); + + // Search for parent at insertion time, if not specified + options.element = element; + options.parent = findParent(element, options); + if (options.themable) $mdTheming(element); + + return element; + } + + /** + * Search for parent at insertion time, if not specified + */ + function findParent(element, options) { + var parent = options.parent; + + // Search for parent at insertion time, if not specified + if (angular.isFunction(parent)) { + parent = parent(options.scope, element, options); + } else if (angular.isString(parent)) { + parent = angular.element($document[0].querySelector(parent)); + } else { + parent = angular.element(parent); + } + + // If parent querySelector/getter function fails, or it's just null, + // find a default. + if (!(parent || {}).length) { + var el; + if ($rootElement[0] && $rootElement[0].querySelector) { + el = $rootElement[0].querySelector(':not(svg) > body'); + } + if (!el) el = $rootElement[0]; + if (el.nodeName == '#comment') { + el = $document[0].body; + } + return angular.element(el); + } + + return parent; + } + + /** + * If auto-hide is enabled, start timer and prepare cancel function + */ + function startAutoHide() { + var autoHideTimer, cancelAutoHide = angular.noop; + + if (options.hideDelay) { + autoHideTimer = $timeout(service.hide, options.hideDelay) ; + cancelAutoHide = function() { + $timeout.cancel(autoHideTimer); + }; + } + + // Cache for subsequent use + options.cancelAutoHide = function() { + cancelAutoHide(); + options.cancelAutoHide = undefined; + }; + } + + /** + * Show the element ( with transitions), notify complete and start + * optional auto-Hide + */ + function showElement(element, options, controller) { + // Trigger onShowing callback before the `show()` starts + var notifyShowing = options.onShowing || angular.noop; + // Trigger onComplete callback when the `show()` finishes + var notifyComplete = options.onComplete || angular.noop; + + // Necessary for consistency between Angular 1.5 and 1.6. + try { + notifyShowing(options.scope, element, options, controller); + } catch (e) { + return $q.reject(e); + } + + return $q(function (resolve, reject) { + try { + // Start transitionIn + $q.when(options.onShow(options.scope, element, options, controller)) + .then(function () { + notifyComplete(options.scope, element, options); + startAutoHide(); + + resolve(element); + }, reject); + + } catch (e) { + reject(e.message); + } + }); + } + + function hideElement(element, options) { + var announceRemoving = options.onRemoving || angular.noop; + + return $q(function (resolve, reject) { + try { + // Start transitionIn + var action = $q.when( options.onRemove(options.scope, element, options) || true ); + + // Trigger callback *before* the remove operation starts + announceRemoving(element, action); + + if (options.$destroy) { + // For $destroy, onRemove should be synchronous + resolve(element); + + if (!options.preserveScope && options.scope ) { + // scope destroy should still be be done after the current digest is done + action.then( function() { options.scope.$destroy(); }); + } + } else { + // Wait until transition-out is done + action.then(function () { + if (!options.preserveScope && options.scope ) { + options.scope.$destroy(); + } + + resolve(element); + }, reject); + } + } catch (e) { + reject(e.message); + } + }); + } + + } + }; + + } + +} + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.core.interaction + * @description + * User interaction detection to provide proper accessibility. + */ +MdInteractionService.$inject = ["$timeout", "$mdUtil"]; +angular + .module('material.core.interaction', []) + .service('$mdInteraction', MdInteractionService); + + +/** + * @ngdoc service + * @name $mdInteraction + * @module material.core.interaction + * + * @description + * + * Service which keeps track of the last interaction type and validates them for several browsers. + * The service hooks into the document's body and listens for touch, mouse and keyboard events. + * + * The most recent interaction type can be retrieved by calling the `getLastInteractionType` method. + * + * Here is an example markup for using the interaction service. + * + * + * var lastType = $mdInteraction.getLastInteractionType(); + * + * if (lastType === 'keyboard') { + * // We only restore the focus for keyboard users. + * restoreFocus(); + * } + * + * + */ +function MdInteractionService($timeout, $mdUtil) { + this.$timeout = $timeout; + this.$mdUtil = $mdUtil; + + this.bodyElement = angular.element(document.body); + this.isBuffering = false; + this.bufferTimeout = null; + this.lastInteractionType = null; + this.lastInteractionTime = null; + + // Type Mappings for the different events + // There will be three three interaction types + // `keyboard`, `mouse` and `touch` + // type `pointer` will be evaluated in `pointerMap` for IE Browser events + this.inputEventMap = { + 'keydown': 'keyboard', + 'mousedown': 'mouse', + 'mouseenter': 'mouse', + 'touchstart': 'touch', + 'pointerdown': 'pointer', + 'MSPointerDown': 'pointer' + }; + + // IE PointerDown events will be validated in `touch` or `mouse` + // Index numbers referenced here: https://msdn.microsoft.com/library/windows/apps/hh466130.aspx + this.iePointerMap = { + 2: 'touch', + 3: 'touch', + 4: 'mouse' + }; + + this.initializeEvents(); +} + +/** + * Initializes the interaction service, by registering all interaction events to the + * body element. + */ +MdInteractionService.prototype.initializeEvents = function() { + // IE browsers can also trigger pointer events, which also leads to an interaction. + var pointerEvent = 'MSPointerEvent' in window ? 'MSPointerDown' : 'PointerEvent' in window ? 'pointerdown' : null; + + this.bodyElement.on('keydown mousedown', this.onInputEvent.bind(this)); + + if ('ontouchstart' in document.documentElement) { + this.bodyElement.on('touchstart', this.onBufferInputEvent.bind(this)); + } + + if (pointerEvent) { + this.bodyElement.on(pointerEvent, this.onInputEvent.bind(this)); + } + +}; + +/** + * Event listener for normal interaction events, which should be tracked. + * @param event {MouseEvent|KeyboardEvent|PointerEvent|TouchEvent} + */ +MdInteractionService.prototype.onInputEvent = function(event) { + if (this.isBuffering) { + return; + } + + var type = this.inputEventMap[event.type]; + + if (type === 'pointer') { + type = this.iePointerMap[event.pointerType] || event.pointerType; + } + + this.lastInteractionType = type; + this.lastInteractionTime = this.$mdUtil.now(); +}; + +/** + * Event listener for interaction events which should be buffered (touch events). + * @param event {TouchEvent} + */ +MdInteractionService.prototype.onBufferInputEvent = function(event) { + this.$timeout.cancel(this.bufferTimeout); + + this.onInputEvent(event); + this.isBuffering = true; + + // The timeout of 650ms is needed to delay the touchstart, because otherwise the touch will call + // the `onInput` function multiple times. + this.bufferTimeout = this.$timeout(function() { + this.isBuffering = false; + }.bind(this), 650, false); + +}; + +/** + * @ngdoc method + * @name $mdInteraction#getLastInteractionType + * @description Retrieves the last interaction type triggered in body. + * @returns {string|null} Last interaction type. + */ +MdInteractionService.prototype.getLastInteractionType = function() { + return this.lastInteractionType; +}; + +/** + * @ngdoc method + * @name $mdInteraction#isUserInvoked + * @description Method to detect whether any interaction happened recently or not. + * @param {number=} checkDelay Time to check for any interaction to have been triggered. + * @returns {boolean} Whether there was any interaction or not. + */ +MdInteractionService.prototype.isUserInvoked = function(checkDelay) { + var delay = angular.isNumber(checkDelay) ? checkDelay : 15; + + // Check for any interaction to be within the specified check time. + return this.lastInteractionTime >= this.$mdUtil.now() - delay; +}; + +})(); +(function(){ +"use strict"; + +(function() { + 'use strict'; + + var $mdUtil, $interpolate, $log; + + var SUFFIXES = /(-gt)?-(sm|md|lg|print)/g; + var WHITESPACE = /\s+/g; + + var FLEX_OPTIONS = ['grow', 'initial', 'auto', 'none', 'noshrink', 'nogrow' ]; + var LAYOUT_OPTIONS = ['row', 'column']; + var ALIGNMENT_MAIN_AXIS= [ "", "start", "center", "end", "stretch", "space-around", "space-between" ]; + var ALIGNMENT_CROSS_AXIS= [ "", "start", "center", "end", "stretch" ]; + + var config = { + /** + * Enable directive attribute-to-class conversions + * Developers can use `` to quickly + * disable the Layout directives and prohibit the injection of Layout classNames + */ + enabled: true, + + /** + * List of mediaQuery breakpoints and associated suffixes + * + * [ + * { suffix: "sm", mediaQuery: "screen and (max-width: 599px)" }, + * { suffix: "md", mediaQuery: "screen and (min-width: 600px) and (max-width: 959px)" } + * ] + */ + breakpoints: [] + }; + + registerLayoutAPI( angular.module('material.core.layout', ['ng']) ); + + /** + * registerLayoutAPI() + * + * The original ngMaterial Layout solution used attribute selectors and CSS. + * + * ```html + *
My Content
+ * ``` + * + * ```css + * [layout] { + * box-sizing: border-box; + * display:flex; + * } + * [layout=column] { + * flex-direction : column + * } + * ``` + * + * Use of attribute selectors creates significant performance impacts in some + * browsers... mainly IE. + * + * This module registers directives that allow the same layout attributes to be + * interpreted and converted to class selectors. The directive will add equivalent classes to each element that + * contains a Layout directive. + * + * ```html + *
My Content
+ *``` + * + * ```css + * .layout { + * box-sizing: border-box; + * display:flex; + * } + * .layout-column { + * flex-direction : column + * } + * ``` + */ + function registerLayoutAPI(module){ + var PREFIX_REGEXP = /^((?:x|data)[\:\-_])/i; + var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g; + + // NOTE: these are also defined in constants::MEDIA_PRIORITY and constants::MEDIA + var BREAKPOINTS = [ "", "xs", "gt-xs", "sm", "gt-sm", "md", "gt-md", "lg", "gt-lg", "xl", "print" ]; + var API_WITH_VALUES = [ "layout", "flex", "flex-order", "flex-offset", "layout-align" ]; + var API_NO_VALUES = [ "show", "hide", "layout-padding", "layout-margin" ]; + + + // Build directive registration functions for the standard Layout API... for all breakpoints. + angular.forEach(BREAKPOINTS, function(mqb) { + + // Attribute directives with expected, observable value(s) + angular.forEach( API_WITH_VALUES, function(name){ + var fullName = mqb ? name + "-" + mqb : name; + module.directive( directiveNormalize(fullName), attributeWithObserve(fullName)); + }); + + // Attribute directives with no expected value(s) + angular.forEach( API_NO_VALUES, function(name){ + var fullName = mqb ? name + "-" + mqb : name; + module.directive( directiveNormalize(fullName), attributeWithoutValue(fullName)); + }); + + }); + + // Register other, special directive functions for the Layout features: + module + + .provider('$$mdLayout' , function() { + // Publish internal service for Layouts + return { + $get : angular.noop, + validateAttributeValue : validateAttributeValue, + validateAttributeUsage : validateAttributeUsage, + /** + * Easy way to disable/enable the Layout API. + * When disabled, this stops all attribute-to-classname generations + */ + disableLayouts : function(isDisabled) { + config.enabled = (isDisabled !== true); + } + }; + }) + + .directive('mdLayoutCss' , disableLayoutDirective ) + .directive('ngCloak' , buildCloakInterceptor('ng-cloak')) + + .directive('layoutWrap' , attributeWithoutValue('layout-wrap')) + .directive('layoutNowrap' , attributeWithoutValue('layout-nowrap')) + .directive('layoutNoWrap' , attributeWithoutValue('layout-no-wrap')) + .directive('layoutFill' , attributeWithoutValue('layout-fill')) + + // !! Deprecated attributes: use the `-lt` (aka less-than) notations + + .directive('layoutLtMd' , warnAttrNotSupported('layout-lt-md', true)) + .directive('layoutLtLg' , warnAttrNotSupported('layout-lt-lg', true)) + .directive('flexLtMd' , warnAttrNotSupported('flex-lt-md', true)) + .directive('flexLtLg' , warnAttrNotSupported('flex-lt-lg', true)) + + .directive('layoutAlignLtMd', warnAttrNotSupported('layout-align-lt-md')) + .directive('layoutAlignLtLg', warnAttrNotSupported('layout-align-lt-lg')) + .directive('flexOrderLtMd' , warnAttrNotSupported('flex-order-lt-md')) + .directive('flexOrderLtLg' , warnAttrNotSupported('flex-order-lt-lg')) + .directive('offsetLtMd' , warnAttrNotSupported('flex-offset-lt-md')) + .directive('offsetLtLg' , warnAttrNotSupported('flex-offset-lt-lg')) + + .directive('hideLtMd' , warnAttrNotSupported('hide-lt-md')) + .directive('hideLtLg' , warnAttrNotSupported('hide-lt-lg')) + .directive('showLtMd' , warnAttrNotSupported('show-lt-md')) + .directive('showLtLg' , warnAttrNotSupported('show-lt-lg')) + + // Determine if + .config( detectDisabledLayouts ); + + /** + * Converts snake_case to camelCase. + * Also there is special case for Moz prefix starting with upper case letter. + * @param name Name to normalize + */ + function directiveNormalize(name) { + return name + .replace(PREFIX_REGEXP, '') + .replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) { + return offset ? letter.toUpperCase() : letter; + }); + } + + } + + + /** + * Detect if any of the HTML tags has a [md-layouts-disabled] attribute; + * If yes, then immediately disable all layout API features + * + * Note: this attribute should be specified on either the HTML or BODY tags + */ + /** + * @ngInject + */ + function detectDisabledLayouts() { + var isDisabled = !!document.querySelector('[md-layouts-disabled]'); + config.enabled = !isDisabled; + } + + /** + * Special directive that will disable ALL Layout conversions of layout + * attribute(s) to classname(s). + * + * + * + * + * + * ... + * + * + * Note: Using md-layout-css directive requires the developer to load the Material + * Layout Attribute stylesheet (which only uses attribute selectors): + * + * `angular-material.layout.css` + * + * Another option is to use the LayoutProvider to configure and disable the attribute + * conversions; this would obviate the use of the `md-layout-css` directive + * + */ + function disableLayoutDirective() { + // Return a 1x-only, first-match attribute directive + config.enabled = false; + + return { + restrict : 'A', + priority : '900' + }; + } + + /** + * Tail-hook ngCloak to delay the uncloaking while Layout transformers + * finish processing. Eliminates flicker with Material.Layouts + */ + function buildCloakInterceptor(className) { + return [ '$timeout', function($timeout){ + return { + restrict : 'A', + priority : -10, // run after normal ng-cloak + compile : function( element ) { + if (!config.enabled) return angular.noop; + + // Re-add the cloak + element.addClass(className); + + return function( scope, element ) { + // Wait while layout injectors configure, then uncloak + // NOTE: $rAF does not delay enough... and this is a 1x-only event, + // $timeout is acceptable. + $timeout( function(){ + element.removeClass(className); + }, 10, false); + }; + } + }; + }]; + } + + + // ********************************************************************************* + // + // These functions create registration functions for ngMaterial Layout attribute directives + // This provides easy translation to switch ngMaterial attribute selectors to + // CLASS selectors and directives; which has huge performance implications + // for IE Browsers + // + // ********************************************************************************* + + /** + * Creates a directive registration function where a possible dynamic attribute + * value will be observed/watched. + * @param {string} className attribute name; eg `layout-gt-md` with value ="row" + */ + function attributeWithObserve(className) { + + return ['$mdUtil', '$interpolate', "$log", function(_$mdUtil_, _$interpolate_, _$log_) { + $mdUtil = _$mdUtil_; + $interpolate = _$interpolate_; + $log = _$log_; + + return { + restrict: 'A', + compile: function(element, attr) { + var linkFn; + if (config.enabled) { + // immediately replace static (non-interpolated) invalid values... + + validateAttributeUsage(className, attr, element, $log); + + validateAttributeValue( className, + getNormalizedAttrValue(className, attr, ""), + buildUpdateFn(element, className, attr) + ); + + linkFn = translateWithValueToCssClass; + } + + // Use for postLink to account for transforms after ng-transclude. + return linkFn || angular.noop; + } + }; + }]; + + /** + * Add as transformed class selector(s), then + * remove the deprecated attribute selector + */ + function translateWithValueToCssClass(scope, element, attrs) { + var updateFn = updateClassWithValue(element, className, attrs); + var unwatch = attrs.$observe(attrs.$normalize(className), updateFn); + + updateFn(getNormalizedAttrValue(className, attrs, "")); + scope.$on("$destroy", function() { unwatch(); }); + } + } + + /** + * Creates a registration function for ngMaterial Layout attribute directive. + * This is a `simple` transpose of attribute usage to class usage; where we ignore + * any attribute value + */ + function attributeWithoutValue(className) { + return ['$mdUtil', '$interpolate', "$log", function(_$mdUtil_, _$interpolate_, _$log_) { + $mdUtil = _$mdUtil_; + $interpolate = _$interpolate_; + $log = _$log_; + + return { + restrict: 'A', + compile: function(element, attr) { + var linkFn; + if (config.enabled) { + // immediately replace static (non-interpolated) invalid values... + + validateAttributeValue( className, + getNormalizedAttrValue(className, attr, ""), + buildUpdateFn(element, className, attr) + ); + + translateToCssClass(null, element); + + // Use for postLink to account for transforms after ng-transclude. + linkFn = translateToCssClass; + } + + return linkFn || angular.noop; + } + }; + }]; + + /** + * Add as transformed class selector, then + * remove the deprecated attribute selector + */ + function translateToCssClass(scope, element) { + element.addClass(className); + } + } + + + + /** + * After link-phase, do NOT remove deprecated layout attribute selector. + * Instead watch the attribute so interpolated data-bindings to layout + * selectors will continue to be supported. + * + * $observe() the className and update with new class (after removing the last one) + * + * e.g. `layout="{{layoutDemo.direction}}"` will update... + * + * NOTE: The value must match one of the specified styles in the CSS. + * For example `flex-gt-md="{{size}}` where `scope.size == 47` will NOT work since + * only breakpoints for 0, 5, 10, 15... 100, 33, 34, 66, 67 are defined. + * + */ + function updateClassWithValue(element, className) { + var lastClass; + + return function updateClassFn(newValue) { + var value = validateAttributeValue(className, newValue || ""); + if ( angular.isDefined(value) ) { + if (lastClass) element.removeClass(lastClass); + lastClass = !value ? className : className + "-" + value.replace(WHITESPACE, "-"); + element.addClass(lastClass); + } + }; + } + + /** + * Provide console warning that this layout attribute has been deprecated + * + */ + function warnAttrNotSupported(className) { + var parts = className.split("-"); + return ["$log", function($log) { + $log.warn(className + "has been deprecated. Please use a `" + parts[0] + "-gt-` variant."); + return angular.noop; + }]; + } + + /** + * Centralize warnings for known flexbox issues (especially IE-related issues) + */ + function validateAttributeUsage(className, attr, element, $log){ + var message, usage, url; + var nodeName = element[0].nodeName.toLowerCase(); + + switch(className.replace(SUFFIXES,"")) { + case "flex": + if ((nodeName == "md-button") || (nodeName == "fieldset")){ + // @see https://github.com/philipwalton/flexbugs#9-some-html-elements-cant-be-flex-containers + // Use
wrapper inside (preferred) or outside + + usage = "<" + nodeName + " " + className + ">"; + url = "https://github.com/philipwalton/flexbugs#9-some-html-elements-cant-be-flex-containers"; + message = "Markup '{0}' may not work as expected in IE Browsers. Consult '{1}' for details."; + + $log.warn( $mdUtil.supplant(message, [usage, url]) ); + } + } + + } + + + /** + * For the Layout attribute value, validate or replace with default + * fallback value + */ + function validateAttributeValue(className, value, updateFn) { + var origValue = value; + + if (!needsInterpolation(value)) { + switch (className.replace(SUFFIXES,"")) { + case 'layout' : + if ( !findIn(value, LAYOUT_OPTIONS) ) { + value = LAYOUT_OPTIONS[0]; // 'row'; + } + break; + + case 'flex' : + if (!findIn(value, FLEX_OPTIONS)) { + if (isNaN(value)) { + value = ''; + } + } + break; + + case 'flex-offset' : + case 'flex-order' : + if (!value || isNaN(+value)) { + value = '0'; + } + break; + + case 'layout-align' : + var axis = extractAlignAxis(value); + value = $mdUtil.supplant("{main}-{cross}",axis); + break; + + case 'layout-padding' : + case 'layout-margin' : + case 'layout-fill' : + case 'layout-wrap' : + case 'layout-nowrap' : + case 'layout-nowrap' : + value = ''; + break; + } + + if (value != origValue) { + (updateFn || angular.noop)(value); + } + } + + return value; + } + + /** + * Replace current attribute value with fallback value + */ + function buildUpdateFn(element, className, attrs) { + return function updateAttrValue(fallback) { + if (!needsInterpolation(fallback)) { + // Do not modify the element's attribute value; so + // uses '' will not + // be affected. Just update the attrs value. + attrs[attrs.$normalize(className)] = fallback; + } + }; + } + + /** + * See if the original value has interpolation symbols: + * e.g. flex-gt-md="{{triggerPoint}}" + */ + function needsInterpolation(value) { + return (value || "").indexOf($interpolate.startSymbol()) > -1; + } + + function getNormalizedAttrValue(className, attrs, defaultVal) { + var normalizedAttr = attrs.$normalize(className); + return attrs[normalizedAttr] ? attrs[normalizedAttr].replace(WHITESPACE, "-") : defaultVal || null; + } + + function findIn(item, list, replaceWith) { + item = replaceWith && item ? item.replace(WHITESPACE, replaceWith) : item; + + var found = false; + if (item) { + list.forEach(function(it) { + it = replaceWith ? it.replace(WHITESPACE, replaceWith) : it; + found = found || (it === item); + }); + } + return found; + } + + function extractAlignAxis(attrValue) { + var axis = { + main : "start", + cross: "stretch" + }, values; + + attrValue = (attrValue || ""); + + if ( attrValue.indexOf("-") === 0 || attrValue.indexOf(" ") === 0) { + // For missing main-axis values + attrValue = "none" + attrValue; + } + + values = attrValue.toLowerCase().trim().replace(WHITESPACE, "-").split("-"); + if ( values.length && (values[0] === "space") ) { + // for main-axis values of "space-around" or "space-between" + values = [ values[0]+"-"+values[1],values[2] ]; + } + + if ( values.length > 0 ) axis.main = values[0] || axis.main; + if ( values.length > 1 ) axis.cross = values[1] || axis.cross; + + if ( ALIGNMENT_MAIN_AXIS.indexOf(axis.main) < 0 ) axis.main = "start"; + if ( ALIGNMENT_CROSS_AXIS.indexOf(axis.cross) < 0 ) axis.cross = "stretch"; + + return axis; + } + + +})(); + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.core.liveannouncer + * @description + * Angular Material Live Announcer to provide accessibility for Voice Readers. + */ +MdLiveAnnouncer.$inject = ["$timeout"]; +angular + .module('material.core') + .service('$mdLiveAnnouncer', MdLiveAnnouncer); + +/** + * @ngdoc service + * @name $mdLiveAnnouncer + * @module material.core.liveannouncer + * + * @description + * + * Service to announce messages to supported screenreaders. + * + * > The `$mdLiveAnnouncer` service is internally used for components to provide proper accessibility. + * + * + * module.controller('AppCtrl', function($mdLiveAnnouncer) { + * // Basic announcement (Polite Mode) + * $mdLiveAnnouncer.announce('Hey Google'); + * + * // Custom announcement (Assertive Mode) + * $mdLiveAnnouncer.announce('Hey Google', 'assertive'); + * }); + * + * + */ +function MdLiveAnnouncer($timeout) { + /** @private @const @type {!angular.$timeout} */ + this._$timeout = $timeout; + + /** @private @const @type {!HTMLElement} */ + this._liveElement = this._createLiveElement(); + + /** @private @const @type {!number} */ + this._announceTimeout = 100; +} + +/** + * @ngdoc method + * @name $mdLiveAnnouncer#announce + * @description Announces messages to supported screenreaders. + * @param {string} message Message to be announced to the screenreader + * @param {'off'|'polite'|'assertive'} politeness The politeness of the announcer element. + */ +MdLiveAnnouncer.prototype.announce = function(message, politeness) { + if (!politeness) { + politeness = 'polite'; + } + + var self = this; + + self._liveElement.textContent = ''; + self._liveElement.setAttribute('aria-live', politeness); + + // This 100ms timeout is necessary for some browser + screen-reader combinations: + // - Both JAWS and NVDA over IE11 will not announce anything without a non-zero timeout. + // - With Chrome and IE11 with NVDA or JAWS, a repeated (identical) message won't be read a + // second time without clearing and then using a non-zero delay. + // (using JAWS 17 at time of this writing). + self._$timeout(function() { + self._liveElement.textContent = message; + }, self._announceTimeout, false); +}; + +/** + * Creates a live announcer element, which listens for DOM changes and announces them + * to the screenreaders. + * @returns {!HTMLElement} + * @private + */ +MdLiveAnnouncer.prototype._createLiveElement = function() { + var liveEl = document.createElement('div'); + + liveEl.classList.add('md-visually-hidden'); + liveEl.setAttribute('role', 'status'); + liveEl.setAttribute('aria-atomic', 'true'); + liveEl.setAttribute('aria-live', 'polite'); + + document.body.appendChild(liveEl); + + return liveEl; +}; + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc service + * @name $$mdMeta + * @module material.core.meta + * + * @description + * + * A provider and a service that simplifies meta tags access + * + * Note: This is intended only for use with dynamic meta tags such as browser color and title. + * Tags that are only processed when the page is rendered (such as `charset`, and `http-equiv`) + * will not work since `$$mdMeta` adds the tags after the page has already been loaded. + * + * ```js + * app.config(function($$mdMetaProvider) { + * var removeMeta = $$mdMetaProvider.setMeta('meta-name', 'content'); + * var metaValue = $$mdMetaProvider.getMeta('meta-name'); // -> 'content' + * + * removeMeta(); + * }); + * + * app.controller('myController', function($$mdMeta) { + * var removeMeta = $$mdMeta.setMeta('meta-name', 'content'); + * var metaValue = $$mdMeta.getMeta('meta-name'); // -> 'content' + * + * removeMeta(); + * }); + * ``` + * + * @returns {$$mdMeta.$service} + * + */ +angular.module('material.core.meta', []) + .provider('$$mdMeta', function () { + var head = angular.element(document.head); + var metaElements = {}; + + /** + * Checks if the requested element was written manually and maps it + * + * @param {string} name meta tag 'name' attribute value + * @returns {boolean} returns true if there is an element with the requested name + */ + function mapExistingElement(name) { + if (metaElements[name]) { + return true; + } + + var element = document.getElementsByName(name)[0]; + + if (!element) { + return false; + } + + metaElements[name] = angular.element(element); + + return true; + } + + /** + * @ngdoc method + * @name $$mdMeta#setMeta + * + * @description + * Creates meta element with the 'name' and 'content' attributes, + * if the meta tag is already created than we replace the 'content' value + * + * @param {string} name meta tag 'name' attribute value + * @param {string} content meta tag 'content' attribute value + * @returns {function} remove function + * + */ + function setMeta(name, content) { + mapExistingElement(name); + + if (!metaElements[name]) { + var newMeta = angular.element(''); + head.append(newMeta); + metaElements[name] = newMeta; + } + else { + metaElements[name].attr('content', content); + } + + return function () { + metaElements[name].attr('content', ''); + metaElements[name].remove(); + delete metaElements[name]; + }; + } + + /** + * @ngdoc method + * @name $$mdMeta#getMeta + * + * @description + * Gets the 'content' attribute value of the wanted meta element + * + * @param {string} name meta tag 'name' attribute value + * @returns {string} content attribute value + */ + function getMeta(name) { + if (!mapExistingElement(name)) { + throw Error('$$mdMeta: could not find a meta tag with the name \'' + name + '\''); + } + + return metaElements[name].attr('content'); + } + + var module = { + setMeta: setMeta, + getMeta: getMeta + }; + + return angular.extend({}, module, { + $get: function () { + return module; + } + }); + }); +})(); +(function(){ +"use strict"; + + /** + * @ngdoc module + * @name material.core.componentRegistry + * + * @description + * A component instance registration service. + * Note: currently this as a private service in the SideNav component. + */ + ComponentRegistry.$inject = ["$log", "$q"]; + angular.module('material.core') + .factory('$mdComponentRegistry', ComponentRegistry); + + /* + * @private + * @ngdoc factory + * @name ComponentRegistry + * @module material.core.componentRegistry + * + */ + function ComponentRegistry($log, $q) { + + var self; + var instances = [ ]; + var pendings = { }; + + return self = { + /** + * Used to print an error when an instance for a handle isn't found. + */ + notFoundError: function(handle, msgContext) { + $log.error( (msgContext || "") + 'No instance found for handle', handle); + }, + /** + * Return all registered instances as an array. + */ + getInstances: function() { + return instances; + }, + + /** + * Get a registered instance. + * @param handle the String handle to look up for a registered instance. + */ + get: function(handle) { + if ( !isValidID(handle) ) return null; + + var i, j, instance; + for(i = 0, j = instances.length; i < j; i++) { + instance = instances[i]; + if(instance.$$mdHandle === handle) { + return instance; + } + } + return null; + }, + + /** + * Register an instance. + * @param instance the instance to register + * @param handle the handle to identify the instance under. + */ + register: function(instance, handle) { + if ( !handle ) return angular.noop; + + instance.$$mdHandle = handle; + instances.push(instance); + resolveWhen(); + + return deregister; + + /** + * Remove registration for an instance + */ + function deregister() { + var index = instances.indexOf(instance); + if (index !== -1) { + instances.splice(index, 1); + } + } + + /** + * Resolve any pending promises for this instance + */ + function resolveWhen() { + var dfd = pendings[handle]; + if ( dfd ) { + dfd.forEach(function (promise) { + promise.resolve(instance); + }); + delete pendings[handle]; + } + } + }, + + /** + * Async accessor to registered component instance + * If not available then a promise is created to notify + * all listeners when the instance is registered. + */ + when : function(handle) { + if ( isValidID(handle) ) { + var deferred = $q.defer(); + var instance = self.get(handle); + + if ( instance ) { + deferred.resolve( instance ); + } else { + if (pendings[handle] === undefined) { + pendings[handle] = []; + } + pendings[handle].push(deferred); + } + + return deferred.promise; + } + return $q.reject("Invalid `md-component-id` value."); + } + + }; + + function isValidID(handle){ + return handle && (handle !== ""); + } + + } + +})(); +(function(){ +"use strict"; + +(function() { + 'use strict'; + + /** + * @ngdoc service + * @name $mdButtonInkRipple + * @module material.core + * + * @description + * Provides ripple effects for md-button. See $mdInkRipple service for all possible configuration options. + * + * @param {object=} scope Scope within the current context + * @param {object=} element The element the ripple effect should be applied to + * @param {object=} options (Optional) Configuration options to override the default ripple configuration + */ + + MdButtonInkRipple.$inject = ["$mdInkRipple"]; + angular.module('material.core') + .factory('$mdButtonInkRipple', MdButtonInkRipple); + + function MdButtonInkRipple($mdInkRipple) { + return { + attach: function attachRipple(scope, element, options) { + options = angular.extend(optionsForElement(element), options); + + return $mdInkRipple.attach(scope, element, options); + } + }; + + function optionsForElement(element) { + if (element.hasClass('md-icon-button')) { + return { + isMenuItem: element.hasClass('md-menu-item'), + fitRipple: true, + center: true + }; + } else { + return { + isMenuItem: element.hasClass('md-menu-item'), + dimBackground: true + }; + } + } + } +})(); + +})(); +(function(){ +"use strict"; + +(function() { + 'use strict'; + + /** + * @ngdoc service + * @name $mdCheckboxInkRipple + * @module material.core + * + * @description + * Provides ripple effects for md-checkbox. See $mdInkRipple service for all possible configuration options. + * + * @param {object=} scope Scope within the current context + * @param {object=} element The element the ripple effect should be applied to + * @param {object=} options (Optional) Configuration options to override the defaultripple configuration + */ + + MdCheckboxInkRipple.$inject = ["$mdInkRipple"]; + angular.module('material.core') + .factory('$mdCheckboxInkRipple', MdCheckboxInkRipple); + + function MdCheckboxInkRipple($mdInkRipple) { + return { + attach: attach + }; + + function attach(scope, element, options) { + return $mdInkRipple.attach(scope, element, angular.extend({ + center: true, + dimBackground: false, + fitRipple: true + }, options)); + } + } +})(); + +})(); +(function(){ +"use strict"; + +(function() { + 'use strict'; + + /** + * @ngdoc service + * @name $mdListInkRipple + * @module material.core + * + * @description + * Provides ripple effects for md-list. See $mdInkRipple service for all possible configuration options. + * + * @param {object=} scope Scope within the current context + * @param {object=} element The element the ripple effect should be applied to + * @param {object=} options (Optional) Configuration options to override the defaultripple configuration + */ + + MdListInkRipple.$inject = ["$mdInkRipple"]; + angular.module('material.core') + .factory('$mdListInkRipple', MdListInkRipple); + + function MdListInkRipple($mdInkRipple) { + return { + attach: attach + }; + + function attach(scope, element, options) { + return $mdInkRipple.attach(scope, element, angular.extend({ + center: false, + dimBackground: true, + outline: false, + rippleSize: 'full' + }, options)); + } + } +})(); + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.core.ripple + * @description + * Ripple + */ +InkRippleCtrl.$inject = ["$scope", "$element", "rippleOptions", "$window", "$timeout", "$mdUtil", "$mdColorUtil"]; +InkRippleDirective.$inject = ["$mdButtonInkRipple", "$mdCheckboxInkRipple"]; +angular.module('material.core') + .provider('$mdInkRipple', InkRippleProvider) + .directive('mdInkRipple', InkRippleDirective) + .directive('mdNoInk', attrNoDirective) + .directive('mdNoBar', attrNoDirective) + .directive('mdNoStretch', attrNoDirective); + +var DURATION = 450; + +/** + * @ngdoc directive + * @name mdInkRipple + * @module material.core.ripple + * + * @description + * The `md-ink-ripple` directive allows you to specify the ripple color or id a ripple is allowed. + * + * @param {string|boolean} md-ink-ripple A color string `#FF0000` or boolean (`false` or `0`) for preventing ripple + * + * @usage + * ### String values + * + * + * Ripples in red + * + * + * + * Not rippling + * + * + * + * ### Interpolated values + * + * + * Ripples with the return value of 'randomColor' function + * + * + * + * Ripples if 'canRipple' function return value is not 'false' or '0' + * + * + */ +function InkRippleDirective ($mdButtonInkRipple, $mdCheckboxInkRipple) { + return { + controller: angular.noop, + link: function (scope, element, attr) { + attr.hasOwnProperty('mdInkRippleCheckbox') + ? $mdCheckboxInkRipple.attach(scope, element) + : $mdButtonInkRipple.attach(scope, element); + } + }; +} + +/** + * @ngdoc service + * @name $mdInkRipple + * @module material.core.ripple + * + * @description + * `$mdInkRipple` is a service for adding ripples to any element + * + * @usage + * + * app.factory('$myElementInkRipple', function($mdInkRipple) { + * return { + * attach: function (scope, element, options) { + * return $mdInkRipple.attach(scope, element, angular.extend({ + * center: false, + * dimBackground: true + * }, options)); + * } + * }; + * }); + * + * app.controller('myController', function ($scope, $element, $myElementInkRipple) { + * $scope.onClick = function (ev) { + * $myElementInkRipple.attach($scope, angular.element(ev.target), { center: true }); + * } + * }); + * + * + * ### Disabling ripples globally + * If you want to disable ink ripples globally, for all components, you can call the + * `disableInkRipple` method in your app's config. + * + * + * app.config(function ($mdInkRippleProvider) { + * $mdInkRippleProvider.disableInkRipple(); + * }); + */ + +function InkRippleProvider () { + var isDisabledGlobally = false; + + return { + disableInkRipple: disableInkRipple, + $get: ["$injector", function($injector) { + return { attach: attach }; + + /** + * @ngdoc method + * @name $mdInkRipple#attach + * + * @description + * Attaching given scope, element and options to inkRipple controller + * + * @param {object=} scope Scope within the current context + * @param {object=} element The element the ripple effect should be applied to + * @param {object=} options (Optional) Configuration options to override the defaultRipple configuration + * * `center` - Whether the ripple should start from the center of the container element + * * `dimBackground` - Whether the background should be dimmed with the ripple color + * * `colorElement` - The element the ripple should take its color from, defined by css property `color` + * * `fitRipple` - Whether the ripple should fill the element + */ + function attach (scope, element, options) { + if (isDisabledGlobally || element.controller('mdNoInk')) return angular.noop; + return $injector.instantiate(InkRippleCtrl, { + $scope: scope, + $element: element, + rippleOptions: options + }); + } + }] + }; + + /** + * @ngdoc method + * @name $mdInkRipple#disableInkRipple + * + * @description + * A config-time method that, when called, disables ripples globally. + */ + function disableInkRipple () { + isDisabledGlobally = true; + } +} + +/** + * Controller used by the ripple service in order to apply ripples + * @ngInject + */ +function InkRippleCtrl ($scope, $element, rippleOptions, $window, $timeout, $mdUtil, $mdColorUtil) { + this.$window = $window; + this.$timeout = $timeout; + this.$mdUtil = $mdUtil; + this.$mdColorUtil = $mdColorUtil; + this.$scope = $scope; + this.$element = $element; + this.options = rippleOptions; + this.mousedown = false; + this.ripples = []; + this.timeout = null; // Stores a reference to the most-recent ripple timeout + this.lastRipple = null; + + $mdUtil.valueOnUse(this, 'container', this.createContainer); + + this.$element.addClass('md-ink-ripple'); + + // attach method for unit tests + ($element.controller('mdInkRipple') || {}).createRipple = angular.bind(this, this.createRipple); + ($element.controller('mdInkRipple') || {}).setColor = angular.bind(this, this.color); + + this.bindEvents(); +} + + +/** + * Either remove or unlock any remaining ripples when the user mouses off of the element (either by + * mouseup or mouseleave event) + */ +function autoCleanup (self, cleanupFn) { + + if ( self.mousedown || self.lastRipple ) { + self.mousedown = false; + self.$mdUtil.nextTick( angular.bind(self, cleanupFn), false); + } + +} + + +/** + * Returns the color that the ripple should be (either based on CSS or hard-coded) + * @returns {string} + */ +InkRippleCtrl.prototype.color = function (value) { + var self = this; + + // If assigning a color value, apply it to background and the ripple color + if (angular.isDefined(value)) { + self._color = self._parseColor(value); + } + + // If color lookup, use assigned, defined, or inherited + return self._color || self._parseColor( self.inkRipple() ) || self._parseColor( getElementColor() ); + + /** + * Finds the color element and returns its text color for use as default ripple color + * @returns {string} + */ + function getElementColor () { + var items = self.options && self.options.colorElement ? self.options.colorElement : []; + var elem = items.length ? items[ 0 ] : self.$element[ 0 ]; + + return elem ? self.$window.getComputedStyle(elem).color : 'rgb(0,0,0)'; + } +}; + +/** + * Updating the ripple colors based on the current inkRipple value + * or the element's computed style color + */ +InkRippleCtrl.prototype.calculateColor = function () { + return this.color(); +}; + + +/** + * Takes a string color and converts it to RGBA format + * @param color {string} + * @param [multiplier] {int} + * @returns {string} + */ + +InkRippleCtrl.prototype._parseColor = function parseColor (color, multiplier) { + multiplier = multiplier || 1; + var colorUtil = this.$mdColorUtil; + + if (!color) return; + if (color.indexOf('rgba') === 0) return color.replace(/\d?\.?\d*\s*\)\s*$/, (0.1 * multiplier).toString() + ')'); + if (color.indexOf('rgb') === 0) return colorUtil.rgbToRgba(color); + if (color.indexOf('#') === 0) return colorUtil.hexToRgba(color); + +}; + +/** + * Binds events to the root element for + */ +InkRippleCtrl.prototype.bindEvents = function () { + this.$element.on('mousedown', angular.bind(this, this.handleMousedown)); + this.$element.on('mouseup touchend', angular.bind(this, this.handleMouseup)); + this.$element.on('mouseleave', angular.bind(this, this.handleMouseup)); + this.$element.on('touchmove', angular.bind(this, this.handleTouchmove)); +}; + +/** + * Create a new ripple on every mousedown event from the root element + * @param event {MouseEvent} + */ +InkRippleCtrl.prototype.handleMousedown = function (event) { + if ( this.mousedown ) return; + + // When jQuery is loaded, we have to get the original event + if (event.hasOwnProperty('originalEvent')) event = event.originalEvent; + this.mousedown = true; + if (this.options.center) { + this.createRipple(this.container.prop('clientWidth') / 2, this.container.prop('clientWidth') / 2); + } else { + + // We need to calculate the relative coordinates if the target is a sublayer of the ripple element + if (event.srcElement !== this.$element[0]) { + var layerRect = this.$element[0].getBoundingClientRect(); + var layerX = event.clientX - layerRect.left; + var layerY = event.clientY - layerRect.top; + + this.createRipple(layerX, layerY); + } else { + this.createRipple(event.offsetX, event.offsetY); + } + } +}; + +/** + * Either remove or unlock any remaining ripples when the user mouses off of the element (either by + * mouseup, touchend or mouseleave event) + */ +InkRippleCtrl.prototype.handleMouseup = function () { + autoCleanup(this, this.clearRipples); +}; + +/** + * Either remove or unlock any remaining ripples when the user mouses off of the element (by + * touchmove) + */ +InkRippleCtrl.prototype.handleTouchmove = function () { + autoCleanup(this, this.deleteRipples); +}; + +/** + * Cycles through all ripples and attempts to remove them. + */ +InkRippleCtrl.prototype.deleteRipples = function () { + for (var i = 0; i < this.ripples.length; i++) { + this.ripples[ i ].remove(); + } +}; + +/** + * Cycles through all ripples and attempts to remove them with fade. + * Depending on logic within `fadeInComplete`, some removals will be postponed. + */ +InkRippleCtrl.prototype.clearRipples = function () { + for (var i = 0; i < this.ripples.length; i++) { + this.fadeInComplete(this.ripples[ i ]); + } +}; + +/** + * Creates the ripple container element + * @returns {*} + */ +InkRippleCtrl.prototype.createContainer = function () { + var container = angular.element('
'); + this.$element.append(container); + return container; +}; + +InkRippleCtrl.prototype.clearTimeout = function () { + if (this.timeout) { + this.$timeout.cancel(this.timeout); + this.timeout = null; + } +}; + +InkRippleCtrl.prototype.isRippleAllowed = function () { + var element = this.$element[0]; + do { + if (!element.tagName || element.tagName === 'BODY') break; + + if (element && angular.isFunction(element.hasAttribute)) { + if (element.hasAttribute('disabled')) return false; + if (this.inkRipple() === 'false' || this.inkRipple() === '0') return false; + } + + } while (element = element.parentNode); + return true; +}; + +/** + * The attribute `md-ink-ripple` may be a static or interpolated + * color value OR a boolean indicator (used to disable ripples) + */ +InkRippleCtrl.prototype.inkRipple = function () { + return this.$element.attr('md-ink-ripple'); +}; + +/** + * Creates a new ripple and adds it to the container. Also tracks ripple in `this.ripples`. + * @param left + * @param top + */ +InkRippleCtrl.prototype.createRipple = function (left, top) { + if (!this.isRippleAllowed()) return; + + var ctrl = this; + var colorUtil = ctrl.$mdColorUtil; + var ripple = angular.element('
'); + var width = this.$element.prop('clientWidth'); + var height = this.$element.prop('clientHeight'); + var x = Math.max(Math.abs(width - left), left) * 2; + var y = Math.max(Math.abs(height - top), top) * 2; + var size = getSize(this.options.fitRipple, x, y); + var color = this.calculateColor(); + + ripple.css({ + left: left + 'px', + top: top + 'px', + background: 'black', + width: size + 'px', + height: size + 'px', + backgroundColor: colorUtil.rgbaToRgb(color), + borderColor: colorUtil.rgbaToRgb(color) + }); + this.lastRipple = ripple; + + // we only want one timeout to be running at a time + this.clearTimeout(); + this.timeout = this.$timeout(function () { + ctrl.clearTimeout(); + if (!ctrl.mousedown) ctrl.fadeInComplete(ripple); + }, DURATION * 0.35, false); + + if (this.options.dimBackground) this.container.css({ backgroundColor: color }); + this.container.append(ripple); + this.ripples.push(ripple); + ripple.addClass('md-ripple-placed'); + + this.$mdUtil.nextTick(function () { + + ripple.addClass('md-ripple-scaled md-ripple-active'); + ctrl.$timeout(function () { + ctrl.clearRipples(); + }, DURATION, false); + + }, false); + + function getSize (fit, x, y) { + return fit + ? Math.max(x, y) + : Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)); + } +}; + + + +/** + * After fadeIn finishes, either kicks off the fade-out animation or queues the element for removal on mouseup + * @param ripple + */ +InkRippleCtrl.prototype.fadeInComplete = function (ripple) { + if (this.lastRipple === ripple) { + if (!this.timeout && !this.mousedown) { + this.removeRipple(ripple); + } + } else { + this.removeRipple(ripple); + } +}; + +/** + * Kicks off the animation for removing a ripple + * @param ripple {Element} + */ +InkRippleCtrl.prototype.removeRipple = function (ripple) { + var ctrl = this; + var index = this.ripples.indexOf(ripple); + if (index < 0) return; + this.ripples.splice(this.ripples.indexOf(ripple), 1); + ripple.removeClass('md-ripple-active'); + ripple.addClass('md-ripple-remove'); + if (this.ripples.length === 0) this.container.css({ backgroundColor: '' }); + // use a 2-second timeout in order to allow for the animation to finish + // we don't actually care how long the animation takes + this.$timeout(function () { + ctrl.fadeOutComplete(ripple); + }, DURATION, false); +}; + +/** + * Removes the provided ripple from the DOM + * @param ripple + */ +InkRippleCtrl.prototype.fadeOutComplete = function (ripple) { + ripple.remove(); + this.lastRipple = null; +}; + +/** + * Used to create an empty directive. This is used to track flag-directives whose children may have + * functionality based on them. + * + * Example: `md-no-ink` will potentially be used by all child directives. + */ +function attrNoDirective () { + return { controller: angular.noop }; +} + +})(); +(function(){ +"use strict"; + +(function() { + 'use strict'; + + /** + * @ngdoc service + * @name $mdTabInkRipple + * @module material.core + * + * @description + * Provides ripple effects for md-tabs. See $mdInkRipple service for all possible configuration options. + * + * @param {object=} scope Scope within the current context + * @param {object=} element The element the ripple effect should be applied to + * @param {object=} options (Optional) Configuration options to override the defaultripple configuration + */ + + MdTabInkRipple.$inject = ["$mdInkRipple"]; + angular.module('material.core') + .factory('$mdTabInkRipple', MdTabInkRipple); + + function MdTabInkRipple($mdInkRipple) { + return { + attach: attach + }; + + function attach(scope, element, options) { + return $mdInkRipple.attach(scope, element, angular.extend({ + center: false, + dimBackground: true, + outline: false, + rippleSize: 'full' + }, options)); + } + } +})(); + +})(); +(function(){ +"use strict"; + +angular.module('material.core.theming.palette', []) +.constant('$mdColorPalette', { + 'red': { + '50': '#ffebee', + '100': '#ffcdd2', + '200': '#ef9a9a', + '300': '#e57373', + '400': '#ef5350', + '500': '#f44336', + '600': '#e53935', + '700': '#d32f2f', + '800': '#c62828', + '900': '#b71c1c', + 'A100': '#ff8a80', + 'A200': '#ff5252', + 'A400': '#ff1744', + 'A700': '#d50000', + 'contrastDefaultColor': 'light', + 'contrastDarkColors': '50 100 200 300 A100', + 'contrastStrongLightColors': '400 500 600 700 A200 A400 A700' + }, + 'pink': { + '50': '#fce4ec', + '100': '#f8bbd0', + '200': '#f48fb1', + '300': '#f06292', + '400': '#ec407a', + '500': '#e91e63', + '600': '#d81b60', + '700': '#c2185b', + '800': '#ad1457', + '900': '#880e4f', + 'A100': '#ff80ab', + 'A200': '#ff4081', + 'A400': '#f50057', + 'A700': '#c51162', + 'contrastDefaultColor': 'light', + 'contrastDarkColors': '50 100 200 A100', + 'contrastStrongLightColors': '500 600 A200 A400 A700' + }, + 'purple': { + '50': '#f3e5f5', + '100': '#e1bee7', + '200': '#ce93d8', + '300': '#ba68c8', + '400': '#ab47bc', + '500': '#9c27b0', + '600': '#8e24aa', + '700': '#7b1fa2', + '800': '#6a1b9a', + '900': '#4a148c', + 'A100': '#ea80fc', + 'A200': '#e040fb', + 'A400': '#d500f9', + 'A700': '#aa00ff', + 'contrastDefaultColor': 'light', + 'contrastDarkColors': '50 100 200 A100', + 'contrastStrongLightColors': '300 400 A200 A400 A700' + }, + 'deep-purple': { + '50': '#ede7f6', + '100': '#d1c4e9', + '200': '#b39ddb', + '300': '#9575cd', + '400': '#7e57c2', + '500': '#673ab7', + '600': '#5e35b1', + '700': '#512da8', + '800': '#4527a0', + '900': '#311b92', + 'A100': '#b388ff', + 'A200': '#7c4dff', + 'A400': '#651fff', + 'A700': '#6200ea', + 'contrastDefaultColor': 'light', + 'contrastDarkColors': '50 100 200 A100', + 'contrastStrongLightColors': '300 400 A200' + }, + 'indigo': { + '50': '#e8eaf6', + '100': '#c5cae9', + '200': '#9fa8da', + '300': '#7986cb', + '400': '#5c6bc0', + '500': '#3f51b5', + '600': '#3949ab', + '700': '#303f9f', + '800': '#283593', + '900': '#1a237e', + 'A100': '#8c9eff', + 'A200': '#536dfe', + 'A400': '#3d5afe', + 'A700': '#304ffe', + 'contrastDefaultColor': 'light', + 'contrastDarkColors': '50 100 200 A100', + 'contrastStrongLightColors': '300 400 A200 A400' + }, + 'blue': { + '50': '#e3f2fd', + '100': '#bbdefb', + '200': '#90caf9', + '300': '#64b5f6', + '400': '#42a5f5', + '500': '#2196f3', + '600': '#1e88e5', + '700': '#1976d2', + '800': '#1565c0', + '900': '#0d47a1', + 'A100': '#82b1ff', + 'A200': '#448aff', + 'A400': '#2979ff', + 'A700': '#2962ff', + 'contrastDefaultColor': 'light', + 'contrastDarkColors': '50 100 200 300 400 A100', + 'contrastStrongLightColors': '500 600 700 A200 A400 A700' + }, + 'light-blue': { + '50': '#e1f5fe', + '100': '#b3e5fc', + '200': '#81d4fa', + '300': '#4fc3f7', + '400': '#29b6f6', + '500': '#03a9f4', + '600': '#039be5', + '700': '#0288d1', + '800': '#0277bd', + '900': '#01579b', + 'A100': '#80d8ff', + 'A200': '#40c4ff', + 'A400': '#00b0ff', + 'A700': '#0091ea', + 'contrastDefaultColor': 'dark', + 'contrastLightColors': '600 700 800 900 A700', + 'contrastStrongLightColors': '600 700 800 A700' + }, + 'cyan': { + '50': '#e0f7fa', + '100': '#b2ebf2', + '200': '#80deea', + '300': '#4dd0e1', + '400': '#26c6da', + '500': '#00bcd4', + '600': '#00acc1', + '700': '#0097a7', + '800': '#00838f', + '900': '#006064', + 'A100': '#84ffff', + 'A200': '#18ffff', + 'A400': '#00e5ff', + 'A700': '#00b8d4', + 'contrastDefaultColor': 'dark', + 'contrastLightColors': '700 800 900', + 'contrastStrongLightColors': '700 800 900' + }, + 'teal': { + '50': '#e0f2f1', + '100': '#b2dfdb', + '200': '#80cbc4', + '300': '#4db6ac', + '400': '#26a69a', + '500': '#009688', + '600': '#00897b', + '700': '#00796b', + '800': '#00695c', + '900': '#004d40', + 'A100': '#a7ffeb', + 'A200': '#64ffda', + 'A400': '#1de9b6', + 'A700': '#00bfa5', + 'contrastDefaultColor': 'dark', + 'contrastLightColors': '500 600 700 800 900', + 'contrastStrongLightColors': '500 600 700' + }, + 'green': { + '50': '#e8f5e9', + '100': '#c8e6c9', + '200': '#a5d6a7', + '300': '#81c784', + '400': '#66bb6a', + '500': '#4caf50', + '600': '#43a047', + '700': '#388e3c', + '800': '#2e7d32', + '900': '#1b5e20', + 'A100': '#b9f6ca', + 'A200': '#69f0ae', + 'A400': '#00e676', + 'A700': '#00c853', + 'contrastDefaultColor': 'dark', + 'contrastLightColors': '500 600 700 800 900', + 'contrastStrongLightColors': '500 600 700' + }, + 'light-green': { + '50': '#f1f8e9', + '100': '#dcedc8', + '200': '#c5e1a5', + '300': '#aed581', + '400': '#9ccc65', + '500': '#8bc34a', + '600': '#7cb342', + '700': '#689f38', + '800': '#558b2f', + '900': '#33691e', + 'A100': '#ccff90', + 'A200': '#b2ff59', + 'A400': '#76ff03', + 'A700': '#64dd17', + 'contrastDefaultColor': 'dark', + 'contrastLightColors': '700 800 900', + 'contrastStrongLightColors': '700 800 900' + }, + 'lime': { + '50': '#f9fbe7', + '100': '#f0f4c3', + '200': '#e6ee9c', + '300': '#dce775', + '400': '#d4e157', + '500': '#cddc39', + '600': '#c0ca33', + '700': '#afb42b', + '800': '#9e9d24', + '900': '#827717', + 'A100': '#f4ff81', + 'A200': '#eeff41', + 'A400': '#c6ff00', + 'A700': '#aeea00', + 'contrastDefaultColor': 'dark', + 'contrastLightColors': '900', + 'contrastStrongLightColors': '900' + }, + 'yellow': { + '50': '#fffde7', + '100': '#fff9c4', + '200': '#fff59d', + '300': '#fff176', + '400': '#ffee58', + '500': '#ffeb3b', + '600': '#fdd835', + '700': '#fbc02d', + '800': '#f9a825', + '900': '#f57f17', + 'A100': '#ffff8d', + 'A200': '#ffff00', + 'A400': '#ffea00', + 'A700': '#ffd600', + 'contrastDefaultColor': 'dark' + }, + 'amber': { + '50': '#fff8e1', + '100': '#ffecb3', + '200': '#ffe082', + '300': '#ffd54f', + '400': '#ffca28', + '500': '#ffc107', + '600': '#ffb300', + '700': '#ffa000', + '800': '#ff8f00', + '900': '#ff6f00', + 'A100': '#ffe57f', + 'A200': '#ffd740', + 'A400': '#ffc400', + 'A700': '#ffab00', + 'contrastDefaultColor': 'dark' + }, + 'orange': { + '50': '#fff3e0', + '100': '#ffe0b2', + '200': '#ffcc80', + '300': '#ffb74d', + '400': '#ffa726', + '500': '#ff9800', + '600': '#fb8c00', + '700': '#f57c00', + '800': '#ef6c00', + '900': '#e65100', + 'A100': '#ffd180', + 'A200': '#ffab40', + 'A400': '#ff9100', + 'A700': '#ff6d00', + 'contrastDefaultColor': 'dark', + 'contrastLightColors': '800 900', + 'contrastStrongLightColors': '800 900' + }, + 'deep-orange': { + '50': '#fbe9e7', + '100': '#ffccbc', + '200': '#ffab91', + '300': '#ff8a65', + '400': '#ff7043', + '500': '#ff5722', + '600': '#f4511e', + '700': '#e64a19', + '800': '#d84315', + '900': '#bf360c', + 'A100': '#ff9e80', + 'A200': '#ff6e40', + 'A400': '#ff3d00', + 'A700': '#dd2c00', + 'contrastDefaultColor': 'light', + 'contrastDarkColors': '50 100 200 300 400 A100 A200', + 'contrastStrongLightColors': '500 600 700 800 900 A400 A700' + }, + 'brown': { + '50': '#efebe9', + '100': '#d7ccc8', + '200': '#bcaaa4', + '300': '#a1887f', + '400': '#8d6e63', + '500': '#795548', + '600': '#6d4c41', + '700': '#5d4037', + '800': '#4e342e', + '900': '#3e2723', + 'A100': '#d7ccc8', + 'A200': '#bcaaa4', + 'A400': '#8d6e63', + 'A700': '#5d4037', + 'contrastDefaultColor': 'light', + 'contrastDarkColors': '50 100 200 A100 A200', + 'contrastStrongLightColors': '300 400' + }, + 'grey': { + '50': '#fafafa', + '100': '#f5f5f5', + '200': '#eeeeee', + '300': '#e0e0e0', + '400': '#bdbdbd', + '500': '#9e9e9e', + '600': '#757575', + '700': '#616161', + '800': '#424242', + '900': '#212121', + 'A100': '#ffffff', + 'A200': '#000000', + 'A400': '#303030', + 'A700': '#616161', + 'contrastDefaultColor': 'dark', + 'contrastLightColors': '600 700 800 900 A200 A400 A700' + }, + 'blue-grey': { + '50': '#eceff1', + '100': '#cfd8dc', + '200': '#b0bec5', + '300': '#90a4ae', + '400': '#78909c', + '500': '#607d8b', + '600': '#546e7a', + '700': '#455a64', + '800': '#37474f', + '900': '#263238', + 'A100': '#cfd8dc', + 'A200': '#b0bec5', + 'A400': '#78909c', + 'A700': '#455a64', + 'contrastDefaultColor': 'light', + 'contrastDarkColors': '50 100 200 300 A100 A200', + 'contrastStrongLightColors': '400 500 700' + } +}); + +})(); +(function(){ +"use strict"; + +(function(angular) { + 'use strict'; +/** + * @ngdoc module + * @name material.core.theming + * @description + * Theming + */ +detectDisabledThemes.$inject = ["$mdThemingProvider"]; +ThemingDirective.$inject = ["$mdTheming", "$interpolate", "$parse", "$mdUtil", "$q", "$log"]; +ThemableDirective.$inject = ["$mdTheming"]; +ThemingProvider.$inject = ["$mdColorPalette", "$$mdMetaProvider"]; +generateAllThemes.$inject = ["$injector", "$mdTheming"]; +angular.module('material.core.theming', ['material.core.theming.palette', 'material.core.meta']) + .directive('mdTheme', ThemingDirective) + .directive('mdThemable', ThemableDirective) + .directive('mdThemesDisabled', disableThemesDirective ) + .provider('$mdTheming', ThemingProvider) + .config( detectDisabledThemes ) + .run(generateAllThemes); + +/** + * Detect if the HTML or the BODY tags has a [md-themes-disabled] attribute + * If yes, then immediately disable all theme stylesheet generation and DOM injection + */ +/** + * @ngInject + */ +function detectDisabledThemes($mdThemingProvider) { + var isDisabled = !!document.querySelector('[md-themes-disabled]'); + $mdThemingProvider.disableTheming(isDisabled); +} + +/** + * @ngdoc service + * @name $mdThemingProvider + * @module material.core.theming + * + * @description Provider to configure the `$mdTheming` service. + * + * ### Default Theme + * The `$mdThemingProvider` uses by default the following theme configuration: + * + * - Primary Palette: `Blue` + * - Accent Palette: `Pink` + * - Warn Palette: `Deep-Orange` + * - Background Palette: `Grey` + * + * If you don't want to use the `md-theme` directive on the elements itself, you may want to overwrite + * the default theme.
+ * This can be done by using the following markup. + * + * + * myAppModule.config(function($mdThemingProvider) { + * $mdThemingProvider + * .theme('default') + * .primaryPalette('blue') + * .accentPalette('teal') + * .warnPalette('red') + * .backgroundPalette('grey'); + * }); + * + * + + * ### Dynamic Themes + * + * By default, if you change a theme at runtime, the `$mdTheming` service will not detect those changes.
+ * If you have an application, which changes its theme on runtime, you have to enable theme watching. + * + * + * myAppModule.config(function($mdThemingProvider) { + * // Enable theme watching. + * $mdThemingProvider.alwaysWatchTheme(true); + * }); + * + * + * ### Custom Theme Styles + * + * Sometimes you may want to use your own theme styles for some custom components.
+ * You are able to register your own styles by using the following markup. + * + * + * myAppModule.config(function($mdThemingProvider) { + * // Register our custom stylesheet into the theming provider. + * $mdThemingProvider.registerStyles(STYLESHEET); + * }); + * + * + * The `registerStyles` method only accepts strings as value, so you're actually not able to load an external + * stylesheet file into the `$mdThemingProvider`. + * + * If it's necessary to load an external stylesheet, we suggest using a bundler, which supports including raw content, + * like [raw-loader](https://github.com/webpack/raw-loader) for `webpack`. + * + * + * myAppModule.config(function($mdThemingProvider) { + * // Register your custom stylesheet into the theming provider. + * $mdThemingProvider.registerStyles(require('../styles/my-component.theme.css')); + * }); + * + * + * ### Browser color + * + * Enables browser header coloring + * for more info please visit: + * https://developers.google.com/web/fundamentals/design-and-ui/browser-customization/theme-color + * + * Options parameter:
+ * `theme` - A defined theme via `$mdThemeProvider` to use the palettes from. Default is `default` theme.
+ * `palette` - Can be any one of the basic material design palettes, extended defined palettes and 'primary', + * 'accent', 'background' and 'warn'. Default is `primary`.
+ * `hue` - The hue from the selected palette. Default is `800`
+ * + * + * myAppModule.config(function($mdThemingProvider) { + * // Enable browser color + * $mdThemingProvider.enableBrowserColor({ + * theme: 'myTheme', // Default is 'default' + * palette: 'accent', // Default is 'primary', any basic material palette and extended palettes are available + * hue: '200' // Default is '800' + * }); + * }); + * + */ + +/** + * @ngdoc method + * @name $mdThemingProvider#registerStyles + * @param {string} styles The styles to be appended to Angular Material's built in theme css. + */ +/** + * @ngdoc method + * @name $mdThemingProvider#setNonce + * @param {string} nonceValue The nonce to be added as an attribute to the theme style tags. + * Setting a value allows the use of CSP policy without using the unsafe-inline directive. + */ + +/** + * @ngdoc method + * @name $mdThemingProvider#setDefaultTheme + * @param {string} themeName Default theme name to be applied to elements. Default value is `default`. + */ + +/** + * @ngdoc method + * @name $mdThemingProvider#alwaysWatchTheme + * @param {boolean} watch Whether or not to always watch themes for changes and re-apply + * classes when they change. Default is `false`. Enabling can reduce performance. + */ + +/** + * @ngdoc method + * @name $mdThemingProvider#enableBrowserColor + * @param {Object=} options Options object for the browser color
+ * `theme` - A defined theme via `$mdThemeProvider` to use the palettes from. Default is `default` theme.
+ * `palette` - Can be any one of the basic material design palettes, extended defined palettes and 'primary', + * 'accent', 'background' and 'warn'. Default is `primary`.
+ * `hue` - The hue from the selected palette. Default is `800`
+ * @returns {Function} remove function of the browser color + */ + +/* Some Example Valid Theming Expressions + * ======================================= + * + * Intention group expansion: (valid for primary, accent, warn, background) + * + * {{primary-100}} - grab shade 100 from the primary palette + * {{primary-100-0.7}} - grab shade 100, apply opacity of 0.7 + * {{primary-100-contrast}} - grab shade 100's contrast color + * {{primary-hue-1}} - grab the shade assigned to hue-1 from the primary palette + * {{primary-hue-1-0.7}} - apply 0.7 opacity to primary-hue-1 + * {{primary-color}} - Generates .md-hue-1, .md-hue-2, .md-hue-3 with configured shades set for each hue + * {{primary-color-0.7}} - Apply 0.7 opacity to each of the above rules + * {{primary-contrast}} - Generates .md-hue-1, .md-hue-2, .md-hue-3 with configured contrast (ie. text) color shades set for each hue + * {{primary-contrast-0.7}} - Apply 0.7 opacity to each of the above rules + * + * Foreground expansion: Applies rgba to black/white foreground text + * + * {{foreground-1}} - used for primary text + * {{foreground-2}} - used for secondary text/divider + * {{foreground-3}} - used for disabled text + * {{foreground-4}} - used for dividers + * + */ + +// In memory generated CSS rules; registered by theme.name +var GENERATED = { }; + +// In memory storage of defined themes and color palettes (both loaded by CSS, and user specified) +var PALETTES; + +// Text Colors on light and dark backgrounds +// @see https://www.google.com/design/spec/style/color.html#color-text-background-colors +var DARK_FOREGROUND = { + name: 'dark', + '1': 'rgba(0,0,0,0.87)', + '2': 'rgba(0,0,0,0.54)', + '3': 'rgba(0,0,0,0.38)', + '4': 'rgba(0,0,0,0.12)' +}; +var LIGHT_FOREGROUND = { + name: 'light', + '1': 'rgba(255,255,255,1.0)', + '2': 'rgba(255,255,255,0.7)', + '3': 'rgba(255,255,255,0.5)', + '4': 'rgba(255,255,255,0.12)' +}; + +var DARK_SHADOW = '1px 1px 0px rgba(0,0,0,0.4), -1px -1px 0px rgba(0,0,0,0.4)'; +var LIGHT_SHADOW = ''; + +var DARK_CONTRAST_COLOR = colorToRgbaArray('rgba(0,0,0,0.87)'); +var LIGHT_CONTRAST_COLOR = colorToRgbaArray('rgba(255,255,255,0.87)'); +var STRONG_LIGHT_CONTRAST_COLOR = colorToRgbaArray('rgb(255,255,255)'); + +var THEME_COLOR_TYPES = ['primary', 'accent', 'warn', 'background']; +var DEFAULT_COLOR_TYPE = 'primary'; + +// A color in a theme will use these hues by default, if not specified by user. +var LIGHT_DEFAULT_HUES = { + 'accent': { + 'default': 'A200', + 'hue-1': 'A100', + 'hue-2': 'A400', + 'hue-3': 'A700' + }, + 'background': { + 'default': '50', + 'hue-1': 'A100', + 'hue-2': '100', + 'hue-3': '300' + } +}; + +var DARK_DEFAULT_HUES = { + 'background': { + 'default': 'A400', + 'hue-1': '800', + 'hue-2': '900', + 'hue-3': 'A200' + } +}; +THEME_COLOR_TYPES.forEach(function(colorType) { + // Color types with unspecified default hues will use these default hue values + var defaultDefaultHues = { + 'default': '500', + 'hue-1': '300', + 'hue-2': '800', + 'hue-3': 'A100' + }; + if (!LIGHT_DEFAULT_HUES[colorType]) LIGHT_DEFAULT_HUES[colorType] = defaultDefaultHues; + if (!DARK_DEFAULT_HUES[colorType]) DARK_DEFAULT_HUES[colorType] = defaultDefaultHues; +}); + +var VALID_HUE_VALUES = [ + '50', '100', '200', '300', '400', '500', '600', + '700', '800', '900', 'A100', 'A200', 'A400', 'A700' +]; + +var themeConfig = { + disableTheming : false, // Generate our themes at run time; also disable stylesheet DOM injection + generateOnDemand : false, // Whether or not themes are to be generated on-demand (vs. eagerly). + registeredStyles : [], // Custom styles registered to be used in the theming of custom components. + nonce : null // Nonce to be added as an attribute to the generated themes style tags. +}; + +/** + * + */ +function ThemingProvider($mdColorPalette, $$mdMetaProvider) { + ThemingService.$inject = ["$rootScope", "$mdUtil", "$q", "$log"]; + PALETTES = { }; + var THEMES = { }; + + var themingProvider; + + var alwaysWatchTheme = false; + var defaultTheme = 'default'; + + // Load JS Defined Palettes + angular.extend(PALETTES, $mdColorPalette); + + // Default theme defined in core.js + + /** + * Adds `theme-color` and `msapplication-navbutton-color` meta tags with the color parameter + * @param {string} color Hex value of the wanted browser color + * @returns {Function} Remove function of the meta tags + */ + var setBrowserColor = function (color) { + // Chrome, Firefox OS and Opera + var removeChrome = $$mdMetaProvider.setMeta('theme-color', color); + // Windows Phone + var removeWindows = $$mdMetaProvider.setMeta('msapplication-navbutton-color', color); + + return function () { + removeChrome(); + removeWindows(); + }; + }; + + /** + * Enables browser header coloring + * for more info please visit: + * https://developers.google.com/web/fundamentals/design-and-ui/browser-customization/theme-color + * + * The default color is `800` from `primary` palette of the `default` theme + * + * options are: + * `theme` - A defined theme via `$mdThemeProvider` to use the palettes from. Default is `default` theme + * `palette` - Can be any one of the basic material design palettes, extended defined palettes and 'primary', + * 'accent', 'background' and 'warn'. Default is `primary` + * `hue` - The hue from the selected palette. Default is `800` + * + * @param {Object=} options Options object for the browser color + * @returns {Function} remove function of the browser color + */ + var enableBrowserColor = function (options) { + options = angular.isObject(options) ? options : {}; + + var theme = options.theme || 'default'; + var hue = options.hue || '800'; + + var palette = PALETTES[options.palette] || + PALETTES[THEMES[theme].colors[options.palette || 'primary'].name]; + + var color = angular.isObject(palette[hue]) ? palette[hue].hex : palette[hue]; + + return setBrowserColor(color); + }; + + return themingProvider = { + definePalette: definePalette, + extendPalette: extendPalette, + theme: registerTheme, + + /** + * return a read-only clone of the current theme configuration + */ + configuration : function() { + return angular.extend( { }, themeConfig, { + defaultTheme : defaultTheme, + alwaysWatchTheme : alwaysWatchTheme, + registeredStyles : [].concat(themeConfig.registeredStyles) + }); + }, + + /** + * Easy way to disable theming without having to use + * `.constant("$MD_THEME_CSS","");` This disables + * all dynamic theme style sheet generations and injections... + */ + disableTheming: function(isDisabled) { + themeConfig.disableTheming = angular.isUndefined(isDisabled) || !!isDisabled; + }, + + registerStyles: function(styles) { + themeConfig.registeredStyles.push(styles); + }, + + setNonce: function(nonceValue) { + themeConfig.nonce = nonceValue; + }, + + generateThemesOnDemand: function(onDemand) { + themeConfig.generateOnDemand = onDemand; + }, + + setDefaultTheme: function(theme) { + defaultTheme = theme; + }, + + alwaysWatchTheme: function(alwaysWatch) { + alwaysWatchTheme = alwaysWatch; + }, + + enableBrowserColor: enableBrowserColor, + + $get: ThemingService, + _LIGHT_DEFAULT_HUES: LIGHT_DEFAULT_HUES, + _DARK_DEFAULT_HUES: DARK_DEFAULT_HUES, + _PALETTES: PALETTES, + _THEMES: THEMES, + _parseRules: parseRules, + _rgba: rgba + }; + + // Example: $mdThemingProvider.definePalette('neonRed', { 50: '#f5fafa', ... }); + function definePalette(name, map) { + map = map || {}; + PALETTES[name] = checkPaletteValid(name, map); + return themingProvider; + } + + // Returns an new object which is a copy of a given palette `name` with variables from + // `map` overwritten + // Example: var neonRedMap = $mdThemingProvider.extendPalette('red', { 50: '#f5fafafa' }); + function extendPalette(name, map) { + return checkPaletteValid(name, angular.extend({}, PALETTES[name] || {}, map) ); + } + + // Make sure that palette has all required hues + function checkPaletteValid(name, map) { + var missingColors = VALID_HUE_VALUES.filter(function(field) { + return !map[field]; + }); + if (missingColors.length) { + throw new Error("Missing colors %1 in palette %2!" + .replace('%1', missingColors.join(', ')) + .replace('%2', name)); + } + + return map; + } + + // Register a theme (which is a collection of color palettes to use with various states + // ie. warn, accent, primary ) + // Optionally inherit from an existing theme + // $mdThemingProvider.theme('custom-theme').primaryPalette('red'); + function registerTheme(name, inheritFrom) { + if (THEMES[name]) return THEMES[name]; + + inheritFrom = inheritFrom || 'default'; + + var parentTheme = typeof inheritFrom === 'string' ? THEMES[inheritFrom] : inheritFrom; + var theme = new Theme(name); + + if (parentTheme) { + angular.forEach(parentTheme.colors, function(color, colorType) { + theme.colors[colorType] = { + name: color.name, + // Make sure a COPY of the hues is given to the child color, + // not the same reference. + hues: angular.extend({}, color.hues) + }; + }); + } + THEMES[name] = theme; + + return theme; + } + + function Theme(name) { + var self = this; + self.name = name; + self.colors = {}; + + self.dark = setDark; + setDark(false); + + function setDark(isDark) { + isDark = arguments.length === 0 ? true : !!isDark; + + // If no change, abort + if (isDark === self.isDark) return; + + self.isDark = isDark; + + self.foregroundPalette = self.isDark ? LIGHT_FOREGROUND : DARK_FOREGROUND; + self.foregroundShadow = self.isDark ? DARK_SHADOW : LIGHT_SHADOW; + + // Light and dark themes have different default hues. + // Go through each existing color type for this theme, and for every + // hue value that is still the default hue value from the previous light/dark setting, + // set it to the default hue value from the new light/dark setting. + var newDefaultHues = self.isDark ? DARK_DEFAULT_HUES : LIGHT_DEFAULT_HUES; + var oldDefaultHues = self.isDark ? LIGHT_DEFAULT_HUES : DARK_DEFAULT_HUES; + angular.forEach(newDefaultHues, function(newDefaults, colorType) { + var color = self.colors[colorType]; + var oldDefaults = oldDefaultHues[colorType]; + if (color) { + for (var hueName in color.hues) { + if (color.hues[hueName] === oldDefaults[hueName]) { + color.hues[hueName] = newDefaults[hueName]; + } + } + } + }); + + return self; + } + + THEME_COLOR_TYPES.forEach(function(colorType) { + var defaultHues = (self.isDark ? DARK_DEFAULT_HUES : LIGHT_DEFAULT_HUES)[colorType]; + self[colorType + 'Palette'] = function setPaletteType(paletteName, hues) { + var color = self.colors[colorType] = { + name: paletteName, + hues: angular.extend({}, defaultHues, hues) + }; + + Object.keys(color.hues).forEach(function(name) { + if (!defaultHues[name]) { + throw new Error("Invalid hue name '%1' in theme %2's %3 color %4. Available hue names: %4" + .replace('%1', name) + .replace('%2', self.name) + .replace('%3', paletteName) + .replace('%4', Object.keys(defaultHues).join(', ')) + ); + } + }); + Object.keys(color.hues).map(function(key) { + return color.hues[key]; + }).forEach(function(hueValue) { + if (VALID_HUE_VALUES.indexOf(hueValue) == -1) { + throw new Error("Invalid hue value '%1' in theme %2's %3 color %4. Available hue values: %5" + .replace('%1', hueValue) + .replace('%2', self.name) + .replace('%3', colorType) + .replace('%4', paletteName) + .replace('%5', VALID_HUE_VALUES.join(', ')) + ); + } + }); + return self; + }; + + self[colorType + 'Color'] = function() { + var args = Array.prototype.slice.call(arguments); + console.warn('$mdThemingProviderTheme.' + colorType + 'Color() has been deprecated. ' + + 'Use $mdThemingProviderTheme.' + colorType + 'Palette() instead.'); + return self[colorType + 'Palette'].apply(self, args); + }; + }); + } + + /** + * @ngdoc service + * @name $mdTheming + * @module material.core.theming + * + * @description + * + * Service that makes an element apply theming related classes to itself. + * + * + * app.directive('myFancyDirective', function($mdTheming) { + * return { + * restrict: 'e', + * link: function(scope, el, attrs) { + * $mdTheming(el); + * } + * }; + * }); + * + * @param {element=} element to apply theming to + */ + + /** + * @ngdoc property + * @name $mdTheming#THEMES + * @description + * Property to get all the themes defined + * @returns {Object} All the themes defined with their properties + */ + + /** + * @ngdoc property + * @name $mdTheming#PALETTES + * @description + * Property to get all the palettes defined + * @returns {Object} All the palettes defined with their colors + */ + + /** + * @ngdoc method + * @name $mdTheming#registered + * @description + * Determine is specified theme name is a valid, registered theme + * @param {string} themeName the theme to check if registered + * @returns {boolean} whether the theme is registered or not + */ + + /** + * @ngdoc method + * @name $mdTheming#defaultTheme + * @description + * Returns the default theme + * @returns {string} The default theme + */ + + /** + * @ngdoc method + * @name $mdTheming#generateTheme + * @description + * Lazy generate themes - by default, every theme is generated when defined. + * You can disable this in the configuration section using the + * `$mdThemingProvider.generateThemesOnDemand(true);` + * + * The theme name that is passed in must match the name of the theme that was defined as part of the configuration block. + * + * @param name {string} theme name to generate + */ + + /** + * @ngdoc method + * @name $mdTheming#setBrowserColor + * @description + * Sets browser header coloring + * for more info please visit: + * https://developers.google.com/web/fundamentals/design-and-ui/browser-customization/theme-color + * + * The default color is `800` from `primary` palette of the `default` theme + * + * options are:
+ * `theme` - A defined theme via `$mdThemeProvider` to use the palettes from. Default is `default` theme.
+ * `palette` - Can be any one of the basic material design palettes, extended defined palettes and 'primary', + * 'accent', 'background' and 'warn'. Default is `primary`
+ * `hue` - The hue from the selected palette. Default is `800` + * + * @param {Object} options Options object for the browser color + * @returns {Function} remove function of the browser color + */ + + /* @ngInject */ + function ThemingService($rootScope, $mdUtil, $q, $log) { + // Allow us to be invoked via a linking function signature. + var applyTheme = function (scope, el) { + if (el === undefined) { el = scope; scope = undefined; } + if (scope === undefined) { scope = $rootScope; } + applyTheme.inherit(el, el); + }; + + Object.defineProperty(applyTheme, 'THEMES', { + get: function () { + return angular.extend({}, THEMES); + } + }); + Object.defineProperty(applyTheme, 'PALETTES', { + get: function () { + return angular.extend({}, PALETTES); + } + }); + Object.defineProperty(applyTheme, 'ALWAYS_WATCH', { + get: function () { + return alwaysWatchTheme; + } + }); + applyTheme.inherit = inheritTheme; + applyTheme.registered = registered; + applyTheme.defaultTheme = function() { return defaultTheme; }; + applyTheme.generateTheme = function(name) { generateTheme(THEMES[name], name, themeConfig.nonce); }; + applyTheme.defineTheme = function(name, options) { + options = options || {}; + + var theme = registerTheme(name); + + if (options.primary) { + theme.primaryPalette(options.primary); + } + if (options.accent) { + theme.accentPalette(options.accent); + } + if (options.warn) { + theme.warnPalette(options.warn); + } + if (options.background) { + theme.backgroundPalette(options.background); + } + if (options.dark){ + theme.dark(); + } + + this.generateTheme(name); + + return $q.resolve(name); + }; + applyTheme.setBrowserColor = enableBrowserColor; + + return applyTheme; + + /** + * Determine is specified theme name is a valid, registered theme + */ + function registered(themeName) { + if (themeName === undefined || themeName === '') return true; + return applyTheme.THEMES[themeName] !== undefined; + } + + /** + * Get theme name for the element, then update with Theme CSS class + */ + function inheritTheme (el, parent) { + var ctrl = parent.controller('mdTheme') || el.data('$mdThemeController'); + + updateThemeClass(lookupThemeName()); + + if (ctrl) { + var watchTheme = alwaysWatchTheme || + ctrl.$shouldWatch || + $mdUtil.parseAttributeBoolean(el.attr('md-theme-watch')); + + var unwatch = ctrl.registerChanges(function (name) { + updateThemeClass(name); + + if (!watchTheme) { + unwatch(); + } + else { + el.on('$destroy', unwatch); + } + }); + } + + /** + * Find the theme name from the parent controller or element data + */ + function lookupThemeName() { + // As a few components (dialog) add their controllers later, we should also watch for a controller init. + return ctrl && ctrl.$mdTheme || (defaultTheme == 'default' ? '' : defaultTheme); + } + + /** + * Remove old theme class and apply a new one + * NOTE: if not a valid theme name, then the current name is not changed + */ + function updateThemeClass(theme) { + if (!theme) return; + if (!registered(theme)) { + $log.warn('Attempted to use unregistered theme \'' + theme + '\'. ' + + 'Register it with $mdThemingProvider.theme().'); + } + + var oldTheme = el.data('$mdThemeName'); + if (oldTheme) el.removeClass('md-' + oldTheme +'-theme'); + el.addClass('md-' + theme + '-theme'); + el.data('$mdThemeName', theme); + if (ctrl) { + el.data('$mdThemeController', ctrl); + } + } + } + + } +} + +function ThemingDirective($mdTheming, $interpolate, $parse, $mdUtil, $q, $log) { + return { + priority: 101, // has to be more than 100 to be before interpolation (issue on IE) + link: { + pre: function(scope, el, attrs) { + var registeredCallbacks = []; + + var startSymbol = $interpolate.startSymbol(); + var endSymbol = $interpolate.endSymbol(); + + var theme = attrs.mdTheme.trim(); + + var hasInterpolation = + theme.substr(0, startSymbol.length) === startSymbol && + theme.lastIndexOf(endSymbol) === theme.length - endSymbol.length; + + var oneTimeOperator = '::'; + var oneTimeBind = attrs.mdTheme + .split(startSymbol).join('') + .split(endSymbol).join('') + .trim() + .substr(0, oneTimeOperator.length) === oneTimeOperator; + + var ctrl = { + registerChanges: function (cb, context) { + if (context) { + cb = angular.bind(context, cb); + } + + registeredCallbacks.push(cb); + + return function () { + var index = registeredCallbacks.indexOf(cb); + + if (index > -1) { + registeredCallbacks.splice(index, 1); + } + }; + }, + $setTheme: function (theme) { + if (!$mdTheming.registered(theme)) { + $log.warn('attempted to use unregistered theme \'' + theme + '\''); + } + + ctrl.$mdTheme = theme; + + // Iterating backwards to support unregistering during iteration + // http://stackoverflow.com/a/9882349/890293 + // we don't use `reverse()` of array because it mutates the array and we don't want it to get re-indexed + for (var i = registeredCallbacks.length; i--;) { + registeredCallbacks[i](theme); + } + }, + $shouldWatch: $mdUtil.parseAttributeBoolean(el.attr('md-theme-watch')) || + $mdTheming.ALWAYS_WATCH || + (hasInterpolation && !oneTimeBind) + }; + + el.data('$mdThemeController', ctrl); + + var getTheme = function () { + var interpolation = $interpolate(attrs.mdTheme)(scope); + return $parse(interpolation)(scope) || interpolation; + }; + + var setParsedTheme = function (theme) { + if (typeof theme === 'string') { + return ctrl.$setTheme(theme); + } + + $q.when( angular.isFunction(theme) ? theme() : theme ) + .then(function(name){ + ctrl.$setTheme(name); + }); + }; + + setParsedTheme(getTheme()); + + var unwatch = scope.$watch(getTheme, function(theme) { + if (theme) { + setParsedTheme(theme); + + if (!ctrl.$shouldWatch) { + unwatch(); + } + } + }); + } + } + }; +} + +/** + * Special directive that will disable ALL runtime Theme style generation and DOM injection + * + * + * + * + * + * ... + * + * + * Note: Using md-themes-css directive requires the developer to load external + * theme stylesheets; e.g. custom themes from Material-Tools: + * + * `angular-material.themes.css` + * + * Another option is to use the ThemingProvider to configure and disable the attribute + * conversions; this would obviate the use of the `md-themes-css` directive + * + */ +function disableThemesDirective() { + themeConfig.disableTheming = true; + + // Return a 1x-only, first-match attribute directive + return { + restrict : 'A', + priority : '900' + }; +} + +function ThemableDirective($mdTheming) { + return $mdTheming; +} + +function parseRules(theme, colorType, rules) { + checkValidPalette(theme, colorType); + + rules = rules.replace(/THEME_NAME/g, theme.name); + var generatedRules = []; + var color = theme.colors[colorType]; + + var themeNameRegex = new RegExp('\\.md-' + theme.name + '-theme', 'g'); + // Matches '{{ primary-color }}', etc + var hueRegex = new RegExp('(\'|")?{{\\s*(' + colorType + ')-(color|contrast)-?(\\d\\.?\\d*)?\\s*}}(\"|\')?','g'); + var simpleVariableRegex = /'?"?\{\{\s*([a-zA-Z]+)-(A?\d+|hue\-[0-3]|shadow|default)-?(\d\.?\d*)?(contrast)?\s*\}\}'?"?/g; + var palette = PALETTES[color.name]; + + // find and replace simple variables where we use a specific hue, not an entire palette + // eg. "{{primary-100}}" + //\(' + THEME_COLOR_TYPES.join('\|') + '\)' + rules = rules.replace(simpleVariableRegex, function(match, colorType, hue, opacity, contrast) { + if (colorType === 'foreground') { + if (hue == 'shadow') { + return theme.foregroundShadow; + } else { + return theme.foregroundPalette[hue] || theme.foregroundPalette['1']; + } + } + + // `default` is also accepted as a hue-value, because the background palettes are + // using it as a name for the default hue. + if (hue.indexOf('hue') === 0 || hue === 'default') { + hue = theme.colors[colorType].hues[hue]; + } + + return rgba( (PALETTES[ theme.colors[colorType].name ][hue] || '')[contrast ? 'contrast' : 'value'], opacity ); + }); + + // For each type, generate rules for each hue (ie. default, md-hue-1, md-hue-2, md-hue-3) + angular.forEach(color.hues, function(hueValue, hueName) { + var newRule = rules + .replace(hueRegex, function(match, _, colorType, hueType, opacity) { + return rgba(palette[hueValue][hueType === 'color' ? 'value' : 'contrast'], opacity); + }); + if (hueName !== 'default') { + newRule = newRule.replace(themeNameRegex, '.md-' + theme.name + '-theme.md-' + hueName); + } + + // Don't apply a selector rule to the default theme, making it easier to override + // styles of the base-component + if (theme.name == 'default') { + var themeRuleRegex = /((?:\s|>|\.|\w|-|:|\(|\)|\[|\]|"|'|=)*)\.md-default-theme((?:\s|>|\.|\w|-|:|\(|\)|\[|\]|"|'|=)*)/g; + + newRule = newRule.replace(themeRuleRegex, function(match, start, end) { + return match + ', ' + start + end; + }); + } + generatedRules.push(newRule); + }); + + return generatedRules; +} + +var rulesByType = {}; + +// Generate our themes at run time given the state of THEMES and PALETTES +function generateAllThemes($injector, $mdTheming) { + var head = document.head; + var firstChild = head ? head.firstElementChild : null; + var themeCss = !themeConfig.disableTheming && $injector.has('$MD_THEME_CSS') ? $injector.get('$MD_THEME_CSS') : ''; + + // Append our custom registered styles to the theme stylesheet. + themeCss += themeConfig.registeredStyles.join(''); + + if ( !firstChild ) return; + if (themeCss.length === 0) return; // no rules, so no point in running this expensive task + + // Expose contrast colors for palettes to ensure that text is always readable + angular.forEach(PALETTES, sanitizePalette); + + // MD_THEME_CSS is a string generated by the build process that includes all the themable + // components as templates + + // Break the CSS into individual rules + var rules = themeCss + .split(/\}(?!(\}|'|"|;))/) + .filter(function(rule) { return rule && rule.trim().length; }) + .map(function(rule) { return rule.trim() + '}'; }); + + + var ruleMatchRegex = new RegExp('md-(' + THEME_COLOR_TYPES.join('|') + ')', 'g'); + + THEME_COLOR_TYPES.forEach(function(type) { + rulesByType[type] = ''; + }); + + + // Sort the rules based on type, allowing us to do color substitution on a per-type basis + rules.forEach(function(rule) { + var match = rule.match(ruleMatchRegex); + // First: test that if the rule has '.md-accent', it goes into the accent set of rules + for (var i = 0, type; type = THEME_COLOR_TYPES[i]; i++) { + if (rule.indexOf('.md-' + type) > -1) { + return rulesByType[type] += rule; + } + } + + // If no eg 'md-accent' class is found, try to just find 'accent' in the rule and guess from + // there + for (i = 0; type = THEME_COLOR_TYPES[i]; i++) { + if (rule.indexOf(type) > -1) { + return rulesByType[type] += rule; + } + } + + // Default to the primary array + return rulesByType[DEFAULT_COLOR_TYPE] += rule; + }); + + // If themes are being generated on-demand, quit here. The user will later manually + // call generateTheme to do this on a theme-by-theme basis. + if (themeConfig.generateOnDemand) return; + + angular.forEach($mdTheming.THEMES, function(theme) { + if (!GENERATED[theme.name] && !($mdTheming.defaultTheme() !== 'default' && theme.name === 'default')) { + generateTheme(theme, theme.name, themeConfig.nonce); + } + }); + + + // ************************* + // Internal functions + // ************************* + + // The user specifies a 'default' contrast color as either light or dark, + // then explicitly lists which hues are the opposite contrast (eg. A100 has dark, A200 has light) + function sanitizePalette(palette, name) { + var defaultContrast = palette.contrastDefaultColor; + var lightColors = palette.contrastLightColors || []; + var strongLightColors = palette.contrastStrongLightColors || []; + var darkColors = palette.contrastDarkColors || []; + + // These colors are provided as space-separated lists + if (typeof lightColors === 'string') lightColors = lightColors.split(' '); + if (typeof strongLightColors === 'string') strongLightColors = strongLightColors.split(' '); + if (typeof darkColors === 'string') darkColors = darkColors.split(' '); + + // Cleanup after ourselves + delete palette.contrastDefaultColor; + delete palette.contrastLightColors; + delete palette.contrastStrongLightColors; + delete palette.contrastDarkColors; + + // Change { 'A100': '#fffeee' } to { 'A100': { value: '#fffeee', contrast:DARK_CONTRAST_COLOR } + angular.forEach(palette, function(hueValue, hueName) { + if (angular.isObject(hueValue)) return; // Already converted + // Map everything to rgb colors + var rgbValue = colorToRgbaArray(hueValue); + if (!rgbValue) { + throw new Error("Color %1, in palette %2's hue %3, is invalid. Hex or rgb(a) color expected." + .replace('%1', hueValue) + .replace('%2', palette.name) + .replace('%3', hueName)); + } + + palette[hueName] = { + hex: palette[hueName], + value: rgbValue, + contrast: getContrastColor() + }; + function getContrastColor() { + if (defaultContrast === 'light') { + if (darkColors.indexOf(hueName) > -1) { + return DARK_CONTRAST_COLOR; + } else { + return strongLightColors.indexOf(hueName) > -1 ? STRONG_LIGHT_CONTRAST_COLOR + : LIGHT_CONTRAST_COLOR; + } + } else { + if (lightColors.indexOf(hueName) > -1) { + return strongLightColors.indexOf(hueName) > -1 ? STRONG_LIGHT_CONTRAST_COLOR + : LIGHT_CONTRAST_COLOR; + } else { + return DARK_CONTRAST_COLOR; + } + } + } + }); + } +} + +function generateTheme(theme, name, nonce) { + var head = document.head; + var firstChild = head ? head.firstElementChild : null; + + if (!GENERATED[name]) { + // For each theme, use the color palettes specified for + // `primary`, `warn` and `accent` to generate CSS rules. + THEME_COLOR_TYPES.forEach(function(colorType) { + var styleStrings = parseRules(theme, colorType, rulesByType[colorType]); + while (styleStrings.length) { + var styleContent = styleStrings.shift(); + if (styleContent) { + var style = document.createElement('style'); + style.setAttribute('md-theme-style', ''); + if (nonce) { + style.setAttribute('nonce', nonce); + } + style.appendChild(document.createTextNode(styleContent)); + head.insertBefore(style, firstChild); + } + } + }); + + GENERATED[theme.name] = true; + } + +} + + +function checkValidPalette(theme, colorType) { + // If theme attempts to use a palette that doesnt exist, throw error + if (!PALETTES[ (theme.colors[colorType] || {}).name ]) { + throw new Error( + "You supplied an invalid color palette for theme %1's %2 palette. Available palettes: %3" + .replace('%1', theme.name) + .replace('%2', colorType) + .replace('%3', Object.keys(PALETTES).join(', ')) + ); + } +} + +function colorToRgbaArray(clr) { + if (angular.isArray(clr) && clr.length == 3) return clr; + if (/^rgb/.test(clr)) { + return clr.replace(/(^\s*rgba?\(|\)\s*$)/g, '').split(',').map(function(value, i) { + return i == 3 ? parseFloat(value, 10) : parseInt(value, 10); + }); + } + if (clr.charAt(0) == '#') clr = clr.substring(1); + if (!/^([a-fA-F0-9]{3}){1,2}$/g.test(clr)) return; + + var dig = clr.length / 3; + var red = clr.substr(0, dig); + var grn = clr.substr(dig, dig); + var blu = clr.substr(dig * 2); + if (dig === 1) { + red += red; + grn += grn; + blu += blu; + } + return [parseInt(red, 16), parseInt(grn, 16), parseInt(blu, 16)]; +} + +function rgba(rgbArray, opacity) { + if ( !rgbArray ) return "rgb('0,0,0')"; + + if (rgbArray.length == 4) { + rgbArray = angular.copy(rgbArray); + opacity ? rgbArray.pop() : opacity = rgbArray.pop(); + } + return opacity && (typeof opacity == 'number' || (typeof opacity == 'string' && opacity.length)) ? + 'rgba(' + rgbArray.join(',') + ',' + opacity + ')' : + 'rgb(' + rgbArray.join(',') + ')'; +} + + +})(window.angular); + +})(); +(function(){ +"use strict"; + +// Polyfill angular < 1.4 (provide $animateCss) +angular + .module('material.core') + .factory('$$mdAnimate', ["$q", "$timeout", "$mdConstant", "$animateCss", function($q, $timeout, $mdConstant, $animateCss){ + + // Since $$mdAnimate is injected into $mdUtil... use a wrapper function + // to subsequently inject $mdUtil as an argument to the AnimateDomUtils + + return function($mdUtil) { + return AnimateDomUtils( $mdUtil, $q, $timeout, $mdConstant, $animateCss); + }; + }]); + +/** + * Factory function that requires special injections + */ +function AnimateDomUtils($mdUtil, $q, $timeout, $mdConstant, $animateCss) { + var self; + return self = { + /** + * + */ + translate3d : function( target, from, to, options ) { + return $animateCss(target, { + from: from, + to: to, + addClass: options.transitionInClass, + removeClass: options.transitionOutClass, + duration: options.duration + }) + .start() + .then(function(){ + // Resolve with reverser function... + return reverseTranslate; + }); + + /** + * Specific reversal of the request translate animation above... + */ + function reverseTranslate (newFrom) { + return $animateCss(target, { + to: newFrom || from, + addClass: options.transitionOutClass, + removeClass: options.transitionInClass, + duration: options.duration + }).start(); + + } + }, + + /** + * Listen for transitionEnd event (with optional timeout) + * Announce completion or failure via promise handlers + */ + waitTransitionEnd: function (element, opts) { + var TIMEOUT = 3000; // fallback is 3 secs + + return $q(function(resolve, reject){ + opts = opts || { }; + + // If there is no transition is found, resolve immediately + // + // NOTE: using $mdUtil.nextTick() causes delays/issues + if (noTransitionFound(opts.cachedTransitionStyles)) { + TIMEOUT = 0; + } + + var timer = $timeout(finished, opts.timeout || TIMEOUT); + element.on($mdConstant.CSS.TRANSITIONEND, finished); + + /** + * Upon timeout or transitionEnd, reject or resolve (respectively) this promise. + * NOTE: Make sure this transitionEnd didn't bubble up from a child + */ + function finished(ev) { + if ( ev && ev.target !== element[0]) return; + + if ( ev ) $timeout.cancel(timer); + element.off($mdConstant.CSS.TRANSITIONEND, finished); + + // Never reject since ngAnimate may cause timeouts due missed transitionEnd events + resolve(); + + } + + /** + * Checks whether or not there is a transition. + * + * @param styles The cached styles to use for the calculation. If null, getComputedStyle() + * will be used. + * + * @returns {boolean} True if there is no transition/duration; false otherwise. + */ + function noTransitionFound(styles) { + styles = styles || window.getComputedStyle(element[0]); + + return styles.transitionDuration == '0s' || (!styles.transition && !styles.transitionProperty); + } + + }); + }, + + calculateTransformValues: function (element, originator) { + var origin = originator.element; + var bounds = originator.bounds; + + if (origin || bounds) { + var originBnds = origin ? self.clientRect(origin) || currentBounds() : self.copyRect(bounds); + var dialogRect = self.copyRect(element[0].getBoundingClientRect()); + var dialogCenterPt = self.centerPointFor(dialogRect); + var originCenterPt = self.centerPointFor(originBnds); + + return { + centerX: originCenterPt.x - dialogCenterPt.x, + centerY: originCenterPt.y - dialogCenterPt.y, + scaleX: Math.round(100 * Math.min(0.5, originBnds.width / dialogRect.width)) / 100, + scaleY: Math.round(100 * Math.min(0.5, originBnds.height / dialogRect.height)) / 100 + }; + } + return {centerX: 0, centerY: 0, scaleX: 0.5, scaleY: 0.5}; + + /** + * This is a fallback if the origin information is no longer valid, then the + * origin bounds simply becomes the current bounds for the dialogContainer's parent + */ + function currentBounds() { + var cntr = element ? element.parent() : null; + var parent = cntr ? cntr.parent() : null; + + return parent ? self.clientRect(parent) : null; + } + }, + + /** + * Calculate the zoom transform from dialog to origin. + * + * We use this to set the dialog position immediately; + * then the md-transition-in actually translates back to + * `translate3d(0,0,0) scale(1.0)`... + * + * NOTE: all values are rounded to the nearest integer + */ + calculateZoomToOrigin: function (element, originator) { + var zoomTemplate = "translate3d( {centerX}px, {centerY}px, 0 ) scale( {scaleX}, {scaleY} )"; + var buildZoom = angular.bind(null, $mdUtil.supplant, zoomTemplate); + + return buildZoom(self.calculateTransformValues(element, originator)); + }, + + /** + * Calculate the slide transform from panel to origin. + * NOTE: all values are rounded to the nearest integer + */ + calculateSlideToOrigin: function (element, originator) { + var slideTemplate = "translate3d( {centerX}px, {centerY}px, 0 )"; + var buildSlide = angular.bind(null, $mdUtil.supplant, slideTemplate); + + return buildSlide(self.calculateTransformValues(element, originator)); + }, + + /** + * Enhance raw values to represent valid css stylings... + */ + toCss : function( raw ) { + var css = { }; + var lookups = 'left top right bottom width height x y min-width min-height max-width max-height'; + + angular.forEach(raw, function(value,key) { + if ( angular.isUndefined(value) ) return; + + if ( lookups.indexOf(key) >= 0 ) { + css[key] = value + 'px'; + } else { + switch (key) { + case 'transition': + convertToVendor(key, $mdConstant.CSS.TRANSITION, value); + break; + case 'transform': + convertToVendor(key, $mdConstant.CSS.TRANSFORM, value); + break; + case 'transformOrigin': + convertToVendor(key, $mdConstant.CSS.TRANSFORM_ORIGIN, value); + break; + case 'font-size': + css['font-size'] = value; // font sizes aren't always in px + break; + } + } + }); + + return css; + + function convertToVendor(key, vendor, value) { + angular.forEach(vendor.split(' '), function (key) { + css[key] = value; + }); + } + }, + + /** + * Convert the translate CSS value to key/value pair(s). + */ + toTransformCss: function (transform, addTransition, transition) { + var css = {}; + angular.forEach($mdConstant.CSS.TRANSFORM.split(' '), function (key) { + css[key] = transform; + }); + + if (addTransition) { + transition = transition || "all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) !important"; + css.transition = transition; + } + + return css; + }, + + /** + * Clone the Rect and calculate the height/width if needed + */ + copyRect: function (source, destination) { + if (!source) return null; + + destination = destination || {}; + + angular.forEach('left top right bottom width height'.split(' '), function (key) { + destination[key] = Math.round(source[key]); + }); + + destination.width = destination.width || (destination.right - destination.left); + destination.height = destination.height || (destination.bottom - destination.top); + + return destination; + }, + + /** + * Calculate ClientRect of element; return null if hidden or zero size + */ + clientRect: function (element) { + var bounds = angular.element(element)[0].getBoundingClientRect(); + var isPositiveSizeClientRect = function (rect) { + return rect && (rect.width > 0) && (rect.height > 0); + }; + + // If the event origin element has zero size, it has probably been hidden. + return isPositiveSizeClientRect(bounds) ? self.copyRect(bounds) : null; + }, + + /** + * Calculate 'rounded' center point of Rect + */ + centerPointFor: function (targetRect) { + return targetRect ? { + x: Math.round(targetRect.left + (targetRect.width / 2)), + y: Math.round(targetRect.top + (targetRect.height / 2)) + } : { x : 0, y : 0 }; + } + + }; +} + + +})(); +(function(){ +"use strict"; + +if (angular.version.minor >= 4) { + angular.module('material.core.animate', []); +} else { +(function() { + "use strict"; + + var forEach = angular.forEach; + + var WEBKIT = angular.isDefined(document.documentElement.style.WebkitAppearance); + var TRANSITION_PROP = WEBKIT ? 'WebkitTransition' : 'transition'; + var ANIMATION_PROP = WEBKIT ? 'WebkitAnimation' : 'animation'; + var PREFIX = WEBKIT ? '-webkit-' : ''; + + var TRANSITION_EVENTS = (WEBKIT ? 'webkitTransitionEnd ' : '') + 'transitionend'; + var ANIMATION_EVENTS = (WEBKIT ? 'webkitAnimationEnd ' : '') + 'animationend'; + + var $$ForceReflowFactory = ['$document', function($document) { + return function() { + return $document[0].body.clientWidth + 1; + }; + }]; + + var $$rAFMutexFactory = ['$$rAF', function($$rAF) { + return function() { + var passed = false; + $$rAF(function() { + passed = true; + }); + return function(fn) { + passed ? fn() : $$rAF(fn); + }; + }; + }]; + + var $$AnimateRunnerFactory = ['$q', '$$rAFMutex', function($q, $$rAFMutex) { + var INITIAL_STATE = 0; + var DONE_PENDING_STATE = 1; + var DONE_COMPLETE_STATE = 2; + + function AnimateRunner(host) { + this.setHost(host); + + this._doneCallbacks = []; + this._runInAnimationFrame = $$rAFMutex(); + this._state = 0; + } + + AnimateRunner.prototype = { + setHost: function(host) { + this.host = host || {}; + }, + + done: function(fn) { + if (this._state === DONE_COMPLETE_STATE) { + fn(); + } else { + this._doneCallbacks.push(fn); + } + }, + + progress: angular.noop, + + getPromise: function() { + if (!this.promise) { + var self = this; + this.promise = $q(function(resolve, reject) { + self.done(function(status) { + status === false ? reject() : resolve(); + }); + }); + } + return this.promise; + }, + + then: function(resolveHandler, rejectHandler) { + return this.getPromise().then(resolveHandler, rejectHandler); + }, + + 'catch': function(handler) { + return this.getPromise()['catch'](handler); + }, + + 'finally': function(handler) { + return this.getPromise()['finally'](handler); + }, + + pause: function() { + if (this.host.pause) { + this.host.pause(); + } + }, + + resume: function() { + if (this.host.resume) { + this.host.resume(); + } + }, + + end: function() { + if (this.host.end) { + this.host.end(); + } + this._resolve(true); + }, + + cancel: function() { + if (this.host.cancel) { + this.host.cancel(); + } + this._resolve(false); + }, + + complete: function(response) { + var self = this; + if (self._state === INITIAL_STATE) { + self._state = DONE_PENDING_STATE; + self._runInAnimationFrame(function() { + self._resolve(response); + }); + } + }, + + _resolve: function(response) { + if (this._state !== DONE_COMPLETE_STATE) { + forEach(this._doneCallbacks, function(fn) { + fn(response); + }); + this._doneCallbacks.length = 0; + this._state = DONE_COMPLETE_STATE; + } + } + }; + + // Polyfill AnimateRunner.all which is used by input animations + AnimateRunner.all = function(runners, callback) { + var count = 0; + var status = true; + forEach(runners, function(runner) { + runner.done(onProgress); + }); + + function onProgress(response) { + status = status && response; + if (++count === runners.length) { + callback(status); + } + } + }; + + return AnimateRunner; + }]; + + angular + .module('material.core.animate', []) + .factory('$$forceReflow', $$ForceReflowFactory) + .factory('$$AnimateRunner', $$AnimateRunnerFactory) + .factory('$$rAFMutex', $$rAFMutexFactory) + .factory('$animateCss', ['$window', '$$rAF', '$$AnimateRunner', '$$forceReflow', '$$jqLite', '$timeout', '$animate', + function($window, $$rAF, $$AnimateRunner, $$forceReflow, $$jqLite, $timeout, $animate) { + + function init(element, options) { + + var temporaryStyles = []; + var node = getDomNode(element); + var areAnimationsAllowed = node && $animate.enabled(); + + var hasCompleteStyles = false; + var hasCompleteClasses = false; + + if (areAnimationsAllowed) { + if (options.transitionStyle) { + temporaryStyles.push([PREFIX + 'transition', options.transitionStyle]); + } + + if (options.keyframeStyle) { + temporaryStyles.push([PREFIX + 'animation', options.keyframeStyle]); + } + + if (options.delay) { + temporaryStyles.push([PREFIX + 'transition-delay', options.delay + 's']); + } + + if (options.duration) { + temporaryStyles.push([PREFIX + 'transition-duration', options.duration + 's']); + } + + hasCompleteStyles = options.keyframeStyle || + (options.to && (options.duration > 0 || options.transitionStyle)); + hasCompleteClasses = !!options.addClass || !!options.removeClass; + + blockTransition(element, true); + } + + var hasCompleteAnimation = areAnimationsAllowed && (hasCompleteStyles || hasCompleteClasses); + + applyAnimationFromStyles(element, options); + + var animationClosed = false; + var events, eventFn; + + return { + close: $window.close, + start: function() { + var runner = new $$AnimateRunner(); + waitUntilQuiet(function() { + blockTransition(element, false); + if (!hasCompleteAnimation) { + return close(); + } + + forEach(temporaryStyles, function(entry) { + var key = entry[0]; + var value = entry[1]; + node.style[camelCase(key)] = value; + }); + + applyClasses(element, options); + + var timings = computeTimings(element); + if (timings.duration === 0) { + return close(); + } + + var moreStyles = []; + + if (options.easing) { + if (timings.transitionDuration) { + moreStyles.push([PREFIX + 'transition-timing-function', options.easing]); + } + if (timings.animationDuration) { + moreStyles.push([PREFIX + 'animation-timing-function', options.easing]); + } + } + + if (options.delay && timings.animationDelay) { + moreStyles.push([PREFIX + 'animation-delay', options.delay + 's']); + } + + if (options.duration && timings.animationDuration) { + moreStyles.push([PREFIX + 'animation-duration', options.duration + 's']); + } + + forEach(moreStyles, function(entry) { + var key = entry[0]; + var value = entry[1]; + node.style[camelCase(key)] = value; + temporaryStyles.push(entry); + }); + + var maxDelay = timings.delay; + var maxDelayTime = maxDelay * 1000; + var maxDuration = timings.duration; + var maxDurationTime = maxDuration * 1000; + var startTime = Date.now(); + + events = []; + if (timings.transitionDuration) { + events.push(TRANSITION_EVENTS); + } + if (timings.animationDuration) { + events.push(ANIMATION_EVENTS); + } + events = events.join(' '); + eventFn = function(event) { + event.stopPropagation(); + var ev = event.originalEvent || event; + var timeStamp = ev.timeStamp || Date.now(); + var elapsedTime = parseFloat(ev.elapsedTime.toFixed(3)); + if (Math.max(timeStamp - startTime, 0) >= maxDelayTime && elapsedTime >= maxDuration) { + close(); + } + }; + element.on(events, eventFn); + + applyAnimationToStyles(element, options); + + $timeout(close, maxDelayTime + maxDurationTime * 1.5, false); + }); + + return runner; + + function close() { + if (animationClosed) return; + animationClosed = true; + + if (events && eventFn) { + element.off(events, eventFn); + } + applyClasses(element, options); + applyAnimationStyles(element, options); + forEach(temporaryStyles, function(entry) { + node.style[camelCase(entry[0])] = ''; + }); + runner.complete(true); + return runner; + } + } + }; + } + + function applyClasses(element, options) { + if (options.addClass) { + $$jqLite.addClass(element, options.addClass); + options.addClass = null; + } + if (options.removeClass) { + $$jqLite.removeClass(element, options.removeClass); + options.removeClass = null; + } + } + + function computeTimings(element) { + var node = getDomNode(element); + var cs = $window.getComputedStyle(node); + var tdr = parseMaxTime(cs[prop('transitionDuration')]); + var adr = parseMaxTime(cs[prop('animationDuration')]); + var tdy = parseMaxTime(cs[prop('transitionDelay')]); + var ady = parseMaxTime(cs[prop('animationDelay')]); + + adr *= (parseInt(cs[prop('animationIterationCount')], 10) || 1); + var duration = Math.max(adr, tdr); + var delay = Math.max(ady, tdy); + + return { + duration: duration, + delay: delay, + animationDuration: adr, + transitionDuration: tdr, + animationDelay: ady, + transitionDelay: tdy + }; + + function prop(key) { + return WEBKIT ? 'Webkit' + key.charAt(0).toUpperCase() + key.substr(1) + : key; + } + } + + function parseMaxTime(str) { + var maxValue = 0; + var values = (str || "").split(/\s*,\s*/); + forEach(values, function(value) { + // it's always safe to consider only second values and omit `ms` values since + // getComputedStyle will always handle the conversion for us + if (value.charAt(value.length - 1) == 's') { + value = value.substring(0, value.length - 1); + } + value = parseFloat(value) || 0; + maxValue = maxValue ? Math.max(value, maxValue) : value; + }); + return maxValue; + } + + var cancelLastRAFRequest; + var rafWaitQueue = []; + function waitUntilQuiet(callback) { + if (cancelLastRAFRequest) { + cancelLastRAFRequest(); //cancels the request + } + rafWaitQueue.push(callback); + cancelLastRAFRequest = $$rAF(function() { + cancelLastRAFRequest = null; + + // DO NOT REMOVE THIS LINE OR REFACTOR OUT THE `pageWidth` variable. + // PLEASE EXAMINE THE `$$forceReflow` service to understand why. + var pageWidth = $$forceReflow(); + + // we use a for loop to ensure that if the queue is changed + // during this looping then it will consider new requests + for (var i = 0; i < rafWaitQueue.length; i++) { + rafWaitQueue[i](pageWidth); + } + rafWaitQueue.length = 0; + }); + } + + function applyAnimationStyles(element, options) { + applyAnimationFromStyles(element, options); + applyAnimationToStyles(element, options); + } + + function applyAnimationFromStyles(element, options) { + if (options.from) { + element.css(options.from); + options.from = null; + } + } + + function applyAnimationToStyles(element, options) { + if (options.to) { + element.css(options.to); + options.to = null; + } + } + + function getDomNode(element) { + for (var i = 0; i < element.length; i++) { + if (element[i].nodeType === 1) return element[i]; + } + } + + function blockTransition(element, bool) { + var node = getDomNode(element); + var key = camelCase(PREFIX + 'transition-delay'); + node.style[key] = bool ? '-9999s' : ''; + } + + return init; + }]); + + /** + * Older browsers [FF31] expect camelCase + * property keys. + * e.g. + * animation-duration --> animationDuration + */ + function camelCase(str) { + return str.replace(/-[a-z]/g, function(str) { + return str.charAt(1).toUpperCase(); + }); + } + +})(); + +} + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.autocomplete + */ +/* + * @see js folder for autocomplete implementation + */ +angular.module('material.components.autocomplete', [ + 'material.core', + 'material.components.icon', + 'material.components.virtualRepeat' +]); + +})(); +(function(){ +"use strict"; + +/* + * @ngdoc module + * @name material.components.backdrop + * @description Backdrop + */ + +/** + * @ngdoc directive + * @name mdBackdrop + * @module material.components.backdrop + * + * @restrict E + * + * @description + * `` is a backdrop element used by other components, such as dialog and bottom sheet. + * Apply class `opaque` to make the backdrop use the theme backdrop color. + * + */ + +angular + .module('material.components.backdrop', ['material.core']) + .directive('mdBackdrop', ["$mdTheming", "$mdUtil", "$animate", "$rootElement", "$window", "$log", "$$rAF", "$document", function BackdropDirective($mdTheming, $mdUtil, $animate, $rootElement, $window, $log, $$rAF, $document) { + var ERROR_CSS_POSITION = ' may not work properly in a scrolled, static-positioned parent container.'; + + return { + restrict: 'E', + link: postLink + }; + + function postLink(scope, element, attrs) { + // backdrop may be outside the $rootElement, tell ngAnimate to animate regardless + if ($animate.pin) $animate.pin(element, $rootElement); + + var bodyStyles; + + $$rAF(function() { + // If body scrolling has been disabled using mdUtil.disableBodyScroll(), + // adjust the 'backdrop' height to account for the fixed 'body' top offset. + // Note that this can be pretty expensive and is better done inside the $$rAF. + bodyStyles = $window.getComputedStyle($document[0].body); + + if (bodyStyles.position === 'fixed') { + var resizeHandler = $mdUtil.debounce(function(){ + bodyStyles = $window.getComputedStyle($document[0].body); + resize(); + }, 60, null, false); + + resize(); + angular.element($window).on('resize', resizeHandler); + + scope.$on('$destroy', function() { + angular.element($window).off('resize', resizeHandler); + }); + } + + // Often $animate.enter() is used to append the backDrop element + // so let's wait until $animate is done... + var parent = element.parent(); + + if (parent.length) { + if (parent[0].nodeName === 'BODY') { + element.css('position', 'fixed'); + } + + var styles = $window.getComputedStyle(parent[0]); + + if (styles.position === 'static') { + // backdrop uses position:absolute and will not work properly with parent position:static (default) + $log.warn(ERROR_CSS_POSITION); + } + + // Only inherit the parent if the backdrop has a parent. + $mdTheming.inherit(element, parent); + } + }); + + function resize() { + var viewportHeight = parseInt(bodyStyles.height, 10) + Math.abs(parseInt(bodyStyles.top, 10)); + element.css('height', viewportHeight + 'px'); + } + } + + }]); + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.bottomSheet + * @description + * BottomSheet + */ +MdBottomSheetDirective.$inject = ["$mdBottomSheet"]; +MdBottomSheetProvider.$inject = ["$$interimElementProvider"]; +angular + .module('material.components.bottomSheet', [ + 'material.core', + 'material.components.backdrop' + ]) + .directive('mdBottomSheet', MdBottomSheetDirective) + .provider('$mdBottomSheet', MdBottomSheetProvider); + +/* @ngInject */ +function MdBottomSheetDirective($mdBottomSheet) { + return { + restrict: 'E', + link : function postLink(scope, element) { + element.addClass('_md'); // private md component indicator for styling + + // When navigation force destroys an interimElement, then + // listen and $destroy() that interim instance... + scope.$on('$destroy', function() { + $mdBottomSheet.destroy(); + }); + } + }; +} + + +/** + * @ngdoc service + * @name $mdBottomSheet + * @module material.components.bottomSheet + * + * @description + * `$mdBottomSheet` opens a bottom sheet over the app and provides a simple promise API. + * + * ## Restrictions + * + * - The bottom sheet's template must have an outer `` element. + * - Add the `md-grid` class to the bottom sheet for a grid layout. + * - Add the `md-list` class to the bottom sheet for a list layout. + * + * @usage + * + *
+ * + * Open a Bottom Sheet! + * + *
+ *
+ * + * var app = angular.module('app', ['ngMaterial']); + * app.controller('MyController', function($scope, $mdBottomSheet) { + * $scope.openBottomSheet = function() { + * $mdBottomSheet.show({ + * template: 'Hello!' + * }); + * }; + * }); + * + */ + + /** + * @ngdoc method + * @name $mdBottomSheet#show + * + * @description + * Show a bottom sheet with the specified options. + * + * @param {object} options An options object, with the following properties: + * + * - `templateUrl` - `{string=}`: The url of an html template file that will + * be used as the content of the bottom sheet. Restrictions: the template must + * have an outer `md-bottom-sheet` element. + * - `template` - `{string=}`: Same as templateUrl, except this is an actual + * template string. + * - `scope` - `{object=}`: the scope to link the template / controller to. If none is specified, it will create a new child scope. + * This scope will be destroyed when the bottom sheet is removed unless `preserveScope` is set to true. + * - `preserveScope` - `{boolean=}`: whether to preserve the scope when the element is removed. Default is false + * - `controller` - `{string=}`: The controller to associate with this bottom sheet. + * - `locals` - `{string=}`: An object containing key/value pairs. The keys will + * be used as names of values to inject into the controller. For example, + * `locals: {three: 3}` would inject `three` into the controller with the value + * of 3. + * - `clickOutsideToClose` - `{boolean=}`: Whether the user can click outside the bottom sheet to + * close it. Default true. + * - `bindToController` - `{boolean=}`: When set to true, the locals will be bound to the controller instance. + * - `disableBackdrop` - `{boolean=}`: When set to true, the bottomsheet will not show a backdrop. + * - `escapeToClose` - `{boolean=}`: Whether the user can press escape to close the bottom sheet. + * Default true. + * - `resolve` - `{object=}`: Similar to locals, except it takes promises as values + * and the bottom sheet will not open until the promises resolve. + * - `controllerAs` - `{string=}`: An alias to assign the controller to on the scope. + * - `parent` - `{element=}`: The element to append the bottom sheet to. The `parent` may be a `function`, `string`, + * `object`, or null. Defaults to appending to the body of the root element (or the root element) of the application. + * e.g. angular.element(document.getElementById('content')) or "#content" + * - `disableParentScroll` - `{boolean=}`: Whether to disable scrolling while the bottom sheet is open. + * Default true. + * + * @returns {promise} A promise that can be resolved with `$mdBottomSheet.hide()` or + * rejected with `$mdBottomSheet.cancel()`. + */ + +/** + * @ngdoc method + * @name $mdBottomSheet#hide + * + * @description + * Hide the existing bottom sheet and resolve the promise returned from + * `$mdBottomSheet.show()`. This call will close the most recently opened/current bottomsheet (if any). + * + * @param {*=} response An argument for the resolved promise. + * + */ + +/** + * @ngdoc method + * @name $mdBottomSheet#cancel + * + * @description + * Hide the existing bottom sheet and reject the promise returned from + * `$mdBottomSheet.show()`. + * + * @param {*=} response An argument for the rejected promise. + * + */ + +function MdBottomSheetProvider($$interimElementProvider) { + // how fast we need to flick down to close the sheet, pixels/ms + bottomSheetDefaults.$inject = ["$animate", "$mdConstant", "$mdUtil", "$mdTheming", "$mdBottomSheet", "$rootElement", "$mdGesture", "$log"]; + var CLOSING_VELOCITY = 0.5; + var PADDING = 80; // same as css + + return $$interimElementProvider('$mdBottomSheet') + .setDefaults({ + methods: ['disableParentScroll', 'escapeToClose', 'clickOutsideToClose'], + options: bottomSheetDefaults + }); + + /* @ngInject */ + function bottomSheetDefaults($animate, $mdConstant, $mdUtil, $mdTheming, $mdBottomSheet, $rootElement, + $mdGesture, $log) { + var backdrop; + + return { + themable: true, + onShow: onShow, + onRemove: onRemove, + disableBackdrop: false, + escapeToClose: true, + clickOutsideToClose: true, + disableParentScroll: true + }; + + + function onShow(scope, element, options, controller) { + + element = $mdUtil.extractElementByName(element, 'md-bottom-sheet'); + + // prevent tab focus or click focus on the bottom-sheet container + element.attr('tabindex',"-1"); + + // Once the md-bottom-sheet has `ng-cloak` applied on his template the opening animation will not work properly. + // This is a very common problem, so we have to notify the developer about this. + if (element.hasClass('ng-cloak')) { + var message = '$mdBottomSheet: using `` will affect the bottom-sheet opening animations.'; + $log.warn( message, element[0] ); + } + + if (!options.disableBackdrop) { + // Add a backdrop that will close on click + backdrop = $mdUtil.createBackdrop(scope, "md-bottom-sheet-backdrop md-opaque"); + + // Prevent mouse focus on backdrop; ONLY programatic focus allowed. + // This allows clicks on backdrop to propogate to the $rootElement and + // ESC key events to be detected properly. + + backdrop[0].tabIndex = -1; + + if (options.clickOutsideToClose) { + backdrop.on('click', function() { + $mdUtil.nextTick($mdBottomSheet.cancel,true); + }); + } + + $mdTheming.inherit(backdrop, options.parent); + + $animate.enter(backdrop, options.parent, null); + } + + var bottomSheet = new BottomSheet(element, options.parent); + options.bottomSheet = bottomSheet; + + $mdTheming.inherit(bottomSheet.element, options.parent); + + if (options.disableParentScroll) { + options.restoreScroll = $mdUtil.disableScrollAround(bottomSheet.element, options.parent); + } + + return $animate.enter(bottomSheet.element, options.parent, backdrop) + .then(function() { + var focusable = $mdUtil.findFocusTarget(element) || angular.element( + element[0].querySelector('button') || + element[0].querySelector('a') || + element[0].querySelector($mdUtil.prefixer('ng-click', true)) + ) || backdrop; + + if (options.escapeToClose) { + options.rootElementKeyupCallback = function(e) { + if (e.keyCode === $mdConstant.KEY_CODE.ESCAPE) { + $mdUtil.nextTick($mdBottomSheet.cancel,true); + } + }; + + $rootElement.on('keyup', options.rootElementKeyupCallback); + focusable && focusable.focus(); + } + }); + + } + + function onRemove(scope, element, options) { + + var bottomSheet = options.bottomSheet; + + if (!options.disableBackdrop) $animate.leave(backdrop); + return $animate.leave(bottomSheet.element).then(function() { + if (options.disableParentScroll) { + options.restoreScroll(); + delete options.restoreScroll; + } + + bottomSheet.cleanup(); + }); + } + + /** + * BottomSheet class to apply bottom-sheet behavior to an element + */ + function BottomSheet(element, parent) { + var deregister = $mdGesture.register(parent, 'drag', { horizontal: false }); + parent.on('$md.dragstart', onDragStart) + .on('$md.drag', onDrag) + .on('$md.dragend', onDragEnd); + + return { + element: element, + cleanup: function cleanup() { + deregister(); + parent.off('$md.dragstart', onDragStart); + parent.off('$md.drag', onDrag); + parent.off('$md.dragend', onDragEnd); + } + }; + + function onDragStart(ev) { + // Disable transitions on transform so that it feels fast + element.css($mdConstant.CSS.TRANSITION_DURATION, '0ms'); + } + + function onDrag(ev) { + var transform = ev.pointer.distanceY; + if (transform < 5) { + // Slow down drag when trying to drag up, and stop after PADDING + transform = Math.max(-PADDING, transform / 2); + } + element.css($mdConstant.CSS.TRANSFORM, 'translate3d(0,' + (PADDING + transform) + 'px,0)'); + } + + function onDragEnd(ev) { + if (ev.pointer.distanceY > 0 && + (ev.pointer.distanceY > 20 || Math.abs(ev.pointer.velocityY) > CLOSING_VELOCITY)) { + var distanceRemaining = element.prop('offsetHeight') - ev.pointer.distanceY; + var transitionDuration = Math.min(distanceRemaining / ev.pointer.velocityY * 0.75, 500); + element.css($mdConstant.CSS.TRANSITION_DURATION, transitionDuration + 'ms'); + $mdUtil.nextTick($mdBottomSheet.cancel,true); + } else { + element.css($mdConstant.CSS.TRANSITION_DURATION, ''); + element.css($mdConstant.CSS.TRANSFORM, ''); + } + } + } + + } + +} + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.button + * @description + * + * Button + */ +MdButtonDirective.$inject = ["$mdButtonInkRipple", "$mdTheming", "$mdAria", "$mdInteraction"]; +MdAnchorDirective.$inject = ["$mdTheming"]; +angular + .module('material.components.button', [ 'material.core' ]) + .directive('mdButton', MdButtonDirective) + .directive('a', MdAnchorDirective); + + +/** + * @private + * @restrict E + * + * @description + * `a` is an anchor directive used to inherit theme colors for md-primary, md-accent, etc. + * + * @usage + * + * + * + * + * + * + */ +function MdAnchorDirective($mdTheming) { + return { + restrict : 'E', + link : function postLink(scope, element) { + // Make sure to inherit theme so stand-alone anchors + // support theme colors for md-primary, md-accent, etc. + $mdTheming(element); + } + }; +} + + +/** + * @ngdoc directive + * @name mdButton + * @module material.components.button + * + * @restrict E + * + * @description + * `` is a button directive with optional ink ripples (default enabled). + * + * If you supply a `href` or `ng-href` attribute, it will become an `` element. Otherwise, it + * will become a `'; + } + } + + function postLink(scope, element, attr) { + $mdTheming(element); + $mdButtonInkRipple.attach(scope, element); + + // Use async expect to support possible bindings in the button label + $mdAria.expectWithoutText(element, 'aria-label'); + + // For anchor elements, we have to set tabindex manually when the + // element is disabled + if (isAnchor(attr) && angular.isDefined(attr.ngDisabled) ) { + scope.$watch(attr.ngDisabled, function(isDisabled) { + element.attr('tabindex', isDisabled ? -1 : 0); + }); + } + + // disabling click event when disabled is true + element.on('click', function(e){ + if (attr.disabled === true) { + e.preventDefault(); + e.stopImmediatePropagation(); + } + }); + + if (!element.hasClass('md-no-focus')) { + + element.on('focus', function() { + + // Only show the focus effect when being focused through keyboard interaction or programmatically + if (!$mdInteraction.isUserInvoked() || $mdInteraction.getLastInteractionType() === 'keyboard') { + element.addClass('md-focused'); + } + + }); + + element.on('blur', function() { + element.removeClass('md-focused'); + }); + } + + } + +} + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.card + * + * @description + * Card components. + */ +mdCardDirective.$inject = ["$mdTheming"]; +angular.module('material.components.card', [ + 'material.core' + ]) + .directive('mdCard', mdCardDirective); + + +/** + * @ngdoc directive + * @name mdCard + * @module material.components.card + * + * @restrict E + * + * @description + * The `` directive is a container element used within `` containers. + * + * An image included as a direct descendant will fill the card's width. If you want to avoid this, + * you can add the `md-image-no-fill` class to the parent element. The `` + * container will wrap text content and provide padding. An `` element can be + * optionally included to put content flush against the bottom edge of the card. + * + * Action buttons can be included in an `` element, similar to ``. + * You can then position buttons using layout attributes. + * + * Card is built with: + * * `` - Header for the card, holds avatar, text and squared image + * - `` - Card avatar + * - `md-user-avatar` - Class for user image + * - `` + * - `` - Contains elements for the card description + * - `md-title` - Class for the card title + * - `md-subhead` - Class for the card sub header + * * `` - Image for the card + * * `` - Card content title + * - `` + * - `md-headline` - Class for the card content title + * - `md-subhead` - Class for the card content sub header + * - `` - Squared image within the title + * - `md-media-sm` - Class for small image + * - `md-media-md` - Class for medium image + * - `md-media-lg` - Class for large image + * - `md-media-xl` - Class for extra large image + * * `` - Card content + * * `` - Card actions + * - `` - Icon actions + * + * Cards have constant width and variable heights; where the maximum height is limited to what can + * fit within a single view on a platform, but it can temporarily expand as needed. + * + * @usage + * ### Card with optional footer + * + * + * image caption + * + *

Card headline

+ *

Card content

+ *
+ * + * Card footer + * + *
+ *
+ * + * ### Card with actions + * + * + * image caption + * + *

Card headline

+ *

Card content

+ *
+ * + * Action 1 + * Action 2 + * + *
+ *
+ * + * ### Card with header, image, title actions and content + * + * + * + * + * + * + * + * Title + * Sub header + * + * + * image caption + * + * + * Card headline + * Card subheader + * + * + * + * Action 1 + * Action 2 + * + * + * + * + * + * + * + *

+ * Card content + *

+ *
+ *
+ *
+ */ +function mdCardDirective($mdTheming) { + return { + restrict: 'E', + link: function ($scope, $element, attr) { + $element.addClass('_md'); // private md component indicator for styling + $mdTheming($element); + } + }; +} + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.checkbox + * @description Checkbox module! + */ +MdCheckboxDirective.$inject = ["inputDirective", "$mdAria", "$mdConstant", "$mdTheming", "$mdUtil", "$mdInteraction"]; +angular + .module('material.components.checkbox', ['material.core']) + .directive('mdCheckbox', MdCheckboxDirective); + +/** + * @ngdoc directive + * @name mdCheckbox + * @module material.components.checkbox + * @restrict E + * + * @description + * The checkbox directive is used like the normal [angular checkbox](https://docs.angularjs.org/api/ng/input/input%5Bcheckbox%5D). + * + * As per the [material design spec](http://www.google.com/design/spec/style/color.html#color-color-schemes) + * the checkbox is in the accent color by default. The primary color palette may be used with + * the `md-primary` class. + * + * @param {string} ng-model Assignable angular expression to data-bind to. + * @param {string=} name Property name of the form under which the control is published. + * @param {expression=} ng-true-value The value to which the expression should be set when selected. + * @param {expression=} ng-false-value The value to which the expression should be set when not selected. + * @param {string=} ng-change Angular expression to be executed when input changes due to user interaction with the input element. + * @param {boolean=} md-no-ink Use of attribute indicates use of ripple ink effects + * @param {string=} aria-label Adds label to checkbox for accessibility. + * Defaults to checkbox's text. If no default text is found, a warning will be logged. + * @param {expression=} md-indeterminate This determines when the checkbox should be rendered as 'indeterminate'. + * If a truthy expression or no value is passed in the checkbox renders in the md-indeterminate state. + * If falsy expression is passed in it just looks like a normal unchecked checkbox. + * The indeterminate, checked, and unchecked states are mutually exclusive. A box cannot be in any two states at the same time. + * Adding the 'md-indeterminate' attribute overrides any checked/unchecked rendering logic. + * When using the 'md-indeterminate' attribute use 'ng-checked' to define rendering logic instead of using 'ng-model'. + * @param {expression=} ng-checked If this expression evaluates as truthy, the 'md-checked' css class is added to the checkbox and it + * will appear checked. + * + * @usage + * + * + * Finished ? + * + * + * + * No Ink Effects + * + * + * + * Disabled + * + * + * + * + */ +function MdCheckboxDirective(inputDirective, $mdAria, $mdConstant, $mdTheming, $mdUtil, $mdInteraction) { + inputDirective = inputDirective[0]; + + return { + restrict: 'E', + transclude: true, + require: ['^?mdInputContainer', '?ngModel', '?^form'], + priority: $mdConstant.BEFORE_NG_ARIA, + template: + '
' + + '
' + + '
' + + '
', + compile: compile + }; + + // ********************************************************** + // Private Methods + // ********************************************************** + + function compile (tElement, tAttrs) { + tAttrs.$set('tabindex', tAttrs.tabindex || '0'); + tAttrs.$set('type', 'checkbox'); + tAttrs.$set('role', tAttrs.type); + + return { + pre: function(scope, element) { + // Attach a click handler during preLink, in order to immediately stop propagation + // (especially for ng-click) when the checkbox is disabled. + element.on('click', function(e) { + if (this.hasAttribute('disabled')) { + e.stopImmediatePropagation(); + } + }); + }, + post: postLink + }; + + function postLink(scope, element, attr, ctrls) { + var isIndeterminate; + var containerCtrl = ctrls[0]; + var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel(); + var formCtrl = ctrls[2]; + + if (containerCtrl) { + var isErrorGetter = containerCtrl.isErrorGetter || function() { + return ngModelCtrl.$invalid && (ngModelCtrl.$touched || (formCtrl && formCtrl.$submitted)); + }; + + containerCtrl.input = element; + + scope.$watch(isErrorGetter, containerCtrl.setInvalid); + } + + $mdTheming(element); + + // Redirect focus events to the root element, because IE11 is always focusing the container element instead + // of the md-checkbox element. This causes issues when using ngModelOptions: `updateOnBlur` + element.children().on('focus', function() { + element.focus(); + }); + + if ($mdUtil.parseAttributeBoolean(attr.mdIndeterminate)) { + setIndeterminateState(); + scope.$watch(attr.mdIndeterminate, setIndeterminateState); + } + + if (attr.ngChecked) { + scope.$watch(scope.$eval.bind(scope, attr.ngChecked), function(value) { + ngModelCtrl.$setViewValue(value); + ngModelCtrl.$render(); + }); + } + + $$watchExpr('ngDisabled', 'tabindex', { + true: '-1', + false: attr.tabindex + }); + + $mdAria.expectWithText(element, 'aria-label'); + + // Reuse the original input[type=checkbox] directive from Angular core. + // This is a bit hacky as we need our own event listener and own render + // function. + inputDirective.link.pre(scope, { + on: angular.noop, + 0: {} + }, attr, [ngModelCtrl]); + + element.on('click', listener) + .on('keypress', keypressHandler) + .on('focus', function() { + if ($mdInteraction.getLastInteractionType() === 'keyboard') { + element.addClass('md-focused'); + } + }) + .on('blur', function() { + element.removeClass('md-focused'); + }); + + ngModelCtrl.$render = render; + + function $$watchExpr(expr, htmlAttr, valueOpts) { + if (attr[expr]) { + scope.$watch(attr[expr], function(val) { + if (valueOpts[val]) { + element.attr(htmlAttr, valueOpts[val]); + } + }); + } + } + + function keypressHandler(ev) { + var keyCode = ev.which || ev.keyCode; + if (keyCode === $mdConstant.KEY_CODE.SPACE || keyCode === $mdConstant.KEY_CODE.ENTER) { + ev.preventDefault(); + element.addClass('md-focused'); + listener(ev); + } + } + + function listener(ev) { + // skipToggle boolean is used by the switch directive to prevent the click event + // when releasing the drag. There will be always a click if releasing the drag over the checkbox + if (element[0].hasAttribute('disabled') || scope.skipToggle) { + return; + } + + scope.$apply(function() { + // Toggle the checkbox value... + var viewValue = attr.ngChecked ? attr.checked : !ngModelCtrl.$viewValue; + + ngModelCtrl.$setViewValue(viewValue, ev && ev.type); + ngModelCtrl.$render(); + }); + } + + function render() { + // Cast the $viewValue to a boolean since it could be undefined + element.toggleClass('md-checked', !!ngModelCtrl.$viewValue && !isIndeterminate); + } + + function setIndeterminateState(newValue) { + isIndeterminate = newValue !== false; + if (isIndeterminate) { + element.attr('aria-checked', 'mixed'); + } + element.toggleClass('md-indeterminate', isIndeterminate); + } + } + } +} + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.chips + */ +/* + * @see js folder for chips implementation + */ +angular.module('material.components.chips', [ + 'material.core', + 'material.components.autocomplete' +]); + +})(); +(function(){ +"use strict"; + +(function () { + "use strict"; + + /** + * Use a RegExp to check if the `md-colors=""` is static string + * or one that should be observed and dynamically interpolated. + */ + MdColorsDirective.$inject = ["$mdColors", "$mdUtil", "$log", "$parse"]; + MdColorsService.$inject = ["$mdTheming", "$mdUtil", "$log"]; + var STATIC_COLOR_EXPRESSION = /^{((\s|,)*?["'a-zA-Z-]+?\s*?:\s*?('|")[a-zA-Z0-9-.]*('|"))+\s*}$/; + var colorPalettes = null; + + /** + * @ngdoc module + * @name material.components.colors + * + * @description + * Define $mdColors service and a `md-colors=""` attribute directive + */ + angular + .module('material.components.colors', ['material.core']) + .directive('mdColors', MdColorsDirective) + .service('$mdColors', MdColorsService); + + /** + * @ngdoc service + * @name $mdColors + * @module material.components.colors + * + * @description + * With only defining themes, one couldn't get non ngMaterial elements colored with Material colors, + * `$mdColors` service is used by the md-color directive to convert the 1..n color expressions to RGBA values and will apply + * those values to element as CSS property values. + * + * @usage + * + * angular.controller('myCtrl', function ($mdColors) { + * var color = $mdColors.getThemeColor('myTheme-red-200-0.5'); + * ... + * }); + * + * + */ + function MdColorsService($mdTheming, $mdUtil, $log) { + colorPalettes = colorPalettes || Object.keys($mdTheming.PALETTES); + + // Publish service instance + return { + applyThemeColors: applyThemeColors, + getThemeColor: getThemeColor, + hasTheme: hasTheme + }; + + // ******************************************** + // Internal Methods + // ******************************************** + + /** + * @ngdoc method + * @name $mdColors#applyThemeColors + * + * @description + * Gets a color json object, keys are css properties and values are string of the wanted color + * Then calculate the rgba() values based on the theme color parts + * + * @param {DOMElement} element the element to apply the styles on. + * @param {object} colorExpression json object, keys are css properties and values are string of the wanted color, + * for example: `{color: 'red-A200-0.3'}`. + * + * @usage + * + * app.directive('myDirective', function($mdColors) { + * return { + * ... + * link: function (scope, elem) { + * $mdColors.applyThemeColors(elem, {color: 'red'}); + * } + * } + * }); + * + */ + function applyThemeColors(element, colorExpression) { + try { + if (colorExpression) { + // Assign the calculate RGBA color values directly as inline CSS + element.css(interpolateColors(colorExpression)); + } + } catch (e) { + $log.error(e.message); + } + + } + + /** + * @ngdoc method + * @name $mdColors#getThemeColor + * + * @description + * Get parsed color from expression + * + * @param {string} expression string of a color expression (for instance `'red-700-0.8'`) + * + * @returns {string} a css color expression (for instance `rgba(211, 47, 47, 0.8)`) + * + * @usage + * + * angular.controller('myCtrl', function ($mdColors) { + * var color = $mdColors.getThemeColor('myTheme-red-200-0.5'); + * ... + * }); + * + */ + function getThemeColor(expression) { + var color = extractColorOptions(expression); + + return parseColor(color); + } + + /** + * Return the parsed color + * @param color hashmap of color definitions + * @param contrast whether use contrast color for foreground + * @returns rgba color string + */ + function parseColor(color, contrast) { + contrast = contrast || false; + var rgbValues = $mdTheming.PALETTES[color.palette][color.hue]; + + rgbValues = contrast ? rgbValues.contrast : rgbValues.value; + + return $mdUtil.supplant('rgba({0}, {1}, {2}, {3})', + [rgbValues[0], rgbValues[1], rgbValues[2], rgbValues[3] || color.opacity] + ); + } + + /** + * Convert the color expression into an object with scope-interpolated values + * Then calculate the rgba() values based on the theme color parts + * + * @results Hashmap of CSS properties with associated `rgba( )` string vales + * + * + */ + function interpolateColors(themeColors) { + var rgbColors = {}; + + var hasColorProperty = themeColors.hasOwnProperty('color'); + + angular.forEach(themeColors, function (value, key) { + var color = extractColorOptions(value); + var hasBackground = key.indexOf('background') > -1; + + rgbColors[key] = parseColor(color); + if (hasBackground && !hasColorProperty) { + rgbColors.color = parseColor(color, true); + } + }); + + return rgbColors; + } + + /** + * Check if expression has defined theme + * e.g. + * 'myTheme-primary' => true + * 'red-800' => false + */ + function hasTheme(expression) { + return angular.isDefined($mdTheming.THEMES[expression.split('-')[0]]); + } + + /** + * For the evaluated expression, extract the color parts into a hash map + */ + function extractColorOptions(expression) { + var parts = expression.split('-'); + var hasTheme = angular.isDefined($mdTheming.THEMES[parts[0]]); + var theme = hasTheme ? parts.splice(0, 1)[0] : $mdTheming.defaultTheme(); + + return { + theme: theme, + palette: extractPalette(parts, theme), + hue: extractHue(parts, theme), + opacity: parts[2] || 1 + }; + } + + /** + * Calculate the theme palette name + */ + function extractPalette(parts, theme) { + // If the next section is one of the palettes we assume it's a two word palette + // Two word palette can be also written in camelCase, forming camelCase to dash-case + + var isTwoWord = parts.length > 1 && colorPalettes.indexOf(parts[1]) !== -1; + var palette = parts[0].replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); + + if (isTwoWord) palette = parts[0] + '-' + parts.splice(1, 1); + + if (colorPalettes.indexOf(palette) === -1) { + // If the palette is not in the palette list it's one of primary/accent/warn/background + var scheme = $mdTheming.THEMES[theme].colors[palette]; + if (!scheme) { + throw new Error($mdUtil.supplant('mdColors: couldn\'t find \'{palette}\' in the palettes.', {palette: palette})); + } + palette = scheme.name; + } + + return palette; + } + + function extractHue(parts, theme) { + var themeColors = $mdTheming.THEMES[theme].colors; + + if (parts[1] === 'hue') { + var hueNumber = parseInt(parts.splice(2, 1)[0], 10); + + if (hueNumber < 1 || hueNumber > 3) { + throw new Error($mdUtil.supplant('mdColors: \'hue-{hueNumber}\' is not a valid hue, can be only \'hue-1\', \'hue-2\' and \'hue-3\'', {hueNumber: hueNumber})); + } + parts[1] = 'hue-' + hueNumber; + + if (!(parts[0] in themeColors)) { + throw new Error($mdUtil.supplant('mdColors: \'hue-x\' can only be used with [{availableThemes}], but was used with \'{usedTheme}\'', { + availableThemes: Object.keys(themeColors).join(', '), + usedTheme: parts[0] + })); + } + + return themeColors[parts[0]].hues[parts[1]]; + } + + return parts[1] || themeColors[parts[0] in themeColors ? parts[0] : 'primary'].hues['default']; + } + } + + /** + * @ngdoc directive + * @name mdColors + * @module material.components.colors + * + * @restrict A + * + * @description + * `mdColors` directive will apply the theme-based color expression as RGBA CSS style values. + * + * The format will be similar to our color defining in the scss files: + * + * ## `[?theme]-[palette]-[?hue]-[?opacity]` + * - [theme] - default value is the default theme + * - [palette] - can be either palette name or primary/accent/warn/background + * - [hue] - default is 500 (hue-x can be used with primary/accent/warn/background) + * - [opacity] - default is 1 + * + * > `?` indicates optional parameter + * + * @usage + * + *
+ *
+ * Color demo + *
+ *
+ *
+ * + * `mdColors` directive will automatically watch for changes in the expression if it recognizes an interpolation + * expression or a function. For performance options, you can use `::` prefix to the `md-colors` expression + * to indicate a one-time data binding. + * + * + * + * + * + */ + function MdColorsDirective($mdColors, $mdUtil, $log, $parse) { + return { + restrict: 'A', + require: ['^?mdTheme'], + compile: function (tElem, tAttrs) { + var shouldWatch = shouldColorsWatch(); + + return function (scope, element, attrs, ctrl) { + var mdThemeController = ctrl[0]; + + var lastColors = {}; + + var parseColors = function (theme) { + if (typeof theme !== 'string') { + theme = ''; + } + + if (!attrs.mdColors) { + attrs.mdColors = '{}'; + } + + /** + * Json.parse() does not work because the keys are not quoted; + * use $parse to convert to a hash map + */ + var colors = $parse(attrs.mdColors)(scope); + + /** + * If mdTheme is defined up the DOM tree + * we add mdTheme theme to colors who doesn't specified a theme + * + * # example + * + *
+ *
+ * Color demo + *
+ *
+ *
+ * + * 'primary-600' will be 'myTheme-primary-600', + * but 'mySecondTheme-accent-200' will stay the same cause it has a theme prefix + */ + if (mdThemeController) { + Object.keys(colors).forEach(function (prop) { + var color = colors[prop]; + if (!$mdColors.hasTheme(color)) { + colors[prop] = (theme || mdThemeController.$mdTheme) + '-' + color; + } + }); + } + + cleanElement(colors); + + return colors; + }; + + var cleanElement = function (colors) { + if (!angular.equals(colors, lastColors)) { + var keys = Object.keys(lastColors); + + if (lastColors.background && !keys.color) { + keys.push('color'); + } + + keys.forEach(function (key) { + element.css(key, ''); + }); + } + + lastColors = colors; + }; + + /** + * Registering for mgTheme changes and asking mdTheme controller run our callback whenever a theme changes + */ + var unregisterChanges = angular.noop; + + if (mdThemeController) { + unregisterChanges = mdThemeController.registerChanges(function (theme) { + $mdColors.applyThemeColors(element, parseColors(theme)); + }); + } + + scope.$on('$destroy', function () { + unregisterChanges(); + }); + + try { + if (shouldWatch) { + scope.$watch(parseColors, angular.bind(this, + $mdColors.applyThemeColors, element + ), true); + } + else { + $mdColors.applyThemeColors(element, parseColors()); + } + + } + catch (e) { + $log.error(e.message); + } + + }; + + function shouldColorsWatch() { + // Simulate 1x binding and mark mdColorsWatch == false + var rawColorExpression = tAttrs.mdColors; + var bindOnce = rawColorExpression.indexOf('::') > -1; + var isStatic = bindOnce ? true : STATIC_COLOR_EXPRESSION.test(tAttrs.mdColors); + + // Remove it for the postLink... + tAttrs.mdColors = rawColorExpression.replace('::', ''); + + var hasWatchAttr = angular.isDefined(tAttrs.mdColorsWatch); + + return (bindOnce || isStatic) ? false : + hasWatchAttr ? $mdUtil.parseAttributeBoolean(tAttrs.mdColorsWatch) : true; + } + } + }; + + } + + +})(); + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.content + * + * @description + * Scrollable content + */ +mdContentDirective.$inject = ["$mdTheming"]; +angular.module('material.components.content', [ + 'material.core' +]) + .directive('mdContent', mdContentDirective); + +/** + * @ngdoc directive + * @name mdContent + * @module material.components.content + * + * @restrict E + * + * @description + * + * The `` directive is a container element useful for scrollable content. It achieves + * this by setting the CSS `overflow` property to `auto` so that content can properly scroll. + * + * In general, `` components are not designed to be nested inside one another. If + * possible, it is better to make them siblings. This often results in a better user experience as + * having nested scrollbars may confuse the user. + * + * ## Troubleshooting + * + * In some cases, you may wish to apply the `md-no-momentum` class to ensure that Safari's + * momentum scrolling is disabled. Momentum scrolling can cause flickering issues while scrolling + * SVG icons and some other components. + * + * Additionally, we now also offer the `md-no-flicker` class which can be applied to any element + * and uses a Webkit-specific filter of `blur(0px)` that forces GPU rendering of all elements + * inside (which eliminates the flicker on iOS devices). + * + * _Note: Forcing an element to render on the GPU can have unintended side-effects, especially + * related to the z-index of elements. Please use with caution and only on the elements needed._ + * + * @usage + * + * Add the `[layout-padding]` attribute to make the content padded. + * + * + * + * Lorem ipsum dolor sit amet, ne quod novum mei. + * + * + */ + +function mdContentDirective($mdTheming) { + return { + restrict: 'E', + controller: ['$scope', '$element', ContentController], + link: function(scope, element) { + element.addClass('_md'); // private md component indicator for styling + + $mdTheming(element); + scope.$broadcast('$mdContentLoaded', element); + + iosScrollFix(element[0]); + } + }; + + function ContentController($scope, $element) { + this.$scope = $scope; + this.$element = $element; + } +} + +function iosScrollFix(node) { + // IOS FIX: + // If we scroll where there is no more room for the webview to scroll, + // by default the webview itself will scroll up and down, this looks really + // bad. So if we are scrolling to the very top or bottom, add/subtract one + angular.element(node).on('$md.pressdown', function(ev) { + // Only touch events + if (ev.pointer.type !== 't') return; + // Don't let a child content's touchstart ruin it for us. + if (ev.$materialScrollFixed) return; + ev.$materialScrollFixed = true; + + if (node.scrollTop === 0) { + node.scrollTop = 1; + } else if (node.scrollHeight === node.scrollTop + node.offsetHeight) { + node.scrollTop -= 1; + } + }); +} + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.datepicker + * @description Module for the datepicker component. + */ + +angular.module('material.components.datepicker', [ + 'material.core', + 'material.components.icon', + 'material.components.virtualRepeat' +]); + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.dialog + */ +MdDialogDirective.$inject = ["$$rAF", "$mdTheming", "$mdDialog"]; +MdDialogProvider.$inject = ["$$interimElementProvider"]; +angular + .module('material.components.dialog', [ + 'material.core', + 'material.components.backdrop' + ]) + .directive('mdDialog', MdDialogDirective) + .provider('$mdDialog', MdDialogProvider); + +/** + * @ngdoc directive + * @name mdDialog + * @module material.components.dialog + * + * @restrict E + * + * @description + * `` - The dialog's template must be inside this element. + * + * Inside, use an `` element for the dialog's content, and use + * an `` element for the dialog's actions. + * + * ## CSS + * - `.md-dialog-content` - class that sets the padding on the content as the spec file + * + * ## Notes + * - If you specify an `id` for the ``, the `` will have the same `id` + * prefixed with `dialogContent_`. + * + * @usage + * ### Dialog template + * + * + * + * + * + *

Number {{item}}

+ *
+ *
+ *
+ * + * Close Dialog + * + *
+ *
+ */ +function MdDialogDirective($$rAF, $mdTheming, $mdDialog) { + return { + restrict: 'E', + link: function(scope, element) { + element.addClass('_md'); // private md component indicator for styling + + $mdTheming(element); + $$rAF(function() { + var images; + var content = element[0].querySelector('md-dialog-content'); + + if (content) { + images = content.getElementsByTagName('img'); + addOverflowClass(); + //-- delayed image loading may impact scroll height, check after images are loaded + angular.element(images).on('load', addOverflowClass); + } + + scope.$on('$destroy', function() { + $mdDialog.destroy(element); + }); + + /** + * + */ + function addOverflowClass() { + element.toggleClass('md-content-overflow', content.scrollHeight > content.clientHeight); + } + + + }); + } + }; +} + +/** + * @ngdoc service + * @name $mdDialog + * @module material.components.dialog + * + * @description + * `$mdDialog` opens a dialog over the app to inform users about critical information or require + * them to make decisions. There are two approaches for setup: a simple promise API + * and regular object syntax. + * + * ## Restrictions + * + * - The dialog is always given an isolate scope. + * - The dialog's template must have an outer `` element. + * Inside, use an `` element for the dialog's content, and use + * an `` element for the dialog's actions. + * - Dialogs must cover the entire application to keep interactions inside of them. + * Use the `parent` option to change where dialogs are appended. + * + * ## Sizing + * - Complex dialogs can be sized with `flex="percentage"`, i.e. `flex="66"`. + * - Default max-width is 80% of the `rootElement` or `parent`. + * + * ## CSS + * - `.md-dialog-content` - class that sets the padding on the content as the spec file + * + * @usage + * + *
+ *
+ * + * Employee Alert! + * + *
+ *
+ * + * Custom Dialog + * + *
+ *
+ * + * Close Alert + * + *
+ *
+ * + * Greet Employee + * + *
+ *
+ *
+ * + * ### JavaScript: object syntax + * + * (function(angular, undefined){ + * "use strict"; + * + * angular + * .module('demoApp', ['ngMaterial']) + * .controller('AppCtrl', AppController); + * + * function AppController($scope, $mdDialog) { + * var alert; + * $scope.showAlert = showAlert; + * $scope.showDialog = showDialog; + * $scope.items = [1, 2, 3]; + * + * // Internal method + * function showAlert() { + * alert = $mdDialog.alert({ + * title: 'Attention', + * textContent: 'This is an example of how easy dialogs can be!', + * ok: 'Close' + * }); + * + * $mdDialog + * .show( alert ) + * .finally(function() { + * alert = undefined; + * }); + * } + * + * function showDialog($event) { + * var parentEl = angular.element(document.body); + * $mdDialog.show({ + * parent: parentEl, + * targetEvent: $event, + * template: + * '' + + * ' '+ + * ' '+ + * ' '+ + * '

Number {{item}}

' + + * ' '+ + * '
'+ + * '
' + + * ' ' + + * ' ' + + * ' Close Dialog' + + * ' ' + + * ' ' + + * '
', + * locals: { + * items: $scope.items + * }, + * controller: DialogController + * }); + * function DialogController($scope, $mdDialog, items) { + * $scope.items = items; + * $scope.closeDialog = function() { + * $mdDialog.hide(); + * } + * } + * } + * } + * })(angular); + *
+ * + * ### Multiple Dialogs + * Using the `multiple` option for the `$mdDialog` service allows developers to show multiple dialogs + * at the same time. + * + * + * // From plain options + * $mdDialog.show({ + * multiple: true + * }); + * + * // From a dialog preset + * $mdDialog.show( + * $mdDialog + * .alert() + * .multiple(true) + * ); + * + * + * + * ### Pre-Rendered Dialogs + * By using the `contentElement` option, it is possible to use an already existing element in the DOM. + * + * > Pre-rendered dialogs will be not linked to any scope and will not instantiate any new controller.
+ * > You can manually link the elements to a scope or instantiate a controller from the template (`ng-controller`) + * + * + * $scope.showPrerenderedDialog = function() { + * $mdDialog.show({ + * contentElement: '#myStaticDialog', + * parent: angular.element(document.body) + * }); + * }; + * + * + * When using a string as value, `$mdDialog` will automatically query the DOM for the specified CSS selector. + * + * + *
+ *
+ * + * This is a pre-rendered dialog. + * + *
+ *
+ *
+ * + * **Notice**: It is important, to use the `.md-dialog-container` as the content element, otherwise the dialog + * will not show up. + * + * It also possible to use a DOM element for the `contentElement` option. + * - `contentElement: document.querySelector('#myStaticDialog')` + * - `contentElement: angular.element(TEMPLATE)` + * + * When using a `template` as content element, it will be not compiled upon open. + * This allows you to compile the element yourself and use it each time the dialog opens. + * + * ### Custom Presets + * Developers are also able to create their own preset, which can be easily used without repeating + * their options each time. + * + * + * $mdDialogProvider.addPreset('testPreset', { + * options: function() { + * return { + * template: + * '' + + * 'This is a custom preset' + + * '', + * controllerAs: 'dialog', + * bindToController: true, + * clickOutsideToClose: true, + * escapeToClose: true + * }; + * } + * }); + * + * + * After you created your preset at config phase, you can easily access it. + * + * + * $mdDialog.show( + * $mdDialog.testPreset() + * ); + * + * + * ### JavaScript: promise API syntax, custom dialog template + * + * (function(angular, undefined){ + * "use strict"; + * + * angular + * .module('demoApp', ['ngMaterial']) + * .controller('EmployeeController', EmployeeEditor) + * .controller('GreetingController', GreetingController); + * + * // Fictitious Employee Editor to show how to use simple and complex dialogs. + * + * function EmployeeEditor($scope, $mdDialog) { + * var alert; + * + * $scope.showAlert = showAlert; + * $scope.closeAlert = closeAlert; + * $scope.showGreeting = showCustomGreeting; + * + * $scope.hasAlert = function() { return !!alert }; + * $scope.userName = $scope.userName || 'Bobby'; + * + * // Dialog #1 - Show simple alert dialog and cache + * // reference to dialog instance + * + * function showAlert() { + * alert = $mdDialog.alert() + * .title('Attention, ' + $scope.userName) + * .textContent('This is an example of how easy dialogs can be!') + * .ok('Close'); + * + * $mdDialog + * .show( alert ) + * .finally(function() { + * alert = undefined; + * }); + * } + * + * // Close the specified dialog instance and resolve with 'finished' flag + * // Normally this is not needed, just use '$mdDialog.hide()' to close + * // the most recent dialog popup. + * + * function closeAlert() { + * $mdDialog.hide( alert, "finished" ); + * alert = undefined; + * } + * + * // Dialog #2 - Demonstrate more complex dialogs construction and popup. + * + * function showCustomGreeting($event) { + * $mdDialog.show({ + * targetEvent: $event, + * template: + * '' + + * + * ' Hello {{ employee }}!' + + * + * ' ' + + * ' ' + + * ' Close Greeting' + + * ' ' + + * ' ' + + * '', + * controller: 'GreetingController', + * onComplete: afterShowAnimation, + * locals: { employee: $scope.userName } + * }); + * + * // When the 'enter' animation finishes... + * + * function afterShowAnimation(scope, element, options) { + * // post-show code here: DOM element focus, etc. + * } + * } + * + * // Dialog #3 - Demonstrate use of ControllerAs and passing $scope to dialog + * // Here we used ng-controller="GreetingController as vm" and + * // $scope.vm === + * + * function showCustomGreeting() { + * + * $mdDialog.show({ + * clickOutsideToClose: true, + * + * scope: $scope, // use parent scope in template + * preserveScope: true, // do not forget this if use parent scope + + * // Since GreetingController is instantiated with ControllerAs syntax + * // AND we are passing the parent '$scope' to the dialog, we MUST + * // use 'vm.' in the template markup + * + * template: '' + + * ' ' + + * ' Hi There {{vm.employee}}' + + * ' ' + + * '', + * + * controller: function DialogController($scope, $mdDialog) { + * $scope.closeDialog = function() { + * $mdDialog.hide(); + * } + * } + * }); + * } + * + * } + * + * // Greeting controller used with the more complex 'showCustomGreeting()' custom dialog + * + * function GreetingController($scope, $mdDialog, employee) { + * // Assigned from construction locals options... + * $scope.employee = employee; + * + * $scope.closeDialog = function() { + * // Easily hides most recent dialog shown... + * // no specific instance reference is needed. + * $mdDialog.hide(); + * }; + * } + * + * })(angular); + * + */ + +/** + * @ngdoc method + * @name $mdDialog#alert + * + * @description + * Builds a preconfigured dialog with the specified message. + * + * @returns {obj} an `$mdDialogPreset` with the chainable configuration methods: + * + * - $mdDialogPreset#title(string) - Sets the alert title. + * - $mdDialogPreset#textContent(string) - Sets the alert message. + * - $mdDialogPreset#htmlContent(string) - Sets the alert message as HTML. Requires ngSanitize + * module to be loaded. HTML is not run through Angular's compiler. + * - $mdDialogPreset#ok(string) - Sets the alert "Okay" button text. + * - $mdDialogPreset#theme(string) - Sets the theme of the alert dialog. + * - $mdDialogPreset#targetEvent(DOMClickEvent=) - A click's event object. When passed in as an option, + * the location of the click will be used as the starting point for the opening animation + * of the the dialog. + * + */ + +/** + * @ngdoc method + * @name $mdDialog#confirm + * + * @description + * Builds a preconfigured dialog with the specified message. You can call show and the promise returned + * will be resolved only if the user clicks the confirm action on the dialog. + * + * @returns {obj} an `$mdDialogPreset` with the chainable configuration methods: + * + * Additionally, it supports the following methods: + * + * - $mdDialogPreset#title(string) - Sets the confirm title. + * - $mdDialogPreset#textContent(string) - Sets the confirm message. + * - $mdDialogPreset#htmlContent(string) - Sets the confirm message as HTML. Requires ngSanitize + * module to be loaded. HTML is not run through Angular's compiler. + * - $mdDialogPreset#ok(string) - Sets the confirm "Okay" button text. + * - $mdDialogPreset#cancel(string) - Sets the confirm "Cancel" button text. + * - $mdDialogPreset#theme(string) - Sets the theme of the confirm dialog. + * - $mdDialogPreset#targetEvent(DOMClickEvent=) - A click's event object. When passed in as an option, + * the location of the click will be used as the starting point for the opening animation + * of the the dialog. + * + */ + +/** + * @ngdoc method + * @name $mdDialog#prompt + * + * @description + * Builds a preconfigured dialog with the specified message and input box. You can call show and the promise returned + * will be resolved only if the user clicks the prompt action on the dialog, passing the input value as the first argument. + * + * @returns {obj} an `$mdDialogPreset` with the chainable configuration methods: + * + * Additionally, it supports the following methods: + * + * - $mdDialogPreset#title(string) - Sets the prompt title. + * - $mdDialogPreset#textContent(string) - Sets the prompt message. + * - $mdDialogPreset#htmlContent(string) - Sets the prompt message as HTML. Requires ngSanitize + * module to be loaded. HTML is not run through Angular's compiler. + * - $mdDialogPreset#placeholder(string) - Sets the placeholder text for the input. + * - $mdDialogPreset#initialValue(string) - Sets the initial value for the prompt input. + * - $mdDialogPreset#ok(string) - Sets the prompt "Okay" button text. + * - $mdDialogPreset#cancel(string) - Sets the prompt "Cancel" button text. + * - $mdDialogPreset#theme(string) - Sets the theme of the prompt dialog. + * - $mdDialogPreset#targetEvent(DOMClickEvent=) - A click's event object. When passed in as an option, + * the location of the click will be used as the starting point for the opening animation + * of the the dialog. + * + */ + +/** + * @ngdoc method + * @name $mdDialog#show + * + * @description + * Show a dialog with the specified options. + * + * @param {object} optionsOrPreset Either provide an `$mdDialogPreset` returned from `alert()`, and + * `confirm()`, or an options object with the following properties: + * - `templateUrl` - `{string=}`: The url of a template that will be used as the content + * of the dialog. + * - `template` - `{string=}`: HTML template to show in the dialog. This **must** be trusted HTML + * with respect to Angular's [$sce service](https://docs.angularjs.org/api/ng/service/$sce). + * This template should **never** be constructed with any kind of user input or user data. + * - `contentElement` - `{string|Element}`: Instead of using a template, which will be compiled each time a + * dialog opens, you can also use a DOM element.
+ * * When specifying an element, which is present on the DOM, `$mdDialog` will temporary fetch the element into + * the dialog and restores it at the old DOM position upon close. + * * When specifying a string, the string be used as a CSS selector, to lookup for the element in the DOM. + * - `autoWrap` - `{boolean=}`: Whether or not to automatically wrap the template with a + * `` tag if one is not provided. Defaults to true. Can be disabled if you provide a + * custom dialog directive. + * - `targetEvent` - `{DOMClickEvent=}`: A click's event object. When passed in as an option, + * the location of the click will be used as the starting point for the opening animation + * of the the dialog. + * - `openFrom` - `{string|Element|object}`: The query selector, DOM element or the Rect object + * that is used to determine the bounds (top, left, height, width) from which the Dialog will + * originate. + * - `closeTo` - `{string|Element|object}`: The query selector, DOM element or the Rect object + * that is used to determine the bounds (top, left, height, width) to which the Dialog will + * target. + * - `scope` - `{object=}`: the scope to link the template / controller to. If none is specified, + * it will create a new isolate scope. + * This scope will be destroyed when the dialog is removed unless `preserveScope` is set to true. + * - `preserveScope` - `{boolean=}`: whether to preserve the scope when the element is removed. Default is false + * - `disableParentScroll` - `{boolean=}`: Whether to disable scrolling while the dialog is open. + * Default true. + * - `hasBackdrop` - `{boolean=}`: Whether there should be an opaque backdrop behind the dialog. + * Default true. + * - `clickOutsideToClose` - `{boolean=}`: Whether the user can click outside the dialog to + * close it. Default false. + * - `escapeToClose` - `{boolean=}`: Whether the user can press escape to close the dialog. + * Default true. + * - `focusOnOpen` - `{boolean=}`: An option to override focus behavior on open. Only disable if + * focusing some other way, as focus management is required for dialogs to be accessible. + * Defaults to true. + * - `controller` - `{function|string=}`: The controller to associate with the dialog. The controller + * will be injected with the local `$mdDialog`, which passes along a scope for the dialog. + * - `locals` - `{object=}`: An object containing key/value pairs. The keys will be used as names + * of values to inject into the controller. For example, `locals: {three: 3}` would inject + * `three` into the controller, with the value 3. If `bindToController` is true, they will be + * copied to the controller instead. + * - `bindToController` - `bool`: bind the locals to the controller, instead of passing them in. + * - `resolve` - `{object=}`: Similar to locals, except it takes promises as values, and the + * dialog will not open until all of the promises resolve. + * - `controllerAs` - `{string=}`: An alias to assign the controller to on the scope. + * - `parent` - `{element=}`: The element to append the dialog to. Defaults to appending + * to the root element of the application. + * - `onShowing` - `function(scope, element)`: Callback function used to announce the show() action is + * starting. + * - `onComplete` - `function(scope, element)`: Callback function used to announce when the show() action is + * finished. + * - `onRemoving` - `function(element, removePromise)`: Callback function used to announce the + * close/hide() action is starting. This allows developers to run custom animations + * in parallel the close animations. + * - `fullscreen` `{boolean=}`: An option to toggle whether the dialog should show in fullscreen + * or not. Defaults to `false`. + * @returns {promise} A promise that can be resolved with `$mdDialog.hide()` or + * rejected with `$mdDialog.cancel()`. + */ + +/** + * @ngdoc method + * @name $mdDialog#hide + * + * @description + * Hide an existing dialog and resolve the promise returned from `$mdDialog.show()`. + * + * @param {*=} response An argument for the resolved promise. + * + * @returns {promise} A promise that is resolved when the dialog has been closed. + */ + +/** + * @ngdoc method + * @name $mdDialog#cancel + * + * @description + * Hide an existing dialog and reject the promise returned from `$mdDialog.show()`. + * + * @param {*=} response An argument for the rejected promise. + * + * @returns {promise} A promise that is resolved when the dialog has been closed. + */ + +function MdDialogProvider($$interimElementProvider) { + // Elements to capture and redirect focus when the user presses tab at the dialog boundary. + advancedDialogOptions.$inject = ["$mdDialog", "$mdConstant"]; + dialogDefaultOptions.$inject = ["$mdDialog", "$mdAria", "$mdUtil", "$mdConstant", "$animate", "$document", "$window", "$rootElement", "$log", "$injector", "$mdTheming", "$interpolate", "$mdInteraction"]; + var topFocusTrap, bottomFocusTrap; + + return $$interimElementProvider('$mdDialog') + .setDefaults({ + methods: ['disableParentScroll', 'hasBackdrop', 'clickOutsideToClose', 'escapeToClose', + 'targetEvent', 'closeTo', 'openFrom', 'parent', 'fullscreen', 'multiple'], + options: dialogDefaultOptions + }) + .addPreset('alert', { + methods: ['title', 'htmlContent', 'textContent', 'content', 'ariaLabel', 'ok', 'theme', + 'css'], + options: advancedDialogOptions + }) + .addPreset('confirm', { + methods: ['title', 'htmlContent', 'textContent', 'content', 'ariaLabel', 'ok', 'cancel', + 'theme', 'css'], + options: advancedDialogOptions + }) + .addPreset('prompt', { + methods: ['title', 'htmlContent', 'textContent', 'initialValue', 'content', 'placeholder', 'ariaLabel', + 'ok', 'cancel', 'theme', 'css'], + options: advancedDialogOptions + }); + + /* @ngInject */ + function advancedDialogOptions($mdDialog, $mdConstant) { + return { + template: [ + '', + ' ', + '

{{ dialog.title }}

', + '
', + '
', + '

{{::dialog.mdTextContent}}

', + '
', + ' ', + ' ', + ' ', + '
', + ' ', + ' ', + ' {{ dialog.cancel }}', + ' ', + ' ', + ' {{ dialog.ok }}', + ' ', + ' ', + '
' + ].join('').replace(/\s\s+/g, ''), + controller: function mdDialogCtrl() { + var isPrompt = this.$type == 'prompt'; + + if (isPrompt && this.initialValue) { + this.result = this.initialValue; + } + + this.hide = function() { + $mdDialog.hide(isPrompt ? this.result : true); + }; + this.abort = function() { + $mdDialog.cancel(); + }; + this.keypress = function($event) { + if ($event.keyCode === $mdConstant.KEY_CODE.ENTER) { + $mdDialog.hide(this.result); + } + }; + }, + controllerAs: 'dialog', + bindToController: true, + }; + } + + /* @ngInject */ + function dialogDefaultOptions($mdDialog, $mdAria, $mdUtil, $mdConstant, $animate, $document, $window, $rootElement, + $log, $injector, $mdTheming, $interpolate, $mdInteraction) { + + return { + hasBackdrop: true, + isolateScope: true, + onCompiling: beforeCompile, + onShow: onShow, + onShowing: beforeShow, + onRemove: onRemove, + clickOutsideToClose: false, + escapeToClose: true, + targetEvent: null, + closeTo: null, + openFrom: null, + focusOnOpen: true, + disableParentScroll: true, + autoWrap: true, + fullscreen: false, + transformTemplate: function(template, options) { + // Make the dialog container focusable, because otherwise the focus will be always redirected to + // an element outside of the container, and the focus trap won't work probably.. + // Also the tabindex is needed for the `escapeToClose` functionality, because + // the keyDown event can't be triggered when the focus is outside of the container. + var startSymbol = $interpolate.startSymbol(); + var endSymbol = $interpolate.endSymbol(); + var theme = startSymbol + (options.themeWatch ? '' : '::') + 'theme' + endSymbol; + return '
' + validatedTemplate(template) + '
'; + + /** + * The specified template should contain a wrapper element.... + */ + function validatedTemplate(template) { + if (options.autoWrap && !/<\/md-dialog>/g.test(template)) { + return '' + (template || '') + ''; + } else { + return template || ''; + } + } + } + }; + + function beforeCompile(options) { + // Automatically apply the theme, if the user didn't specify a theme explicitly. + // Those option changes need to be done, before the compilation has started, because otherwise + // the option changes will be not available in the $mdCompilers locales. + options.defaultTheme = $mdTheming.defaultTheme(); + + detectTheming(options); + } + + function beforeShow(scope, element, options, controller) { + + if (controller) { + var mdHtmlContent = controller.htmlContent || options.htmlContent || ''; + var mdTextContent = controller.textContent || options.textContent || + controller.content || options.content || ''; + + if (mdHtmlContent && !$injector.has('$sanitize')) { + throw Error('The ngSanitize module must be loaded in order to use htmlContent.'); + } + + if (mdHtmlContent && mdTextContent) { + throw Error('md-dialog cannot have both `htmlContent` and `textContent`'); + } + + // Only assign the content if nothing throws, otherwise it'll still be compiled. + controller.mdHtmlContent = mdHtmlContent; + controller.mdTextContent = mdTextContent; + } + } + + /** Show method for dialogs */ + function onShow(scope, element, options, controller) { + angular.element($document[0].body).addClass('md-dialog-is-showing'); + + var dialogElement = element.find('md-dialog'); + + // Once a dialog has `ng-cloak` applied on his template the dialog animation will not work properly. + // This is a very common problem, so we have to notify the developer about this. + if (dialogElement.hasClass('ng-cloak')) { + var message = '$mdDialog: using `` will affect the dialog opening animations.'; + $log.warn( message, element[0] ); + } + + captureParentAndFromToElements(options); + configureAria(dialogElement, options); + showBackdrop(scope, element, options); + activateListeners(element, options); + + return dialogPopIn(element, options) + .then(function() { + lockScreenReader(element, options); + warnDeprecatedActions(); + focusOnOpen(); + }); + + /** + * Check to see if they used the deprecated .md-actions class and log a warning + */ + function warnDeprecatedActions() { + if (element[0].querySelector('.md-actions')) { + $log.warn('Using a class of md-actions is deprecated, please use .'); + } + } + + /** + * For alerts, focus on content... otherwise focus on + * the close button (or equivalent) + */ + function focusOnOpen() { + if (options.focusOnOpen) { + var target = $mdUtil.findFocusTarget(element) || findCloseButton() || dialogElement; + target.focus(); + } + + /** + * If no element with class dialog-close, try to find the last + * button child in md-actions and assume it is a close button. + * + * If we find no actions at all, log a warning to the console. + */ + function findCloseButton() { + return element[0].querySelector('.dialog-close, md-dialog-actions button:last-child'); + } + } + } + + /** + * Remove function for all dialogs + */ + function onRemove(scope, element, options) { + options.deactivateListeners(); + options.unlockScreenReader(); + options.hideBackdrop(options.$destroy); + + // Remove the focus traps that we added earlier for keeping focus within the dialog. + if (topFocusTrap && topFocusTrap.parentNode) { + topFocusTrap.parentNode.removeChild(topFocusTrap); + } + + if (bottomFocusTrap && bottomFocusTrap.parentNode) { + bottomFocusTrap.parentNode.removeChild(bottomFocusTrap); + } + + // For navigation $destroy events, do a quick, non-animated removal, + // but for normal closes (from clicks, etc) animate the removal + return !!options.$destroy ? detachAndClean() : animateRemoval().then( detachAndClean ); + + /** + * For normal closes, animate the removal. + * For forced closes (like $destroy events), skip the animations + */ + function animateRemoval() { + return dialogPopOut(element, options); + } + + /** + * Detach the element + */ + function detachAndClean() { + angular.element($document[0].body).removeClass('md-dialog-is-showing'); + + // Reverse the container stretch if using a content element. + if (options.contentElement) { + options.reverseContainerStretch(); + } + + // Exposed cleanup function from the $mdCompiler. + options.cleanupElement(); + + // Restores the focus to the origin element if the last interaction upon opening was a keyboard. + if (!options.$destroy && options.originInteraction === 'keyboard') { + options.origin.focus(); + } + } + } + + function detectTheming(options) { + // Once the user specifies a targetEvent, we will automatically try to find the correct + // nested theme. + var targetEl; + if (options.targetEvent && options.targetEvent.target) { + targetEl = angular.element(options.targetEvent.target); + } + + var themeCtrl = targetEl && targetEl.controller('mdTheme'); + + if (!themeCtrl) { + return; + } + + options.themeWatch = themeCtrl.$shouldWatch; + + var theme = options.theme || themeCtrl.$mdTheme; + + if (theme) { + options.scope.theme = theme; + } + + var unwatch = themeCtrl.registerChanges(function (newTheme) { + options.scope.theme = newTheme; + + if (!options.themeWatch) { + unwatch(); + } + }); + } + + /** + * Capture originator/trigger/from/to element information (if available) + * and the parent container for the dialog; defaults to the $rootElement + * unless overridden in the options.parent + */ + function captureParentAndFromToElements(options) { + options.origin = angular.extend({ + element: null, + bounds: null, + focus: angular.noop + }, options.origin || {}); + + options.parent = getDomElement(options.parent, $rootElement); + options.closeTo = getBoundingClientRect(getDomElement(options.closeTo)); + options.openFrom = getBoundingClientRect(getDomElement(options.openFrom)); + + if ( options.targetEvent ) { + options.origin = getBoundingClientRect(options.targetEvent.target, options.origin); + options.originInteraction = $mdInteraction.getLastInteractionType(); + } + + + /** + * Identify the bounding RECT for the target element + * + */ + function getBoundingClientRect (element, orig) { + var source = angular.element((element || {})); + if (source && source.length) { + // Compute and save the target element's bounding rect, so that if the + // element is hidden when the dialog closes, we can shrink the dialog + // back to the same position it expanded from. + // + // Checking if the source is a rect object or a DOM element + var bounds = {top:0,left:0,height:0,width:0}; + var hasFn = angular.isFunction(source[0].getBoundingClientRect); + + return angular.extend(orig || {}, { + element : hasFn ? source : undefined, + bounds : hasFn ? source[0].getBoundingClientRect() : angular.extend({}, bounds, source[0]), + focus : angular.bind(source, source.focus), + }); + } + } + + /** + * If the specifier is a simple string selector, then query for + * the DOM element. + */ + function getDomElement(element, defaultElement) { + if (angular.isString(element)) { + element = $document[0].querySelector(element); + } + + // If we have a reference to a raw dom element, always wrap it in jqLite + return angular.element(element || defaultElement); + } + + } + + /** + * Listen for escape keys and outside clicks to auto close + */ + function activateListeners(element, options) { + var window = angular.element($window); + var onWindowResize = $mdUtil.debounce(function() { + stretchDialogContainerToViewport(element, options); + }, 60); + + var removeListeners = []; + var smartClose = function() { + // Only 'confirm' dialogs have a cancel button... escape/clickOutside will + // cancel or fallback to hide. + var closeFn = ( options.$type == 'alert' ) ? $mdDialog.hide : $mdDialog.cancel; + $mdUtil.nextTick(closeFn, true); + }; + + if (options.escapeToClose) { + var parentTarget = options.parent; + var keyHandlerFn = function(ev) { + if (ev.keyCode === $mdConstant.KEY_CODE.ESCAPE) { + ev.stopPropagation(); + ev.preventDefault(); + + smartClose(); + } + }; + + // Add keydown listeners + element.on('keydown', keyHandlerFn); + parentTarget.on('keydown', keyHandlerFn); + + // Queue remove listeners function + removeListeners.push(function() { + + element.off('keydown', keyHandlerFn); + parentTarget.off('keydown', keyHandlerFn); + + }); + } + + // Register listener to update dialog on window resize + window.on('resize', onWindowResize); + + removeListeners.push(function() { + window.off('resize', onWindowResize); + }); + + if (options.clickOutsideToClose) { + var target = element; + var sourceElem; + + // Keep track of the element on which the mouse originally went down + // so that we can only close the backdrop when the 'click' started on it. + // A simple 'click' handler does not work, + // it sets the target object as the element the mouse went down on. + var mousedownHandler = function(ev) { + sourceElem = ev.target; + }; + + // We check if our original element and the target is the backdrop + // because if the original was the backdrop and the target was inside the dialog + // we don't want to dialog to close. + var mouseupHandler = function(ev) { + if (sourceElem === target[0] && ev.target === target[0]) { + ev.stopPropagation(); + ev.preventDefault(); + + smartClose(); + } + }; + + // Add listeners + target.on('mousedown', mousedownHandler); + target.on('mouseup', mouseupHandler); + + // Queue remove listeners function + removeListeners.push(function() { + target.off('mousedown', mousedownHandler); + target.off('mouseup', mouseupHandler); + }); + } + + // Attach specific `remove` listener handler + options.deactivateListeners = function() { + removeListeners.forEach(function(removeFn) { + removeFn(); + }); + options.deactivateListeners = null; + }; + } + + /** + * Show modal backdrop element... + */ + function showBackdrop(scope, element, options) { + + if (options.disableParentScroll) { + // !! DO this before creating the backdrop; since disableScrollAround() + // configures the scroll offset; which is used by mdBackDrop postLink() + options.restoreScroll = $mdUtil.disableScrollAround(element, options.parent); + } + + if (options.hasBackdrop) { + options.backdrop = $mdUtil.createBackdrop(scope, "md-dialog-backdrop md-opaque"); + $animate.enter(options.backdrop, options.parent); + } + + /** + * Hide modal backdrop element... + */ + options.hideBackdrop = function hideBackdrop($destroy) { + if (options.backdrop) { + if ( !!$destroy ) options.backdrop.remove(); + else $animate.leave(options.backdrop); + } + + + if (options.disableParentScroll) { + options.restoreScroll && options.restoreScroll(); + delete options.restoreScroll; + } + + options.hideBackdrop = null; + }; + } + + /** + * Inject ARIA-specific attributes appropriate for Dialogs + */ + function configureAria(element, options) { + + var role = (options.$type === 'alert') ? 'alertdialog' : 'dialog'; + var dialogContent = element.find('md-dialog-content'); + var existingDialogId = element.attr('id'); + var dialogContentId = 'dialogContent_' + (existingDialogId || $mdUtil.nextUid()); + + element.attr({ + 'role': role, + 'tabIndex': '-1' + }); + + if (dialogContent.length === 0) { + dialogContent = element; + // If the dialog element already had an ID, don't clobber it. + if (existingDialogId) { + dialogContentId = existingDialogId; + } + } + + dialogContent.attr('id', dialogContentId); + element.attr('aria-describedby', dialogContentId); + + if (options.ariaLabel) { + $mdAria.expect(element, 'aria-label', options.ariaLabel); + } + else { + $mdAria.expectAsync(element, 'aria-label', function() { + var words = dialogContent.text().split(/\s+/); + if (words.length > 3) words = words.slice(0, 3).concat('...'); + return words.join(' '); + }); + } + + // Set up elements before and after the dialog content to capture focus and + // redirect back into the dialog. + topFocusTrap = document.createElement('div'); + topFocusTrap.classList.add('md-dialog-focus-trap'); + topFocusTrap.tabIndex = 0; + + bottomFocusTrap = topFocusTrap.cloneNode(false); + + // When focus is about to move out of the dialog, we want to intercept it and redirect it + // back to the dialog element. + var focusHandler = function() { + element.focus(); + }; + topFocusTrap.addEventListener('focus', focusHandler); + bottomFocusTrap.addEventListener('focus', focusHandler); + + // The top focus trap inserted immeidately before the md-dialog element (as a sibling). + // The bottom focus trap is inserted at the very end of the md-dialog element (as a child). + element[0].parentNode.insertBefore(topFocusTrap, element[0]); + element.after(bottomFocusTrap); + } + + /** + * Prevents screen reader interaction behind modal window + * on swipe interfaces + */ + function lockScreenReader(element, options) { + var isHidden = true; + + // get raw DOM node + walkDOM(element[0]); + + options.unlockScreenReader = function() { + isHidden = false; + walkDOM(element[0]); + + options.unlockScreenReader = null; + }; + + /** + * Walk DOM to apply or remove aria-hidden on sibling nodes + * and parent sibling nodes + * + */ + function walkDOM(element) { + while (element.parentNode) { + if (element === document.body) { + return; + } + var children = element.parentNode.children; + for (var i = 0; i < children.length; i++) { + // skip over child if it is an ascendant of the dialog + // or a script or style tag + if (element !== children[i] && !isNodeOneOf(children[i], ['SCRIPT', 'STYLE'])) { + children[i].setAttribute('aria-hidden', isHidden); + } + } + + walkDOM(element = element.parentNode); + } + } + } + + /** + * Ensure the dialog container fill-stretches to the viewport + */ + function stretchDialogContainerToViewport(container, options) { + var isFixed = $window.getComputedStyle($document[0].body).position == 'fixed'; + var backdrop = options.backdrop ? $window.getComputedStyle(options.backdrop[0]) : null; + var height = backdrop ? Math.min($document[0].body.clientHeight, Math.ceil(Math.abs(parseInt(backdrop.height, 10)))) : 0; + + var previousStyles = { + top: container.css('top'), + height: container.css('height') + }; + + // If the body is fixed, determine the distance to the viewport in relative from the parent. + var parentTop = Math.abs(options.parent[0].getBoundingClientRect().top); + + container.css({ + top: (isFixed ? parentTop : 0) + 'px', + height: height ? height + 'px' : '100%' + }); + + return function() { + // Reverts the modified styles back to the previous values. + // This is needed for contentElements, which should have the same styles after close + // as before. + container.css(previousStyles); + }; + } + + /** + * Dialog open and pop-in animation + */ + function dialogPopIn(container, options) { + // Add the `md-dialog-container` to the DOM + options.parent.append(container); + options.reverseContainerStretch = stretchDialogContainerToViewport(container, options); + + var dialogEl = container.find('md-dialog'); + var animator = $mdUtil.dom.animator; + var buildTranslateToOrigin = animator.calculateZoomToOrigin; + var translateOptions = {transitionInClass: 'md-transition-in', transitionOutClass: 'md-transition-out'}; + var from = animator.toTransformCss(buildTranslateToOrigin(dialogEl, options.openFrom || options.origin)); + var to = animator.toTransformCss(""); // defaults to center display (or parent or $rootElement) + + dialogEl.toggleClass('md-dialog-fullscreen', !!options.fullscreen); + + return animator + .translate3d(dialogEl, from, to, translateOptions) + .then(function(animateReversal) { + + // Build a reversal translate function synced to this translation... + options.reverseAnimate = function() { + delete options.reverseAnimate; + + if (options.closeTo) { + // Using the opposite classes to create a close animation to the closeTo element + translateOptions = {transitionInClass: 'md-transition-out', transitionOutClass: 'md-transition-in'}; + from = to; + to = animator.toTransformCss(buildTranslateToOrigin(dialogEl, options.closeTo)); + + return animator + .translate3d(dialogEl, from, to,translateOptions); + } + + return animateReversal( + to = animator.toTransformCss( + // in case the origin element has moved or is hidden, + // let's recalculate the translateCSS + buildTranslateToOrigin(dialogEl, options.origin) + ) + ); + + }; + + // Function to revert the generated animation styles on the dialog element. + // Useful when using a contentElement instead of a template. + options.clearAnimate = function() { + delete options.clearAnimate; + + // Remove the transition classes, added from $animateCSS, since those can't be removed + // by reversely running the animator. + dialogEl.removeClass([ + translateOptions.transitionOutClass, + translateOptions.transitionInClass + ].join(' ')); + + // Run the animation reversely to remove the previous added animation styles. + return animator.translate3d(dialogEl, to, animator.toTransformCss(''), {}); + }; + + return true; + }); + } + + /** + * Dialog close and pop-out animation + */ + function dialogPopOut(container, options) { + return options.reverseAnimate().then(function() { + if (options.contentElement) { + // When we use a contentElement, we want the element to be the same as before. + // That means, that we have to clear all the animation properties, like transform. + options.clearAnimate(); + } + }); + } + + /** + * Utility function to filter out raw DOM nodes + */ + function isNodeOneOf(elem, nodeTypeArray) { + if (nodeTypeArray.indexOf(elem.nodeName) !== -1) { + return true; + } + } + + } +} + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.divider + * @description Divider module! + */ +MdDividerDirective.$inject = ["$mdTheming"]; +angular.module('material.components.divider', [ + 'material.core' +]) + .directive('mdDivider', MdDividerDirective); + +/** + * @ngdoc directive + * @name mdDivider + * @module material.components.divider + * @restrict E + * + * @description + * 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. + * + * @param {boolean=} md-inset Add this attribute to activate the inset divider style. + * @usage + * + * + * + * + * + * + */ +function MdDividerDirective($mdTheming) { + return { + restrict: 'E', + link: $mdTheming + }; +} + +})(); +(function(){ +"use strict"; + +(function() { + 'use strict'; + + /** + * @ngdoc module + * @name material.components.fabActions + */ + MdFabActionsDirective.$inject = ["$mdUtil"]; + angular + .module('material.components.fabActions', ['material.core']) + .directive('mdFabActions', MdFabActionsDirective); + + /** + * @ngdoc directive + * @name mdFabActions + * @module material.components.fabActions + * + * @restrict E + * + * @description + * The `` directive is used inside of a `` or + * `` directive to mark an element (or elements) as the actions and setup the + * proper event listeners. + * + * @usage + * See the `` or `` directives for example usage. + */ + function MdFabActionsDirective($mdUtil) { + return { + restrict: 'E', + + require: ['^?mdFabSpeedDial', '^?mdFabToolbar'], + + compile: function(element, attributes) { + var children = element.children(); + + var hasNgRepeat = $mdUtil.prefixer().hasAttribute(children, 'ng-repeat'); + + // Support both ng-repeat and static content + if (hasNgRepeat) { + children.addClass('md-fab-action-item'); + } else { + // Wrap every child in a new div and add a class that we can scale/fling independently + children.wrap('
'); + } + } + }; + } + +})(); + +})(); +(function(){ +"use strict"; + +(function() { + 'use strict'; + + MdFabController.$inject = ["$scope", "$element", "$animate", "$mdUtil", "$mdConstant", "$timeout"]; + angular.module('material.components.fabShared', ['material.core']) + .controller('MdFabController', MdFabController); + + function MdFabController($scope, $element, $animate, $mdUtil, $mdConstant, $timeout) { + var vm = this; + var initialAnimationAttempts = 0; + + // NOTE: We use async eval(s) below to avoid conflicts with any existing digest loops + + vm.open = function() { + $scope.$evalAsync("vm.isOpen = true"); + }; + + vm.close = function() { + // Async eval to avoid conflicts with existing digest loops + $scope.$evalAsync("vm.isOpen = false"); + + // Focus the trigger when the element closes so users can still tab to the next item + $element.find('md-fab-trigger')[0].focus(); + }; + + // Toggle the open/close state when the trigger is clicked + vm.toggle = function() { + $scope.$evalAsync("vm.isOpen = !vm.isOpen"); + }; + + /* + * Angular Lifecycle hook for newer Angular versions. + * Bindings are not guaranteed to have been assigned in the controller, but they are in the $onInit hook. + */ + vm.$onInit = function() { + setupDefaults(); + setupListeners(); + setupWatchers(); + + fireInitialAnimations(); + }; + + // For Angular 1.4 and older, where there are no lifecycle hooks but bindings are pre-assigned, + // manually call the $onInit hook. + if (angular.version.major === 1 && angular.version.minor <= 4) { + this.$onInit(); + } + + function setupDefaults() { + // Set the default direction to 'down' if none is specified + vm.direction = vm.direction || 'down'; + + // Set the default to be closed + vm.isOpen = vm.isOpen || false; + + // Start the keyboard interaction at the first action + resetActionIndex(); + + // Add an animations waiting class so we know not to run + $element.addClass('md-animations-waiting'); + } + + function setupListeners() { + var eventTypes = [ + 'click', 'focusin', 'focusout' + ]; + + // Add our listeners + angular.forEach(eventTypes, function(eventType) { + $element.on(eventType, parseEvents); + }); + + // Remove our listeners when destroyed + $scope.$on('$destroy', function() { + angular.forEach(eventTypes, function(eventType) { + $element.off(eventType, parseEvents); + }); + + // remove any attached keyboard handlers in case element is removed while + // speed dial is open + disableKeyboard(); + }); + } + + var closeTimeout; + function parseEvents(event) { + // If the event is a click, just handle it + if (event.type == 'click') { + handleItemClick(event); + } + + // If we focusout, set a timeout to close the element + if (event.type == 'focusout' && !closeTimeout) { + closeTimeout = $timeout(function() { + vm.close(); + }, 100, false); + } + + // If we see a focusin and there is a timeout about to run, cancel it so we stay open + if (event.type == 'focusin' && closeTimeout) { + $timeout.cancel(closeTimeout); + closeTimeout = null; + } + } + + function resetActionIndex() { + vm.currentActionIndex = -1; + } + + function setupWatchers() { + // Watch for changes to the direction and update classes/attributes + $scope.$watch('vm.direction', function(newDir, oldDir) { + // Add the appropriate classes so we can target the direction in the CSS + $animate.removeClass($element, 'md-' + oldDir); + $animate.addClass($element, 'md-' + newDir); + + // Reset the action index since it may have changed + resetActionIndex(); + }); + + var trigger, actions; + + // Watch for changes to md-open + $scope.$watch('vm.isOpen', function(isOpen) { + // Reset the action index since it may have changed + resetActionIndex(); + + // We can't get the trigger/actions outside of the watch because the component hasn't been + // linked yet, so we wait until the first watch fires to cache them. + if (!trigger || !actions) { + trigger = getTriggerElement(); + actions = getActionsElement(); + } + + if (isOpen) { + enableKeyboard(); + } else { + disableKeyboard(); + } + + var toAdd = isOpen ? 'md-is-open' : ''; + var toRemove = isOpen ? '' : 'md-is-open'; + + // Set the proper ARIA attributes + trigger.attr('aria-haspopup', true); + trigger.attr('aria-expanded', isOpen); + actions.attr('aria-hidden', !isOpen); + + // Animate the CSS classes + $animate.setClass($element, toAdd, toRemove); + }); + } + + function fireInitialAnimations() { + // If the element is actually visible on the screen + if ($element[0].scrollHeight > 0) { + // Fire our animation + $animate.addClass($element, '_md-animations-ready').then(function() { + // Remove the waiting class + $element.removeClass('md-animations-waiting'); + }); + } + + // Otherwise, try for up to 1 second before giving up + else if (initialAnimationAttempts < 10) { + $timeout(fireInitialAnimations, 100); + + // Increment our counter + initialAnimationAttempts = initialAnimationAttempts + 1; + } + } + + function enableKeyboard() { + $element.on('keydown', keyPressed); + + // On the next tick, setup a check for outside clicks; we do this on the next tick to avoid + // clicks/touches that result in the isOpen attribute changing (e.g. a bound radio button) + $mdUtil.nextTick(function() { + angular.element(document).on('click touchend', checkForOutsideClick); + }); + + // TODO: On desktop, we should be able to reset the indexes so you cannot tab through, but + // this breaks accessibility, especially on mobile, since you have no arrow keys to press + //resetActionTabIndexes(); + } + + function disableKeyboard() { + $element.off('keydown', keyPressed); + angular.element(document).off('click touchend', checkForOutsideClick); + } + + function checkForOutsideClick(event) { + if (event.target) { + var closestTrigger = $mdUtil.getClosest(event.target, 'md-fab-trigger'); + var closestActions = $mdUtil.getClosest(event.target, 'md-fab-actions'); + + if (!closestTrigger && !closestActions) { + vm.close(); + } + } + } + + function keyPressed(event) { + switch (event.which) { + case $mdConstant.KEY_CODE.ESCAPE: vm.close(); event.preventDefault(); return false; + case $mdConstant.KEY_CODE.LEFT_ARROW: doKeyLeft(event); return false; + case $mdConstant.KEY_CODE.UP_ARROW: doKeyUp(event); return false; + case $mdConstant.KEY_CODE.RIGHT_ARROW: doKeyRight(event); return false; + case $mdConstant.KEY_CODE.DOWN_ARROW: doKeyDown(event); return false; + } + } + + function doActionPrev(event) { + focusAction(event, -1); + } + + function doActionNext(event) { + focusAction(event, 1); + } + + function focusAction(event, direction) { + var actions = resetActionTabIndexes(); + + // Increment/decrement the counter with restrictions + vm.currentActionIndex = vm.currentActionIndex + direction; + vm.currentActionIndex = Math.min(actions.length - 1, vm.currentActionIndex); + vm.currentActionIndex = Math.max(0, vm.currentActionIndex); + + // Focus the element + var focusElement = angular.element(actions[vm.currentActionIndex]).children()[0]; + angular.element(focusElement).attr('tabindex', 0); + focusElement.focus(); + + // Make sure the event doesn't bubble and cause something else + event.preventDefault(); + event.stopImmediatePropagation(); + } + + function resetActionTabIndexes() { + // Grab all of the actions + var actions = getActionsElement()[0].querySelectorAll('.md-fab-action-item'); + + // Disable all other actions for tabbing + angular.forEach(actions, function(action) { + angular.element(angular.element(action).children()[0]).attr('tabindex', -1); + }); + + return actions; + } + + function doKeyLeft(event) { + if (vm.direction === 'left') { + doActionNext(event); + } else { + doActionPrev(event); + } + } + + function doKeyUp(event) { + if (vm.direction === 'down') { + doActionPrev(event); + } else { + doActionNext(event); + } + } + + function doKeyRight(event) { + if (vm.direction === 'left') { + doActionPrev(event); + } else { + doActionNext(event); + } + } + + function doKeyDown(event) { + if (vm.direction === 'up') { + doActionPrev(event); + } else { + doActionNext(event); + } + } + + function isTrigger(element) { + return $mdUtil.getClosest(element, 'md-fab-trigger'); + } + + function isAction(element) { + return $mdUtil.getClosest(element, 'md-fab-actions'); + } + + function handleItemClick(event) { + if (isTrigger(event.target)) { + vm.toggle(); + } + + if (isAction(event.target)) { + vm.close(); + } + } + + function getTriggerElement() { + return $element.find('md-fab-trigger'); + } + + function getActionsElement() { + return $element.find('md-fab-actions'); + } + } +})(); + +})(); +(function(){ +"use strict"; + +(function() { + 'use strict'; + + /** + * The duration of the CSS animation in milliseconds. + * + * @type {number} + */ + MdFabSpeedDialFlingAnimation.$inject = ["$timeout"]; + MdFabSpeedDialScaleAnimation.$inject = ["$timeout"]; + var cssAnimationDuration = 300; + + /** + * @ngdoc module + * @name material.components.fabSpeedDial + */ + angular + // Declare our module + .module('material.components.fabSpeedDial', [ + 'material.core', + 'material.components.fabShared', + 'material.components.fabActions' + ]) + + // Register our directive + .directive('mdFabSpeedDial', MdFabSpeedDialDirective) + + // Register our custom animations + .animation('.md-fling', MdFabSpeedDialFlingAnimation) + .animation('.md-scale', MdFabSpeedDialScaleAnimation) + + // Register a service for each animation so that we can easily inject them into unit tests + .service('mdFabSpeedDialFlingAnimation', MdFabSpeedDialFlingAnimation) + .service('mdFabSpeedDialScaleAnimation', MdFabSpeedDialScaleAnimation); + + /** + * @ngdoc directive + * @name mdFabSpeedDial + * @module material.components.fabSpeedDial + * + * @restrict E + * + * @description + * The `` directive is used to present a series of popup elements (usually + * ``s) for quick access to common actions. + * + * There are currently two animations available by applying one of the following classes to + * the component: + * + * - `md-fling` - The speed dial items appear from underneath the trigger and move into their + * appropriate positions. + * - `md-scale` - The speed dial items appear in their proper places by scaling from 0% to 100%. + * + * You may also easily position the trigger by applying one one of the following classes to the + * `` element: + * - `md-fab-top-left` + * - `md-fab-top-right` + * - `md-fab-bottom-left` + * - `md-fab-bottom-right` + * + * These CSS classes use `position: absolute`, so you need to ensure that the container element + * also uses `position: absolute` or `position: relative` in order for them to work. + * + * Additionally, you may use the standard `ng-mouseenter` and `ng-mouseleave` directives to + * open or close the speed dial. However, if you wish to allow users to hover over the empty + * space where the actions will appear, you must also add the `md-hover-full` class to the speed + * dial element. Without this, the hover effect will only occur on top of the trigger. + * + * See the demos for more information. + * + * ## Troubleshooting + * + * If your speed dial shows the closing animation upon launch, you may need to use `ng-cloak` on + * the parent container to ensure that it is only visible once ready. We have plans to remove this + * necessity in the future. + * + * @usage + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * @param {string} md-direction From which direction you would like the speed dial to appear + * relative to the trigger element. + * @param {expression=} md-open Programmatically control whether or not the speed-dial is visible. + */ + function MdFabSpeedDialDirective() { + return { + restrict: 'E', + + scope: { + direction: '@?mdDirection', + isOpen: '=?mdOpen' + }, + + bindToController: true, + controller: 'MdFabController', + controllerAs: 'vm', + + link: FabSpeedDialLink + }; + + function FabSpeedDialLink(scope, element) { + // Prepend an element to hold our CSS variables so we can use them in the animations below + element.prepend('
'); + } + } + + function MdFabSpeedDialFlingAnimation($timeout) { + function delayDone(done) { $timeout(done, cssAnimationDuration, false); } + + function runAnimation(element) { + // Don't run if we are still waiting and we are not ready + if (element.hasClass('md-animations-waiting') && !element.hasClass('_md-animations-ready')) { + return; + } + + var el = element[0]; + var ctrl = element.controller('mdFabSpeedDial'); + var items = el.querySelectorAll('.md-fab-action-item'); + + // Grab our trigger element + var triggerElement = el.querySelector('md-fab-trigger'); + + // Grab our element which stores CSS variables + var variablesElement = el.querySelector('._md-css-variables'); + + // Setup JS variables based on our CSS variables + var startZIndex = parseInt(window.getComputedStyle(variablesElement).zIndex); + + // Always reset the items to their natural position/state + angular.forEach(items, function(item, index) { + var styles = item.style; + + styles.transform = styles.webkitTransform = ''; + styles.transitionDelay = ''; + styles.opacity = 1; + + // Make the items closest to the trigger have the highest z-index + styles.zIndex = (items.length - index) + startZIndex; + }); + + // Set the trigger to be above all of the actions so they disappear behind it. + triggerElement.style.zIndex = startZIndex + items.length + 1; + + // If the control is closed, hide the items behind the trigger + if (!ctrl.isOpen) { + angular.forEach(items, function(item, index) { + var newPosition, axis; + var styles = item.style; + + // Make sure to account for differences in the dimensions of the trigger verses the items + // so that we can properly center everything; this helps hide the item's shadows behind + // the trigger. + var triggerItemHeightOffset = (triggerElement.clientHeight - item.clientHeight) / 2; + var triggerItemWidthOffset = (triggerElement.clientWidth - item.clientWidth) / 2; + + switch (ctrl.direction) { + case 'up': + newPosition = (item.scrollHeight * (index + 1) + triggerItemHeightOffset); + axis = 'Y'; + break; + case 'down': + newPosition = -(item.scrollHeight * (index + 1) + triggerItemHeightOffset); + axis = 'Y'; + break; + case 'left': + newPosition = (item.scrollWidth * (index + 1) + triggerItemWidthOffset); + axis = 'X'; + break; + case 'right': + newPosition = -(item.scrollWidth * (index + 1) + triggerItemWidthOffset); + axis = 'X'; + break; + } + + var newTranslate = 'translate' + axis + '(' + newPosition + 'px)'; + + styles.transform = styles.webkitTransform = newTranslate; + }); + } + } + + return { + addClass: function(element, className, done) { + if (element.hasClass('md-fling')) { + runAnimation(element); + delayDone(done); + } else { + done(); + } + }, + removeClass: function(element, className, done) { + runAnimation(element); + delayDone(done); + } + }; + } + + function MdFabSpeedDialScaleAnimation($timeout) { + function delayDone(done) { $timeout(done, cssAnimationDuration, false); } + + var delay = 65; + + function runAnimation(element) { + var el = element[0]; + var ctrl = element.controller('mdFabSpeedDial'); + var items = el.querySelectorAll('.md-fab-action-item'); + + // Grab our element which stores CSS variables + var variablesElement = el.querySelector('._md-css-variables'); + + // Setup JS variables based on our CSS variables + var startZIndex = parseInt(window.getComputedStyle(variablesElement).zIndex); + + // Always reset the items to their natural position/state + angular.forEach(items, function(item, index) { + var styles = item.style, + offsetDelay = index * delay; + + styles.opacity = ctrl.isOpen ? 1 : 0; + styles.transform = styles.webkitTransform = ctrl.isOpen ? 'scale(1)' : 'scale(0)'; + styles.transitionDelay = (ctrl.isOpen ? offsetDelay : (items.length - offsetDelay)) + 'ms'; + + // Make the items closest to the trigger have the highest z-index + styles.zIndex = (items.length - index) + startZIndex; + }); + } + + return { + addClass: function(element, className, done) { + runAnimation(element); + delayDone(done); + }, + + removeClass: function(element, className, done) { + runAnimation(element); + delayDone(done); + } + }; + } +})(); + +})(); +(function(){ +"use strict"; + +(function() { + 'use strict'; + + /** + * @ngdoc module + * @name material.components.fabToolbar + */ + angular + // Declare our module + .module('material.components.fabToolbar', [ + 'material.core', + 'material.components.fabShared', + 'material.components.fabActions' + ]) + + // Register our directive + .directive('mdFabToolbar', MdFabToolbarDirective) + + // Register our custom animations + .animation('.md-fab-toolbar', MdFabToolbarAnimation) + + // Register a service for the animation so that we can easily inject it into unit tests + .service('mdFabToolbarAnimation', MdFabToolbarAnimation); + + /** + * @ngdoc directive + * @name mdFabToolbar + * @module material.components.fabToolbar + * + * @restrict E + * + * @description + * + * The `` directive is used to present a toolbar of elements (usually ``s) + * for quick access to common actions when a floating action button is activated (via click or + * keyboard navigation). + * + * You may also easily position the trigger by applying one one of the following classes to the + * `` element: + * - `md-fab-top-left` + * - `md-fab-top-right` + * - `md-fab-bottom-left` + * - `md-fab-bottom-right` + * + * These CSS classes use `position: absolute`, so you need to ensure that the container element + * also uses `position: absolute` or `position: relative` in order for them to work. + * + * @usage + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * @param {string} md-direction From which direction you would like the toolbar items to appear + * relative to the trigger element. Supports `left` and `right` directions. + * @param {expression=} md-open Programmatically control whether or not the toolbar is visible. + */ + function MdFabToolbarDirective() { + return { + restrict: 'E', + transclude: true, + template: '
' + + '
' + + '
', + + scope: { + direction: '@?mdDirection', + isOpen: '=?mdOpen' + }, + + bindToController: true, + controller: 'MdFabController', + controllerAs: 'vm', + + link: link + }; + + function link(scope, element, attributes) { + // Add the base class for animations + element.addClass('md-fab-toolbar'); + + // Prepend the background element to the trigger's button + element.find('md-fab-trigger').find('button') + .prepend('
'); + } + } + + function MdFabToolbarAnimation() { + + function runAnimation(element, className, done) { + // If no className was specified, don't do anything + if (!className) { + return; + } + + var el = element[0]; + var ctrl = element.controller('mdFabToolbar'); + + // Grab the relevant child elements + var backgroundElement = el.querySelector('.md-fab-toolbar-background'); + var triggerElement = el.querySelector('md-fab-trigger button'); + var toolbarElement = el.querySelector('md-toolbar'); + var iconElement = el.querySelector('md-fab-trigger button md-icon'); + var actions = element.find('md-fab-actions').children(); + + // If we have both elements, use them to position the new background + if (triggerElement && backgroundElement) { + // Get our variables + var color = window.getComputedStyle(triggerElement).getPropertyValue('background-color'); + var width = el.offsetWidth; + var height = el.offsetHeight; + + // Make it twice as big as it should be since we scale from the center + var scale = 2 * (width / triggerElement.offsetWidth); + + // Set some basic styles no matter what animation we're doing + backgroundElement.style.backgroundColor = color; + backgroundElement.style.borderRadius = width + 'px'; + + // If we're open + if (ctrl.isOpen) { + // Turn on toolbar pointer events when closed + toolbarElement.style.pointerEvents = 'inherit'; + + backgroundElement.style.width = triggerElement.offsetWidth + 'px'; + backgroundElement.style.height = triggerElement.offsetHeight + 'px'; + backgroundElement.style.transform = 'scale(' + scale + ')'; + + // Set the next close animation to have the proper delays + backgroundElement.style.transitionDelay = '0ms'; + iconElement && (iconElement.style.transitionDelay = '.3s'); + + // Apply a transition delay to actions + angular.forEach(actions, function(action, index) { + action.style.transitionDelay = (actions.length - index) * 25 + 'ms'; + }); + } else { + // Turn off toolbar pointer events when closed + toolbarElement.style.pointerEvents = 'none'; + + // Scale it back down to the trigger's size + backgroundElement.style.transform = 'scale(1)'; + + // Reset the position + backgroundElement.style.top = '0'; + + if (element.hasClass('md-right')) { + backgroundElement.style.left = '0'; + backgroundElement.style.right = null; + } + + if (element.hasClass('md-left')) { + backgroundElement.style.right = '0'; + backgroundElement.style.left = null; + } + + // Set the next open animation to have the proper delays + backgroundElement.style.transitionDelay = '200ms'; + iconElement && (iconElement.style.transitionDelay = '0ms'); + + // Apply a transition delay to actions + angular.forEach(actions, function(action, index) { + action.style.transitionDelay = 200 + (index * 25) + 'ms'; + }); + } + } + } + + return { + addClass: function(element, className, done) { + runAnimation(element, className, done); + done(); + }, + + removeClass: function(element, className, done) { + runAnimation(element, className, done); + done(); + } + }; + } +})(); + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.gridList + */ +GridListController.$inject = ["$mdUtil"]; +GridLayoutFactory.$inject = ["$mdUtil"]; +GridListDirective.$inject = ["$interpolate", "$mdConstant", "$mdGridLayout", "$mdMedia"]; +GridTileDirective.$inject = ["$mdMedia"]; +angular.module('material.components.gridList', ['material.core']) + .directive('mdGridList', GridListDirective) + .directive('mdGridTile', GridTileDirective) + .directive('mdGridTileFooter', GridTileCaptionDirective) + .directive('mdGridTileHeader', GridTileCaptionDirective) + .factory('$mdGridLayout', GridLayoutFactory); + +/** + * @ngdoc directive + * @name mdGridList + * @module material.components.gridList + * @restrict E + * @description + * Grid lists are an alternative to standard list views. Grid lists are distinct + * from grids used for layouts and other visual presentations. + * + * A grid list is best suited to presenting a homogenous data type, typically + * images, and is optimized for visual comprehension and differentiating between + * like data types. + * + * A grid list is a continuous element consisting of tessellated, regular + * subdivisions called cells that contain tiles (`md-grid-tile`). + * + * Concept of grid explained visually + * Grid concepts legend + * + * Cells are arrayed vertically and horizontally within the grid. + * + * Tiles hold content and can span one or more cells vertically or horizontally. + * + * ### Responsive Attributes + * + * The `md-grid-list` directive supports "responsive" attributes, which allow + * different `md-cols`, `md-gutter` and `md-row-height` values depending on the + * currently matching media query. + * + * In order to set a responsive attribute, first define the fallback value with + * the standard attribute name, then add additional attributes with the + * following convention: `{base-attribute-name}-{media-query-name}="{value}"` + * (ie. `md-cols-lg="8"`) + * + * @param {number} md-cols Number of columns in the grid. + * @param {string} md-row-height One of + *
    + *
  • CSS length - Fixed height rows (eg. `8px` or `1rem`)
  • + *
  • `{width}:{height}` - Ratio of width to height (eg. + * `md-row-height="16:9"`)
  • + *
  • `"fit"` - Height will be determined by subdividing the available + * height by the number of rows
  • + *
+ * @param {string=} md-gutter The amount of space between tiles in CSS units + * (default 1px) + * @param {expression=} md-on-layout Expression to evaluate after layout. Event + * object is available as `$event`, and contains performance information. + * + * @usage + * Basic: + * + * + * + * + * + * + * Fixed-height rows: + * + * + * + * + * + * + * Fit rows: + * + * + * + * + * + * + * Using responsive attributes: + * + * + * + * + * + */ +function GridListDirective($interpolate, $mdConstant, $mdGridLayout, $mdMedia) { + return { + restrict: 'E', + controller: GridListController, + scope: { + mdOnLayout: '&' + }, + link: postLink + }; + + function postLink(scope, element, attrs, ctrl) { + element.addClass('_md'); // private md component indicator for styling + + // Apply semantics + element.attr('role', 'list'); + + // Provide the controller with a way to trigger layouts. + ctrl.layoutDelegate = layoutDelegate; + + var invalidateLayout = angular.bind(ctrl, ctrl.invalidateLayout), + unwatchAttrs = watchMedia(); + scope.$on('$destroy', unwatchMedia); + + /** + * Watches for changes in media, invalidating layout as necessary. + */ + function watchMedia() { + for (var mediaName in $mdConstant.MEDIA) { + $mdMedia(mediaName); // initialize + $mdMedia.getQuery($mdConstant.MEDIA[mediaName]) + .addListener(invalidateLayout); + } + return $mdMedia.watchResponsiveAttributes( + ['md-cols', 'md-row-height', 'md-gutter'], attrs, layoutIfMediaMatch); + } + + function unwatchMedia() { + ctrl.layoutDelegate = angular.noop; + + unwatchAttrs(); + for (var mediaName in $mdConstant.MEDIA) { + $mdMedia.getQuery($mdConstant.MEDIA[mediaName]) + .removeListener(invalidateLayout); + } + } + + /** + * Performs grid layout if the provided mediaName matches the currently + * active media type. + */ + function layoutIfMediaMatch(mediaName) { + if (mediaName == null) { + // TODO(shyndman): It would be nice to only layout if we have + // instances of attributes using this media type + ctrl.invalidateLayout(); + } else if ($mdMedia(mediaName)) { + ctrl.invalidateLayout(); + } + } + + var lastLayoutProps; + + /** + * Invokes the layout engine, and uses its results to lay out our + * tile elements. + * + * @param {boolean} tilesInvalidated Whether tiles have been + * added/removed/moved since the last layout. This is to avoid situations + * where tiles are replaced with properties identical to their removed + * counterparts. + */ + function layoutDelegate(tilesInvalidated) { + var tiles = getTileElements(); + var props = { + tileSpans: getTileSpans(tiles), + colCount: getColumnCount(), + rowMode: getRowMode(), + rowHeight: getRowHeight(), + gutter: getGutter() + }; + + if (!tilesInvalidated && angular.equals(props, lastLayoutProps)) { + return; + } + + var performance = + $mdGridLayout(props.colCount, props.tileSpans, tiles) + .map(function(tilePositions, rowCount) { + return { + grid: { + element: element, + style: getGridStyle(props.colCount, rowCount, + props.gutter, props.rowMode, props.rowHeight) + }, + tiles: tilePositions.map(function(ps, i) { + return { + element: angular.element(tiles[i]), + style: getTileStyle(ps.position, ps.spans, + props.colCount, rowCount, + props.gutter, props.rowMode, props.rowHeight) + } + }) + } + }) + .reflow() + .performance(); + + // Report layout + scope.mdOnLayout({ + $event: { + performance: performance + } + }); + + lastLayoutProps = props; + } + + // Use $interpolate to do some simple string interpolation as a convenience. + + var startSymbol = $interpolate.startSymbol(); + var endSymbol = $interpolate.endSymbol(); + + // Returns an expression wrapped in the interpolator's start and end symbols. + function expr(exprStr) { + return startSymbol + exprStr + endSymbol; + } + + // The amount of space a single 1x1 tile would take up (either width or height), used as + // a basis for other calculations. This consists of taking the base size percent (as would be + // if evenly dividing the size between cells), and then subtracting the size of one gutter. + // However, since there are no gutters on the edges, each tile only uses a fration + // (gutterShare = numGutters / numCells) of the gutter size. (Imagine having one gutter per + // tile, and then breaking up the extra gutter on the edge evenly among the cells). + var UNIT = $interpolate(expr('share') + '% - (' + expr('gutter') + ' * ' + expr('gutterShare') + ')'); + + // The horizontal or vertical position of a tile, e.g., the 'top' or 'left' property value. + // The position comes the size of a 1x1 tile plus gutter for each previous tile in the + // row/column (offset). + var POSITION = $interpolate('calc((' + expr('unit') + ' + ' + expr('gutter') + ') * ' + expr('offset') + ')'); + + // The actual size of a tile, e.g., width or height, taking rowSpan or colSpan into account. + // This is computed by multiplying the base unit by the rowSpan/colSpan, and then adding back + // in the space that the gutter would normally have used (which was already accounted for in + // the base unit calculation). + var DIMENSION = $interpolate('calc((' + expr('unit') + ') * ' + expr('span') + ' + (' + expr('span') + ' - 1) * ' + expr('gutter') + ')'); + + /** + * Gets the styles applied to a tile element described by the given parameters. + * @param {{row: number, col: number}} position The row and column indices of the tile. + * @param {{row: number, col: number}} spans The rowSpan and colSpan of the tile. + * @param {number} colCount The number of columns. + * @param {number} rowCount The number of rows. + * @param {string} gutter The amount of space between tiles. This will be something like + * '5px' or '2em'. + * @param {string} rowMode The row height mode. Can be one of: + * 'fixed': all rows have a fixed size, given by rowHeight, + * 'ratio': row height defined as a ratio to width, or + * 'fit': fit to the grid-list element height, divinding evenly among rows. + * @param {string|number} rowHeight The height of a row. This is only used for 'fixed' mode and + * for 'ratio' mode. For 'ratio' mode, this is the *ratio* of width-to-height (e.g., 0.75). + * @returns {Object} Map of CSS properties to be applied to the style element. Will define + * values for top, left, width, height, marginTop, and paddingTop. + */ + function getTileStyle(position, spans, colCount, rowCount, gutter, rowMode, rowHeight) { + // TODO(shyndman): There are style caching opportunities here. + + // Percent of the available horizontal space that one column takes up. + var hShare = (1 / colCount) * 100; + + // Fraction of the gutter size that each column takes up. + var hGutterShare = (colCount - 1) / colCount; + + // Base horizontal size of a column. + var hUnit = UNIT({share: hShare, gutterShare: hGutterShare, gutter: gutter}); + + // The width and horizontal position of each tile is always calculated the same way, but the + // height and vertical position depends on the rowMode. + var style = { + left: POSITION({ unit: hUnit, offset: position.col, gutter: gutter }), + width: DIMENSION({ unit: hUnit, span: spans.col, gutter: gutter }), + // resets + paddingTop: '', + marginTop: '', + top: '', + height: '' + }; + + switch (rowMode) { + case 'fixed': + // In fixed mode, simply use the given rowHeight. + style.top = POSITION({ unit: rowHeight, offset: position.row, gutter: gutter }); + style.height = DIMENSION({ unit: rowHeight, span: spans.row, gutter: gutter }); + break; + + case 'ratio': + // Percent of the available vertical space that one row takes up. Here, rowHeight holds + // the ratio value. For example, if the width:height ratio is 4:3, rowHeight = 1.333. + var vShare = hShare / rowHeight; + + // Base veritcal size of a row. + var vUnit = UNIT({ share: vShare, gutterShare: hGutterShare, gutter: gutter }); + + // padidngTop and marginTop are used to maintain the given aspect ratio, as + // a percentage-based value for these properties is applied to the *width* of the + // containing block. See http://www.w3.org/TR/CSS2/box.html#margin-properties + style.paddingTop = DIMENSION({ unit: vUnit, span: spans.row, gutter: gutter}); + style.marginTop = POSITION({ unit: vUnit, offset: position.row, gutter: gutter }); + break; + + case 'fit': + // Fraction of the gutter size that each column takes up. + var vGutterShare = (rowCount - 1) / rowCount; + + // Percent of the available vertical space that one row takes up. + var vShare = (1 / rowCount) * 100; + + // Base vertical size of a row. + var vUnit = UNIT({share: vShare, gutterShare: vGutterShare, gutter: gutter}); + + style.top = POSITION({unit: vUnit, offset: position.row, gutter: gutter}); + style.height = DIMENSION({unit: vUnit, span: spans.row, gutter: gutter}); + break; + } + + return style; + } + + function getGridStyle(colCount, rowCount, gutter, rowMode, rowHeight) { + var style = {}; + + switch(rowMode) { + case 'fixed': + style.height = DIMENSION({ unit: rowHeight, span: rowCount, gutter: gutter }); + style.paddingBottom = ''; + break; + + case 'ratio': + // rowHeight is width / height + var hGutterShare = colCount === 1 ? 0 : (colCount - 1) / colCount, + hShare = (1 / colCount) * 100, + vShare = hShare * (1 / rowHeight), + vUnit = UNIT({ share: vShare, gutterShare: hGutterShare, gutter: gutter }); + + style.height = ''; + style.paddingBottom = DIMENSION({ unit: vUnit, span: rowCount, gutter: gutter}); + break; + + case 'fit': + // noop, as the height is user set + break; + } + + return style; + } + + function getTileElements() { + return [].filter.call(element.children(), function(ele) { + return ele.tagName == 'MD-GRID-TILE' && !ele.$$mdDestroyed; + }); + } + + /** + * Gets an array of objects containing the rowspan and colspan for each tile. + * @returns {Array<{row: number, col: number}>} + */ + function getTileSpans(tileElements) { + return [].map.call(tileElements, function(ele) { + var ctrl = angular.element(ele).controller('mdGridTile'); + return { + row: parseInt( + $mdMedia.getResponsiveAttribute(ctrl.$attrs, 'md-rowspan'), 10) || 1, + col: parseInt( + $mdMedia.getResponsiveAttribute(ctrl.$attrs, 'md-colspan'), 10) || 1 + }; + }); + } + + function getColumnCount() { + var colCount = parseInt($mdMedia.getResponsiveAttribute(attrs, 'md-cols'), 10); + if (isNaN(colCount)) { + throw 'md-grid-list: md-cols attribute was not found, or contained a non-numeric value'; + } + return colCount; + } + + function getGutter() { + return applyDefaultUnit($mdMedia.getResponsiveAttribute(attrs, 'md-gutter') || 1); + } + + function getRowHeight() { + var rowHeight = $mdMedia.getResponsiveAttribute(attrs, 'md-row-height'); + if (!rowHeight) { + throw 'md-grid-list: md-row-height attribute was not found'; + } + + switch (getRowMode()) { + case 'fixed': + return applyDefaultUnit(rowHeight); + case 'ratio': + var whRatio = rowHeight.split(':'); + return parseFloat(whRatio[0]) / parseFloat(whRatio[1]); + case 'fit': + return 0; // N/A + } + } + + function getRowMode() { + var rowHeight = $mdMedia.getResponsiveAttribute(attrs, 'md-row-height'); + if (!rowHeight) { + throw 'md-grid-list: md-row-height attribute was not found'; + } + + if (rowHeight == 'fit') { + return 'fit'; + } else if (rowHeight.indexOf(':') !== -1) { + return 'ratio'; + } else { + return 'fixed'; + } + } + + function applyDefaultUnit(val) { + return /\D$/.test(val) ? val : val + 'px'; + } + } +} + +/* @ngInject */ +function GridListController($mdUtil) { + this.layoutInvalidated = false; + this.tilesInvalidated = false; + this.$timeout_ = $mdUtil.nextTick; + this.layoutDelegate = angular.noop; +} + +GridListController.prototype = { + invalidateTiles: function() { + this.tilesInvalidated = true; + this.invalidateLayout(); + }, + + invalidateLayout: function() { + if (this.layoutInvalidated) { + return; + } + this.layoutInvalidated = true; + this.$timeout_(angular.bind(this, this.layout)); + }, + + layout: function() { + try { + this.layoutDelegate(this.tilesInvalidated); + } finally { + this.layoutInvalidated = false; + this.tilesInvalidated = false; + } + } +}; + + +/* @ngInject */ +function GridLayoutFactory($mdUtil) { + var defaultAnimator = GridTileAnimator; + + /** + * Set the reflow animator callback + */ + GridLayout.animateWith = function(customAnimator) { + defaultAnimator = !angular.isFunction(customAnimator) ? GridTileAnimator : customAnimator; + }; + + return GridLayout; + + /** + * Publish layout function + */ + function GridLayout(colCount, tileSpans) { + var self, layoutInfo, gridStyles, layoutTime, mapTime, reflowTime; + + layoutTime = $mdUtil.time(function() { + layoutInfo = calculateGridFor(colCount, tileSpans); + }); + + return self = { + + /** + * An array of objects describing each tile's position in the grid. + */ + layoutInfo: function() { + return layoutInfo; + }, + + /** + * Maps grid positioning to an element and a set of styles using the + * provided updateFn. + */ + map: function(updateFn) { + mapTime = $mdUtil.time(function() { + var info = self.layoutInfo(); + gridStyles = updateFn(info.positioning, info.rowCount); + }); + return self; + }, + + /** + * Default animator simply sets the element.css( ). An alternate + * animator can be provided as an argument. The function has the following + * signature: + * + * function({grid: {element: JQLite, style: Object}, tiles: Array<{element: JQLite, style: Object}>) + */ + reflow: function(animatorFn) { + reflowTime = $mdUtil.time(function() { + var animator = animatorFn || defaultAnimator; + animator(gridStyles.grid, gridStyles.tiles); + }); + return self; + }, + + /** + * Timing for the most recent layout run. + */ + performance: function() { + return { + tileCount: tileSpans.length, + layoutTime: layoutTime, + mapTime: mapTime, + reflowTime: reflowTime, + totalTime: layoutTime + mapTime + reflowTime + }; + } + }; + } + + /** + * Default Gridlist animator simple sets the css for each element; + * NOTE: any transitions effects must be manually set in the CSS. + * e.g. + * + * md-grid-tile { + * transition: all 700ms ease-out 50ms; + * } + * + */ + function GridTileAnimator(grid, tiles) { + grid.element.css(grid.style); + tiles.forEach(function(t) { + t.element.css(t.style); + }) + } + + /** + * Calculates the positions of tiles. + * + * The algorithm works as follows: + * An Array with length colCount (spaceTracker) keeps track of + * available tiling positions, where elements of value 0 represents an + * empty position. Space for a tile is reserved by finding a sequence of + * 0s with length <= than the tile's colspan. When such a space has been + * found, the occupied tile positions are incremented by the tile's + * rowspan value, as these positions have become unavailable for that + * many rows. + * + * If the end of a row has been reached without finding space for the + * tile, spaceTracker's elements are each decremented by 1 to a minimum + * of 0. Rows are searched in this fashion until space is found. + */ + function calculateGridFor(colCount, tileSpans) { + var curCol = 0, + curRow = 0, + spaceTracker = newSpaceTracker(); + + return { + positioning: tileSpans.map(function(spans, i) { + return { + spans: spans, + position: reserveSpace(spans, i) + }; + }), + rowCount: curRow + Math.max.apply(Math, spaceTracker) + }; + + function reserveSpace(spans, i) { + if (spans.col > colCount) { + throw 'md-grid-list: Tile at position ' + i + ' has a colspan ' + + '(' + spans.col + ') that exceeds the column count ' + + '(' + colCount + ')'; + } + + var start = 0, + end = 0; + + // TODO(shyndman): This loop isn't strictly necessary if you can + // determine the minimum number of rows before a space opens up. To do + // this, recognize that you've iterated across an entire row looking for + // space, and if so fast-forward by the minimum rowSpan count. Repeat + // until the required space opens up. + while (end - start < spans.col) { + if (curCol >= colCount) { + nextRow(); + continue; + } + + start = spaceTracker.indexOf(0, curCol); + if (start === -1 || (end = findEnd(start + 1)) === -1) { + start = end = 0; + nextRow(); + continue; + } + + curCol = end + 1; + } + + adjustRow(start, spans.col, spans.row); + curCol = start + spans.col; + + return { + col: start, + row: curRow + }; + } + + function nextRow() { + curCol = 0; + curRow++; + adjustRow(0, colCount, -1); // Decrement row spans by one + } + + function adjustRow(from, cols, by) { + for (var i = from; i < from + cols; i++) { + spaceTracker[i] = Math.max(spaceTracker[i] + by, 0); + } + } + + function findEnd(start) { + var i; + for (i = start; i < spaceTracker.length; i++) { + if (spaceTracker[i] !== 0) { + return i; + } + } + + if (i === spaceTracker.length) { + return i; + } + } + + function newSpaceTracker() { + var tracker = []; + for (var i = 0; i < colCount; i++) { + tracker.push(0); + } + return tracker; + } + } +} + +/** + * @ngdoc directive + * @name mdGridTile + * @module material.components.gridList + * @restrict E + * @description + * Tiles contain the content of an `md-grid-list`. They span one or more grid + * cells vertically or horizontally, and use `md-grid-tile-{footer,header}` to + * display secondary content. + * + * ### Responsive Attributes + * + * The `md-grid-tile` directive supports "responsive" attributes, which allow + * different `md-rowspan` and `md-colspan` values depending on the currently + * matching media query. + * + * In order to set a responsive attribute, first define the fallback value with + * the standard attribute name, then add additional attributes with the + * following convention: `{base-attribute-name}-{media-query-name}="{value}"` + * (ie. `md-colspan-sm="4"`) + * + * @param {number=} md-colspan The number of columns to span (default 1). Cannot + * exceed the number of columns in the grid. Supports interpolation. + * @param {number=} md-rowspan The number of rows to span (default 1). Supports + * interpolation. + * + * @usage + * With header: + * + * + * + *

This is a header

+ *
+ *
+ *
+ * + * With footer: + * + * + * + *

This is a footer

+ *
+ *
+ *
+ * + * Spanning multiple rows/columns: + * + * + * + * + * + * Responsive attributes: + * + * + * + * + */ +function GridTileDirective($mdMedia) { + return { + restrict: 'E', + require: '^mdGridList', + template: '
', + transclude: true, + scope: {}, + // Simple controller that exposes attributes to the grid directive + controller: ["$attrs", function($attrs) { + this.$attrs = $attrs; + }], + link: postLink + }; + + function postLink(scope, element, attrs, gridCtrl) { + // Apply semantics + element.attr('role', 'listitem'); + + // If our colspan or rowspan changes, trigger a layout + var unwatchAttrs = $mdMedia.watchResponsiveAttributes(['md-colspan', 'md-rowspan'], + attrs, angular.bind(gridCtrl, gridCtrl.invalidateLayout)); + + // Tile registration/deregistration + gridCtrl.invalidateTiles(); + scope.$on('$destroy', function() { + // Mark the tile as destroyed so it is no longer considered in layout, + // even if the DOM element sticks around (like during a leave animation) + element[0].$$mdDestroyed = true; + unwatchAttrs(); + gridCtrl.invalidateLayout(); + }); + + if (angular.isDefined(scope.$parent.$index)) { + scope.$watch(function() { return scope.$parent.$index; }, + function indexChanged(newIdx, oldIdx) { + if (newIdx === oldIdx) { + return; + } + gridCtrl.invalidateTiles(); + }); + } + } +} + + +function GridTileCaptionDirective() { + return { + template: '
', + transclude: true + }; +} + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.icon + * @description + * Icon + */ +angular.module('material.components.icon', ['material.core']); + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.input + */ +mdInputContainerDirective.$inject = ["$mdTheming", "$parse"]; +inputTextareaDirective.$inject = ["$mdUtil", "$window", "$mdAria", "$timeout", "$mdGesture"]; +mdMaxlengthDirective.$inject = ["$animate", "$mdUtil"]; +placeholderDirective.$inject = ["$compile"]; +ngMessageDirective.$inject = ["$mdUtil"]; +mdSelectOnFocusDirective.$inject = ["$timeout"]; +mdInputInvalidMessagesAnimation.$inject = ["$$AnimateRunner", "$animateCss", "$mdUtil", "$log"]; +ngMessagesAnimation.$inject = ["$$AnimateRunner", "$animateCss", "$mdUtil", "$log"]; +ngMessageAnimation.$inject = ["$$AnimateRunner", "$animateCss", "$mdUtil", "$log"]; +var inputModule = angular.module('material.components.input', [ + 'material.core' + ]) + .directive('mdInputContainer', mdInputContainerDirective) + .directive('label', labelDirective) + .directive('input', inputTextareaDirective) + .directive('textarea', inputTextareaDirective) + .directive('mdMaxlength', mdMaxlengthDirective) + .directive('placeholder', placeholderDirective) + .directive('ngMessages', ngMessagesDirective) + .directive('ngMessage', ngMessageDirective) + .directive('ngMessageExp', ngMessageDirective) + .directive('mdSelectOnFocus', mdSelectOnFocusDirective) + + .animation('.md-input-invalid', mdInputInvalidMessagesAnimation) + .animation('.md-input-messages-animation', ngMessagesAnimation) + .animation('.md-input-message-animation', ngMessageAnimation); + +// If we are running inside of tests; expose some extra services so that we can test them +if (window._mdMocksIncluded) { + inputModule.service('$$mdInput', function() { + return { + // special accessor to internals... useful for testing + messages: { + show : showInputMessages, + hide : hideInputMessages, + getElement : getMessagesElement + } + } + }) + + // Register a service for each animation so that we can easily inject them into unit tests + .service('mdInputInvalidAnimation', mdInputInvalidMessagesAnimation) + .service('mdInputMessagesAnimation', ngMessagesAnimation) + .service('mdInputMessageAnimation', ngMessageAnimation); +} + +/** + * @ngdoc directive + * @name mdInputContainer + * @module material.components.input + * + * @restrict E + * + * @description + * `` is the parent of any input or textarea element. + * + * Input and textarea elements will not behave properly unless the md-input-container + * parent is provided. + * + * A single `` should contain only one `` element, otherwise it will throw an error. + * + * Exception: Hidden inputs (``) are ignored and will not throw an error, so + * you may combine these with other inputs. + * + * Note: When using `ngMessages` with your input element, make sure the message and container elements + * are *block* elements, otherwise animations applied to the messages will not look as intended. Either use a `div` and + * apply the `ng-message` and `ng-messages` classes respectively, or use the `md-block` class on your element. + * + * @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. + * @param md-no-float {boolean=} When present, `placeholder` attributes on the input will not be converted to floating + * labels. + * + * @usage + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *

When disabling floating labels

+ * + * + * + * + * + * + * + */ +function mdInputContainerDirective($mdTheming, $parse) { + + ContainerCtrl.$inject = ["$scope", "$element", "$attrs", "$animate"]; + var INPUT_TAGS = ['INPUT', 'TEXTAREA', 'SELECT', 'MD-SELECT']; + + var LEFT_SELECTORS = INPUT_TAGS.reduce(function(selectors, isel) { + return selectors.concat(['md-icon ~ ' + isel, '.md-icon ~ ' + isel]); + }, []).join(","); + + var RIGHT_SELECTORS = INPUT_TAGS.reduce(function(selectors, isel) { + return selectors.concat([isel + ' ~ md-icon', isel + ' ~ .md-icon']); + }, []).join(","); + + return { + restrict: 'E', + compile: compile, + controller: ContainerCtrl + }; + + function compile(tElement) { + // Check for both a left & right icon + var leftIcon = tElement[0].querySelector(LEFT_SELECTORS); + var rightIcon = tElement[0].querySelector(RIGHT_SELECTORS); + + if (leftIcon) { tElement.addClass('md-icon-left'); } + if (rightIcon) { tElement.addClass('md-icon-right'); } + + return function postLink(scope, element) { + $mdTheming(element); + }; + } + + function ContainerCtrl($scope, $element, $attrs, $animate) { + var self = this; + + self.isErrorGetter = $attrs.mdIsError && $parse($attrs.mdIsError); + + self.delegateClick = function() { + self.input.focus(); + }; + self.element = $element; + self.setFocused = function(isFocused) { + $element.toggleClass('md-input-focused', !!isFocused); + }; + self.setHasValue = function(hasValue) { + $element.toggleClass('md-input-has-value', !!hasValue); + }; + self.setHasPlaceholder = function(hasPlaceholder) { + $element.toggleClass('md-input-has-placeholder', !!hasPlaceholder); + }; + self.setInvalid = function(isInvalid) { + if (isInvalid) { + $animate.addClass($element, 'md-input-invalid'); + } else { + $animate.removeClass($element, 'md-input-invalid'); + } + }; + $scope.$watch(function() { + return self.label && self.input; + }, function(hasLabelAndInput) { + if (hasLabelAndInput && !self.label.attr('for')) { + self.label.attr('for', self.input.attr('id')); + } + }); + } +} + +function labelDirective() { + return { + restrict: 'E', + require: '^?mdInputContainer', + link: function(scope, element, attr, containerCtrl) { + if (!containerCtrl || attr.mdNoFloat || element.hasClass('md-container-ignore')) return; + + containerCtrl.label = element; + scope.$on('$destroy', function() { + containerCtrl.label = null; + }); + } + }; +} + +/** + * @ngdoc directive + * @name mdInput + * @restrict E + * @module material.components.input + * + * @description + * You can use any `` or ` + *
+ *
This is required!
+ *
That's too long!
+ *
+ *
+ * + * + * + * + * + * + * + * + * + *

Notes

+ * + * - Requires [ngMessages](https://docs.angularjs.org/api/ngMessages). + * - Behaves like the [AngularJS input directive](https://docs.angularjs.org/api/ng/directive/input). + * + * The `md-input` and `md-input-container` directives use very specific positioning to achieve the + * error animation effects. Therefore, it is *not* advised to use the Layout system inside of the + * `` tags. Instead, use relative or absolute positioning. + * + * + *

Textarea directive

+ * The `textarea` element within a `md-input-container` has the following specific behavior: + * - By default the `textarea` grows as the user types. This can be disabled via the `md-no-autogrow` + * attribute. + * - If a `textarea` has the `rows` attribute, it will treat the `rows` as the minimum height and will + * continue growing as the user types. For example a textarea with `rows="3"` will be 3 lines of text + * high initially. If no rows are specified, the directive defaults to 1. + * - The textarea's height gets set on initialization, as well as while the user is typing. In certain situations + * (e.g. while animating) the directive might have been initialized, before the element got it's final height. In + * those cases, you can trigger a resize manually by broadcasting a `md-resize-textarea` event on the scope. + * - If you wan't a `textarea` to stop growing at a certain point, you can specify the `max-rows` attribute. + * - The textarea's bottom border acts as a handle which users can drag, in order to resize the element vertically. + * Once the user has resized a `textarea`, the autogrowing functionality becomes disabled. If you don't want a + * `textarea` to be resizeable by the user, you can add the `md-no-resize` attribute. + */ + +function inputTextareaDirective($mdUtil, $window, $mdAria, $timeout, $mdGesture) { + return { + restrict: 'E', + require: ['^?mdInputContainer', '?ngModel', '?^form'], + link: postLink + }; + + function postLink(scope, element, attr, ctrls) { + + var containerCtrl = ctrls[0]; + var hasNgModel = !!ctrls[1]; + var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel(); + var parentForm = ctrls[2]; + var isReadonly = angular.isDefined(attr.readonly); + var mdNoAsterisk = $mdUtil.parseAttributeBoolean(attr.mdNoAsterisk); + var tagName = element[0].tagName.toLowerCase(); + + + if (!containerCtrl) return; + if (attr.type === 'hidden') { + element.attr('aria-hidden', 'true'); + return; + } else if (containerCtrl.input) { + if (containerCtrl.input[0].contains(element[0])) { + return; + } else { + throw new Error(" can only have *one* , + * + * + * + */ +function mdSelectOnFocusDirective($timeout) { + + return { + restrict: 'A', + link: postLink + }; + + function postLink(scope, element, attr) { + if (element[0].nodeName !== 'INPUT' && element[0].nodeName !== "TEXTAREA") return; + + var preventMouseUp = false; + + element + .on('focus', onFocus) + .on('mouseup', onMouseUp); + + scope.$on('$destroy', function() { + element + .off('focus', onFocus) + .off('mouseup', onMouseUp); + }); + + function onFocus() { + preventMouseUp = true; + + $timeout(function() { + // Use HTMLInputElement#select to fix firefox select issues. + // The debounce is here for Edge's sake, otherwise the selection doesn't work. + element[0].select(); + + // This should be reset from inside the `focus`, because the event might + // have originated from something different than a click, e.g. a keyboard event. + preventMouseUp = false; + }, 1, false); + } + + // Prevents the default action of the first `mouseup` after a focus. + // This is necessary, because browsers fire a `mouseup` right after the element + // has been focused. In some browsers (Firefox in particular) this can clear the + // selection. There are examples of the problem in issue #7487. + function onMouseUp(event) { + if (preventMouseUp) { + event.preventDefault(); + } + } + } +} + +var visibilityDirectives = ['ngIf', 'ngShow', 'ngHide', 'ngSwitchWhen', 'ngSwitchDefault']; +function ngMessagesDirective() { + return { + restrict: 'EA', + link: postLink, + + // This is optional because we don't want target *all* ngMessage instances, just those inside of + // mdInputContainer. + require: '^^?mdInputContainer' + }; + + function postLink(scope, element, attrs, inputContainer) { + // If we are not a child of an input container, don't do anything + if (!inputContainer) return; + + // Add our animation class + element.toggleClass('md-input-messages-animation', true); + + // Add our md-auto-hide class to automatically hide/show messages when container is invalid + element.toggleClass('md-auto-hide', true); + + // If we see some known visibility directives, remove the md-auto-hide class + if (attrs.mdAutoHide == 'false' || hasVisibiltyDirective(attrs)) { + element.toggleClass('md-auto-hide', false); + } + } + + function hasVisibiltyDirective(attrs) { + return visibilityDirectives.some(function(attr) { + return attrs[attr]; + }); + } +} + +function ngMessageDirective($mdUtil) { + return { + restrict: 'EA', + compile: compile, + priority: 100 + }; + + function compile(tElement) { + if (!isInsideInputContainer(tElement)) { + + // When the current element is inside of a document fragment, then we need to check for an input-container + // in the postLink, because the element will be later added to the DOM and is currently just in a temporary + // fragment, which causes the input-container check to fail. + if (isInsideFragment()) { + return function (scope, element) { + if (isInsideInputContainer(element)) { + // Inside of the postLink function, a ngMessage directive will be a comment element, because it's + // currently hidden. To access the shown element, we need to use the element from the compile function. + initMessageElement(tElement); + } + }; + } + } else { + initMessageElement(tElement); + } + + function isInsideFragment() { + var nextNode = tElement[0]; + while (nextNode = nextNode.parentNode) { + if (nextNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { + return true; + } + } + return false; + } + + function isInsideInputContainer(element) { + return !!$mdUtil.getClosest(element, "md-input-container"); + } + + function initMessageElement(element) { + // Add our animation class + element.toggleClass('md-input-message-animation', true); + } + } +} + +var $$AnimateRunner, $animateCss, $mdUtil, $log; + +function mdInputInvalidMessagesAnimation($$AnimateRunner, $animateCss, $mdUtil, $log) { + saveSharedServices($$AnimateRunner, $animateCss, $mdUtil, $log); + + return { + addClass: function(element, className, done) { + showInputMessages(element, done); + } + + // NOTE: We do not need the removeClass method, because the message ng-leave animation will fire + }; +} + +function ngMessagesAnimation($$AnimateRunner, $animateCss, $mdUtil, $log) { + saveSharedServices($$AnimateRunner, $animateCss, $mdUtil, $log); + + return { + enter: function(element, done) { + showInputMessages(element, done); + }, + + leave: function(element, done) { + hideInputMessages(element, done); + }, + + addClass: function(element, className, done) { + if (className == "ng-hide") { + hideInputMessages(element, done); + } else { + done(); + } + }, + + removeClass: function(element, className, done) { + if (className == "ng-hide") { + showInputMessages(element, done); + } else { + done(); + } + } + }; +} + +function ngMessageAnimation($$AnimateRunner, $animateCss, $mdUtil, $log) { + saveSharedServices($$AnimateRunner, $animateCss, $mdUtil, $log); + + return { + enter: function(element, done) { + var animator = showMessage(element); + + animator.start().done(done); + }, + + leave: function(element, done) { + var animator = hideMessage(element); + + animator.start().done(done); + } + }; +} + +function showInputMessages(element, done) { + var animators = [], animator; + var messages = getMessagesElement(element); + var children = messages.children(); + + if (messages.length == 0 || children.length == 0) { + $log.warn('mdInput messages show animation called on invalid messages element: ', element); + done(); + return; + } + + angular.forEach(children, function(child) { + animator = showMessage(angular.element(child)); + + animators.push(animator.start()); + }); + + $$AnimateRunner.all(animators, done); +} + +function hideInputMessages(element, done) { + var animators = [], animator; + var messages = getMessagesElement(element); + var children = messages.children(); + + if (messages.length == 0 || children.length == 0) { + $log.warn('mdInput messages hide animation called on invalid messages element: ', element); + done(); + return; + } + + angular.forEach(children, function(child) { + animator = hideMessage(angular.element(child)); + + animators.push(animator.start()); + }); + + $$AnimateRunner.all(animators, done); +} + +function showMessage(element) { + var height = parseInt(window.getComputedStyle(element[0]).height); + var topMargin = parseInt(window.getComputedStyle(element[0]).marginTop); + + var messages = getMessagesElement(element); + var container = getInputElement(element); + + // Check to see if the message is already visible so we can skip + var alreadyVisible = (topMargin > -height); + + // If we have the md-auto-hide class, the md-input-invalid animation will fire, so we can skip + if (alreadyVisible || (messages.hasClass('md-auto-hide') && !container.hasClass('md-input-invalid'))) { + return $animateCss(element, {}); + } + + return $animateCss(element, { + event: 'enter', + structural: true, + from: {"opacity": 0, "margin-top": -height + "px"}, + to: {"opacity": 1, "margin-top": "0"}, + duration: 0.3 + }); +} + +function hideMessage(element) { + var height = element[0].offsetHeight; + var styles = window.getComputedStyle(element[0]); + + // If we are already hidden, just return an empty animation + if (parseInt(styles.opacity) === 0) { + return $animateCss(element, {}); + } + + // Otherwise, animate + return $animateCss(element, { + event: 'leave', + structural: true, + from: {"opacity": 1, "margin-top": 0}, + to: {"opacity": 0, "margin-top": -height + "px"}, + duration: 0.3 + }); +} + +function getInputElement(element) { + var inputContainer = element.controller('mdInputContainer'); + + return inputContainer.element; +} + +function getMessagesElement(element) { + // If we ARE the messages element, just return ourself + if (element.hasClass('md-input-messages-animation')) { + return element; + } + + // If we are a ng-message element, we need to traverse up the DOM tree + if (element.hasClass('md-input-message-animation')) { + return angular.element($mdUtil.getClosest(element, function(node) { + return node.classList.contains('md-input-messages-animation'); + })); + } + + // Otherwise, we can traverse down + return angular.element(element[0].querySelector('.md-input-messages-animation')); +} + +function saveSharedServices(_$$AnimateRunner_, _$animateCss_, _$mdUtil_, _$log_) { + $$AnimateRunner = _$$AnimateRunner_; + $animateCss = _$animateCss_; + $mdUtil = _$mdUtil_; + $log = _$log_; +} + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.list + * @description + * List module + */ +MdListController.$inject = ["$scope", "$element", "$mdListInkRipple"]; +mdListDirective.$inject = ["$mdTheming"]; +mdListItemDirective.$inject = ["$mdAria", "$mdConstant", "$mdUtil", "$timeout"]; +angular.module('material.components.list', [ + 'material.core' +]) + .controller('MdListController', MdListController) + .directive('mdList', mdListDirective) + .directive('mdListItem', mdListItemDirective); + +/** + * @ngdoc directive + * @name mdList + * @module material.components.list + * + * @restrict E + * + * @description + * The `` directive is a list container for 1..n `` tags. + * + * @usage + * + * + * + * + *
+ *

{{item.title}}

+ *

{{item.description}}

+ *
+ *
+ *
+ *
+ */ + +function mdListDirective($mdTheming) { + return { + restrict: 'E', + compile: function(tEl) { + tEl[0].setAttribute('role', 'list'); + return $mdTheming; + } + }; +} +/** + * @ngdoc directive + * @name mdListItem + * @module material.components.list + * + * @restrict E + * + * @description + * A `md-list-item` element can be used to represent some information in a row.
+ * + * @usage + * ### Single Row Item + * + * + * Single Row Item + * + * + * + * ### Multiple Lines + * By using the following markup, you will be able to have two lines inside of one `md-list-item`. + * + * + * + *
+ *

First Line

+ *

Second Line

+ *
+ *
+ *
+ * + * It is also possible to have three lines inside of one list item. + * + * + * + *
+ *

First Line

+ *

Second Line

+ *

Third Line

+ *
+ *
+ *
+ * + * ### Secondary Items + * Secondary items are elements which will be aligned at the end of the `md-list-item`. + * + * + * + * Single Row Item + * + * Secondary Button + * + * + * + * + * It also possible to have multiple secondary items inside of one `md-list-item`. + * + * + * + * Single Row Item + * First Button + * Second Button + * + * + * + * ### Proxy Item + * Proxies are elements, which will execute their specific action on click
+ * Currently supported proxy items are + * - `md-checkbox` (Toggle) + * - `md-switch` (Toggle) + * - `md-menu` (Open) + * + * This means, when using a supported proxy item inside of `md-list-item`, the list item will + * automatically become clickable and executes the associated action of the proxy element on click. + * + * It is possible to disable this behavior by applying the `md-no-proxy` class to the list item. + * + * + * + * No Proxy List + * + * + * + * + * Here are a few examples of proxy elements inside of a list item. + * + * + * + * First Line + * + * + * + * + * The `md-checkbox` element will be automatically detected as a proxy element and will toggle on click. + * + * + * + * First Line + * + * + * + * + * The recognized `md-switch` will toggle its state, when the user clicks on the `md-list-item`. + * + * It is also possible to have a `md-menu` inside of a `md-list-item`. + * + * + *

Click anywhere to fire the secondary action

+ * + * + * + * + * + * + * + * Redial + * + * + * + * + * Check voicemail + * + * + * + * + * + * Notifications + * + * + * + * + *
+ *
+ * + * The menu will automatically open, when the users clicks on the `md-list-item`.
+ * + * If the developer didn't specify any position mode on the menu, the `md-list-item` will automatically detect the + * position mode and applies it to the `md-menu`. + * + * ### Avatars + * Sometimes you may want to have some avatars inside of the `md-list-item `.
+ * You are able to create a optimized icon for the list item, by applying the `.md-avatar` class on the `` element. + * + * + * + * + * Alan Turing + * + * + * When using `` for an avatar, you have to use the `.md-avatar-icon` class. + * + * + * + * Timothy Kopra + * + * + * + * In cases, you have a `md-list-item`, which doesn't have any avatar, + * but you want to align it with the other avatar items, you have to use the `.md-offset` class. + * + * + * + * Jon Doe + * + * + * + * ### DOM modification + * The `md-list-item` component automatically detects if the list item should be clickable. + * + * --- + * If the `md-list-item` is clickable, we wrap all content inside of a `
` and create + * an overlaying button, which will will execute the given actions (like `ng-href`, `ng-click`) + * + * We create an overlaying button, instead of wrapping all content inside of the button, + * because otherwise some elements may not be clickable inside of the button. + * + * --- + * When using a secondary item inside of your list item, the `md-list-item` component will automatically create + * a secondary container at the end of the `md-list-item`, which contains all secondary items. + * + * The secondary item container is not static, because otherwise the overflow will not work properly on the + * list item. + * + */ +function mdListItemDirective($mdAria, $mdConstant, $mdUtil, $timeout) { + var proxiedTypes = ['md-checkbox', 'md-switch', 'md-menu']; + return { + restrict: 'E', + controller: 'MdListController', + compile: function(tEl, tAttrs) { + + // Check for proxy controls (no ng-click on parent, and a control inside) + var secondaryItems = tEl[0].querySelectorAll('.md-secondary'); + var hasProxiedElement; + var proxyElement; + var itemContainer = tEl; + + tEl[0].setAttribute('role', 'listitem'); + + if (tAttrs.ngClick || tAttrs.ngDblclick || tAttrs.ngHref || tAttrs.href || tAttrs.uiSref || tAttrs.ngAttrUiSref) { + wrapIn('button'); + } else if (!tEl.hasClass('md-no-proxy')) { + + for (var i = 0, type; type = proxiedTypes[i]; ++i) { + if (proxyElement = tEl[0].querySelector(type)) { + hasProxiedElement = true; + break; + } + } + + if (hasProxiedElement) { + wrapIn('div'); + } else { + tEl.addClass('md-no-proxy'); + } + + } + + wrapSecondaryItems(); + setupToggleAria(); + + if (hasProxiedElement && proxyElement.nodeName === "MD-MENU") { + setupProxiedMenu(); + } + + function setupToggleAria() { + var toggleTypes = ['md-switch', 'md-checkbox']; + var toggle; + + for (var i = 0, toggleType; toggleType = toggleTypes[i]; ++i) { + if (toggle = tEl.find(toggleType)[0]) { + if (!toggle.hasAttribute('aria-label')) { + var p = tEl.find('p')[0]; + if (!p) return; + toggle.setAttribute('aria-label', 'Toggle ' + p.textContent); + } + } + } + } + + function setupProxiedMenu() { + var menuEl = angular.element(proxyElement); + + var isEndAligned = menuEl.parent().hasClass('md-secondary-container') || + proxyElement.parentNode.firstElementChild !== proxyElement; + + var xAxisPosition = 'left'; + + if (isEndAligned) { + // When the proxy item is aligned at the end of the list, we have to set the origin to the end. + xAxisPosition = 'right'; + } + + // Set the position mode / origin of the proxied menu. + if (!menuEl.attr('md-position-mode')) { + menuEl.attr('md-position-mode', xAxisPosition + ' target'); + } + + // Apply menu open binding to menu button + var menuOpenButton = menuEl.children().eq(0); + if (!hasClickEvent(menuOpenButton[0])) { + menuOpenButton.attr('ng-click', '$mdMenu.open($event)'); + } + + if (!menuOpenButton.attr('aria-label')) { + menuOpenButton.attr('aria-label', 'Open List Menu'); + } + } + + function wrapIn(type) { + if (type == 'div') { + itemContainer = angular.element('
'); + itemContainer.append(tEl.contents()); + tEl.addClass('md-proxy-focus'); + } else { + // Element which holds the default list-item content. + itemContainer = angular.element( + '
'+ + '
'+ + '
' + ); + + // Button which shows ripple and executes primary action. + var buttonWrap = angular.element( + '' + ); + + copyAttributes(tEl[0], buttonWrap[0]); + + // If there is no aria-label set on the button (previously copied over if present) + // we determine the label from the content and copy it to the button. + if (!buttonWrap.attr('aria-label')) { + buttonWrap.attr('aria-label', $mdAria.getText(tEl)); + } + + // We allow developers to specify the `md-no-focus` class, to disable the focus style + // on the button executor. Once more classes should be forwarded, we should probably make the + // class forward more generic. + if (tEl.hasClass('md-no-focus')) { + buttonWrap.addClass('md-no-focus'); + } + + // Append the button wrap before our list-item content, because it will overlay in relative. + itemContainer.prepend(buttonWrap); + itemContainer.children().eq(1).append(tEl.contents()); + + tEl.addClass('_md-button-wrap'); + } + + tEl[0].setAttribute('tabindex', '-1'); + tEl.append(itemContainer); + } + + function wrapSecondaryItems() { + var secondaryItemsWrapper = angular.element('
'); + + angular.forEach(secondaryItems, function(secondaryItem) { + wrapSecondaryItem(secondaryItem, secondaryItemsWrapper); + }); + + itemContainer.append(secondaryItemsWrapper); + } + + function wrapSecondaryItem(secondaryItem, container) { + // If the current secondary item is not a button, but contains a ng-click attribute, + // the secondary item will be automatically wrapped inside of a button. + if (secondaryItem && !isButton(secondaryItem) && secondaryItem.hasAttribute('ng-click')) { + + $mdAria.expect(secondaryItem, 'aria-label'); + var buttonWrapper = angular.element(''); + + // Copy the attributes from the secondary item to the generated button. + // We also support some additional attributes from the secondary item, + // because some developers may use a ngIf, ngHide, ngShow on their item. + copyAttributes(secondaryItem, buttonWrapper[0], ['ng-if', 'ng-hide', 'ng-show']); + + secondaryItem.setAttribute('tabindex', '-1'); + buttonWrapper.append(secondaryItem); + + secondaryItem = buttonWrapper[0]; + } + + if (secondaryItem && (!hasClickEvent(secondaryItem) || (!tAttrs.ngClick && isProxiedElement(secondaryItem)))) { + // In this case we remove the secondary class, so we can identify it later, when we searching for the + // proxy items. + angular.element(secondaryItem).removeClass('md-secondary'); + } + + tEl.addClass('md-with-secondary'); + container.append(secondaryItem); + } + + /** + * Copies attributes from a source element to the destination element + * By default the function will copy the most necessary attributes, supported + * by the button executor for clickable list items. + * @param source Element with the specified attributes + * @param destination Element which will retrieve the attributes + * @param extraAttrs Additional attributes, which will be copied over. + */ + function copyAttributes(source, destination, extraAttrs) { + var copiedAttrs = $mdUtil.prefixer([ + 'ng-if', 'ng-click', 'ng-dblclick', 'aria-label', 'ng-disabled', 'ui-sref', + 'href', 'ng-href', 'target', 'ng-attr-ui-sref', 'ui-sref-opts' + ]); + + if (extraAttrs) { + copiedAttrs = copiedAttrs.concat($mdUtil.prefixer(extraAttrs)); + } + + angular.forEach(copiedAttrs, function(attr) { + if (source.hasAttribute(attr)) { + destination.setAttribute(attr, source.getAttribute(attr)); + source.removeAttribute(attr); + } + }); + } + + function isProxiedElement(el) { + return proxiedTypes.indexOf(el.nodeName.toLowerCase()) != -1; + } + + function isButton(el) { + var nodeName = el.nodeName.toUpperCase(); + + return nodeName == "MD-BUTTON" || nodeName == "BUTTON"; + } + + function hasClickEvent (element) { + var attr = element.attributes; + for (var i = 0; i < attr.length; i++) { + if (tAttrs.$normalize(attr[i].name) === 'ngClick') return true; + } + return false; + } + + return postLink; + + function postLink($scope, $element, $attr, ctrl) { + $element.addClass('_md'); // private md component indicator for styling + + var proxies = [], + firstElement = $element[0].firstElementChild, + isButtonWrap = $element.hasClass('_md-button-wrap'), + clickChild = isButtonWrap ? firstElement.firstElementChild : firstElement, + hasClick = clickChild && hasClickEvent(clickChild), + noProxies = $element.hasClass('md-no-proxy'); + + computeProxies(); + computeClickable(); + + if (proxies.length) { + angular.forEach(proxies, function(proxy) { + proxy = angular.element(proxy); + + $scope.mouseActive = false; + proxy.on('mousedown', function() { + $scope.mouseActive = true; + $timeout(function(){ + $scope.mouseActive = false; + }, 100); + }) + .on('focus', function() { + if ($scope.mouseActive === false) { $element.addClass('md-focused'); } + proxy.on('blur', function proxyOnBlur() { + $element.removeClass('md-focused'); + proxy.off('blur', proxyOnBlur); + }); + }); + }); + } + + + function computeProxies() { + + if (firstElement && firstElement.children && !hasClick && !noProxies) { + + angular.forEach(proxiedTypes, function(type) { + + // All elements which are not capable for being used a proxy have the .md-secondary class + // applied. These items had been sorted out in the secondary wrap function. + angular.forEach(firstElement.querySelectorAll(type + ':not(.md-secondary)'), function(child) { + proxies.push(child); + }); + }); + + } + } + + function computeClickable() { + if (proxies.length == 1 || hasClick) { + $element.addClass('md-clickable'); + + if (!hasClick) { + ctrl.attachRipple($scope, angular.element($element[0].querySelector('.md-no-style'))); + } + } + } + + function isEventFromControl(event) { + var forbiddenControls = ['md-slider']; + + // If there is no path property in the event, then we can assume that the event was not bubbled. + if (!event.path) { + return forbiddenControls.indexOf(event.target.tagName.toLowerCase()) !== -1; + } + + // We iterate the event path up and check for a possible component. + // Our maximum index to search, is the list item root. + var maxPath = event.path.indexOf($element.children()[0]); + + for (var i = 0; i < maxPath; i++) { + if (forbiddenControls.indexOf(event.path[i].tagName.toLowerCase()) !== -1) { + return true; + } + } + } + + var clickChildKeypressListener = function(e) { + if (e.target.nodeName != 'INPUT' && e.target.nodeName != 'TEXTAREA' && !e.target.isContentEditable) { + var keyCode = e.which || e.keyCode; + if (keyCode == $mdConstant.KEY_CODE.SPACE) { + if (clickChild) { + clickChild.click(); + e.preventDefault(); + e.stopPropagation(); + } + } + } + }; + + if (!hasClick && !proxies.length) { + clickChild && clickChild.addEventListener('keypress', clickChildKeypressListener); + } + + $element.off('click'); + $element.off('keypress'); + + if (proxies.length == 1 && clickChild) { + $element.children().eq(0).on('click', function(e) { + // When the event is coming from an control and it should not trigger the proxied element + // then we are skipping. + if (isEventFromControl(e)) return; + + var parentButton = $mdUtil.getClosest(e.target, 'BUTTON'); + if (!parentButton && clickChild.contains(e.target)) { + angular.forEach(proxies, function(proxy) { + if (e.target !== proxy && !proxy.contains(e.target)) { + if (proxy.nodeName === 'MD-MENU') { + proxy = proxy.children[0]; + } + angular.element(proxy).triggerHandler('click'); + } + }); + } + }); + } + + $scope.$on('$destroy', function () { + clickChild && clickChild.removeEventListener('keypress', clickChildKeypressListener); + }); + } + } + }; +} + +/* + * @private + * @ngdoc controller + * @name MdListController + * @module material.components.list + * + */ +function MdListController($scope, $element, $mdListInkRipple) { + var ctrl = this; + ctrl.attachRipple = attachRipple; + + function attachRipple (scope, element) { + var options = {}; + $mdListInkRipple.attach(scope, element, options); + } +} + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.menu + */ + +angular.module('material.components.menu', [ + 'material.core', + 'material.components.backdrop' +]); + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.menuBar + */ + +angular.module('material.components.menuBar', [ + 'material.core', + 'material.components.icon', + 'material.components.menu' +]); + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.panel + */ +MdPanelService.$inject = ["presets", "$rootElement", "$rootScope", "$injector", "$window"]; +angular + .module('material.components.panel', [ + 'material.core', + 'material.components.backdrop' + ]) + .provider('$mdPanel', MdPanelProvider); + + +/***************************************************************************** + * PUBLIC DOCUMENTATION * + *****************************************************************************/ + + +/** + * @ngdoc service + * @name $mdPanelProvider + * @module material.components.panel + * + * @description + * `$mdPanelProvider` allows users to create configuration presets that will be + * stored within a cached presets object. When the configuration is needed, the + * user can request the preset by passing it as the first parameter in the + * `$mdPanel.create` or `$mdPanel.open` methods. + * + * @usage + * + * (function(angular, undefined) { + * 'use strict'; + * + * angular + * .module('demoApp', ['ngMaterial']) + * .config(DemoConfig) + * .controller('DemoCtrl', DemoCtrl) + * .controller('DemoMenuCtrl', DemoMenuCtrl); + * + * function DemoConfig($mdPanelProvider) { + * $mdPanelProvider.definePreset('demoPreset', { + * attachTo: angular.element(document.body), + * controller: DemoMenuCtrl, + * controllerAs: 'ctrl', + * template: '' + + * '', + * panelClass: 'menu-panel-container', + * focusOnOpen: false, + * zIndex: 100, + * propagateContainerEvents: true, + * groupName: 'menus' + * }); + * } + * + * function PanelProviderCtrl($mdPanel) { + * this.navigation = { + * name: 'navigation', + * items: [ + * 'Home', + * 'About', + * 'Contact' + * ] + * }; + * this.favorites = { + * name: 'favorites', + * items: [ + * 'Add to Favorites' + * ] + * }; + * this.more = { + * name: 'more', + * items: [ + * 'Account', + * 'Sign Out' + * ] + * }; + * + * $mdPanel.newPanelGroup('menus', { + * maxOpen: 2 + * }); + * + * this.showMenu = function($event, menu) { + * $mdPanel.open('demoPreset', { + * id: 'menu_' + menu.name, + * position: $mdPanel.newPanelPosition() + * .relativeTo($event.srcElement) + * .addPanelPosition( + * $mdPanel.xPosition.ALIGN_START, + * $mdPanel.yPosition.BELOW + * ), + * locals: { + * items: menu.items + * }, + * openFrom: $event + * }); + * }; + * } + * + * function PanelMenuCtrl(mdPanelRef) { + * this.closeMenu = function() { + * mdPanelRef && mdPanelRef.close(); + * }; + * } + * })(angular); + * + */ + +/** + * @ngdoc method + * @name $mdPanelProvider#definePreset + * @description + * Takes the passed in preset name and preset configuration object and adds it + * to the `_presets` object of the provider. This `_presets` object is then + * passed along to the `$mdPanel` service. + * + * @param {string} name Preset name. + * @param {!Object} preset Specific configuration object that can contain any + * and all of the parameters avaialble within the `$mdPanel.create` method. + * However, parameters that pertain to id, position, animation, and user + * interaction are not allowed and will be removed from the preset + * configuration. + */ + + +/***************************************************************************** + * MdPanel Service * + *****************************************************************************/ + + +/** + * @ngdoc service + * @name $mdPanel + * @module material.components.panel + * + * @description + * `$mdPanel` is a robust, low-level service for creating floating panels on + * the screen. It can be used to implement tooltips, dialogs, pop-ups, etc. + * + * @usage + * + * (function(angular, undefined) { + * 'use strict'; + * + * angular + * .module('demoApp', ['ngMaterial']) + * .controller('DemoDialogController', DialogController); + * + * var panelRef; + * + * function showPanel($event) { + * var panelPosition = $mdPanel.newPanelPosition() + * .absolute() + * .top('50%') + * .left('50%'); + * + * var panelAnimation = $mdPanel.newPanelAnimation() + * .targetEvent($event) + * .defaultAnimation('md-panel-animate-fly') + * .closeTo('.show-button'); + * + * var config = { + * attachTo: angular.element(document.body), + * controller: DialogController, + * controllerAs: 'ctrl', + * position: panelPosition, + * animation: panelAnimation, + * targetEvent: $event, + * templateUrl: 'dialog-template.html', + * clickOutsideToClose: true, + * escapeToClose: true, + * focusOnOpen: true + * } + * + * $mdPanel.open(config) + * .then(function(result) { + * panelRef = result; + * }); + * } + * + * function DialogController(MdPanelRef) { + * function closeDialog() { + * if (MdPanelRef) MdPanelRef.close(); + * } + * } + * })(angular); + * + */ + +/** + * @ngdoc method + * @name $mdPanel#create + * @description + * Creates a panel with the specified options. + * + * @param config {!Object=} Specific configuration object that may contain the + * following properties: + * + * - `id` - `{string=}`: An ID to track the panel by. When an ID is provided, + * the created panel is added to a tracked panels object. Any subsequent + * requests made to create a panel with that ID are ignored. This is useful + * in having the panel service not open multiple panels from the same user + * interaction when there is no backdrop and events are propagated. Defaults + * to an arbitrary string that is not tracked. + * - `template` - `{string=}`: HTML template to show in the panel. This + * **must** be trusted HTML with respect to Angular’s + * [$sce service](https://docs.angularjs.org/api/ng/service/$sce). + * - `templateUrl` - `{string=}`: The URL that will be used as the content of + * the panel. + * - `contentElement` - `{(string|!angular.JQLite|!Element)=}`: Pre-compiled + * element to be used as the panel's content. + * - `controller` - `{(function|string)=}`: The controller to associate with + * the panel. The controller can inject a reference to the returned + * panelRef, which allows the panel to be closed, hidden, and shown. Any + * fields passed in through locals or resolve will be bound to the + * controller. + * - `controllerAs` - `{string=}`: An alias to assign the controller to on + * the scope. + * - `bindToController` - `{boolean=}`: Binds locals to the controller + * instead of passing them in. Defaults to true, as this is a best + * practice. + * - `locals` - `{Object=}`: An object containing key/value pairs. The keys + * will be used as names of values to inject into the controller. For + * example, `locals: {three: 3}` would inject `three` into the controller, + * with the value 3. + * - `resolve` - `{Object=}`: Similar to locals, except it takes promises as + * values. The panel will not open until all of the promises resolve. + * - `attachTo` - `{(string|!angular.JQLite|!Element)=}`: The element to + * attach the panel to. Defaults to appending to the root element of the + * application. + * - `propagateContainerEvents` - `{boolean=}`: Whether pointer or touch + * events should be allowed to propagate 'go through' the container, aka the + * wrapper, of the panel. Defaults to false. + * - `panelClass` - `{string=}`: A css class to apply to the panel element. + * This class should define any borders, box-shadow, etc. for the panel. + * - `zIndex` - `{number=}`: The z-index to place the panel at. + * Defaults to 80. + * - `position` - `{MdPanelPosition=}`: An MdPanelPosition object that + * specifies the alignment of the panel. For more information, see + * `MdPanelPosition`. + * - `clickOutsideToClose` - `{boolean=}`: Whether the user can click + * outside the panel to close it. Defaults to false. + * - `escapeToClose` - `{boolean=}`: Whether the user can press escape to + * close the panel. Defaults to false. + * - `onCloseSuccess` - `{function(!panelRef, string)=}`: Function that is + * called after the close successfully finishes. The first parameter passed + * into this function is the current panelRef and the 2nd is an optional + * string explaining the close reason. The currently supported closeReasons + * can be found in the MdPanelRef.closeReasons enum. These are by default + * passed along by the panel. + * - `trapFocus` - `{boolean=}`: Whether focus should be trapped within the + * panel. If `trapFocus` is true, the user will not be able to interact + * with the rest of the page until the panel is dismissed. Defaults to + * false. + * - `focusOnOpen` - `{boolean=}`: An option to override focus behavior on + * open. Only disable if focusing some other way, as focus management is + * required for panels to be accessible. Defaults to true. + * - `fullscreen` - `{boolean=}`: Whether the panel should be full screen. + * Applies the class `._md-panel-fullscreen` to the panel on open. Defaults + * to false. + * - `animation` - `{MdPanelAnimation=}`: An MdPanelAnimation object that + * specifies the animation of the panel. For more information, see + * `MdPanelAnimation`. + * - `hasBackdrop` - `{boolean=}`: Whether there should be an opaque backdrop + * behind the panel. Defaults to false. + * - `disableParentScroll` - `{boolean=}`: Whether the user can scroll the + * page behind the panel. Defaults to false. + * - `onDomAdded` - `{function=}`: Callback function used to announce when + * the panel is added to the DOM. + * - `onOpenComplete` - `{function=}`: Callback function used to announce + * when the open() action is finished. + * - `onRemoving` - `{function=}`: Callback function used to announce the + * close/hide() action is starting. + * - `onDomRemoved` - `{function=}`: Callback function used to announce when + * the panel is removed from the DOM. + * - `origin` - `{(string|!angular.JQLite|!Element)=}`: The element to focus + * on when the panel closes. This is commonly the element which triggered + * the opening of the panel. If you do not use `origin`, you need to control + * the focus manually. + * - `groupName` - `{(string|!Array)=}`: A group name or an array of + * group names. The group name is used for creating a group of panels. The + * group is used for configuring the number of open panels and identifying + * specific behaviors for groups. For instance, all tooltips could be + * identified using the same groupName. + * + * @returns {!MdPanelRef} panelRef + */ + +/** + * @ngdoc method + * @name $mdPanel#open + * @description + * Calls the create method above, then opens the panel. This is a shortcut for + * creating and then calling open manually. If custom methods need to be + * called when the panel is added to the DOM or opened, do not use this method. + * Instead create the panel, chain promises on the domAdded and openComplete + * methods, and call open from the returned panelRef. + * + * @param {!Object=} config Specific configuration object that may contain + * the properties defined in `$mdPanel.create`. + * @returns {!angular.$q.Promise} panelRef A promise that resolves + * to an instance of the panel. + */ + +/** + * @ngdoc method + * @name $mdPanel#newPanelPosition + * @description + * Returns a new instance of the MdPanelPosition object. Use this to create + * the position config object. + * + * @returns {!MdPanelPosition} panelPosition + */ + +/** + * @ngdoc method + * @name $mdPanel#newPanelAnimation + * @description + * Returns a new instance of the MdPanelAnimation object. Use this to create + * the animation config object. + * + * @returns {!MdPanelAnimation} panelAnimation + */ + +/** + * @ngdoc method + * @name $mdPanel#newPanelGroup + * @description + * Creates a panel group and adds it to a tracked list of panel groups. + * + * @param {string} groupName Name of the group to create. + * @param {!Object=} config Specific configuration object that may contain the + * following properties: + * + * - `maxOpen` - `{number=}`: The maximum number of panels that are allowed to + * be open within a defined panel group. + * + * @returns {!Object, + * openPanels: !Array, + * maxOpen: number}>} panelGroup + */ + +/** + * @ngdoc method + * @name $mdPanel#setGroupMaxOpen + * @description + * Sets the maximum number of panels in a group that can be opened at a given + * time. + * + * @param {string} groupName The name of the group to configure. + * @param {number} maxOpen The maximum number of panels that can be + * opened. Infinity can be passed in to remove the maxOpen limit. + */ + + +/***************************************************************************** + * MdPanelRef * + *****************************************************************************/ + + +/** + * @ngdoc type + * @name MdPanelRef + * @module material.components.panel + * @description + * A reference to a created panel. This reference contains a unique id for the + * panel, along with the following properties: + * + * - `id` - `{string}`: The unique id for the panel. This id is used to track + * when a panel was interacted with. + * - `config` - `{!Object=}`: The entire config object that was used in + * create. + * - `isAttached` - `{boolean}`: Whether the panel is attached to the DOM. + * Visibility to the user does not factor into isAttached. + * - `panelContainer` - `{angular.JQLite}`: The wrapper element containing the + * panel. This property is added in order to have access to the `addClass`, + * `removeClass`, `toggleClass`, etc methods. + * - `panelEl` - `{angular.JQLite}`: The panel element. This property is added + * in order to have access to the `addClass`, `removeClass`, `toggleClass`, + * etc methods. + */ + +/** + * @ngdoc method + * @name MdPanelRef#open + * @description + * Attaches and shows the panel. + * + * @returns {!angular.$q.Promise} A promise that is resolved when the panel is + * opened. + */ + +/** + * @ngdoc method + * @name MdPanelRef#close + * @description + * Hides and detaches the panel. Note that this will **not** destroy the panel. + * If you don't intend on using the panel again, call the {@link #destroy + * destroy} method afterwards. + * + * @returns {!angular.$q.Promise} A promise that is resolved when the panel is + * closed. + */ + +/** + * @ngdoc method + * @name MdPanelRef#attach + * @description + * Create the panel elements and attach them to the DOM. The panel will be + * hidden by default. + * + * @returns {!angular.$q.Promise} A promise that is resolved when the panel is + * attached. + */ + +/** + * @ngdoc method + * @name MdPanelRef#detach + * @description + * Removes the panel from the DOM. This will NOT hide the panel before removing + * it. + * + * @returns {!angular.$q.Promise} A promise that is resolved when the panel is + * detached. + */ + +/** + * @ngdoc method + * @name MdPanelRef#show + * @description + * Shows the panel. + * + * @returns {!angular.$q.Promise} A promise that is resolved when the panel has + * shown and animations are completed. + */ + +/** + * @ngdoc method + * @name MdPanelRef#hide + * @description + * Hides the panel. + * + * @returns {!angular.$q.Promise} A promise that is resolved when the panel has + * hidden and animations are completed. + */ + +/** + * @ngdoc method + * @name MdPanelRef#destroy + * @description + * Destroys the panel. The panel cannot be opened again after this is called. + */ + +/** + * @ngdoc method + * @name MdPanelRef#addClass + * @deprecated + * This method is in the process of being deprecated in favor of using the panel + * and container JQLite elements that are referenced in the MdPanelRef object. + * Full deprecation is scheduled for material 1.2. + * @description + * Adds a class to the panel. DO NOT use this hide/show the panel. + * + * @param {string} newClass class to be added. + * @param {boolean} toElement Whether or not to add the class to the panel + * element instead of the container. + */ + +/** + * @ngdoc method + * @name MdPanelRef#removeClass + * @deprecated + * This method is in the process of being deprecated in favor of using the panel + * and container JQLite elements that are referenced in the MdPanelRef object. + * Full deprecation is scheduled for material 1.2. + * @description + * Removes a class from the panel. DO NOT use this to hide/show the panel. + * + * @param {string} oldClass Class to be removed. + * @param {boolean} fromElement Whether or not to remove the class from the + * panel element instead of the container. + */ + +/** + * @ngdoc method + * @name MdPanelRef#toggleClass + * @deprecated + * This method is in the process of being deprecated in favor of using the panel + * and container JQLite elements that are referenced in the MdPanelRef object. + * Full deprecation is scheduled for material 1.2. + * @description + * Toggles a class on the panel. DO NOT use this to hide/show the panel. + * + * @param {string} toggleClass Class to be toggled. + * @param {boolean} onElement Whether or not to remove the class from the panel + * element instead of the container. + */ + +/** + * @ngdoc method + * @name MdPanelRef#updatePosition + * @description + * Updates the position configuration of a panel. Use this to update the + * position of a panel that is open, without having to close and re-open the + * panel. + * + * @param {!MdPanelPosition} position + */ + +/** + * @ngdoc method + * @name MdPanelRef#addToGroup + * @description + * Adds a panel to a group if the panel does not exist within the group already. + * A panel can only exist within a single group. + * + * @param {string} groupName The name of the group to add the panel to. + */ + +/** + * @ngdoc method + * @name MdPanelRef#removeFromGroup + * @description + * Removes a panel from a group if the panel exists within that group. The group + * must be created ahead of time. + * + * @param {string} groupName The name of the group. + */ + +/** + * @ngdoc method + * @name MdPanelRef#registerInterceptor + * @description + * Registers an interceptor with the panel. The callback should return a promise, + * which will allow the action to continue when it gets resolved, or will + * prevent an action if it is rejected. The interceptors are called sequentially + * and it reverse order. `type` must be one of the following + * values available on `$mdPanel.interceptorTypes`: + * * `CLOSE` - Gets called before the panel begins closing. + * + * @param {string} type Type of interceptor. + * @param {!angular.$q.Promise} callback Callback to be registered. + * @returns {!MdPanelRef} + */ + +/** + * @ngdoc method + * @name MdPanelRef#removeInterceptor + * @description + * Removes a registered interceptor. + * + * @param {string} type Type of interceptor to be removed. + * @param {function(): !angular.$q.Promise} callback Interceptor to be removed. + * @returns {!MdPanelRef} + */ + +/** + * @ngdoc method + * @name MdPanelRef#removeAllInterceptors + * @description + * Removes all interceptors. If a type is supplied, only the + * interceptors of that type will be cleared. + * + * @param {string=} type Type of interceptors to be removed. + * @returns {!MdPanelRef} + */ + +/** + * @ngdoc method + * @name MdPanelRef#updateAnimation + * @description + * Updates the animation configuration for a panel. You can use this to change + * the panel's animation without having to re-create it. + * + * @param {!MdPanelAnimation} animation + */ + + +/***************************************************************************** + * MdPanelPosition * + *****************************************************************************/ + + +/** + * @ngdoc type + * @name MdPanelPosition + * @module material.components.panel + * @description + * + * Object for configuring the position of the panel. + * + * @usage + * + * #### Centering the panel + * + * + * new MdPanelPosition().absolute().center(); + * + * + * #### Overlapping the panel with an element + * + * + * new MdPanelPosition() + * .relativeTo(someElement) + * .addPanelPosition( + * $mdPanel.xPosition.ALIGN_START, + * $mdPanel.yPosition.ALIGN_TOPS + * ); + * + * + * #### Aligning the panel with the bottom of an element + * + * + * new MdPanelPosition() + * .relativeTo(someElement) + * .addPanelPosition($mdPanel.xPosition.CENTER, $mdPanel.yPosition.BELOW); + * + */ + +/** + * @ngdoc method + * @name MdPanelPosition#absolute + * @description + * Positions the panel absolutely relative to the parent element. If the parent + * is document.body, this is equivalent to positioning the panel absolutely + * within the viewport. + * + * @returns {!MdPanelPosition} + */ + +/** + * @ngdoc method + * @name MdPanelPosition#relativeTo + * @description + * Positions the panel relative to a specific element. + * + * @param {string|!Element|!angular.JQLite} element Query selector, DOM element, + * or angular element to position the panel with respect to. + * @returns {!MdPanelPosition} + */ + +/** + * @ngdoc method + * @name MdPanelPosition#top + * @description + * Sets the value of `top` for the panel. Clears any previously set vertical + * position. + * + * @param {string=} top Value of `top`. Defaults to '0'. + * @returns {!MdPanelPosition} + */ + +/** + * @ngdoc method + * @name MdPanelPosition#bottom + * @description + * Sets the value of `bottom` for the panel. Clears any previously set vertical + * position. + * + * @param {string=} bottom Value of `bottom`. Defaults to '0'. + * @returns {!MdPanelPosition} + */ + +/** + * @ngdoc method + * @name MdPanelPosition#start + * @description + * Sets the panel to the start of the page - `left` if `ltr` or `right` for + * `rtl`. Clears any previously set horizontal position. + * + * @param {string=} start Value of position. Defaults to '0'. + * @returns {!MdPanelPosition} + */ + +/** + * @ngdoc method + * @name MdPanelPosition#end + * @description + * Sets the panel to the end of the page - `right` if `ltr` or `left` for `rtl`. + * Clears any previously set horizontal position. + * + * @param {string=} end Value of position. Defaults to '0'. + * @returns {!MdPanelPosition} + */ + +/** + * @ngdoc method + * @name MdPanelPosition#left + * @description + * Sets the value of `left` for the panel. Clears any previously set + * horizontal position. + * + * @param {string=} left Value of `left`. Defaults to '0'. + * @returns {!MdPanelPosition} + */ + +/** + * @ngdoc method + * @name MdPanelPosition#right + * @description + * Sets the value of `right` for the panel. Clears any previously set + * horizontal position. + * + * @param {string=} right Value of `right`. Defaults to '0'. + * @returns {!MdPanelPosition} + */ + +/** + * @ngdoc method + * @name MdPanelPosition#centerHorizontally + * @description + * Centers the panel horizontally in the viewport. Clears any previously set + * horizontal position. + * + * @returns {!MdPanelPosition} + */ + +/** + * @ngdoc method + * @name MdPanelPosition#centerVertically + * @description + * Centers the panel vertically in the viewport. Clears any previously set + * vertical position. + * + * @returns {!MdPanelPosition} + */ + +/** + * @ngdoc method + * @name MdPanelPosition#center + * @description + * Centers the panel horizontally and vertically in the viewport. This is + * equivalent to calling both `centerHorizontally` and `centerVertically`. + * Clears any previously set horizontal and vertical positions. + * + * @returns {!MdPanelPosition} + */ + +/** + * @ngdoc method + * @name MdPanelPosition#addPanelPosition + * @description + * Sets the x and y position for the panel relative to another element. Can be + * called multiple times to specify an ordered list of panel positions. The + * first position which allows the panel to be completely on-screen will be + * chosen; the last position will be chose whether it is on-screen or not. + * + * xPosition must be one of the following values available on + * $mdPanel.xPosition: + * + * + * CENTER | ALIGN_START | ALIGN_END | OFFSET_START | OFFSET_END + * + *
+ *    *************
+ *    *           *
+ *    *   PANEL   *
+ *    *           *
+ *    *************
+ *   A B    C    D E
+ *
+ * A: OFFSET_START (for LTR displays)
+ * B: ALIGN_START (for LTR displays)
+ * C: CENTER
+ * D: ALIGN_END (for LTR displays)
+ * E: OFFSET_END (for LTR displays)
+ * 
+ * + * yPosition must be one of the following values available on + * $mdPanel.yPosition: + * + * CENTER | ALIGN_TOPS | ALIGN_BOTTOMS | ABOVE | BELOW + * + *
+ *   F
+ *   G *************
+ *     *           *
+ *   H *   PANEL   *
+ *     *           *
+ *   I *************
+ *   J
+ *
+ * F: BELOW
+ * G: ALIGN_TOPS
+ * H: CENTER
+ * I: ALIGN_BOTTOMS
+ * J: ABOVE
+ * 
+ * + * @param {string} xPosition + * @param {string} yPosition + * @returns {!MdPanelPosition} + */ + +/** + * @ngdoc method + * @name MdPanelPosition#withOffsetX + * @description + * Sets the value of the offset in the x-direction. + * + * @param {string} offsetX + * @returns {!MdPanelPosition} + */ + +/** + * @ngdoc method + * @name MdPanelPosition#withOffsetY + * @description + * Sets the value of the offset in the y-direction. + * + * @param {string} offsetY + * @returns {!MdPanelPosition} + */ + + +/***************************************************************************** + * MdPanelAnimation * + *****************************************************************************/ + + +/** + * @ngdoc type + * @name MdPanelAnimation + * @module material.components.panel + * @description + * Animation configuration object. To use, create an MdPanelAnimation with the + * desired properties, then pass the object as part of $mdPanel creation. + * + * @usage + * + * + * var panelAnimation = new MdPanelAnimation() + * .openFrom(myButtonEl) + * .duration(1337) + * .closeTo('.my-button') + * .withAnimation($mdPanel.animation.SCALE); + * + * $mdPanel.create({ + * animation: panelAnimation + * }); + * + */ + +/** + * @ngdoc method + * @name MdPanelAnimation#openFrom + * @description + * Specifies where to start the open animation. `openFrom` accepts a + * click event object, query selector, DOM element, or a Rect object that + * is used to determine the bounds. When passed a click event, the location + * of the click will be used as the position to start the animation. + * + * @param {string|!Element|!Event|{top: number, left: number}} + * @returns {!MdPanelAnimation} + */ + +/** + * @ngdoc method + * @name MdPanelAnimation#closeTo + * @description + * Specifies where to animate the panel close. `closeTo` accepts a + * query selector, DOM element, or a Rect object that is used to determine + * the bounds. + * + * @param {string|!Element|{top: number, left: number}} + * @returns {!MdPanelAnimation} + */ + +/** + * @ngdoc method + * @name MdPanelAnimation#withAnimation + * @description + * Specifies the animation class. + * + * There are several default animations that can be used: + * ($mdPanel.animation) + * SLIDE: The panel slides in and out from the specified + * elements. It will not fade in or out. + * SCALE: The panel scales in and out. Slide and fade are + * included in this animation. + * FADE: The panel fades in and out. + * + * Custom classes will by default fade in and out unless + * "transition: opacity 1ms" is added to the to custom class. + * + * @param {string|{open: string, close: string}} cssClass + * @returns {!MdPanelAnimation} + */ + +/** + * @ngdoc method + * @name MdPanelAnimation#duration + * @description + * Specifies the duration of the animation in milliseconds. The `duration` + * method accepts either a number or an object with separate open and close + * durations. + * + * @param {number|{open: number, close: number}} duration + * @returns {!MdPanelAnimation} + */ + + +/***************************************************************************** + * PUBLIC DOCUMENTATION * + *****************************************************************************/ + + +var MD_PANEL_Z_INDEX = 80; +var MD_PANEL_HIDDEN = '_md-panel-hidden'; +var FOCUS_TRAP_TEMPLATE = angular.element( + '
'); + +var _presets = {}; + + +/** + * A provider that is used for creating presets for the panel API. + * @final @constructor @ngInject + */ +function MdPanelProvider() { + return { + 'definePreset': definePreset, + 'getAllPresets': getAllPresets, + 'clearPresets': clearPresets, + '$get': $getProvider() + }; +} + + +/** + * Takes the passed in panel configuration object and adds it to the `_presets` + * object at the specified name. + * @param {string} name Name of the preset to set. + * @param {!Object} preset Specific configuration object that can contain any + * and all of the parameters avaialble within the `$mdPanel.create` method. + * However, parameters that pertain to id, position, animation, and user + * interaction are not allowed and will be removed from the preset + * configuration. + */ +function definePreset(name, preset) { + if (!name || !preset) { + throw new Error('mdPanelProvider: The panel preset definition is ' + + 'malformed. The name and preset object are required.'); + } else if (_presets.hasOwnProperty(name)) { + throw new Error('mdPanelProvider: The panel preset you have requested ' + + 'has already been defined.'); + } + + // Delete any property on the preset that is not allowed. + delete preset.id; + delete preset.position; + delete preset.animation; + + _presets[name] = preset; +} + + +/** + * Gets a clone of the `_presets`. + * @return {!Object} + */ +function getAllPresets() { + return angular.copy(_presets); +} + + +/** + * Clears all of the stored presets. + */ +function clearPresets() { + _presets = {}; +} + + +/** + * Represents the `$get` method of the Angular provider. From here, a new + * reference to the MdPanelService is returned where the needed arguments are + * passed in including the MdPanelProvider `_presets`. + * @param {!Object} _presets + * @param {!angular.JQLite} $rootElement + * @param {!angular.Scope} $rootScope + * @param {!angular.$injector} $injector + * @param {!angular.$window} $window + */ +function $getProvider() { + return [ + '$rootElement', '$rootScope', '$injector', '$window', + function($rootElement, $rootScope, $injector, $window) { + return new MdPanelService(_presets, $rootElement, $rootScope, + $injector, $window); + } + ]; +} + + +/***************************************************************************** + * MdPanel Service * + *****************************************************************************/ + + +/** + * A service that is used for controlling/displaying panels on the screen. + * @param {!Object} presets + * @param {!angular.JQLite} $rootElement + * @param {!angular.Scope} $rootScope + * @param {!angular.$injector} $injector + * @param {!angular.$window} $window + * @final @constructor @ngInject + */ +function MdPanelService(presets, $rootElement, $rootScope, $injector, $window) { + /** + * Default config options for the panel. + * Anything angular related needs to be done later. Therefore + * scope: $rootScope.$new(true), + * attachTo: $rootElement, + * are added later. + * @private {!Object} + */ + this._defaultConfigOptions = { + bindToController: true, + clickOutsideToClose: false, + disableParentScroll: false, + escapeToClose: false, + focusOnOpen: true, + fullscreen: false, + hasBackdrop: false, + propagateContainerEvents: false, + transformTemplate: angular.bind(this, this._wrapTemplate), + trapFocus: false, + zIndex: MD_PANEL_Z_INDEX + }; + + /** @private {!Object} */ + this._config = {}; + + /** @private {!Object} */ + this._presets = presets; + + /** @private @const */ + this._$rootElement = $rootElement; + + /** @private @const */ + this._$rootScope = $rootScope; + + /** @private @const */ + this._$injector = $injector; + + /** @private @const */ + this._$window = $window; + + /** @private @const */ + this._$mdUtil = this._$injector.get('$mdUtil'); + + /** @private {!Object} */ + this._trackedPanels = {}; + + /** + * @private {!Object, + * openPanels: !Array, + * maxOpen: number}>} + */ + this._groups = Object.create(null); + + /** + * Default animations that can be used within the panel. + * @type {enum} + */ + this.animation = MdPanelAnimation.animation; + + /** + * Possible values of xPosition for positioning the panel relative to + * another element. + * @type {enum} + */ + this.xPosition = MdPanelPosition.xPosition; + + /** + * Possible values of yPosition for positioning the panel relative to + * another element. + * @type {enum} + */ + this.yPosition = MdPanelPosition.yPosition; + + /** + * Possible values for the interceptors that can be registered on a panel. + * @type {enum} + */ + this.interceptorTypes = MdPanelRef.interceptorTypes; + + /** + * Possible values for closing of a panel. + * @type {enum} + */ + this.closeReasons = MdPanelRef.closeReasons; + + /** + * Possible values of absolute position. + * @type {enum} + */ + this.absPosition = MdPanelPosition.absPosition; +} + + +/** + * Creates a panel with the specified options. + * @param {string=} preset Name of a preset configuration that can be used to + * extend the panel configuration. + * @param {!Object=} config Configuration object for the panel. + * @returns {!MdPanelRef} + */ +MdPanelService.prototype.create = function(preset, config) { + if (typeof preset === 'string') { + preset = this._getPresetByName(preset); + } else if (typeof preset === 'object' && + (angular.isUndefined(config) || !config)) { + config = preset; + preset = {}; + } + + preset = preset || {}; + config = config || {}; + + // If the passed-in config contains an ID and the ID is within _trackedPanels, + // return the tracked panel after updating its config with the passed-in + // config. + if (angular.isDefined(config.id) && this._trackedPanels[config.id]) { + var trackedPanel = this._trackedPanels[config.id]; + angular.extend(trackedPanel.config, config); + return trackedPanel; + } + + // Combine the passed-in config, the _defaultConfigOptions, and the preset + // configuration into the `_config`. + this._config = angular.extend({ + // If no ID is set within the passed-in config, then create an arbitrary ID. + id: config.id || 'panel_' + this._$mdUtil.nextUid(), + scope: this._$rootScope.$new(true), + attachTo: this._$rootElement + }, this._defaultConfigOptions, config, preset); + + // Create the panelRef and add it to the `_trackedPanels` object. + var panelRef = new MdPanelRef(this._config, this._$injector); + this._trackedPanels[config.id] = panelRef; + + // Add the panel to each of its requested groups. + if (this._config.groupName) { + if (angular.isString(this._config.groupName)) { + this._config.groupName = [this._config.groupName]; + } + angular.forEach(this._config.groupName, function(group) { + panelRef.addToGroup(group); + }); + } + + this._config.scope.$on('$destroy', angular.bind(panelRef, panelRef.detach)); + + return panelRef; +}; + + +/** + * Creates and opens a panel with the specified options. + * @param {string=} preset Name of a preset configuration that can be used to + * extend the panel configuration. + * @param {!Object=} config Configuration object for the panel. + * @returns {!angular.$q.Promise} The panel created from create. + */ +MdPanelService.prototype.open = function(preset, config) { + var panelRef = this.create(preset, config); + return panelRef.open().then(function() { + return panelRef; + }); +}; + + +/** + * Gets a specific preset configuration object saved within `_presets`. + * @param {string} preset Name of the preset to search for. + * @returns {!Object} The preset configuration object. + */ +MdPanelService.prototype._getPresetByName = function(preset) { + if (!this._presets[preset]) { + throw new Error('mdPanel: The panel preset configuration that you ' + + 'requested does not exist. Use the $mdPanelProvider to create a ' + + 'preset before requesting one.'); + } + return this._presets[preset]; +}; + + +/** + * Returns a new instance of the MdPanelPosition. Use this to create the + * positioning object. + * @returns {!MdPanelPosition} + */ +MdPanelService.prototype.newPanelPosition = function() { + return new MdPanelPosition(this._$injector); +}; + + +/** + * Returns a new instance of the MdPanelAnimation. Use this to create the + * animation object. + * @returns {!MdPanelAnimation} + */ +MdPanelService.prototype.newPanelAnimation = function() { + return new MdPanelAnimation(this._$injector); +}; + + +/** + * Creates a panel group and adds it to a tracked list of panel groups. + * @param groupName {string} Name of the group to create. + * @param config {!Object=} Specific configuration object that may contain the + * following properties: + * + * - `maxOpen` - `{number=}`: The maximum number of panels that are allowed + * open within a defined panel group. + * + * @returns {!Object, + * openPanels: !Array, + * maxOpen: number}>} panelGroup + */ +MdPanelService.prototype.newPanelGroup = function(groupName, config) { + if (!this._groups[groupName]) { + config = config || {}; + var group = { + panels: [], + openPanels: [], + maxOpen: config.maxOpen > 0 ? config.maxOpen : Infinity + }; + this._groups[groupName] = group; + } + return this._groups[groupName]; +}; + + +/** + * Sets the maximum number of panels in a group that can be opened at a given + * time. + * @param {string} groupName The name of the group to configure. + * @param {number} maxOpen The maximum number of panels that can be + * opened. Infinity can be passed in to remove the maxOpen limit. + */ +MdPanelService.prototype.setGroupMaxOpen = function(groupName, maxOpen) { + if (this._groups[groupName]) { + this._groups[groupName].maxOpen = maxOpen; + } else { + throw new Error('mdPanel: Group does not exist yet. Call newPanelGroup().'); + } +}; + + +/** + * Determines if the current number of open panels within a group exceeds the + * limit of allowed open panels. + * @param {string} groupName The name of the group to check. + * @returns {boolean} true if open count does exceed maxOpen and false if not. + * @private + */ +MdPanelService.prototype._openCountExceedsMaxOpen = function(groupName) { + if (this._groups[groupName]) { + var group = this._groups[groupName]; + return group.maxOpen > 0 && group.openPanels.length > group.maxOpen; + } + return false; +}; + + +/** + * Closes the first open panel within a specific group. + * @param {string} groupName The name of the group. + * @private + */ +MdPanelService.prototype._closeFirstOpenedPanel = function(groupName) { + this._groups[groupName].openPanels[0].close(); +}; + + +/** + * Wraps the users template in two elements, md-panel-outer-wrapper, which + * covers the entire attachTo element, and md-panel, which contains only the + * template. This allows the panel control over positioning, animations, + * and similar properties. + * @param {string} origTemplate The original template. + * @returns {string} The wrapped template. + * @private + */ +MdPanelService.prototype._wrapTemplate = function(origTemplate) { + var template = origTemplate || ''; + + // The panel should be initially rendered offscreen so we can calculate + // height and width for positioning. + return '' + + '
' + + '
' + template + '
' + + '
'; +}; + + +/** + * Wraps a content element in a md-panel-outer wrapper and + * positions it off-screen. Allows for proper control over positoning + * and animations. + * @param {!angular.JQLite} contentElement Element to be wrapped. + * @return {!angular.JQLite} Wrapper element. + * @private + */ +MdPanelService.prototype._wrapContentElement = function(contentElement) { + var wrapper = angular.element('
'); + + contentElement.addClass('md-panel').css('left', '-9999px'); + wrapper.append(contentElement); + + return wrapper; +}; + + +/***************************************************************************** + * MdPanelRef * + *****************************************************************************/ + + +/** + * A reference to a created panel. This reference contains a unique id for the + * panel, along with properties/functions used to control the panel. + * @param {!Object} config + * @param {!angular.$injector} $injector + * @final @constructor + */ +function MdPanelRef(config, $injector) { + // Injected variables. + /** @private @const {!angular.$q} */ + this._$q = $injector.get('$q'); + + /** @private @const {!angular.$mdCompiler} */ + this._$mdCompiler = $injector.get('$mdCompiler'); + + /** @private @const {!angular.$mdConstant} */ + this._$mdConstant = $injector.get('$mdConstant'); + + /** @private @const {!angular.$mdUtil} */ + this._$mdUtil = $injector.get('$mdUtil'); + + /** @private @const {!angular.$mdTheming} */ + this._$mdTheming = $injector.get('$mdTheming'); + + /** @private @const {!angular.Scope} */ + this._$rootScope = $injector.get('$rootScope'); + + /** @private @const {!angular.$animate} */ + this._$animate = $injector.get('$animate'); + + /** @private @const {!MdPanelRef} */ + this._$mdPanel = $injector.get('$mdPanel'); + + /** @private @const {!angular.$log} */ + this._$log = $injector.get('$log'); + + /** @private @const {!angular.$window} */ + this._$window = $injector.get('$window'); + + /** @private @const {!Function} */ + this._$$rAF = $injector.get('$$rAF'); + + // Public variables. + /** + * Unique id for the panelRef. + * @type {string} + */ + this.id = config.id; + + /** @type {!Object} */ + this.config = config; + + /** @type {!angular.JQLite|undefined} */ + this.panelContainer; + + /** @type {!angular.JQLite|undefined} */ + this.panelEl; + + /** + * Whether the panel is attached. This is synchronous. When attach is called, + * isAttached is set to true. When detach is called, isAttached is set to + * false. + * @type {boolean} + */ + this.isAttached = false; + + // Private variables. + /** @private {Array} */ + this._removeListeners = []; + + /** @private {!angular.JQLite|undefined} */ + this._topFocusTrap; + + /** @private {!angular.JQLite|undefined} */ + this._bottomFocusTrap; + + /** @private {!$mdPanel|undefined} */ + this._backdropRef; + + /** @private {Function?} */ + this._restoreScroll = null; + + /** + * Keeps track of all the panel interceptors. + * @private {!Object} + */ + this._interceptors = Object.create(null); + + /** + * Cleanup function, provided by `$mdCompiler` and assigned after the element + * has been compiled. When `contentElement` is used, the function is used to + * restore the element to it's proper place in the DOM. + * @private {!Function} + */ + this._compilerCleanup = null; + + /** + * Cache for saving and restoring element inline styles, CSS classes etc. + * @type {{styles: string, classes: string}} + */ + this._restoreCache = { + styles: '', + classes: '' + }; +} + + +MdPanelRef.interceptorTypes = { + CLOSE: 'onClose' +}; + + +/** + * Opens an already created and configured panel. If the panel is already + * visible, does nothing. + * @returns {!angular.$q.Promise} A promise that is resolved when + * the panel is opened and animations finish. + */ +MdPanelRef.prototype.open = function() { + var self = this; + return this._$q(function(resolve, reject) { + var done = self._done(resolve, self); + var show = self._simpleBind(self.show, self); + var checkGroupMaxOpen = function() { + if (self.config.groupName) { + angular.forEach(self.config.groupName, function(group) { + if (self._$mdPanel._openCountExceedsMaxOpen(group)) { + self._$mdPanel._closeFirstOpenedPanel(group); + } + }); + } + }; + + self.attach() + .then(show) + .then(checkGroupMaxOpen) + .then(done) + .catch(reject); + }); +}; + + +/** + * Closes the panel. + * @param {string} closeReason The event type that triggered the close. + * @returns {!angular.$q.Promise} A promise that is resolved when + * the panel is closed and animations finish. + */ +MdPanelRef.prototype.close = function(closeReason) { + var self = this; + + return this._$q(function(resolve, reject) { + self._callInterceptors(MdPanelRef.interceptorTypes.CLOSE).then(function() { + var done = self._done(resolve, self); + var detach = self._simpleBind(self.detach, self); + var onCloseSuccess = self.config['onCloseSuccess'] || angular.noop; + onCloseSuccess = angular.bind(self, onCloseSuccess, self, closeReason); + + self.hide() + .then(detach) + .then(done) + .then(onCloseSuccess) + .catch(reject); + }, reject); + }); +}; + + +/** + * Attaches the panel. The panel will be hidden afterwards. + * @returns {!angular.$q.Promise} A promise that is resolved when + * the panel is attached. + */ +MdPanelRef.prototype.attach = function() { + if (this.isAttached && this.panelEl) { + return this._$q.when(this); + } + + var self = this; + return this._$q(function(resolve, reject) { + var done = self._done(resolve, self); + var onDomAdded = self.config['onDomAdded'] || angular.noop; + var addListeners = function(response) { + self.isAttached = true; + self._addEventListeners(); + return response; + }; + + self._$q.all([ + self._createBackdrop(), + self._createPanel() + .then(addListeners) + .catch(reject) + ]).then(onDomAdded) + .then(done) + .catch(reject); + }); +}; + + +/** + * Only detaches the panel. Will NOT hide the panel first. + * @returns {!angular.$q.Promise} A promise that is resolved when + * the panel is detached. + */ +MdPanelRef.prototype.detach = function() { + if (!this.isAttached) { + return this._$q.when(this); + } + + var self = this; + var onDomRemoved = self.config['onDomRemoved'] || angular.noop; + + var detachFn = function() { + self._removeEventListeners(); + + // Remove the focus traps that we added earlier for keeping focus within + // the panel. + if (self._topFocusTrap && self._topFocusTrap.parentNode) { + self._topFocusTrap.parentNode.removeChild(self._topFocusTrap); + } + + if (self._bottomFocusTrap && self._bottomFocusTrap.parentNode) { + self._bottomFocusTrap.parentNode.removeChild(self._bottomFocusTrap); + } + + if (self._restoreCache.classes) { + self.panelEl[0].className = self._restoreCache.classes; + } + + // Either restore the saved styles or clear the ones set by mdPanel. + self.panelEl[0].style.cssText = self._restoreCache.styles || ''; + + self._compilerCleanup(); + self.panelContainer.remove(); + self.isAttached = false; + return self._$q.when(self); + }; + + if (this._restoreScroll) { + this._restoreScroll(); + this._restoreScroll = null; + } + + return this._$q(function(resolve, reject) { + var done = self._done(resolve, self); + + self._$q.all([ + detachFn(), + self._backdropRef ? self._backdropRef.detach() : true + ]).then(onDomRemoved) + .then(done) + .catch(reject); + }); +}; + + +/** + * Destroys the panel. The Panel cannot be opened again after this. + */ +MdPanelRef.prototype.destroy = function() { + var self = this; + if (this.config.groupName) { + angular.forEach(this.config.groupName, function(group) { + self.removeFromGroup(group); + }); + } + this.config.scope.$destroy(); + this.config.locals = null; + this._interceptors = null; +}; + + +/** + * Shows the panel. + * @returns {!angular.$q.Promise} A promise that is resolved when + * the panel has shown and animations finish. + */ +MdPanelRef.prototype.show = function() { + if (!this.panelContainer) { + return this._$q(function(resolve, reject) { + reject('mdPanel: Panel does not exist yet. Call open() or attach().'); + }); + } + + if (!this.panelContainer.hasClass(MD_PANEL_HIDDEN)) { + return this._$q.when(this); + } + + var self = this; + var animatePromise = function() { + self.panelContainer.removeClass(MD_PANEL_HIDDEN); + return self._animateOpen(); + }; + + return this._$q(function(resolve, reject) { + var done = self._done(resolve, self); + var onOpenComplete = self.config['onOpenComplete'] || angular.noop; + var addToGroupOpen = function() { + if (self.config.groupName) { + angular.forEach(self.config.groupName, function(group) { + self._$mdPanel._groups[group].openPanels.push(self); + }); + } + }; + + self._$q.all([ + self._backdropRef ? self._backdropRef.show() : self, + animatePromise().then(function() { self._focusOnOpen(); }, reject) + ]).then(onOpenComplete) + .then(addToGroupOpen) + .then(done) + .catch(reject); + }); +}; + + +/** + * Hides the panel. + * @returns {!angular.$q.Promise} A promise that is resolved when + * the panel has hidden and animations finish. + */ +MdPanelRef.prototype.hide = function() { + if (!this.panelContainer) { + return this._$q(function(resolve, reject) { + reject('mdPanel: Panel does not exist yet. Call open() or attach().'); + }); + } + + if (this.panelContainer.hasClass(MD_PANEL_HIDDEN)) { + return this._$q.when(this); + } + + var self = this; + + return this._$q(function(resolve, reject) { + var done = self._done(resolve, self); + var onRemoving = self.config['onRemoving'] || angular.noop; + var hidePanel = function() { + self.panelContainer.addClass(MD_PANEL_HIDDEN); + }; + var removeFromGroupOpen = function() { + if (self.config.groupName) { + var group, index; + angular.forEach(self.config.groupName, function(group) { + group = self._$mdPanel._groups[group]; + index = group.openPanels.indexOf(self); + if (index > -1) { + group.openPanels.splice(index, 1); + } + }); + } + }; + var focusOnOrigin = function() { + var origin = self.config['origin']; + if (origin) { + getElement(origin).focus(); + } + }; + + self._$q.all([ + self._backdropRef ? self._backdropRef.hide() : self, + self._animateClose() + .then(onRemoving) + .then(hidePanel) + .then(removeFromGroupOpen) + .then(focusOnOrigin) + .catch(reject) + ]).then(done, reject); + }); +}; + + +/** + * Add a class to the panel. DO NOT use this to hide/show the panel. + * @deprecated + * This method is in the process of being deprecated in favor of using the panel + * and container JQLite elements that are referenced in the MdPanelRef object. + * Full deprecation is scheduled for material 1.2. + * + * @param {string} newClass Class to be added. + * @param {boolean} toElement Whether or not to add the class to the panel + * element instead of the container. + */ +MdPanelRef.prototype.addClass = function(newClass, toElement) { + this._$log.warn( + 'mdPanel: The addClass method is in the process of being deprecated. ' + + 'Full deprecation is scheduled for the Angular Material 1.2 release. ' + + 'To achieve the same results, use the panelContainer or panelEl ' + + 'JQLite elements that are referenced in MdPanelRef.'); + + if (!this.panelContainer) { + throw new Error( + 'mdPanel: Panel does not exist yet. Call open() or attach().'); + } + + if (!toElement && !this.panelContainer.hasClass(newClass)) { + this.panelContainer.addClass(newClass); + } else if (toElement && !this.panelEl.hasClass(newClass)) { + this.panelEl.addClass(newClass); + } +}; + + +/** + * Remove a class from the panel. DO NOT use this to hide/show the panel. + * @deprecated + * This method is in the process of being deprecated in favor of using the panel + * and container JQLite elements that are referenced in the MdPanelRef object. + * Full deprecation is scheduled for material 1.2. + * + * @param {string} oldClass Class to be removed. + * @param {boolean} fromElement Whether or not to remove the class from the + * panel element instead of the container. + */ +MdPanelRef.prototype.removeClass = function(oldClass, fromElement) { + this._$log.warn( + 'mdPanel: The removeClass method is in the process of being deprecated. ' + + 'Full deprecation is scheduled for the Angular Material 1.2 release. ' + + 'To achieve the same results, use the panelContainer or panelEl ' + + 'JQLite elements that are referenced in MdPanelRef.'); + + if (!this.panelContainer) { + throw new Error( + 'mdPanel: Panel does not exist yet. Call open() or attach().'); + } + + if (!fromElement && this.panelContainer.hasClass(oldClass)) { + this.panelContainer.removeClass(oldClass); + } else if (fromElement && this.panelEl.hasClass(oldClass)) { + this.panelEl.removeClass(oldClass); + } +}; + + +/** + * Toggle a class on the panel. DO NOT use this to hide/show the panel. + * @deprecated + * This method is in the process of being deprecated in favor of using the panel + * and container JQLite elements that are referenced in the MdPanelRef object. + * Full deprecation is scheduled for material 1.2. + * + * @param {string} toggleClass The class to toggle. + * @param {boolean} onElement Whether or not to toggle the class on the panel + * element instead of the container. + */ +MdPanelRef.prototype.toggleClass = function(toggleClass, onElement) { + this._$log.warn( + 'mdPanel: The toggleClass method is in the process of being deprecated. ' + + 'Full deprecation is scheduled for the Angular Material 1.2 release. ' + + 'To achieve the same results, use the panelContainer or panelEl ' + + 'JQLite elements that are referenced in MdPanelRef.'); + + if (!this.panelContainer) { + throw new Error( + 'mdPanel: Panel does not exist yet. Call open() or attach().'); + } + + if (!onElement) { + this.panelContainer.toggleClass(toggleClass); + } else { + this.panelEl.toggleClass(toggleClass); + } +}; + + +/** + * Compiles the panel, according to the passed in config and appends it to + * the DOM. Helps normalize differences in the compilation process between + * using a string template and a content element. + * @returns {!angular.$q.Promise} Promise that is resolved when + * the element has been compiled and added to the DOM. + * @private + */ +MdPanelRef.prototype._compile = function() { + var self = this; + + // Compile the element via $mdCompiler. Note that when using a + // contentElement, the element isn't actually being compiled, rather the + // compiler saves it's place in the DOM and provides a way of restoring it. + return self._$mdCompiler.compile(self.config).then(function(compileData) { + var config = self.config; + + if (config.contentElement) { + var panelEl = compileData.element; + + // Since mdPanel modifies the inline styles and CSS classes, we need + // to save them in order to be able to restore on close. + self._restoreCache.styles = panelEl[0].style.cssText; + self._restoreCache.classes = panelEl[0].className; + + self.panelContainer = self._$mdPanel._wrapContentElement(panelEl); + self.panelEl = panelEl; + } else { + self.panelContainer = compileData.link(config['scope']); + self.panelEl = angular.element( + self.panelContainer[0].querySelector('.md-panel') + ); + } + + // Save a reference to the cleanup function from the compiler. + self._compilerCleanup = compileData.cleanup; + + // Attach the panel to the proper place in the DOM. + getElement(self.config['attachTo']).append(self.panelContainer); + + return self; + }); +}; + + +/** + * Creates a panel and adds it to the dom. + * @returns {!angular.$q.Promise} A promise that is resolved when the panel is + * created. + * @private + */ +MdPanelRef.prototype._createPanel = function() { + var self = this; + + return this._$q(function(resolve, reject) { + if (!self.config.locals) { + self.config.locals = {}; + } + + self.config.locals.mdPanelRef = self; + + self._compile().then(function() { + if (self.config['disableParentScroll']) { + self._restoreScroll = self._$mdUtil.disableScrollAround( + null, + self.panelContainer, + { disableScrollMask: true } + ); + } + + // Add a custom CSS class to the panel element. + if (self.config['panelClass']) { + self.panelEl.addClass(self.config['panelClass']); + } + + // Handle click and touch events for the panel container. + if (self.config['propagateContainerEvents']) { + self.panelContainer.css('pointer-events', 'none'); + } + + // Panel may be outside the $rootElement, tell ngAnimate to animate + // regardless. + if (self._$animate.pin) { + self._$animate.pin( + self.panelContainer, + getElement(self.config['attachTo']) + ); + } + + self._configureTrapFocus(); + self._addStyles().then(function() { + resolve(self); + }, reject); + }, reject); + + }); +}; + + +/** + * Adds the styles for the panel, such as positioning and z-index. Also, + * themes the panel element and panel container using `$mdTheming`. + * @returns {!angular.$q.Promise} + * @private + */ +MdPanelRef.prototype._addStyles = function() { + var self = this; + return this._$q(function(resolve) { + self.panelContainer.css('z-index', self.config['zIndex']); + self.panelEl.css('z-index', self.config['zIndex'] + 1); + + var hideAndResolve = function() { + // Theme the element and container. + self._setTheming(); + + // Remove left: -9999px and add hidden class. + self.panelEl.css('left', ''); + self.panelContainer.addClass(MD_PANEL_HIDDEN); + + resolve(self); + }; + + if (self.config['fullscreen']) { + self.panelEl.addClass('_md-panel-fullscreen'); + hideAndResolve(); + return; // Don't setup positioning. + } + + var positionConfig = self.config['position']; + if (!positionConfig) { + hideAndResolve(); + return; // Don't setup positioning. + } + + // Wait for angular to finish processing the template + self._$rootScope['$$postDigest'](function() { + // Position it correctly. This is necessary so that the panel will have a + // defined height and width. + self._updatePosition(true); + + // Theme the element and container. + self._setTheming(); + + resolve(self); + }); + }); +}; + + +/** + * Sets the `$mdTheming` classes on the `panelContainer` and `panelEl`. + * @private + */ +MdPanelRef.prototype._setTheming = function() { + this._$mdTheming(this.panelEl); + this._$mdTheming(this.panelContainer); +}; + + +/** + * Updates the position configuration of a panel + * @param {!MdPanelPosition} position + */ +MdPanelRef.prototype.updatePosition = function(position) { + if (!this.panelContainer) { + throw new Error( + 'mdPanel: Panel does not exist yet. Call open() or attach().'); + } + + this.config['position'] = position; + this._updatePosition(); +}; + + +/** + * Calculates and updates the position of the panel. + * @param {boolean=} init + * @private + */ +MdPanelRef.prototype._updatePosition = function(init) { + var positionConfig = this.config['position']; + + if (positionConfig) { + positionConfig._setPanelPosition(this.panelEl); + + // Hide the panel now that position is known. + if (init) { + this.panelContainer.addClass(MD_PANEL_HIDDEN); + } + + this.panelEl.css( + MdPanelPosition.absPosition.TOP, + positionConfig.getTop() + ); + this.panelEl.css( + MdPanelPosition.absPosition.BOTTOM, + positionConfig.getBottom() + ); + this.panelEl.css( + MdPanelPosition.absPosition.LEFT, + positionConfig.getLeft() + ); + this.panelEl.css( + MdPanelPosition.absPosition.RIGHT, + positionConfig.getRight() + ); + } +}; + + +/** + * Focuses on the panel or the first focus target. + * @private + */ +MdPanelRef.prototype._focusOnOpen = function() { + if (this.config['focusOnOpen']) { + // Wait for the template to finish rendering to guarantee md-autofocus has + // finished adding the class md-autofocus, otherwise the focusable element + // isn't available to focus. + var self = this; + this._$rootScope['$$postDigest'](function() { + var target = self._$mdUtil.findFocusTarget(self.panelEl) || + self.panelEl; + target.focus(); + }); + } +}; + + +/** + * Shows the backdrop. + * @returns {!angular.$q.Promise} A promise that is resolved when the backdrop + * is created and attached. + * @private + */ +MdPanelRef.prototype._createBackdrop = function() { + if (this.config.hasBackdrop) { + if (!this._backdropRef) { + var backdropAnimation = this._$mdPanel.newPanelAnimation() + .openFrom(this.config.attachTo) + .withAnimation({ + open: '_md-opaque-enter', + close: '_md-opaque-leave' + }); + + if (this.config.animation) { + backdropAnimation.duration(this.config.animation._rawDuration); + } + + var backdropConfig = { + animation: backdropAnimation, + attachTo: this.config.attachTo, + focusOnOpen: false, + panelClass: '_md-panel-backdrop', + zIndex: this.config.zIndex - 1 + }; + + this._backdropRef = this._$mdPanel.create(backdropConfig); + } + if (!this._backdropRef.isAttached) { + return this._backdropRef.attach(); + } + } +}; + + +/** + * Listen for escape keys and outside clicks to auto close. + * @private + */ +MdPanelRef.prototype._addEventListeners = function() { + this._configureEscapeToClose(); + this._configureClickOutsideToClose(); + this._configureScrollListener(); +}; + + +/** + * Remove event listeners added in _addEventListeners. + * @private + */ +MdPanelRef.prototype._removeEventListeners = function() { + this._removeListeners && this._removeListeners.forEach(function(removeFn) { + removeFn(); + }); + this._removeListeners = []; +}; + + +/** + * Setup the escapeToClose event listeners. + * @private + */ +MdPanelRef.prototype._configureEscapeToClose = function() { + if (this.config['escapeToClose']) { + var parentTarget = getElement(this.config['attachTo']); + var self = this; + + var keyHandlerFn = function(ev) { + if (ev.keyCode === self._$mdConstant.KEY_CODE.ESCAPE) { + ev.stopPropagation(); + ev.preventDefault(); + + self.close(MdPanelRef.closeReasons.ESCAPE); + } + }; + + // Add keydown listeners + this.panelContainer.on('keydown', keyHandlerFn); + parentTarget.on('keydown', keyHandlerFn); + + // Queue remove listeners function + this._removeListeners.push(function() { + self.panelContainer.off('keydown', keyHandlerFn); + parentTarget.off('keydown', keyHandlerFn); + }); + } +}; + + +/** + * Setup the clickOutsideToClose event listeners. + * @private + */ +MdPanelRef.prototype._configureClickOutsideToClose = function() { + if (this.config['clickOutsideToClose']) { + var target = this.config['propagateContainerEvents'] ? + angular.element(document.body) : + this.panelContainer; + var sourceEl; + + // Keep track of the element on which the mouse originally went down + // so that we can only close the backdrop when the 'click' started on it. + // A simple 'click' handler does not work, it sets the target object as the + // element the mouse went down on. + var mousedownHandler = function(ev) { + sourceEl = ev.target; + }; + + // We check if our original element and the target is the backdrop + // because if the original was the backdrop and the target was inside the + // panel we don't want to panel to close. + var self = this; + var mouseupHandler = function(ev) { + if (self.config['propagateContainerEvents']) { + + // We check if the sourceEl of the event is the panel element or one + // of it's children. If it is not, then close the panel. + if (sourceEl !== self.panelEl[0] && !self.panelEl[0].contains(sourceEl)) { + self.close(); + } + + } else if (sourceEl === target[0] && ev.target === target[0]) { + ev.stopPropagation(); + ev.preventDefault(); + + self.close(MdPanelRef.closeReasons.CLICK_OUTSIDE); + } + }; + + // Add listeners + target.on('mousedown', mousedownHandler); + target.on('mouseup', mouseupHandler); + + // Queue remove listeners function + this._removeListeners.push(function() { + target.off('mousedown', mousedownHandler); + target.off('mouseup', mouseupHandler); + }); + } +}; + + +/** + * Configures the listeners for updating the panel position on scroll. + * @private +*/ +MdPanelRef.prototype._configureScrollListener = function() { + // No need to bind the event if scrolling is disabled. + if (!this.config['disableParentScroll']) { + var updatePosition = angular.bind(this, this._updatePosition); + var debouncedUpdatePosition = this._$$rAF.throttle(updatePosition); + var self = this; + + var onScroll = function() { + debouncedUpdatePosition(); + }; + + // Add listeners. + this._$window.addEventListener('scroll', onScroll, true); + + // Queue remove listeners function. + this._removeListeners.push(function() { + self._$window.removeEventListener('scroll', onScroll, true); + }); + } +}; + + +/** + * Setup the focus traps. These traps will wrap focus when tabbing past the + * panel. When shift-tabbing, the focus will stick in place. + * @private + */ +MdPanelRef.prototype._configureTrapFocus = function() { + // Focus doesn't remain inside of the panel without this. + this.panelEl.attr('tabIndex', '-1'); + if (this.config['trapFocus']) { + var element = this.panelEl; + // Set up elements before and after the panel to capture focus and + // redirect back into the panel. + this._topFocusTrap = FOCUS_TRAP_TEMPLATE.clone()[0]; + this._bottomFocusTrap = FOCUS_TRAP_TEMPLATE.clone()[0]; + + // When focus is about to move out of the panel, we want to intercept it + // and redirect it back to the panel element. + var focusHandler = function() { + element.focus(); + }; + this._topFocusTrap.addEventListener('focus', focusHandler); + this._bottomFocusTrap.addEventListener('focus', focusHandler); + + // Queue remove listeners function + this._removeListeners.push(this._simpleBind(function() { + this._topFocusTrap.removeEventListener('focus', focusHandler); + this._bottomFocusTrap.removeEventListener('focus', focusHandler); + }, this)); + + // The top focus trap inserted immediately before the md-panel element (as + // a sibling). The bottom focus trap inserted immediately after the + // md-panel element (as a sibling). + element[0].parentNode.insertBefore(this._topFocusTrap, element[0]); + element.after(this._bottomFocusTrap); + } +}; + + +/** + * Updates the animation of a panel. + * @param {!MdPanelAnimation} animation + */ +MdPanelRef.prototype.updateAnimation = function(animation) { + this.config['animation'] = animation; + + if (this._backdropRef) { + this._backdropRef.config.animation.duration(animation._rawDuration); + } +}; + + +/** + * Animate the panel opening. + * @returns {!angular.$q.Promise} A promise that is resolved when the panel has + * animated open. + * @private + */ +MdPanelRef.prototype._animateOpen = function() { + this.panelContainer.addClass('md-panel-is-showing'); + var animationConfig = this.config['animation']; + if (!animationConfig) { + // Promise is in progress, return it. + this.panelContainer.addClass('_md-panel-shown'); + return this._$q.when(this); + } + + var self = this; + return this._$q(function(resolve) { + var done = self._done(resolve, self); + var warnAndOpen = function() { + self._$log.warn( + 'mdPanel: MdPanel Animations failed. ' + + 'Showing panel without animating.'); + done(); + }; + + animationConfig.animateOpen(self.panelEl) + .then(done, warnAndOpen); + }); +}; + + +/** + * Animate the panel closing. + * @returns {!angular.$q.Promise} A promise that is resolved when the panel has + * animated closed. + * @private + */ +MdPanelRef.prototype._animateClose = function() { + var animationConfig = this.config['animation']; + if (!animationConfig) { + this.panelContainer.removeClass('md-panel-is-showing'); + this.panelContainer.removeClass('_md-panel-shown'); + return this._$q.when(this); + } + + var self = this; + return this._$q(function(resolve) { + var done = function() { + self.panelContainer.removeClass('md-panel-is-showing'); + resolve(self); + }; + var warnAndClose = function() { + self._$log.warn( + 'mdPanel: MdPanel Animations failed. ' + + 'Hiding panel without animating.'); + done(); + }; + + animationConfig.animateClose(self.panelEl) + .then(done, warnAndClose); + }); +}; + + +/** + * Registers a interceptor with the panel. The callback should return a promise, + * which will allow the action to continue when it gets resolved, or will + * prevent an action if it is rejected. + * @param {string} type Type of interceptor. + * @param {!angular.$q.Promise} callback Callback to be registered. + * @returns {!MdPanelRef} + */ +MdPanelRef.prototype.registerInterceptor = function(type, callback) { + var error = null; + + if (!angular.isString(type)) { + error = 'Interceptor type must be a string, instead got ' + typeof type; + } else if (!angular.isFunction(callback)) { + error = 'Interceptor callback must be a function, instead got ' + typeof callback; + } + + if (error) { + throw new Error('MdPanel: ' + error); + } + + var interceptors = this._interceptors[type] = this._interceptors[type] || []; + + if (interceptors.indexOf(callback) === -1) { + interceptors.push(callback); + } + + return this; +}; + + +/** + * Removes a registered interceptor. + * @param {string} type Type of interceptor to be removed. + * @param {Function} callback Interceptor to be removed. + * @returns {!MdPanelRef} + */ +MdPanelRef.prototype.removeInterceptor = function(type, callback) { + var index = this._interceptors[type] ? + this._interceptors[type].indexOf(callback) : -1; + + if (index > -1) { + this._interceptors[type].splice(index, 1); + } + + return this; +}; + + +/** + * Removes all interceptors. + * @param {string=} type Type of interceptors to be removed. + * If ommited, all interceptors types will be removed. + * @returns {!MdPanelRef} + */ +MdPanelRef.prototype.removeAllInterceptors = function(type) { + if (type) { + this._interceptors[type] = []; + } else { + this._interceptors = Object.create(null); + } + + return this; +}; + + +/** + * Invokes all the interceptors of a certain type sequantially in + * reverse order. Works in a similar way to `$q.all`, except it + * respects the order of the functions. + * @param {string} type Type of interceptors to be invoked. + * @returns {!angular.$q.Promise} + * @private + */ +MdPanelRef.prototype._callInterceptors = function(type) { + var self = this; + var $q = self._$q; + var interceptors = self._interceptors && self._interceptors[type] || []; + + return interceptors.reduceRight(function(promise, interceptor) { + var isPromiseLike = interceptor && angular.isFunction(interceptor.then); + var response = isPromiseLike ? interceptor : null; + + /** + * For interceptors to reject/cancel subsequent portions of the chain, simply + * return a `$q.reject()` + */ + return promise.then(function() { + if (!response) { + try { + response = interceptor(self); + } catch(e) { + response = $q.reject(e); + } + } + + return response; + }); + }, $q.resolve(self)); +}; + + +/** + * Faster, more basic than angular.bind + * http://jsperf.com/angular-bind-vs-custom-vs-native + * @param {function} callback + * @param {!Object} self + * @return {function} Callback function with a bound self. + */ +MdPanelRef.prototype._simpleBind = function(callback, self) { + return function(value) { + return callback.apply(self, value); + }; +}; + + +/** + * @param {function} callback + * @param {!Object} self + * @return {function} Callback function with a self param. + */ +MdPanelRef.prototype._done = function(callback, self) { + return function() { + callback(self); + }; +}; + + +/** + * Adds a panel to a group if the panel does not exist within the group already. + * A panel can only exist within a single group. + * @param {string} groupName The name of the group. + */ +MdPanelRef.prototype.addToGroup = function(groupName) { + if (!this._$mdPanel._groups[groupName]) { + this._$mdPanel.newPanelGroup(groupName); + } + + var group = this._$mdPanel._groups[groupName]; + var index = group.panels.indexOf(this); + + if (index < 0) { + group.panels.push(this); + } +}; + + +/** + * Removes a panel from a group if the panel exists within that group. The group + * must be created ahead of time. + * @param {string} groupName The name of the group. + */ +MdPanelRef.prototype.removeFromGroup = function(groupName) { + if (!this._$mdPanel._groups[groupName]) { + throw new Error('mdPanel: The group ' + groupName + ' does not exist.'); + } + + var group = this._$mdPanel._groups[groupName]; + var index = group.panels.indexOf(this); + + if (index > -1) { + group.panels.splice(index, 1); + } +}; + + +/** + * Possible default closeReasons for the close function. + * @enum {string} + */ +MdPanelRef.closeReasons = { + CLICK_OUTSIDE: 'clickOutsideToClose', + ESCAPE: 'escapeToClose', +}; + + +/***************************************************************************** + * MdPanelPosition * + *****************************************************************************/ + + +/** + * Position configuration object. To use, create an MdPanelPosition with the + * desired properties, then pass the object as part of $mdPanel creation. + * + * Example: + * + * var panelPosition = new MdPanelPosition() + * .relativeTo(myButtonEl) + * .addPanelPosition( + * $mdPanel.xPosition.CENTER, + * $mdPanel.yPosition.ALIGN_TOPS + * ); + * + * $mdPanel.create({ + * position: panelPosition + * }); + * + * @param {!angular.$injector} $injector + * @final @constructor + */ +function MdPanelPosition($injector) { + /** @private @const {!angular.$window} */ + this._$window = $injector.get('$window'); + + /** @private {boolean} */ + this._isRTL = $injector.get('$mdUtil').bidi() === 'rtl'; + + /** @private @const {!angular.$mdConstant} */ + this._$mdConstant = $injector.get('$mdConstant'); + + /** @private {boolean} */ + this._absolute = false; + + /** @private {!angular.JQLite} */ + this._relativeToEl; + + /** @private {string} */ + this._top = ''; + + /** @private {string} */ + this._bottom = ''; + + /** @private {string} */ + this._left = ''; + + /** @private {string} */ + this._right = ''; + + /** @private {!Array} */ + this._translateX = []; + + /** @private {!Array} */ + this._translateY = []; + + /** @private {!Array<{x:string, y:string}>} */ + this._positions = []; + + /** @private {?{x:string, y:string}} */ + this._actualPosition; +} + + +/** + * Possible values of xPosition. + * @enum {string} + */ +MdPanelPosition.xPosition = { + CENTER: 'center', + ALIGN_START: 'align-start', + ALIGN_END: 'align-end', + OFFSET_START: 'offset-start', + OFFSET_END: 'offset-end' +}; + + +/** + * Possible values of yPosition. + * @enum {string} + */ +MdPanelPosition.yPosition = { + CENTER: 'center', + ALIGN_TOPS: 'align-tops', + ALIGN_BOTTOMS: 'align-bottoms', + ABOVE: 'above', + BELOW: 'below' +}; + + +/** + * Possible values of absolute position. + * @enum {string} + */ +MdPanelPosition.absPosition = { + TOP: 'top', + RIGHT: 'right', + BOTTOM: 'bottom', + LEFT: 'left' +}; + +/** + * Margin between the edges of a panel and the viewport. + * @const {number} + */ +MdPanelPosition.viewportMargin = 8; + + +/** + * Sets absolute positioning for the panel. + * @return {!MdPanelPosition} + */ +MdPanelPosition.prototype.absolute = function() { + this._absolute = true; + return this; +}; + + +/** + * Sets the value of a position for the panel. Clears any previously set + * position. + * @param {string} position Position to set + * @param {string=} value Value of the position. Defaults to '0'. + * @returns {!MdPanelPosition} + * @private + */ +MdPanelPosition.prototype._setPosition = function(position, value) { + if (position === MdPanelPosition.absPosition.RIGHT || + position === MdPanelPosition.absPosition.LEFT) { + this._left = this._right = ''; + } else if ( + position === MdPanelPosition.absPosition.BOTTOM || + position === MdPanelPosition.absPosition.TOP) { + this._top = this._bottom = ''; + } else { + var positions = Object.keys(MdPanelPosition.absPosition).join() + .toLowerCase(); + + throw new Error('mdPanel: Position must be one of ' + positions + '.'); + } + + this['_' + position] = angular.isString(value) ? value : '0'; + + return this; +}; + + +/** + * Sets the value of `top` for the panel. Clears any previously set vertical + * position. + * @param {string=} top Value of `top`. Defaults to '0'. + * @returns {!MdPanelPosition} + */ +MdPanelPosition.prototype.top = function(top) { + return this._setPosition(MdPanelPosition.absPosition.TOP, top); +}; + + +/** + * Sets the value of `bottom` for the panel. Clears any previously set vertical + * position. + * @param {string=} bottom Value of `bottom`. Defaults to '0'. + * @returns {!MdPanelPosition} + */ +MdPanelPosition.prototype.bottom = function(bottom) { + return this._setPosition(MdPanelPosition.absPosition.BOTTOM, bottom); +}; + + +/** + * Sets the panel to the start of the page - `left` if `ltr` or `right` for + * `rtl`. Clears any previously set horizontal position. + * @param {string=} start Value of position. Defaults to '0'. + * @returns {!MdPanelPosition} + */ +MdPanelPosition.prototype.start = function(start) { + var position = this._isRTL ? MdPanelPosition.absPosition.RIGHT : MdPanelPosition.absPosition.LEFT; + return this._setPosition(position, start); +}; + + +/** + * Sets the panel to the end of the page - `right` if `ltr` or `left` for `rtl`. + * Clears any previously set horizontal position. + * @param {string=} end Value of position. Defaults to '0'. + * @returns {!MdPanelPosition} + */ +MdPanelPosition.prototype.end = function(end) { + var position = this._isRTL ? MdPanelPosition.absPosition.LEFT : MdPanelPosition.absPosition.RIGHT; + return this._setPosition(position, end); +}; + + +/** + * Sets the value of `left` for the panel. Clears any previously set + * horizontal position. + * @param {string=} left Value of `left`. Defaults to '0'. + * @returns {!MdPanelPosition} + */ +MdPanelPosition.prototype.left = function(left) { + return this._setPosition(MdPanelPosition.absPosition.LEFT, left); +}; + + +/** + * Sets the value of `right` for the panel. Clears any previously set + * horizontal position. + * @param {string=} right Value of `right`. Defaults to '0'. + * @returns {!MdPanelPosition} +*/ +MdPanelPosition.prototype.right = function(right) { + return this._setPosition(MdPanelPosition.absPosition.RIGHT, right); +}; + + +/** + * Centers the panel horizontally in the viewport. Clears any previously set + * horizontal position. + * @returns {!MdPanelPosition} + */ +MdPanelPosition.prototype.centerHorizontally = function() { + this._left = '50%'; + this._right = ''; + this._translateX = ['-50%']; + return this; +}; + + +/** + * Centers the panel vertically in the viewport. Clears any previously set + * vertical position. + * @returns {!MdPanelPosition} + */ +MdPanelPosition.prototype.centerVertically = function() { + this._top = '50%'; + this._bottom = ''; + this._translateY = ['-50%']; + return this; +}; + + +/** + * Centers the panel horizontally and vertically in the viewport. This is + * equivalent to calling both `centerHorizontally` and `centerVertically`. + * Clears any previously set horizontal and vertical positions. + * @returns {!MdPanelPosition} + */ +MdPanelPosition.prototype.center = function() { + return this.centerHorizontally().centerVertically(); +}; + + +/** + * Sets element for relative positioning. + * @param {string|!Element|!angular.JQLite} element Query selector, DOM element, + * or angular element to set the panel relative to. + * @returns {!MdPanelPosition} + */ +MdPanelPosition.prototype.relativeTo = function(element) { + this._absolute = false; + this._relativeToEl = getElement(element); + return this; +}; + + +/** + * Sets the x and y positions for the panel relative to another element. + * @param {string} xPosition must be one of the MdPanelPosition.xPosition + * values. + * @param {string} yPosition must be one of the MdPanelPosition.yPosition + * values. + * @returns {!MdPanelPosition} + */ +MdPanelPosition.prototype.addPanelPosition = function(xPosition, yPosition) { + if (!this._relativeToEl) { + throw new Error('mdPanel: addPanelPosition can only be used with ' + + 'relative positioning. Set relativeTo first.'); + } + + this._validateXPosition(xPosition); + this._validateYPosition(yPosition); + + this._positions.push({ + x: xPosition, + y: yPosition, + }); + return this; +}; + + +/** + * Ensures that yPosition is a valid position name. Throw an exception if not. + * @param {string} yPosition + */ +MdPanelPosition.prototype._validateYPosition = function(yPosition) { + // empty is ok + if (yPosition == null) { + return; + } + + var positionKeys = Object.keys(MdPanelPosition.yPosition); + var positionValues = []; + for (var key, i = 0; key = positionKeys[i]; i++) { + var position = MdPanelPosition.yPosition[key]; + positionValues.push(position); + + if (position === yPosition) { + return; + } + } + + throw new Error('mdPanel: Panel y position only accepts the following ' + + 'values:\n' + positionValues.join(' | ')); +}; + + +/** + * Ensures that xPosition is a valid position name. Throw an exception if not. + * @param {string} xPosition + */ +MdPanelPosition.prototype._validateXPosition = function(xPosition) { + // empty is ok + if (xPosition == null) { + return; + } + + var positionKeys = Object.keys(MdPanelPosition.xPosition); + var positionValues = []; + for (var key, i = 0; key = positionKeys[i]; i++) { + var position = MdPanelPosition.xPosition[key]; + positionValues.push(position); + if (position === xPosition) { + return; + } + } + + throw new Error('mdPanel: Panel x Position only accepts the following ' + + 'values:\n' + positionValues.join(' | ')); +}; + + +/** + * Sets the value of the offset in the x-direction. This will add to any + * previously set offsets. + * @param {string|function(MdPanelPosition): string} offsetX + * @returns {!MdPanelPosition} + */ +MdPanelPosition.prototype.withOffsetX = function(offsetX) { + this._translateX.push(offsetX); + return this; +}; + + +/** + * Sets the value of the offset in the y-direction. This will add to any + * previously set offsets. + * @param {string|function(MdPanelPosition): string} offsetY + * @returns {!MdPanelPosition} + */ +MdPanelPosition.prototype.withOffsetY = function(offsetY) { + this._translateY.push(offsetY); + return this; +}; + + +/** + * Gets the value of `top` for the panel. + * @returns {string} + */ +MdPanelPosition.prototype.getTop = function() { + return this._top; +}; + + +/** + * Gets the value of `bottom` for the panel. + * @returns {string} + */ +MdPanelPosition.prototype.getBottom = function() { + return this._bottom; +}; + + +/** + * Gets the value of `left` for the panel. + * @returns {string} + */ +MdPanelPosition.prototype.getLeft = function() { + return this._left; +}; + + +/** + * Gets the value of `right` for the panel. + * @returns {string} + */ +MdPanelPosition.prototype.getRight = function() { + return this._right; +}; + + +/** + * Gets the value of `transform` for the panel. + * @returns {string} + */ +MdPanelPosition.prototype.getTransform = function() { + var translateX = this._reduceTranslateValues('translateX', this._translateX); + var translateY = this._reduceTranslateValues('translateY', this._translateY); + + // It's important to trim the result, because the browser will ignore the set + // operation if the string contains only whitespace. + return (translateX + ' ' + translateY).trim(); +}; + + +/** + * Sets the `transform` value for a panel element. + * @param {!angular.JQLite} panelEl + * @returns {!angular.JQLite} + * @private + */ +MdPanelPosition.prototype._setTransform = function(panelEl) { + return panelEl.css(this._$mdConstant.CSS.TRANSFORM, this.getTransform()); +}; + + +/** + * True if the panel is completely on-screen with this positioning; false + * otherwise. + * @param {!angular.JQLite} panelEl + * @return {boolean} + * @private + */ +MdPanelPosition.prototype._isOnscreen = function(panelEl) { + // this works because we always use fixed positioning for the panel, + // which is relative to the viewport. + var left = parseInt(this.getLeft()); + var top = parseInt(this.getTop()); + + if (this._translateX.length || this._translateY.length) { + var prefixedTransform = this._$mdConstant.CSS.TRANSFORM; + var offsets = getComputedTranslations(panelEl, prefixedTransform); + left += offsets.x; + top += offsets.y; + } + + var right = left + panelEl[0].offsetWidth; + var bottom = top + panelEl[0].offsetHeight; + + return (left >= 0) && + (top >= 0) && + (bottom <= this._$window.innerHeight) && + (right <= this._$window.innerWidth); +}; + + +/** + * Gets the first x/y position that can fit on-screen. + * @returns {{x: string, y: string}} + */ +MdPanelPosition.prototype.getActualPosition = function() { + return this._actualPosition; +}; + + +/** + * Reduces a list of translate values to a string that can be used within + * transform. + * @param {string} translateFn + * @param {!Array} values + * @returns {string} + * @private + */ +MdPanelPosition.prototype._reduceTranslateValues = + function(translateFn, values) { + return values.map(function(translation) { + // TODO(crisbeto): this should add the units after #9609 is merged. + var translationValue = angular.isFunction(translation) ? + translation(this) : translation; + return translateFn + '(' + translationValue + ')'; + }, this).join(' '); + }; + + +/** + * Sets the panel position based on the created panel element and best x/y + * positioning. + * @param {!angular.JQLite} panelEl + * @private + */ +MdPanelPosition.prototype._setPanelPosition = function(panelEl) { + // Remove the "position adjusted" class in case it has been added before. + panelEl.removeClass('_md-panel-position-adjusted'); + + // Only calculate the position if necessary. + if (this._absolute) { + this._setTransform(panelEl); + return; + } + + if (this._actualPosition) { + this._calculatePanelPosition(panelEl, this._actualPosition); + this._setTransform(panelEl); + this._constrainToViewport(panelEl); + return; + } + + for (var i = 0; i < this._positions.length; i++) { + this._actualPosition = this._positions[i]; + this._calculatePanelPosition(panelEl, this._actualPosition); + this._setTransform(panelEl); + + if (this._isOnscreen(panelEl)) { + return; + } + } + + this._constrainToViewport(panelEl); +}; + + +/** + * Constrains a panel's position to the viewport. + * @param {!angular.JQLite} panelEl + * @private + */ +MdPanelPosition.prototype._constrainToViewport = function(panelEl) { + var margin = MdPanelPosition.viewportMargin; + var initialTop = this._top; + var initialLeft = this._left; + + if (this.getTop()) { + var top = parseInt(this.getTop()); + var bottom = panelEl[0].offsetHeight + top; + var viewportHeight = this._$window.innerHeight; + + if (top < margin) { + this._top = margin + 'px'; + } else if (bottom > viewportHeight) { + this._top = top - (bottom - viewportHeight + margin) + 'px'; + } + } + + if (this.getLeft()) { + var left = parseInt(this.getLeft()); + var right = panelEl[0].offsetWidth + left; + var viewportWidth = this._$window.innerWidth; + + if (left < margin) { + this._left = margin + 'px'; + } else if (right > viewportWidth) { + this._left = left - (right - viewportWidth + margin) + 'px'; + } + } + + // Class that can be used to re-style the panel if it was repositioned. + panelEl.toggleClass( + '_md-panel-position-adjusted', + this._top !== initialTop || this._left !== initialLeft + ); +}; + + +/** + * Switches between 'start' and 'end'. + * @param {string} position Horizontal position of the panel + * @returns {string} Reversed position + * @private + */ +MdPanelPosition.prototype._reverseXPosition = function(position) { + if (position === MdPanelPosition.xPosition.CENTER) { + return; + } + + var start = 'start'; + var end = 'end'; + + return position.indexOf(start) > -1 ? position.replace(start, end) : position.replace(end, start); +}; + + +/** + * Handles horizontal positioning in rtl or ltr environments. + * @param {string} position Horizontal position of the panel + * @returns {string} The correct position according the page direction + * @private + */ +MdPanelPosition.prototype._bidi = function(position) { + return this._isRTL ? this._reverseXPosition(position) : position; +}; + + +/** + * Calculates the panel position based on the created panel element and the + * provided positioning. + * @param {!angular.JQLite} panelEl + * @param {!{x:string, y:string}} position + * @private + */ +MdPanelPosition.prototype._calculatePanelPosition = function(panelEl, position) { + + var panelBounds = panelEl[0].getBoundingClientRect(); + var panelWidth = panelBounds.width; + var panelHeight = panelBounds.height; + + var targetBounds = this._relativeToEl[0].getBoundingClientRect(); + + var targetLeft = targetBounds.left; + var targetRight = targetBounds.right; + var targetWidth = targetBounds.width; + + switch (this._bidi(position.x)) { + case MdPanelPosition.xPosition.OFFSET_START: + this._left = targetLeft - panelWidth + 'px'; + break; + case MdPanelPosition.xPosition.ALIGN_END: + this._left = targetRight - panelWidth + 'px'; + break; + case MdPanelPosition.xPosition.CENTER: + var left = targetLeft + (0.5 * targetWidth) - (0.5 * panelWidth); + this._left = left + 'px'; + break; + case MdPanelPosition.xPosition.ALIGN_START: + this._left = targetLeft + 'px'; + break; + case MdPanelPosition.xPosition.OFFSET_END: + this._left = targetRight + 'px'; + break; + } + + var targetTop = targetBounds.top; + var targetBottom = targetBounds.bottom; + var targetHeight = targetBounds.height; + + switch (position.y) { + case MdPanelPosition.yPosition.ABOVE: + this._top = targetTop - panelHeight + 'px'; + break; + case MdPanelPosition.yPosition.ALIGN_BOTTOMS: + this._top = targetBottom - panelHeight + 'px'; + break; + case MdPanelPosition.yPosition.CENTER: + var top = targetTop + (0.5 * targetHeight) - (0.5 * panelHeight); + this._top = top + 'px'; + break; + case MdPanelPosition.yPosition.ALIGN_TOPS: + this._top = targetTop + 'px'; + break; + case MdPanelPosition.yPosition.BELOW: + this._top = targetBottom + 'px'; + break; + } +}; + + +/***************************************************************************** + * MdPanelAnimation * + *****************************************************************************/ + + +/** + * Animation configuration object. To use, create an MdPanelAnimation with the + * desired properties, then pass the object as part of $mdPanel creation. + * + * Example: + * + * var panelAnimation = new MdPanelAnimation() + * .openFrom(myButtonEl) + * .closeTo('.my-button') + * .withAnimation($mdPanel.animation.SCALE); + * + * $mdPanel.create({ + * animation: panelAnimation + * }); + * + * @param {!angular.$injector} $injector + * @final @constructor + */ +function MdPanelAnimation($injector) { + /** @private @const {!angular.$mdUtil} */ + this._$mdUtil = $injector.get('$mdUtil'); + + /** + * @private {{element: !angular.JQLite|undefined, bounds: !DOMRect}| + * undefined} + */ + this._openFrom; + + /** + * @private {{element: !angular.JQLite|undefined, bounds: !DOMRect}| + * undefined} + */ + this._closeTo; + + /** @private {string|{open: string, close: string}} */ + this._animationClass = ''; + + /** @private {number} */ + this._openDuration; + + /** @private {number} */ + this._closeDuration; + + /** @private {number|{open: number, close: number}} */ + this._rawDuration; +} + + +/** + * Possible default animations. + * @enum {string} + */ +MdPanelAnimation.animation = { + SLIDE: 'md-panel-animate-slide', + SCALE: 'md-panel-animate-scale', + FADE: 'md-panel-animate-fade' +}; + + +/** + * Specifies where to start the open animation. `openFrom` accepts a + * click event object, query selector, DOM element, or a Rect object that + * is used to determine the bounds. When passed a click event, the location + * of the click will be used as the position to start the animation. + * @param {string|!Element|!Event|{top: number, left: number}} openFrom + * @returns {!MdPanelAnimation} + */ +MdPanelAnimation.prototype.openFrom = function(openFrom) { + // Check if 'openFrom' is an Event. + openFrom = openFrom.target ? openFrom.target : openFrom; + + this._openFrom = this._getPanelAnimationTarget(openFrom); + + if (!this._closeTo) { + this._closeTo = this._openFrom; + } + return this; +}; + + +/** + * Specifies where to animate the panel close. `closeTo` accepts a + * query selector, DOM element, or a Rect object that is used to determine + * the bounds. + * @param {string|!Element|{top: number, left: number}} closeTo + * @returns {!MdPanelAnimation} + */ +MdPanelAnimation.prototype.closeTo = function(closeTo) { + this._closeTo = this._getPanelAnimationTarget(closeTo); + return this; +}; + + +/** + * Specifies the duration of the animation in milliseconds. + * @param {number|{open: number, close: number}} duration + * @returns {!MdPanelAnimation} + */ +MdPanelAnimation.prototype.duration = function(duration) { + if (duration) { + if (angular.isNumber(duration)) { + this._openDuration = this._closeDuration = toSeconds(duration); + } else if (angular.isObject(duration)) { + this._openDuration = toSeconds(duration.open); + this._closeDuration = toSeconds(duration.close); + } + } + + // Save the original value so it can be passed to the backdrop. + this._rawDuration = duration; + + return this; + + function toSeconds(value) { + if (angular.isNumber(value)) return value / 1000; + } +}; + + +/** + * Returns the element and bounds for the animation target. + * @param {string|!Element|{top: number, left: number}} location + * @returns {{element: !angular.JQLite|undefined, bounds: !DOMRect}} + * @private + */ +MdPanelAnimation.prototype._getPanelAnimationTarget = function(location) { + if (angular.isDefined(location.top) || angular.isDefined(location.left)) { + return { + element: undefined, + bounds: { + top: location.top || 0, + left: location.left || 0 + } + }; + } else { + return this._getBoundingClientRect(getElement(location)); + } +}; + + +/** + * Specifies the animation class. + * + * There are several default animations that can be used: + * (MdPanelAnimation.animation) + * SLIDE: The panel slides in and out from the specified + * elements. + * SCALE: The panel scales in and out. + * FADE: The panel fades in and out. + * + * @param {string|{open: string, close: string}} cssClass + * @returns {!MdPanelAnimation} + */ +MdPanelAnimation.prototype.withAnimation = function(cssClass) { + this._animationClass = cssClass; + return this; +}; + + +/** + * Animate the panel open. + * @param {!angular.JQLite} panelEl + * @returns {!angular.$q.Promise} A promise that is resolved when the open + * animation is complete. + */ +MdPanelAnimation.prototype.animateOpen = function(panelEl) { + var animator = this._$mdUtil.dom.animator; + + this._fixBounds(panelEl); + var animationOptions = {}; + + // Include the panel transformations when calculating the animations. + var panelTransform = panelEl[0].style.transform || ''; + + var openFrom = animator.toTransformCss(panelTransform); + var openTo = animator.toTransformCss(panelTransform); + + switch (this._animationClass) { + case MdPanelAnimation.animation.SLIDE: + // Slide should start with opacity: 1. + panelEl.css('opacity', '1'); + + animationOptions = { + transitionInClass: '_md-panel-animate-enter' + }; + + var openSlide = animator.calculateSlideToOrigin( + panelEl, this._openFrom) || ''; + openFrom = animator.toTransformCss(openSlide + ' ' + panelTransform); + break; + + case MdPanelAnimation.animation.SCALE: + animationOptions = { + transitionInClass: '_md-panel-animate-enter' + }; + + var openScale = animator.calculateZoomToOrigin( + panelEl, this._openFrom) || ''; + openFrom = animator.toTransformCss(openScale + ' ' + panelTransform); + break; + + case MdPanelAnimation.animation.FADE: + animationOptions = { + transitionInClass: '_md-panel-animate-enter' + }; + break; + + default: + if (angular.isString(this._animationClass)) { + animationOptions = { + transitionInClass: this._animationClass + }; + } else { + animationOptions = { + transitionInClass: this._animationClass['open'], + transitionOutClass: this._animationClass['close'], + }; + } + } + + animationOptions.duration = this._openDuration; + + return animator + .translate3d(panelEl, openFrom, openTo, animationOptions); +}; + + +/** + * Animate the panel close. + * @param {!angular.JQLite} panelEl + * @returns {!angular.$q.Promise} A promise that resolves when the close + * animation is complete. + */ +MdPanelAnimation.prototype.animateClose = function(panelEl) { + var animator = this._$mdUtil.dom.animator; + var reverseAnimationOptions = {}; + + // Include the panel transformations when calculating the animations. + var panelTransform = panelEl[0].style.transform || ''; + + var closeFrom = animator.toTransformCss(panelTransform); + var closeTo = animator.toTransformCss(panelTransform); + + switch (this._animationClass) { + case MdPanelAnimation.animation.SLIDE: + // Slide should start with opacity: 1. + panelEl.css('opacity', '1'); + reverseAnimationOptions = { + transitionInClass: '_md-panel-animate-leave' + }; + + var closeSlide = animator.calculateSlideToOrigin( + panelEl, this._closeTo) || ''; + closeTo = animator.toTransformCss(closeSlide + ' ' + panelTransform); + break; + + case MdPanelAnimation.animation.SCALE: + reverseAnimationOptions = { + transitionInClass: '_md-panel-animate-scale-out _md-panel-animate-leave' + }; + + var closeScale = animator.calculateZoomToOrigin( + panelEl, this._closeTo) || ''; + closeTo = animator.toTransformCss(closeScale + ' ' + panelTransform); + break; + + case MdPanelAnimation.animation.FADE: + reverseAnimationOptions = { + transitionInClass: '_md-panel-animate-fade-out _md-panel-animate-leave' + }; + break; + + default: + if (angular.isString(this._animationClass)) { + reverseAnimationOptions = { + transitionOutClass: this._animationClass + }; + } else { + reverseAnimationOptions = { + transitionInClass: this._animationClass['close'], + transitionOutClass: this._animationClass['open'] + }; + } + } + + reverseAnimationOptions.duration = this._closeDuration; + + return animator + .translate3d(panelEl, closeFrom, closeTo, reverseAnimationOptions); +}; + + +/** + * Set the height and width to match the panel if not provided. + * @param {!angular.JQLite} panelEl + * @private + */ +MdPanelAnimation.prototype._fixBounds = function(panelEl) { + var panelWidth = panelEl[0].offsetWidth; + var panelHeight = panelEl[0].offsetHeight; + + if (this._openFrom && this._openFrom.bounds.height == null) { + this._openFrom.bounds.height = panelHeight; + } + if (this._openFrom && this._openFrom.bounds.width == null) { + this._openFrom.bounds.width = panelWidth; + } + if (this._closeTo && this._closeTo.bounds.height == null) { + this._closeTo.bounds.height = panelHeight; + } + if (this._closeTo && this._closeTo.bounds.width == null) { + this._closeTo.bounds.width = panelWidth; + } +}; + + +/** + * Identify the bounding RECT for the target element. + * @param {!angular.JQLite} element + * @returns {{element: !angular.JQLite|undefined, bounds: !DOMRect}} + * @private + */ +MdPanelAnimation.prototype._getBoundingClientRect = function(element) { + if (element instanceof angular.element) { + return { + element: element, + bounds: element[0].getBoundingClientRect() + }; + } +}; + + +/***************************************************************************** + * Util Methods * + *****************************************************************************/ + + +/** + * Returns the angular element associated with a css selector or element. + * @param el {string|!angular.JQLite|!Element} + * @returns {!angular.JQLite} + */ +function getElement(el) { + var queryResult = angular.isString(el) ? + document.querySelector(el) : el; + return angular.element(queryResult); +} + + +/** + * Gets the computed values for an element's translateX and translateY in px. + * @param {!angular.JQLite|!Element} el + * @param {string} property + * @return {{x: number, y: number}} + */ +function getComputedTranslations(el, property) { + // The transform being returned by `getComputedStyle` is in the format: + // `matrix(a, b, c, d, translateX, translateY)` if defined and `none` + // if the element doesn't have a transform. + var transform = getComputedStyle(el[0] || el)[property]; + var openIndex = transform.indexOf('('); + var closeIndex = transform.lastIndexOf(')'); + var output = { x: 0, y: 0 }; + + if (openIndex > -1 && closeIndex > -1) { + var parsedValues = transform + .substring(openIndex + 1, closeIndex) + .split(', ') + .slice(-2); + + output.x = parseInt(parsedValues[0]); + output.y = parseInt(parsedValues[1]); + } + + return output; +} + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.navBar + */ + + +MdNavBarController.$inject = ["$element", "$scope", "$timeout", "$mdConstant"]; +MdNavItem.$inject = ["$mdAria", "$$rAF"]; +MdNavItemController.$inject = ["$element"]; +MdNavBar.$inject = ["$mdAria", "$mdTheming"]; +angular.module('material.components.navBar', ['material.core']) + .controller('MdNavBarController', MdNavBarController) + .directive('mdNavBar', MdNavBar) + .controller('MdNavItemController', MdNavItemController) + .directive('mdNavItem', MdNavItem); + + +/***************************************************************************** + * PUBLIC DOCUMENTATION * + *****************************************************************************/ +/** + * @ngdoc directive + * @name mdNavBar + * @module material.components.navBar + * + * @restrict E + * + * @description + * The `` directive renders a list of material tabs that can be used + * for top-level page navigation. Unlike ``, it has no concept of a tab + * body and no bar pagination. + * + * Because it deals with page navigation, certain routing concepts are built-in. + * Route changes via via ng-href, ui-sref, or ng-click events are supported. + * Alternatively, the user could simply watch currentNavItem for changes. + * + * Accessibility functionality is implemented as a site navigator with a + * listbox, according to + * https://www.w3.org/TR/wai-aria-practices/#Site_Navigator_Tabbed_Style + * + * @param {string=} mdSelectedNavItem The name of the current tab; this must + * match the name attribute of `` + * @param {boolean=} mdNoInkBar If set to true, the ink bar will be hidden. + * @param {string=} navBarAriaLabel An aria-label for the nav-bar + * + * @usage + * + * + * + * Page One + * + * Page Two + * Page Three + * + * Page Four + * + * + * + * + * (function() { + * 'use strict'; + * + * $rootScope.$on('$routeChangeSuccess', function(event, current) { + * $scope.currentLink = getCurrentLinkFromRoute(current); + * }); + * }); + * + */ + +/***************************************************************************** + * mdNavItem + *****************************************************************************/ +/** + * @ngdoc directive + * @name mdNavItem + * @module material.components.navBar + * + * @restrict E + * + * @description + * `` describes a page navigation link within the `` + * component. It renders an md-button as the actual link. + * + * Exactly one of the mdNavClick, mdNavHref, mdNavSref attributes are required + * to be specified. + * + * @param {Function=} mdNavClick Function which will be called when the + * link is clicked to change the page. Renders as an `ng-click`. + * @param {string=} mdNavHref url to transition to when this link is clicked. + * Renders as an `ng-href`. + * @param {string=} mdNavSref Ui-router state to transition to when this link is + * clicked. Renders as a `ui-sref`. + * @param {!Object=} srefOpts Ui-router options that are passed to the + * `$state.go()` function. See the [Ui-router documentation for details] + * (https://ui-router.github.io/docs/latest/interfaces/transition.transitionoptions.html). + * @param {string=} name The name of this link. Used by the nav bar to know + * which link is currently selected. + * @param {string=} aria-label Adds alternative text for accessibility + * + * @usage + * See `` for usage. + */ + + +/***************************************************************************** + * IMPLEMENTATION * + *****************************************************************************/ + +function MdNavBar($mdAria, $mdTheming) { + return { + restrict: 'E', + transclude: true, + controller: MdNavBarController, + controllerAs: 'ctrl', + bindToController: true, + scope: { + 'mdSelectedNavItem': '=?', + 'mdNoInkBar': '=?', + 'navBarAriaLabel': '@?', + }, + template: + '
' + + '' + + '' + + '
', + link: function(scope, element, attrs, ctrl) { + $mdTheming(element); + if (!ctrl.navBarAriaLabel) { + $mdAria.expectAsync(element, 'aria-label', angular.noop); + } + }, + }; +} + +/** + * Controller for the nav-bar component. + * + * Accessibility functionality is implemented as a site navigator with a + * listbox, according to + * https://www.w3.org/TR/wai-aria-practices/#Site_Navigator_Tabbed_Style + * @param {!angular.JQLite} $element + * @param {!angular.Scope} $scope + * @param {!angular.Timeout} $timeout + * @param {!Object} $mdConstant + * @constructor + * @final + * @ngInject + */ +function MdNavBarController($element, $scope, $timeout, $mdConstant) { + // Injected variables + /** @private @const {!angular.Timeout} */ + this._$timeout = $timeout; + + /** @private @const {!angular.Scope} */ + this._$scope = $scope; + + /** @private @const {!Object} */ + this._$mdConstant = $mdConstant; + + // Data-bound variables. + /** @type {string} */ + this.mdSelectedNavItem; + + /** @type {string} */ + this.navBarAriaLabel; + + // State variables. + + /** @type {?angular.JQLite} */ + this._navBarEl = $element[0]; + + /** @type {?angular.JQLite} */ + this._inkbar; + + var self = this; + // need to wait for transcluded content to be available + var deregisterTabWatch = this._$scope.$watch(function() { + return self._navBarEl.querySelectorAll('._md-nav-button').length; + }, + function(newLength) { + if (newLength > 0) { + self._initTabs(); + deregisterTabWatch(); + } + }); +} + + + +/** + * Initializes the tab components once they exist. + * @private + */ +MdNavBarController.prototype._initTabs = function() { + this._inkbar = angular.element(this._navBarEl.querySelector('md-nav-ink-bar')); + + var self = this; + this._$timeout(function() { + self._updateTabs(self.mdSelectedNavItem, undefined); + }); + + this._$scope.$watch('ctrl.mdSelectedNavItem', function(newValue, oldValue) { + // Wait a digest before update tabs for products doing + // anything dynamic in the template. + self._$timeout(function() { + self._updateTabs(newValue, oldValue); + }); + }); +}; + +/** + * Set the current tab to be selected. + * @param {string|undefined} newValue New current tab name. + * @param {string|undefined} oldValue Previous tab name. + * @private + */ +MdNavBarController.prototype._updateTabs = function(newValue, oldValue) { + var self = this; + var tabs = this._getTabs(); + + // this._getTabs can return null if nav-bar has not yet been initialized + if(!tabs) + return; + + var oldIndex = -1; + var newIndex = -1; + var newTab = this._getTabByName(newValue); + var oldTab = this._getTabByName(oldValue); + + if (oldTab) { + oldTab.setSelected(false); + oldIndex = tabs.indexOf(oldTab); + } + + if (newTab) { + newTab.setSelected(true); + newIndex = tabs.indexOf(newTab); + } + + this._$timeout(function() { + self._updateInkBarStyles(newTab, newIndex, oldIndex); + }); +}; + +/** + * Repositions the ink bar to the selected tab. + * @private + */ +MdNavBarController.prototype._updateInkBarStyles = function(tab, newIndex, oldIndex) { + this._inkbar.toggleClass('_md-left', newIndex < oldIndex) + .toggleClass('_md-right', newIndex > oldIndex); + + this._inkbar.css({display: newIndex < 0 ? 'none' : ''}); + + if (tab) { + var tabEl = tab.getButtonEl(); + var left = tabEl.offsetLeft; + + this._inkbar.css({left: left + 'px', width: tabEl.offsetWidth + 'px'}); + } +}; + +/** + * Returns an array of the current tabs. + * @return {!Array} + * @private + */ +MdNavBarController.prototype._getTabs = function() { + var controllers = Array.prototype.slice.call( + this._navBarEl.querySelectorAll('.md-nav-item')) + .map(function(el) { + return angular.element(el).controller('mdNavItem') + }); + return controllers.indexOf(undefined) ? controllers : null; +}; + +/** + * Returns the tab with the specified name. + * @param {string} name The name of the tab, found in its name attribute. + * @return {!NavItemController|undefined} + * @private + */ +MdNavBarController.prototype._getTabByName = function(name) { + return this._findTab(function(tab) { + return tab.getName() == name; + }); +}; + +/** + * Returns the selected tab. + * @return {!NavItemController|undefined} + * @private + */ +MdNavBarController.prototype._getSelectedTab = function() { + return this._findTab(function(tab) { + return tab.isSelected(); + }); +}; + +/** + * Returns the focused tab. + * @return {!NavItemController|undefined} + */ +MdNavBarController.prototype.getFocusedTab = function() { + return this._findTab(function(tab) { + return tab.hasFocus(); + }); +}; + +/** + * Find a tab that matches the specified function. + * @private + */ +MdNavBarController.prototype._findTab = function(fn) { + var tabs = this._getTabs(); + for (var i = 0; i < tabs.length; i++) { + if (fn(tabs[i])) { + return tabs[i]; + } + } + + return null; +}; + +/** + * Direct focus to the selected tab when focus enters the nav bar. + */ +MdNavBarController.prototype.onFocus = function() { + var tab = this._getSelectedTab(); + if (tab) { + tab.setFocused(true); + } +}; + +/** + * Move focus from oldTab to newTab. + * @param {!NavItemController} oldTab + * @param {!NavItemController} newTab + * @private + */ +MdNavBarController.prototype._moveFocus = function(oldTab, newTab) { + oldTab.setFocused(false); + newTab.setFocused(true); +}; + +/** + * Responds to keypress events. + * @param {!Event} e + */ +MdNavBarController.prototype.onKeydown = function(e) { + var keyCodes = this._$mdConstant.KEY_CODE; + var tabs = this._getTabs(); + var focusedTab = this.getFocusedTab(); + if (!focusedTab) return; + + var focusedTabIndex = tabs.indexOf(focusedTab); + + // use arrow keys to navigate between tabs + switch (e.keyCode) { + case keyCodes.UP_ARROW: + case keyCodes.LEFT_ARROW: + if (focusedTabIndex > 0) { + this._moveFocus(focusedTab, tabs[focusedTabIndex - 1]); + } + break; + case keyCodes.DOWN_ARROW: + case keyCodes.RIGHT_ARROW: + if (focusedTabIndex < tabs.length - 1) { + this._moveFocus(focusedTab, tabs[focusedTabIndex + 1]); + } + break; + case keyCodes.SPACE: + case keyCodes.ENTER: + // timeout to avoid a "digest already in progress" console error + this._$timeout(function() { + focusedTab.getButtonEl().click(); + }); + break; + } +}; + +/** + * @ngInject + */ +function MdNavItem($mdAria, $$rAF) { + return { + restrict: 'E', + require: ['mdNavItem', '^mdNavBar'], + controller: MdNavItemController, + bindToController: true, + controllerAs: 'ctrl', + replace: true, + transclude: true, + template: function(tElement, tAttrs) { + var hasNavClick = tAttrs.mdNavClick; + var hasNavHref = tAttrs.mdNavHref; + var hasNavSref = tAttrs.mdNavSref; + var hasSrefOpts = tAttrs.srefOpts; + var navigationAttribute; + var navigationOptions; + var buttonTemplate; + + // Cannot specify more than one nav attribute + if ((hasNavClick ? 1:0) + (hasNavHref ? 1:0) + (hasNavSref ? 1:0) > 1) { + throw Error( + 'Must not specify more than one of the md-nav-click, md-nav-href, ' + + 'or md-nav-sref attributes per nav-item directive.' + ); + } + + if (hasNavClick) { + navigationAttribute = 'ng-click="ctrl.mdNavClick()"'; + } else if (hasNavHref) { + navigationAttribute = 'ng-href="{{ctrl.mdNavHref}}"'; + } else if (hasNavSref) { + navigationAttribute = 'ui-sref="{{ctrl.mdNavSref}}"'; + } + + navigationOptions = hasSrefOpts ? 'ui-sref-opts="{{ctrl.srefOpts}}" ' : ''; + + if (navigationAttribute) { + buttonTemplate = '' + + '' + + '' + + ''; + } + + return '' + + '
  • ' + + (buttonTemplate || '') + + '
  • '; + }, + scope: { + 'mdNavClick': '&?', + 'mdNavHref': '@?', + 'mdNavSref': '@?', + 'srefOpts': '=?', + 'name': '@', + }, + link: function(scope, element, attrs, controllers) { + // When accessing the element's contents synchronously, they + // may not be defined yet because of transclusion. There is a higher + // chance that it will be accessible if we wait one frame. + $$rAF(function() { + var mdNavItem = controllers[0]; + var mdNavBar = controllers[1]; + var navButton = angular.element(element[0].querySelector('._md-nav-button')); + + if (!mdNavItem.name) { + mdNavItem.name = angular.element(element[0] + .querySelector('._md-nav-button-text')).text().trim(); + } + + navButton.on('click', function() { + mdNavBar.mdSelectedNavItem = mdNavItem.name; + scope.$apply(); + }); + + $mdAria.expectWithText(element, 'aria-label'); + }); + } + }; +} + +/** + * Controller for the nav-item component. + * @param {!angular.JQLite} $element + * @constructor + * @final + * @ngInject + */ +function MdNavItemController($element) { + + /** @private @const {!angular.JQLite} */ + this._$element = $element; + + // Data-bound variables + + /** @const {?Function} */ + this.mdNavClick; + + /** @const {?string} */ + this.mdNavHref; + + /** @const {?string} */ + this.mdNavSref; + /** @const {?Object} */ + this.srefOpts; + /** @const {?string} */ + this.name; + + // State variables + /** @private {boolean} */ + this._selected = false; + + /** @private {boolean} */ + this._focused = false; +} + +/** + * Returns a map of class names and values for use by ng-class. + * @return {!Object} + */ +MdNavItemController.prototype.getNgClassMap = function() { + return { + 'md-active': this._selected, + 'md-primary': this._selected, + 'md-unselected': !this._selected, + 'md-focused': this._focused, + }; +}; + +/** + * Get the name attribute of the tab. + * @return {string} + */ +MdNavItemController.prototype.getName = function() { + return this.name; +}; + +/** + * Get the button element associated with the tab. + * @return {!Element} + */ +MdNavItemController.prototype.getButtonEl = function() { + return this._$element[0].querySelector('._md-nav-button'); +}; + +/** + * Set the selected state of the tab. + * @param {boolean} isSelected + */ +MdNavItemController.prototype.setSelected = function(isSelected) { + this._selected = isSelected; +}; + +/** + * @return {boolean} + */ +MdNavItemController.prototype.isSelected = function() { + return this._selected; +}; + +/** + * Set the focused state of the tab. + * @param {boolean} isFocused + */ +MdNavItemController.prototype.setFocused = function(isFocused) { + this._focused = isFocused; + + if (isFocused) { + this.getButtonEl().focus(); + } +}; + +/** + * @return {boolean} + */ +MdNavItemController.prototype.hasFocus = function() { + return this._focused; +}; + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.progressCircular + * @description Module for a circular progressbar + */ + +angular.module('material.components.progressCircular', ['material.core']); + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.progressLinear + * @description Linear Progress module! + */ +MdProgressLinearDirective.$inject = ["$mdTheming", "$mdUtil", "$log"]; +angular.module('material.components.progressLinear', [ + 'material.core' +]) + .directive('mdProgressLinear', MdProgressLinearDirective); + +/** + * @ngdoc directive + * @name mdProgressLinear + * @module material.components.progressLinear + * @restrict E + * + * @description + * 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. + * + * 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. + * + * 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. + * + * @param {string} md-mode Select from one of four modes: determinate, indeterminate, buffer or query. + * + * Note: if the `md-mode` value is set as undefined or specified as 1 of the four (4) valid modes, then `indeterminate` + * will be auto-applied as the mode. + * + * Note: if not configured, the `md-mode="indeterminate"` will be auto injected as an attribute. If `value=""` is also specified, however, + * then `md-mode="determinate"` would be auto-injected instead. + * @param {number=} value In determinate and buffer modes, this number represents the percentage of the primary progress bar. Default: 0 + * @param {number=} md-buffer-value In the buffer mode, this number represents the percentage of the secondary progress bar. Default: 0 + * @param {boolean=} ng-disabled Determines whether to disable the progress element. + * + * @usage + * + * + * + * + * + * + * + * + * + * + * + */ +function MdProgressLinearDirective($mdTheming, $mdUtil, $log) { + var MODE_DETERMINATE = "determinate"; + var MODE_INDETERMINATE = "indeterminate"; + var MODE_BUFFER = "buffer"; + var MODE_QUERY = "query"; + var DISABLED_CLASS = "_md-progress-linear-disabled"; + + return { + restrict: 'E', + template: '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ', + compile: compile + }; + + function compile(tElement, tAttrs, transclude) { + tElement.attr('aria-valuemin', 0); + tElement.attr('aria-valuemax', 100); + tElement.attr('role', 'progressbar'); + + return postLink; + } + function postLink(scope, element, attr) { + $mdTheming(element); + + var lastMode; + var isDisabled = attr.hasOwnProperty('disabled'); + var toVendorCSS = $mdUtil.dom.animator.toCss; + var bar1 = angular.element(element[0].querySelector('.md-bar1')); + var bar2 = angular.element(element[0].querySelector('.md-bar2')); + var container = angular.element(element[0].querySelector('.md-container')); + + element + .attr('md-mode', mode()) + .toggleClass(DISABLED_CLASS, isDisabled); + + validateMode(); + watchAttributes(); + + /** + * Watch the value, md-buffer-value, and md-mode attributes + */ + function watchAttributes() { + attr.$observe('value', function(value) { + var percentValue = clamp(value); + element.attr('aria-valuenow', percentValue); + + if (mode() != MODE_QUERY) animateIndicator(bar2, percentValue); + }); + + attr.$observe('mdBufferValue', function(value) { + animateIndicator(bar1, clamp(value)); + }); + + attr.$observe('disabled', function(value) { + if (value === true || value === false) { + isDisabled = !!value; + } else { + isDisabled = angular.isDefined(value); + } + + element.toggleClass(DISABLED_CLASS, isDisabled); + container.toggleClass(lastMode, !isDisabled); + }); + + attr.$observe('mdMode', function(mode) { + if (lastMode) container.removeClass( lastMode ); + + switch( mode ) { + case MODE_QUERY: + case MODE_BUFFER: + case MODE_DETERMINATE: + case MODE_INDETERMINATE: + container.addClass( lastMode = "md-mode-" + mode ); + break; + default: + container.addClass( lastMode = "md-mode-" + MODE_INDETERMINATE ); + break; + } + }); + } + + /** + * Auto-defaults the mode to either `determinate` or `indeterminate` mode; if not specified + */ + function validateMode() { + if ( angular.isUndefined(attr.mdMode) ) { + var hasValue = angular.isDefined(attr.value); + var mode = hasValue ? MODE_DETERMINATE : MODE_INDETERMINATE; + var info = "Auto-adding the missing md-mode='{0}' to the ProgressLinear element"; + + //$log.debug( $mdUtil.supplant(info, [mode]) ); + + element.attr("md-mode", mode); + attr.mdMode = mode; + } + } + + /** + * Is the md-mode a valid option? + */ + function mode() { + var value = (attr.mdMode || "").trim(); + if ( value ) { + switch(value) { + case MODE_DETERMINATE: + case MODE_INDETERMINATE: + case MODE_BUFFER: + case MODE_QUERY: + break; + default: + value = MODE_INDETERMINATE; + break; + } + } + return value; + } + + /** + * Manually set CSS to animate the Determinate indicator based on the specified + * percentage value (0-100). + */ + function animateIndicator(target, value) { + if ( isDisabled || !mode() ) return; + + var to = $mdUtil.supplant("translateX({0}%) scale({1},1)", [ (value-100)/2, value/100 ]); + var styles = toVendorCSS({ transform : to }); + angular.element(target).css( styles ); + } + } + + /** + * Clamps the value to be between 0 and 100. + * @param {number} value The value to clamp. + * @returns {number} + */ + function clamp(value) { + return Math.max(0, Math.min(value || 0, 100)); + } +} + + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.radioButton + * @description radioButton module! + */ +mdRadioGroupDirective.$inject = ["$mdUtil", "$mdConstant", "$mdTheming", "$timeout"]; +mdRadioButtonDirective.$inject = ["$mdAria", "$mdUtil", "$mdTheming"]; +angular.module('material.components.radioButton', [ + 'material.core' +]) + .directive('mdRadioGroup', mdRadioGroupDirective) + .directive('mdRadioButton', mdRadioButtonDirective); + +/** + * @ngdoc directive + * @module material.components.radioButton + * @name mdRadioGroup + * + * @restrict E + * + * @description + * The `` directive identifies a grouping + * container for the 1..n grouped radio buttons; specified using nested + * `` tags. + * + * As per the [material design spec](http://www.google.com/design/spec/style/color.html#color-ui-color-application) + * the radio button is in the accent color by default. The primary color palette may be used with + * the `md-primary` class. + * + * Note: `` and `` handle tabindex differently + * than the native `` controls. Whereas the native controls + * force the user to tab through all the radio buttons, `` + * is focusable, and by default the ``s are not. + * + * @param {string} ng-model Assignable angular expression to data-bind to. + * @param {boolean=} md-no-ink Use of attribute indicates flag to disable ink ripple effects. + * + * @usage + * + * + * + * + * + * {{ d.label }} + * + * + * + * + * + * + */ +function mdRadioGroupDirective($mdUtil, $mdConstant, $mdTheming, $timeout) { + RadioGroupController.prototype = createRadioGroupControllerProto(); + + return { + restrict: 'E', + controller: ['$element', RadioGroupController], + require: ['mdRadioGroup', '?ngModel'], + link: { pre: linkRadioGroup } + }; + + function linkRadioGroup(scope, element, attr, ctrls) { + element.addClass('_md'); // private md component indicator for styling + $mdTheming(element); + + var rgCtrl = ctrls[0]; + var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel(); + + rgCtrl.init(ngModelCtrl); + + scope.mouseActive = false; + + element + .attr({ + 'role': 'radiogroup', + 'tabIndex': element.attr('tabindex') || '0' + }) + .on('keydown', keydownListener) + .on('mousedown', function(event) { + scope.mouseActive = true; + $timeout(function() { + scope.mouseActive = false; + }, 100); + }) + .on('focus', function() { + if(scope.mouseActive === false) { + rgCtrl.$element.addClass('md-focused'); + } + }) + .on('blur', function() { + rgCtrl.$element.removeClass('md-focused'); + }); + + /** + * + */ + function setFocus() { + if (!element.hasClass('md-focused')) { element.addClass('md-focused'); } + } + + /** + * + */ + function keydownListener(ev) { + var keyCode = ev.which || ev.keyCode; + + // Only listen to events that we originated ourselves + // so that we don't trigger on things like arrow keys in + // inputs. + + if (keyCode != $mdConstant.KEY_CODE.ENTER && + ev.currentTarget != ev.target) { + return; + } + + switch (keyCode) { + case $mdConstant.KEY_CODE.LEFT_ARROW: + case $mdConstant.KEY_CODE.UP_ARROW: + ev.preventDefault(); + rgCtrl.selectPrevious(); + setFocus(); + break; + + case $mdConstant.KEY_CODE.RIGHT_ARROW: + case $mdConstant.KEY_CODE.DOWN_ARROW: + ev.preventDefault(); + rgCtrl.selectNext(); + setFocus(); + break; + + case $mdConstant.KEY_CODE.ENTER: + var form = angular.element($mdUtil.getClosest(element[0], 'form')); + if (form.length > 0) { + form.triggerHandler('submit'); + } + break; + } + + } + } + + function RadioGroupController($element) { + this._radioButtonRenderFns = []; + this.$element = $element; + } + + function createRadioGroupControllerProto() { + return { + init: function(ngModelCtrl) { + this._ngModelCtrl = ngModelCtrl; + this._ngModelCtrl.$render = angular.bind(this, this.render); + }, + add: function(rbRender) { + this._radioButtonRenderFns.push(rbRender); + }, + remove: function(rbRender) { + var index = this._radioButtonRenderFns.indexOf(rbRender); + if (index !== -1) { + this._radioButtonRenderFns.splice(index, 1); + } + }, + render: function() { + this._radioButtonRenderFns.forEach(function(rbRender) { + rbRender(); + }); + }, + setViewValue: function(value, eventType) { + this._ngModelCtrl.$setViewValue(value, eventType); + // update the other radio buttons as well + this.render(); + }, + getViewValue: function() { + return this._ngModelCtrl.$viewValue; + }, + selectNext: function() { + return changeSelectedButton(this.$element, 1); + }, + selectPrevious: function() { + return changeSelectedButton(this.$element, -1); + }, + setActiveDescendant: function (radioId) { + this.$element.attr('aria-activedescendant', radioId); + }, + isDisabled: function() { + return this.$element[0].hasAttribute('disabled'); + } + }; + } + /** + * Change the radio group's selected button by a given increment. + * If no button is selected, select the first button. + */ + function changeSelectedButton(parent, increment) { + // Coerce all child radio buttons into an array, then wrap then in an iterator + var buttons = $mdUtil.iterator(parent[0].querySelectorAll('md-radio-button'), true); + + if (buttons.count()) { + var validate = function (button) { + // If disabled, then NOT valid + return !angular.element(button).attr("disabled"); + }; + + var selected = parent[0].querySelector('md-radio-button.md-checked'); + var target = buttons[increment < 0 ? 'previous' : 'next'](selected, validate) || buttons.first(); + + // Activate radioButton's click listener (triggerHandler won't create a real click event) + angular.element(target).triggerHandler('click'); + } + } + +} + +/** + * @ngdoc directive + * @module material.components.radioButton + * @name mdRadioButton + * + * @restrict E + * + * @description + * The ``directive is the child directive required to be used within `` elements. + * + * While similar to the `` directive, + * the `` directive provides ink effects, ARIA support, and + * supports use within named radio groups. + * + * @param {string} ngModel Assignable angular expression to data-bind to. + * @param {string=} ngChange Angular expression to be executed when input changes due to user + * interaction with the input element. + * @param {string} ngValue Angular expression which sets the value to which the expression should + * be set when selected. + * @param {string} value The value to which the expression should be set when selected. + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} aria-label Adds label to radio button for accessibility. + * Defaults to radio button's text. If no text content is available, a warning will be logged. + * + * @usage + * + * + * + * Label 1 + * + * + * + * Green + * + * + * + * + */ +function mdRadioButtonDirective($mdAria, $mdUtil, $mdTheming) { + + var CHECKED_CSS = 'md-checked'; + + return { + restrict: 'E', + require: '^mdRadioGroup', + transclude: true, + template: '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ', + link: link + }; + + function link(scope, element, attr, rgCtrl) { + var lastChecked; + + $mdTheming(element); + configureAria(element, scope); + + // ngAria overwrites the aria-checked inside a $watch for ngValue. + // We should defer the initialization until all the watches have fired. + // This can also be fixed by removing the `lastChecked` check, but that'll + // cause more DOM manipulation on each digest. + if (attr.ngValue) { + $mdUtil.nextTick(initialize, false); + } else { + initialize(); + } + + /** + * Initializes the component. + */ + function initialize() { + if (!rgCtrl) { + throw 'RadioButton: No RadioGroupController could be found.'; + } + + rgCtrl.add(render); + attr.$observe('value', render); + + element + .on('click', listener) + .on('$destroy', function() { + rgCtrl.remove(render); + }); + } + + /** + * On click functionality. + */ + function listener(ev) { + if (element[0].hasAttribute('disabled') || rgCtrl.isDisabled()) return; + + scope.$apply(function() { + rgCtrl.setViewValue(attr.value, ev && ev.type); + }); + } + + /** + * Add or remove the `.md-checked` class from the RadioButton (and conditionally its parent). + * Update the `aria-activedescendant` attribute. + */ + function render() { + var checked = rgCtrl.getViewValue() == attr.value; + + if (checked === lastChecked) return; + + if (element[0].parentNode.nodeName.toLowerCase() !== 'md-radio-group') { + // If the radioButton is inside a div, then add class so highlighting will work + element.parent().toggleClass(CHECKED_CSS, checked); + } + + if (checked) { + rgCtrl.setActiveDescendant(element.attr('id')); + } + + lastChecked = checked; + + element + .attr('aria-checked', checked) + .toggleClass(CHECKED_CSS, checked); + } + + /** + * Inject ARIA-specific attributes appropriate for each radio button + */ + function configureAria(element, scope){ + element.attr({ + id: attr.id || 'radio_' + $mdUtil.nextUid(), + role: 'radio', + 'aria-checked': 'false' + }); + + $mdAria.expectWithText(element, 'aria-label'); + } + } +} + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.select + */ + +/*************************************************** + + ### TODO - POST RC1 ### + - [ ] Abstract placement logic in $mdSelect service to $mdMenu service + + ***************************************************/ + +SelectDirective.$inject = ["$mdSelect", "$mdUtil", "$mdConstant", "$mdTheming", "$mdAria", "$parse", "$sce", "$injector"]; +SelectMenuDirective.$inject = ["$parse", "$mdUtil", "$mdConstant", "$mdTheming"]; +OptionDirective.$inject = ["$mdButtonInkRipple", "$mdUtil"]; +SelectProvider.$inject = ["$$interimElementProvider"]; +var SELECT_EDGE_MARGIN = 8; +var selectNextId = 0; +var CHECKBOX_SELECTION_INDICATOR = + angular.element('
    '); + +angular.module('material.components.select', [ + 'material.core', + 'material.components.backdrop' + ]) + .directive('mdSelect', SelectDirective) + .directive('mdSelectMenu', SelectMenuDirective) + .directive('mdOption', OptionDirective) + .directive('mdOptgroup', OptgroupDirective) + .directive('mdSelectHeader', SelectHeaderDirective) + .provider('$mdSelect', SelectProvider); + +/** + * @ngdoc directive + * @name mdSelect + * @restrict E + * @module material.components.select + * + * @description Displays a select box, bound to an ng-model. + * + * When the select is required and uses a floating label, then the label will automatically contain + * an asterisk (`*`). This behavior can be disabled by using the `md-no-asterisk` attribute. + * + * By default, the select will display with an underline to match other form elements. This can be + * disabled by applying the `md-no-underline` CSS class. + * + * ### Option Params + * + * When applied, `md-option-empty` will mark the option as "empty" allowing the option to clear the + * select and put it back in it's default state. You may supply this attribute on any option you + * wish, however, it is automatically applied to an option whose `value` or `ng-value` are not + * defined. + * + * **Automatically Applied** + * + * - `` + * - `` + * - `` + * - `` + * - `` + * + * **NOT Automatically Applied** + * + * - `` + * - `` + * - `` + * - `` (this evaluates to the string `"undefined"`) + * - <md-option ng-value="{{someValueThatMightBeUndefined}}"> + * + * **Note:** A value of `undefined` ***is considered a valid value*** (and does not auto-apply this + * attribute) since you may wish this to be your "Not Available" or "None" option. + * + * **Note:** Using the `value` attribute (as opposed to `ng-value`) always evaluates to a string, so + * `value="null"` will require the test `ng-if="myValue != 'null'"` rather than `ng-if="!myValue"`. + * + * @param {expression} ng-model The model! + * @param {boolean=} multiple When set to true, allows for more than one option to be selected. The model is an array with the selected choices. + * @param {expression=} md-on-close Expression to be evaluated when the select is closed. + * @param {expression=} md-on-open Expression to be evaluated when opening the select. + * Will hide the select options and show a spinner until the evaluated promise resolves. + * @param {expression=} md-selected-text Expression to be evaluated that will return a string + * to be displayed as a placeholder in the select input box when it is closed. The value + * will be treated as *text* (not html). + * @param {expression=} md-selected-html Expression to be evaluated that will return a string + * to be displayed as a placeholder in the select input box when it is closed. The value + * will be treated as *html*. The value must either be explicitly marked as trustedHtml or + * the ngSanitize module must be loaded. + * @param {string=} placeholder Placeholder hint text. + * @param md-no-asterisk {boolean=} When set to true, an asterisk will not be appended to the + * floating label. **Note:** This attribute is only evaluated once; it is not watched. + * @param {string=} aria-label Optional label for accessibility. Only necessary if no placeholder or + * explicit label is present. + * @param {string=} md-container-class Class list to get applied to the `.md-select-menu-container` + * element (for custom styling). + * + * @usage + * With a placeholder (label and aria-label are added dynamically) + * + * + * + * {{ opt }} + * + * + * + * + * With an explicit label + * + * + * + * + * {{ opt }} + * + * + * + * + * With a select-header + * + * When a developer needs to put more than just a text label in the + * md-select-menu, they should use the md-select-header. + * The user can put custom HTML inside of the header and style it to their liking. + * One common use case of this would be a sticky search bar. + * + * When using the md-select-header the labels that would previously be added to the + * OptGroupDirective are ignored. + * + * + * + * + * + * Neighborhoods - + * + * {{ opt }} + * + * + * + * + * ## Selects and object equality + * When using a `md-select` to pick from a list of objects, it is important to realize how javascript handles + * equality. Consider the following example: + * + * angular.controller('MyCtrl', function($scope) { + * $scope.users = [ + * { id: 1, name: 'Bob' }, + * { id: 2, name: 'Alice' }, + * { id: 3, name: 'Steve' } + * ]; + * $scope.selectedUser = { id: 1, name: 'Bob' }; + * }); + * + * + *
    + * + * {{ user.name }} + * + *
    + *
    + * + * At first one might expect that the select should be populated with "Bob" as the selected user. However, + * this is not true. To determine whether something is selected, + * `ngModelController` is looking at whether `$scope.selectedUser == (any user in $scope.users);`; + * + * Javascript's `==` operator does not check for deep equality (ie. that all properties + * on the object are the same), but instead whether the objects are *the same object in memory*. + * In this case, we have two instances of identical objects, but they exist in memory as unique + * entities. Because of this, the select will have no value populated for a selected user. + * + * To get around this, `ngModelController` provides a `track by` option that allows us to specify a different + * expression which will be used for the equality operator. As such, we can update our `html` to + * make use of this by specifying the `ng-model-options="{trackBy: '$value.id'}"` on the `md-select` + * element. This converts our equality expression to be + * `$scope.selectedUser.id == (any id in $scope.users.map(function(u) { return u.id; }));` + * which results in Bob being selected as desired. + * + * Working HTML: + * + *
    + * + * {{ user.name }} + * + *
    + *
    + */ +function SelectDirective($mdSelect, $mdUtil, $mdConstant, $mdTheming, $mdAria, $parse, $sce, + $injector) { + var keyCodes = $mdConstant.KEY_CODE; + var NAVIGATION_KEYS = [keyCodes.SPACE, keyCodes.ENTER, keyCodes.UP_ARROW, keyCodes.DOWN_ARROW]; + + return { + restrict: 'E', + require: ['^?mdInputContainer', 'mdSelect', 'ngModel', '?^form'], + compile: compile, + controller: function() { + } // empty placeholder controller to be initialized in link + }; + + function compile(element, attr) { + // add the select value that will hold our placeholder or selected option value + var valueEl = angular.element(''); + valueEl.append(''); + valueEl.addClass('md-select-value'); + if (!valueEl[0].hasAttribute('id')) { + valueEl.attr('id', 'select_value_label_' + $mdUtil.nextUid()); + } + + // There's got to be an md-content inside. If there's not one, let's add it. + if (!element.find('md-content').length) { + element.append(angular.element('').append(element.contents())); + } + + + // Add progress spinner for md-options-loading + if (attr.mdOnOpen) { + + // Show progress indicator while loading async + // Use ng-hide for `display:none` so the indicator does not interfere with the options list + element + .find('md-content') + .prepend(angular.element( + '
    ' + + ' ' + + '
    ' + )); + + // Hide list [of item options] while loading async + element + .find('md-option') + .attr('ng-show', '$$loadingAsyncDone'); + } + + if (attr.name) { + var autofillClone = angular.element(',