commit 46e6cc67777f5ac3580de438f785e5ee0b60e275 Author: xinni-ge Date: Wed Mar 7 13:52:37 2018 +0900 Add angular-material 1.1.5 An initial commit, providing javascript, scss and meta files. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b3085b8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +*.pyc +*.sw? +*.sqlite3 +.DS_STORE +*.egg-info +.venv +.tox +build +dist diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..60ad70d --- /dev/null +++ b/LICENSE @@ -0,0 +1,10 @@ +The MIT License + +Copyright (c) 2014-2016 Google, Inc. http://angularjs.org + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..af3cdd3 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,6 @@ +include README.txt +recursive-include xstatic * +global-exclude *.pyc +global-exclude *.pyo +global-exclude *.orig +global-exclude *.rej diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..9ff8368 --- /dev/null +++ b/README.txt @@ -0,0 +1,13 @@ +XStatic-Angular-Material +-------------- + +Angular-Material JavaScript and SCSS library packaged for setuptools (easy_install) / pip. + +This package is intended to be used by **any** project that needs these files. + +It intentionally does **not** provide any extra code except some metadata +**nor** has any extra requirements. You MAY use some minimal support code from +the XStatic base package, if you like. + +You can find more info about the xstatic packaging way in the package `XStatic`. + diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..2da64c6 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,20 @@ +[metadata] +name = XStatic-Angular-Material +description = Angular-Material 1.1.5 (XStatic packaging standard) +description-file = README.rst +maintainer = xinni.ge +maintainer-email = xinni.ge@ntt.com +home-page = https://github.com/angular/material +keywords = angular_material xstatic +license = MIT +zip_safe = False +namespace_packages = + xstatic + xstatic.pkg + +[files] +packages = + xstatic + +[bdist_wheel] +universal = True diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..19c79fb --- /dev/null +++ b/setup.py @@ -0,0 +1,25 @@ +from setuptools import setup, find_packages +from xstatic.pkg import angular_material as xs + +# The README.txt file should be written in reST so that PyPI can use +# it to generate your project's PyPI page. +long_description = open('README.txt').read() + +setup( + name=xs.PACKAGE_NAME, + version=xs.PACKAGE_VERSION, + description=xs.DESCRIPTION, + long_description=long_description, + classifiers=xs.CLASSIFIERS, + keywords=xs.KEYWORDS, + maintainer=xs.MAINTAINER, + maintainer_email=xs.MAINTAINER_EMAIL, + license=xs.LICENSE, + url=xs.HOMEPAGE, + platforms=xs.PLATFORMS, + packages=find_packages(), + namespace_packages=['xstatic', 'xstatic.pkg'], + include_package_data=True, + zip_safe=False, + install_requires=[], +) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..9e60b20 --- /dev/null +++ b/tox.ini @@ -0,0 +1,8 @@ +[tox] +minversion = 1.6 +skipsdist = True +envlist = py27,py33,py34 + +[testenv:venv] +commands = {posargs} + diff --git a/xstatic/__init__.py b/xstatic/__init__.py new file mode 100644 index 0000000..de40ea7 --- /dev/null +++ b/xstatic/__init__.py @@ -0,0 +1 @@ +__import__('pkg_resources').declare_namespace(__name__) diff --git a/xstatic/pkg/__init__.py b/xstatic/pkg/__init__.py new file mode 100644 index 0000000..de40ea7 --- /dev/null +++ b/xstatic/pkg/__init__.py @@ -0,0 +1 @@ +__import__('pkg_resources').declare_namespace(__name__) diff --git a/xstatic/pkg/angular_material/__init__.py b/xstatic/pkg/angular_material/__init__.py new file mode 100644 index 0000000..b436da5 --- /dev/null +++ b/xstatic/pkg/angular_material/__init__.py @@ -0,0 +1,49 @@ +""" +XStatic resource package + +See package 'XStatic' for documentation and basic tools. +""" + +DISPLAY_NAME = 'Angular-Material' # official name, upper/lowercase allowed, no spaces +PACKAGE_NAME = 'XStatic-%s' % DISPLAY_NAME # name used for PyPi + +NAME = __name__.split('.')[-1] # package name (e.g. 'foo' or 'foo_bar') + # please use a all-lowercase valid python + # package name + +VERSION = '1.1.5' # version of the packaged files, please use the upstream + # version number +BUILD = '0' # our package build number, so we can release new builds + # with fixes for xstatic stuff. +PACKAGE_VERSION = VERSION + '.' + BUILD # version used for PyPi + +DESCRIPTION = "%s %s (XStatic packaging standard)" % (DISPLAY_NAME, VERSION) + +PLATFORMS = 'any' +CLASSIFIERS = [] +KEYWORDS = '%s xstatic' % NAME + +# XStatic-* package maintainer: +MAINTAINER = 'xinni.ge' +MAINTAINER_EMAIL = 'xinni.ge@ntt.com' + +# this refers to the project homepage of the stuff we packaged: +HOMEPAGE = 'https://github.com/angular/material' + +# this refers to all files: +LICENSE = 'MIT' + +from os.path import join, dirname +BASE_DIR = join(dirname(__file__), 'data') +# linux package maintainers just can point to their file locations like this: +#BASE_DIR = '/usr/share/javascript/angular/material' + +LOCATIONS = { + # CDN locations (if no public CDN exists, use an empty dict) + # if value is a string, it is a base location, just append relative + # path/filename. if value is a dict, do another lookup using the + # relative path/filename you want. + # your relative path/filenames should usually be without version + # information, because either the base dir/url is exactly for this + # version or the mapping will care for accessing this version. +} diff --git a/xstatic/pkg/angular_material/data/angular-material.js b/xstatic/pkg/angular_material/data/angular-material.js new file mode 100644 index 0000000..406c9aa --- /dev/null +++ b/xstatic/pkg/angular_material/data/angular-material.js @@ -0,0 +1,36390 @@ +/*! + * AngularJS Material Design + * https://github.com/angular/material + * @license MIT + * v1.1.5 + */ +(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.chips","material.components.checkbox","material.components.colors","material.components.content","material.components.datepicker","material.components.dialog","material.components.divider","material.components.fabActions","material.components.fabShared","material.components.fabSpeedDial","material.components.fabToolbar","material.components.icon","material.components.gridList","material.components.input","material.components.list","material.components.menu","material.components.menuBar","material.components.navBar","material.components.panel","material.components.progressCircular","material.components.progressLinear","material.components.radioButton","material.components.select","material.components.showHide","material.components.sidenav","material.components.slider","material.components.sticky","material.components.subheader","material.components.swipe","material.components.switch","material.components.tabs","material.components.toast","material.components.toolbar","material.components.tooltip","material.components.truncate","material.components.virtualRepeat","material.components.whiteframe"]); +})(); +(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" + + "AngularJS Material already has mobile click, tap, and swipe support... \n" + + "ngTouch is not supported with AngularJS 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 AngularJS 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 AngularJS 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); }, + isMetaKey: function(e) { return (e.keyCode >= 91 && e.keyCode <= 93); }, + isFnLockKey: function(e) { return (e.keyCode >= 112 && e.keyCode <= 145); }, + 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); + }, + hasModifierKey: function(e) { + return e.ctrlKey || e.metaKey || e.altKey; + }, + + /** + * 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 AngularJS 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 ) { + 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; + + // Scroll may be set on element (for example by overflow-y: scroll) + // but Chrome is reporting the scrollTop position always on . + // scrollElement will allow to restore the scrollTop position to proper target. + var scrollElement = documentElement.scrollTop > 0 ? documentElement : body; + + 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 scroll position while being fixed + scrollElement.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 AngularJS 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.aria + * @description + * Aria Expectations for AngularJS Material 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 AngularJS 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 AngularJS 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, + hasAriaLabel: hasAriaLabel, + parentHasAriaLabel: parentHasAriaLabel + }; + + /** + * 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; + } + + /** + * Check if expected element has aria label attribute + * @param element + */ + function hasAriaLabel(element) { + var node = angular.element(element)[0] || element; + + /* Check if compatible node type (ie: not HTML Document node) */ + if (!node.hasAttribute) { + return false; + } + + /* Check label or description attributes */ + return node.hasAttribute('aria-label') || node.hasAttribute('aria-labelledby') || node.hasAttribute('aria-describedby'); + } + + /** + * Check if expected element's parent has aria label attribute and has valid role and tagName + * @param element + * @param {optional} level Number of levels deep search should be performed + */ + function parentHasAriaLabel(element, level) { + level = level || 1; + var node = angular.element(element)[0] || element; + if (!node.parentNode) { + return false; + } + if (performCheck(node.parentNode)) { + return true; + } + level--; + if (level) { + return parentHasAriaLabel(node.parentNode, level); + } + return false; + + function performCheck(parentNode) { + if (!hasAriaLabel(parentNode)) { + return false; + } + /* Perform role blacklist check */ + if (parentNode.hasAttribute('role')) { + switch(parentNode.getAttribute('role').toLowerCase()) { + case 'command': + case 'definition': + case 'directory': + case 'grid': + case 'list': + case 'listitem': + case 'log': + case 'marquee': + case 'menu': + case 'menubar': + case 'note': + case 'presentation': + case 'separator': + case 'scrollbar': + case 'status': + case 'tablist': + return false; + } + } + /* Perform tagName blacklist check */ + switch(parentNode.tagName.toLowerCase()) { + case 'abbr': + case 'acronym': + case 'address': + case 'applet': + case 'audio': + case 'b': + case 'bdi': + case 'bdo': + case 'big': + case 'blockquote': + case 'br': + case 'canvas': + case 'caption': + case 'center': + case 'cite': + case 'code': + case 'col': + case 'data': + case 'dd': + case 'del': + case 'dfn': + case 'dir': + case 'div': + case 'dl': + case 'em': + case 'embed': + case 'fieldset': + case 'figcaption': + case 'font': + case 'h1': + case 'h2': + case 'h3': + case 'h4': + case 'h5': + case 'h6': + case 'hgroup': + case 'html': + case 'i': + case 'ins': + case 'isindex': + case 'kbd': + case 'keygen': + case 'label': + case 'legend': + case 'li': + case 'map': + case 'mark': + case 'menu': + case 'object': + case 'ol': + case 'output': + case 'pre': + case 'presentation': + case 'q': + case 'rt': + case 'ruby': + case 'samp': + case 'small': + case 'source': + case 'span': + case 'status': + case 'strike': + case 'strong': + case 'sub': + case 'sup': + case 'svg': + case 'tbody': + case 'td': + case 'th': + case 'thead': + case 'time': + case 'tr': + case 'track': + case 'tt': + case 'ul': + case 'var': + return false; + } + return true; + } + } +} + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.core.compiler + * @description + * AngularJS Material template and element compiler. + */ +angular + .module('material.core') + .provider('$mdCompiler', MdCompilerProvider); + +/** + * @ngdoc provider + * @name $mdCompilerProvider + * + * @description + */ +/** + * @ngdoc service + * @name $mdCompiler + * @module material.core.compiler + * @description + * The $mdCompiler service is an abstraction of AngularJS'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 AngularJS 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. + * }); + * + * + */ +MdCompilerProvider.$inject = ['$compileProvider']; +function MdCompilerProvider($compileProvider) { + + var provider = this; + + /** + * @name $mdCompilerProvider#respectPreAssignBindingsEnabled + * + * @param {boolean=} respected update the respectPreAssignBindingsEnabled state if provided, otherwise just return + * the current Material preAssignBindingsEnabled state + * @returns {*} current value if used as getter or itself (chaining) if used as setter + * + * @description + * Call this method to enable/disable whether Material-specific (dialogs/toasts) controllers respect the AngularJS + * `$compile.preAssignBindingsEnabled` flag. Note that this doesn't affect directives/components created via + * regular AngularJS methods which constitute most Material & user-created components. + * + * @see [AngularJS documentation for `$compile.preAssignBindingsEnabled` + * ](https://code.angularjs.org/1.6.4/docs/api/ng/provider/$compileProvider#preAssignBindingsEnabled) + * for more information. + * + * If disabled (false), the compiler assigns the value of each of the bindings to the + * properties of the controller object before the constructor of this object is called. + * + * If enabled (true) the behavior depends on the AngularJS version used: + * + * - `<1.5.10` - bindings are pre-assigned + * - `>=1.5.10 <1.7` - behaves like set to whatever `$compileProvider.preAssignBindingsEnabled()` reports; if + * the `$compileProvider` flag wasn't set manually, it defaults to pre-assigning bindings + * with AngularJS `1.5.x` & to calling the constructor first with AngularJS `1.6.x`. + * - `>=1.7` - the compiler calls the constructor first before assigning bindings + * + * The default value is `false` but will change to `true` in AngularJS Material 1.2. + * + * It is recommended to set this flag to `true` in AngularJS Material 1.1.x; the only reason it's not set that way + * by default is backwards compatibility. Not setting the flag to `true` when + * `$compileProvider.preAssignBindingsEnabled()` is set to `false` (i.e. default behavior in AngularJS 1.6.0 or newer) + * makes it hard to unit test Material Dialog/Toast controllers using the `$controller` helper as it always follows + * the `$compileProvider.preAssignBindingsEnabled()` value. + */ + // TODO change it to `true` in Material 1.2. + var respectPreAssignBindingsEnabled = false; + this.respectPreAssignBindingsEnabled = function(respected) { + if (angular.isDefined(respected)) { + respectPreAssignBindingsEnabled = respected; + return this; + } + + return respectPreAssignBindingsEnabled; + }; + + /** + * @description + * This function returns `true` if Material-specific (dialogs/toasts) controllers have bindings pre-assigned in + * controller constructors and `false` otherwise. + * + * Note that this doesn't affect directives/components created via regular AngularJS methods which constitute most + * Material & user-created components; their behavior can be checked via `$compileProvider.preAssignBindingsEnabled()` + * in AngularJS `>=1.5.10 <1.7.0`. + * + * @returns {*} current preAssignBindingsEnabled state + */ + function getPreAssignBindingsEnabled() { + if (!respectPreAssignBindingsEnabled) { + // respectPreAssignBindingsEnabled === false + // We're ignoring the AngularJS `$compileProvider.preAssignBindingsEnabled()` value in this case. + return true; + } + + // respectPreAssignBindingsEnabled === true + + if (typeof $compileProvider.preAssignBindingsEnabled === 'function') { + return $compileProvider.preAssignBindingsEnabled(); + } + + // Flag respected but not present => apply logic based on AngularJS version used. + if (angular.version.major === 1 && angular.version.minor < 6) { + // AngularJS <1.5.10 + return true; + } + + // AngularJS >=1.7.0 + return false; + } + + this.$get = ["$q", "$templateRequest", "$injector", "$compile", "$controller", + function($q, $templateRequest, $injector, $compile, $controller) { + return new MdCompilerService($q, $templateRequest, $injector, $compile, $controller); + }]; + + 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 AngularJS compiler. + * The `$mdCompiler` is wrapper around the AngularJS 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. + */ + 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. + */ + 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. + */ + 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 + }); + + // Create the specified controller instance. + var ctrl = self._createController(options, injectLocals, locals); + + // Unique identifier for AngularJS 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 AngularJS $compile link function. + return ngLinkFn(scope); + } + + return compileData; + + }; + + /** + * Creates and instantiates a new controller with the specified options. + * @param {!Object} options Options that include the controller + * @param {!Object} injectLocals Locals to to be provided in the controller DI. + * @param {!Object} locals Locals to be injected to the controller. + * @returns {!Object} Created controller instance. + */ + MdCompilerService.prototype._createController = function(options, injectLocals, locals) { + var invokeCtrl = this.$controller(options.controller, injectLocals, true, options.controllerAs); + + if (getPreAssignBindingsEnabled() && options.bindToController) { + angular.extend(invokeCtrl.instance, locals); + } + + // Instantiate and initialize the specified controller. + var ctrl = invokeCtrl(); + + if (!getPreAssignBindingsEnabled() && options.bindToController) { + angular.extend(invokeCtrl.instance, locals); + } + + // Call the $onInit hook if it's present on the controller. + angular.isFunction(ctrl.$onInit) && ctrl.$onInit(); + + return ctrl; + }; + + /** + * 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}} + */ + 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"; + + +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, maxClickDistance = 6; + +/** + * 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. + * You can also change max click distance (6px by default) if you have issues on some touch screens. + * + * + * app.config(function($mdGestureProvider) { + * + * // For mobile devices without jQuery loaded, do not + * // intercept click events during the capture phase. + * $mdGestureProvider.skipClickHijack(); + * + * // If hijcacking clicks, change default 6px click distance + * $mdGestureProvider.setMaxClickDistance(12); + * + * }); + * + * + */ +function MdGestureProvider() { } + +MdGestureProvider.prototype = { + + // Publish access to setter to configure a variable BEFORE the + // $mdGesture service is instantiated... + skipClickHijack: function() { + return forceSkipClickHijack = true; + }, + + setMaxClickDistance: function(clickDistance) { + maxClickDistance = parseInt(clickDistance); + }, + + /** + * $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) { + 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 && canFocus(ev.target)) { + this.dispatchEvent(ev, 'focus', pointer); + ev.target.focus(); + } + } + }); + + 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 AngularJS Material. 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.type !== 'mousedown' || (!canFocus(ev.target) && !canFocus(document.activeElement))) + ) { + ev.preventDefault(); + ev.stopPropagation(); + } + } + + function clickHijacker(ev) { + var isKeyClick = ev.clientX === 0 && ev.clientY === 0; + var isSubmitEvent = ev.target && ev.target.type === 'submit'; + if (!isKeyClick && !ev.$material && !ev.isIonicTap + && !isInputEventFromLabelClick(ev) + && !isSubmitEvent) { + 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(); + + if (ev.type !== 'pointercancel') { + 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; +} + +/** Checks whether an element can be focused. */ +function canFocus(element) { + return ( + !!element && + element.getAttribute('tabindex') != '-1' && + !element.hasAttribute('disabled') && + ( + element.hasAttribute('tabindex') || + element.hasAttribute('href') || + element.isContentEditable || + ['INPUT', 'SELECT', 'BUTTON', 'TEXTAREA', 'VIDEO', 'AUDIO'].indexOf(element.nodeName) != -1 + ) + ); +} + +})(); +(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"; + +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 AngularJS 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 AngularJS 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 AngularJS 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"; + +(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 AngularJS Material 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 AngularJS Material Layout attribute directives + // This provides easy translation to switch AngularJS Material 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 AngularJS Material 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.trim().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; + + 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 ? value.trim() : ""; + } + + /** + * 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].trim().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 + * AngularJS 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 if 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 AngularJS 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); + * + * $mdTheming.defineTheme('myTheme', { + * primary: 'blue', + * accent: 'pink', + * dark: true + * }) + * } + * }; + * }); + * + * @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 + */ + + /** + * @ngdoc method + * @name $mdTheming#defineTheme + * @description + * Dynamically define a theme by an options object + * + * options are:
+ * `primary` - The primary palette of the theme.
+ * `accent` - The accent palette of the theme.
+ * `warn` - The warn palette of the theme.
+ * `background` - The background palette of the theme.
+ * `dark` - Indicates if it's a dark theme.
+ * + * @param {String} name Theme name to define + * @param {Object} options Theme definition options + * @returns {Promise} A resolved promise with the theme name + */ + + /* @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! Close' + + * '' + * }) + * + * // Fires when the hide() method is used + * .then(function() { + * console.log('You clicked the button to close the bottom sheet!'); + * }) + * + * // Fires when the cancel() method is used + * .catch(function() { + * console.log('You hit escape or clicked the backdrop to close.'); + * }); + * }; + * + * $scope.closeBottomSheet = function($scope, $mdBottomSheet) { + * $mdBottomSheet.hide(); + * } + * + * }); + * + */ + + /** + * @ngdoc method + * @name $mdBottomSheet#show + * + * @description + * Show a bottom sheet with the specified options. + * + * Note: You should always provide a `.catch()` method in case the user hits the + * `esc` key or clicks the background to close. In this case, the `cancel()` method will + * automatically be called on the bottom sheet which will `reject()` the promise. See the @usage + * section above for an example. + * + * Newer versions of Angular will throw a `Possibly unhandled rejection` exception if you forget + * this. + * + * @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). + * + * Note: Use a `.then()` on your `.show()` to handle this callback. + * + * @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()`. + * + * Note: Use a `.catch()` on your `.show()` to handle this callback. + * + * @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.chips + */ +/* + * @see js folder for chips implementation + */ +angular.module('material.components.chips', [ + 'material.core', + 'material.components.autocomplete' +]); + +})(); +(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 AngularJS 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 AngularJS 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.ngClick ? 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"; + +(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 AngularJS Material 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. + * - `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`. + * - `multiple` `{boolean=}`: An option to allow this dialog to display over one that's currently open. + * @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. + MdDialogController.$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', 'required'], + options: advancedDialogOptions + }); + + /* @ngInject */ + function advancedDialogOptions() { + return { + template: [ + '', + ' ', + '

{{ dialog.title }}

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

{{::dialog.mdTextContent}}

', + '
', + ' ', + ' ', + ' ', + '
', + ' ', + ' ', + ' {{ dialog.cancel }}', + ' ', + ' ', + ' {{ dialog.ok }}', + ' ', + ' ', + '
' + ].join('').replace(/\s\s+/g, ''), + controller: MdDialogController, + controllerAs: 'dialog', + bindToController: true, + }; + } + + /** + * Controller for the md-dialog interim elements + * @ngInject + */ + function MdDialogController($mdDialog, $mdConstant) { + // For compatibility with AngularJS 1.6+, we should always use the $onInit hook in + // interimElements. The $mdCompiler simulates the $onInit hook for all versions. + this.$onInit = function() { + 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); + } + }; + }; + } + + /* @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() { + // If dialog title is specified, set aria-label with it + // See https://github.com/angular/material/issues/10582 + if (options.title) { + return options.title; + } else { + 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].hasAttribute('aria-live')) { + 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"); + }; + + /* + * AngularJS Lifecycle hook for newer AngularJS 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 AngularJS 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.icon + * @description + * Icon + */ +angular.module('material.components.icon', ['material.core']); + +})(); +(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 ltr = document.dir != 'rtl' && document.body.dir != 'rtl'; + var style = ltr ? { + left: POSITION({ unit: hUnit, offset: position.col, gutter: gutter }), + width: DIMENSION({ unit: hUnit, span: spans.col, gutter: gutter }), + // resets + paddingTop: '', + marginTop: '', + top: '', + height: '' + } : { + right: 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.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 want 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', 'rel', '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.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.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) { + * // The controller is provided with an import named '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 AngularJS’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. 'mdPanelRef' is a reserved key, and will always + * be set to the created MdPanelRef instance. + * - `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 AngularJS 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 _md-panel-offscreen'); + 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 AngularJS 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 AngularJS 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 AngularJS 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'); + self.panelEl.css('pointer-events', 'all'); + } + + // 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 offscreen class and add hidden class. + self.panelEl.removeClass('_md-panel-offscreen'); + 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.panelEl.removeClass('_md-panel-offscreen'); + 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 position; + } + + 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.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. + * @param {string} ngModel Assignable angular expression to data-bind to. + * @param {string=} ngChange AngularJS expression to be executed when input changes due to user + * interaction with the input element. + * + * @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} ngValue AngularJS 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", "$mdTheming"]; +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. + var mdContentEl = element.find('md-content'); + if (!mdContentEl.length) { + element.append(angular.element('').append(element.contents())); + } + mdContentEl.attr('role', 'presentation'); + + + // 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(',