/*!
* AngularJS Material Design
* https://github.com/angular/material
* @license MIT
* v1.2.3
*/
(function( window, angular, undefined ){
"use strict";
(function(){
"use strict";
angular.module('ngMaterial', ["ng","ngAnimate","ngAria","material.core","material.core.animate","material.core.gestures","material.core.interaction","material.core.layout","material.core.meta","material.core.theming.palette","material.core.theming","material.components.autocomplete","material.components.backdrop","material.components.bottomSheet","material.components.button","material.components.card","material.components.checkbox","material.components.chips","material.components.colors","material.components.content","material.components.datepicker","material.components.dialog","material.components.divider","material.components.fabActions","material.components.fabShared","material.components.fabSpeedDial","material.components.fabToolbar","material.components.gridList","material.components.icon","material.components.input","material.components.list","material.components.menu","material.components.menuBar","material.components.navBar","material.components.panel","material.components.progressCircular","material.components.progressLinear","material.components.radioButton","material.components.select","material.components.showHide","material.components.sidenav","material.components.slider","material.components.sticky","material.components.subheader","material.components.swipe","material.components.switch","material.components.tabs","material.components.toast","material.components.toolbar","material.components.tooltip","material.components.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} cb 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);
/**
* @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 {string} color
* @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 {string} color
* @returns {string}
*/
function rgbToRgba (color) {
return color.replace(')', ', 0.1)').replace('(', 'a(');
}
/**
* Converts an RGBA color to RGB
* @param {string} color
* @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;
/**
* @param {string} name CSS property name
* @return {string} the property name supported by the browser
*/
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]);
}
/**
* @param {!string} input value to convert to camelCase
* @return {string} camelCased version of the input string
*/
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 {any[]} items Array list which this iterator will enumerate
* @param {boolean=} reloop enables iterator to consider the list as an endless loop
* @return {{add: add, next: (function()), last: (function(): any|null), previous: (function()), count: (function(): number), hasNext: (function(*=): Array.length|*|number|boolean), inRange: (function(*): boolean), remove: remove, contains: (function(*=): *|boolean), itemAt: (function(*=): any|null), findBy: (function(*, *): *[]), hasPrevious: (function(*=): Array.length|*|number|boolean), items: (function(): *[]), indexOf: (function(*=): number), first: (function(): any|null)}}
* @constructor
*/
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:
*
*
*
*
*
Breakpoint
*
mediaQuery
*
*
*
*
*
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
*
landscape
*
*
*
portrait
*
portrait
*
*
*
print
*
print
*
*
*
*
* 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;
}]);
}]);
/**
* @param {string|string[]} initialAttributes
* @param {boolean} buildSelector
* @return {string|string[]|{buildSelector: (function(string|string[]): string),
* buildList: (function(string|string[]): string[]),
* hasAttribute: (function(JQLite|Element, string): HTMLElement),
* removeAttribute: (function(JQLite|Element, string): void)}}
* @constructor
*/
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, isIos, isAndroid, isFirefox;
// Support material-tools builds.
if (window.navigator) {
var userAgent = window.navigator.userAgent || window.navigator.vendor || window.opera;
isIos = userAgent.match(/ipad|iphone|ipod/i);
isAndroid = userAgent.match(/android/i);
isFirefox = userAgent.match(/(firefox|minefield)/i);
}
/**
* @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 === '}}'));
// Polyfill document.contains for IE11.
document.contains || (document.contains = function (node) {
return document.body.contains(node);
});
/**
* 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: {},
isIos: isIos,
isAndroid: isAndroid,
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 {!ngModel.NgModelController} ngModelCtrl
* @param {!string} optionName
* @returns {string|number|boolean|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];
},
/**
* Determines the current 'dir'ectional value based on the value of 'dir'
* attribute of the element. If that is not defined, it will try to use
* a 'dir' attribute of the body or html tag.
*
* @param {Object=} attrs a hash object with key-value pairs of normalized
* attribute names and their corresponding attribute values.
* @returns {boolean} true if the element's passed in attributes,
* the document, or the body indicates RTL mode, false otherwise.
*/
isRtl: function(attrs) {
var dir = angular.isDefined(attrs) && attrs.hasOwnProperty('dir') && attrs.dir;
switch (dir) {
case 'ltr':
return false;
case 'rtl':
return true;
}
return ($document[0].dir === 'rtl' || $document[0].body.dir === 'rtl');
},
/**
* 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 = !this.isRtl();
// 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 = !this.isRtl();
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.
* @param nodes
* @return {Array}
*/
nodesToArray: function(nodes) {
var results = [], i;
nodes = nodes || [];
for (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() {
// If body scrolling is disabled, then use the cached viewport top value, otherwise get it
// fresh from the $window.
if ($mdUtil.disableScrollAround._count && $mdUtil.disableScrollAround._viewPortTop) {
return $mdUtil.disableScrollAround._viewPortTop;
} else {
return $window.scrollY || $window.pageYOffset || 0;
}
},
/**
* Finds the proper focus target by searching the DOM.
*
* @param {!JQLite} containerEl
* @param {string=} attributeVal
* @returns {JQLite|undefined}
*/
findFocusTarget: function(containerEl, attributeVal) {
var AUTO_FOCUS = this.prefixer('md-autofocus', true);
var elToFocus;
elToFocus = scanForFocusable(containerEl, attributeVal || AUTO_FOCUS);
// Scan for fallback to 'universal' API
if (!elToFocus) {
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.
* @param {!JQLite} target
* @param {!string} selector
* @return {JQLite|undefined}
*/
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|JQLite=} element Origin Element (not used)
* @param {Element|JQLite=} parent Element to disable scrolling within.
* Defaults to body if none supplied.
* @param {Object=} 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, options);
return $mdUtil.disableScrollAround._restoreScroll = function() {
if (--$mdUtil.disableScrollAround._count <= 0) {
delete $mdUtil.disableScrollAround._viewPortTop;
restoreBody();
restoreElement();
delete $mdUtil.disableScrollAround._restoreScroll;
}
};
/**
* Creates a virtual scrolling mask to prevent touchmove, keyboard, scrollbar clicking,
* and wheel events.
* @param {!Element|!JQLite} elementToDisable
* @param {Object=} scrollMaskOptions Object of options to modify functionality
* - disableScrollMask Boolean of whether or not to create a scroll mask element or
* use the passed parent element.
* @returns {Function}
*/
function disableElementScroll(elementToDisable, scrollMaskOptions) {
var scrollMask;
var wrappedElementToDisable = angular.element(elementToDisable || body);
if (scrollMaskOptions.disableScrollMask) {
scrollMask = wrappedElementToDisable;
} else {
scrollMask = angular.element(
'
' +
' ' +
'
');
wrappedElementToDisable.append(scrollMask);
}
/**
* @param {Event} $event
*/
function preventDefault($event) {
$event.preventDefault();
}
scrollMask.on('wheel touchmove', preventDefault);
return function restoreElementScroll() {
scrollMask.off('wheel touchmove', preventDefault);
if (!scrollMaskOptions.disableScrollMask && scrollMask[0].parentNode) {
scrollMask[0].parentNode.removeChild(scrollMask[0]);
}
};
}
// 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();
$mdUtil.disableScrollAround._viewPortTop = viewportTop;
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';
}
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.
* @param {Element|JQLite} element to focus
*/
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
};
},
/**
* @param {Function} func original function to be debounced
* @param {number} wait number of milliseconds to delay (since last debounce reset).
* Default value 10 msecs.
* @param {Object} scope in which to apply the function after debouncing ends
* @param {boolean} invokeApply should the $timeout trigger $digest() dirty checking
* @return {Function} 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.
*/
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);
};
},
/**
* The function will not be called unless it has been more than `delay` milliseconds since the
* last call.
* @param {Function} func original function to throttle
* @param {number} delay number of milliseconds to delay
* @return {Function} a function that can only be triggered every `delay` milliseconds.
*/
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.
* @param {Object} scope to disconnect
*/
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().
* @param {Object} scope to reconnect
*/
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;
}
},
/**
* Get an element's siblings matching a given tag name.
*
* @param {JQLite|angular.element|HTMLElement} element Element to start walking the DOM from
* @param {string} tagName HTML tag name to match against
* @returns {Object[]} JQLite
*/
getSiblings: function getSiblings(element, tagName) {
var upperCasedTagName = tagName.toUpperCase();
if (element instanceof angular.element) {
element = element[0];
}
var siblings = Array.prototype.filter.call(element.parentNode.children, function(node) {
return element !== node && node.tagName.toUpperCase() === upperCasedTagName;
});
return siblings.map(function (sibling) {
return angular.element(sibling);
});
},
/**
* getClosest replicates jQuery.closest() to walk up the DOM tree until it finds a matching
* nodeName.
*
* @param {Node} el Element to start walking the DOM from
* @param {string|function} validateWith 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 {boolean=} onlyParent Only start checking from the parent element, not `el`.
* @returns {Node|null} closest matching parent Node or null if not found
*/
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)
* @param {Node} node
* @param {Node} child
* @returns {Node}
*/
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 {JQLite} element to scan
* @param {string} nodeName of node to find (e.g. 'md-dialog')
* @param {boolean=} scanDeep optional flag to allow deep scans; defaults to 'false'.
* @param {boolean=} warnNotFound 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 {Function} callback function to be called after the tick
* @param {boolean=} digest true to call $rootScope.$digest() after callback
* @param {Object=} scope associated with callback. If the scope is destroyed, the callback will
* be skipped.
* @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
* overridden 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 {!Element} 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|void} 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;
});
},
/**
* Gets the inner HTML content of the given HTMLElement.
* Only intended for use with SVG or Symbol elements in IE11.
* @param {Element} element
* @returns {string} the inner HTML of the element passed in
*/
getInnerHTML: function(element) {
// For SVG or Symbol elements, innerHTML returns `undefined` in IE.
// Reference: https://stackoverflow.com/q/28129956/633107
// The XMLSerializer API is supported on IE11 and is the recommended workaround.
var serializer = new XMLSerializer();
return Array.prototype.map.call(element.childNodes, function (child) {
return serializer.serializeToString(child);
}).join('');
},
/**
* Gets the outer HTML content of the given HTMLElement.
* Only intended for use with SVG or Symbol elements in IE11.
* @param {Element} element
* @returns {string} the outer HTML of the element passed in
*/
getOuterHTML: function(element) {
// For SVG or Symbol elements, outerHTML returns `undefined` in IE.
// Reference: https://stackoverflow.com/q/29888050/633107
// The XMLSerializer API is supported on IE11 and is the recommended workaround.
var serializer = new XMLSerializer();
return serializer.serializeToString(element);
},
/**
* Support: IE 9-11 only
* documentMode is an IE-only property
* http://msdn.microsoft.com/en-us/library/ie/cc196988(v=vs.85).aspx
*/
msie: window.document.documentMode,
getTouchAction: function() {
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;
}
}
},
/**
* @param {Event} event the event to calculate the bubble path for
* @return {EventTarget[]} the set of nodes that this event could bubble up to
*/
getEventPath: function(event) {
var path = [];
var currentTarget = event.target;
while (currentTarget) {
path.push(currentTarget);
currentTarget = currentTarget.parentElement;
}
if (path.indexOf(window) === -1 && path.indexOf(document) === -1)
path.push(document);
if (path.indexOf(window) === -1)
path.push(window);
return path;
},
/**
* Gets the string the user has entered and removes Regex identifiers
* @param {string} term
* @returns {string} sanitized string
*/
sanitize: function(term) {
if (!term) return term;
return term.replace(/[\\^$*+?.()|{}[]/g, '\\$&');
},
/**********************************************************************************************
* The following functions were sourced from
* https://github.com/angular/components/blob/3c37e4b1c1cb74a3d0a90d173240fc730d21d9d4/src/cdk/a11y/interactivity-checker/interactivity-checker.ts
**********************************************************************************************/
/**
* Gets whether an element is disabled.
* @param {HTMLElement} element Element to be checked.
* @returns {boolean} Whether the element is disabled.
*/
isDisabled: function(element) {
// This does not capture some cases, such as a non-form control with a disabled attribute or
// a form control inside of a disabled form, but should capture the most common cases.
return element.hasAttribute('disabled');
},
/**
* Gets whether an element is visible for the purposes of interactivity.
*
* This will capture states like `display: none` and `visibility: hidden`, but not things like
* being clipped by an `overflow: hidden` parent or being outside the viewport.
*
* @param {HTMLElement} element
* @returns {boolean} Whether the element is visible.
*/
isVisible: function(element) {
return $mdUtil.hasGeometry(element) && getComputedStyle(element).visibility === 'visible';
},
/**
* Gets whether an element can be reached via Tab key.
* Assumes that the element has already been checked with isFocusable.
* @param {HTMLElement} element Element to be checked.
* @returns {boolean} Whether the element is tabbable.
*/
isTabbable: function(element) {
var frameElement = $mdUtil.getFrameElement($mdUtil.getWindow(element));
if (frameElement) {
// Frame elements inherit their tabindex onto all child elements.
if ($mdUtil.getTabIndexValue(frameElement) === -1) {
return false;
}
// Browsers disable tabbing to an element inside of an invisible frame.
if (!$mdUtil.isVisible(frameElement)) {
return false;
}
}
var nodeName = element.nodeName.toLowerCase();
var tabIndexValue = $mdUtil.getTabIndexValue(element);
if (element.hasAttribute('contenteditable')) {
return tabIndexValue !== -1;
}
if (nodeName === 'iframe' || nodeName === 'object') {
// The frame or object's content may be tabbable depending on the content, but it's
// not possibly to reliably detect the content of the frames. We always consider such
// elements as non-tabbable.
return false;
}
// In iOS, the browser only considers some specific elements as tabbable.
if (isIos && !$mdUtil.isPotentiallyTabbableIOS(element)) {
return false;
}
if (nodeName === 'audio') {
// Audio elements without controls enabled are never tabbable, regardless
// of the tabindex attribute explicitly being set.
if (!element.hasAttribute('controls')) {
return false;
}
// Audio elements with controls are by default tabbable unless the
// tabindex attribute is set to `-1` explicitly.
return tabIndexValue !== -1;
}
if (nodeName === 'video') {
// For all video elements, if the tabindex attribute is set to `-1`, the video
// is not tabbable. Note: We cannot rely on the default `HTMLElement.tabIndex`
// property as that one is set to `-1` in Chrome, Edge and Safari v13.1. The
// tabindex attribute is the source of truth here.
if (tabIndexValue === -1) {
return false;
}
// If the tabindex is explicitly set, and not `-1` (as per check before), the
// video element is always tabbable (regardless of whether it has controls or not).
if (tabIndexValue !== null) {
return true;
}
// Otherwise (when no explicit tabindex is set), a video is only tabbable if it
// has controls enabled. Firefox is special as videos are always tabbable regardless
// of whether there are controls or not.
return isFirefox || element.hasAttribute('controls');
}
return element.tabIndex >= 0;
},
/**
* Gets whether an element can be focused by the user.
* @param {HTMLElement} element Element to be checked.
* @returns {boolean} Whether the element is focusable.
*/
isFocusable: function(element) {
// Perform checks in order of left to most expensive.
// Again, naive approach that does not capture many edge cases and browser quirks.
return $mdUtil.isPotentiallyFocusable(element) && !$mdUtil.isDisabled(element) &&
$mdUtil.isVisible(element);
},
/**
* Gets whether an element is potentially focusable without taking current visible/disabled
* state into account.
* @param {HTMLElement} element
* @returns {boolean}
*/
isPotentiallyFocusable: function(element) {
// Inputs are potentially focusable *unless* they're type="hidden".
if ($mdUtil.isHiddenInput(element)) {
return false;
}
return $mdUtil.isNativeFormElement(element) ||
$mdUtil.isAnchorWithHref(element) ||
element.hasAttribute('contenteditable') ||
$mdUtil.hasValidTabIndex(element);
},
/**
* Checks whether the specified element is potentially tabbable on iOS.
* @param {HTMLElement} element
* @returns {boolean}
*/
isPotentiallyTabbableIOS: function(element) {
var nodeName = element.nodeName.toLowerCase();
var inputType = nodeName === 'input' && element.type;
return inputType === 'text'
|| inputType === 'password'
|| nodeName === 'select'
|| nodeName === 'textarea';
},
/**
* Returns the parsed tabindex from the element attributes instead of returning the
* evaluated tabindex from the browsers defaults.
* @param {HTMLElement} element
* @returns {null|number}
*/
getTabIndexValue: function(element) {
if (!$mdUtil.hasValidTabIndex(element)) {
return null;
}
// See browser issue in Gecko https://bugzilla.mozilla.org/show_bug.cgi?id=1128054
var tabIndex = parseInt(element.getAttribute('tabindex') || '', 10);
return isNaN(tabIndex) ? -1 : tabIndex;
},
/**
* Gets whether an element has a valid tabindex.
* @param {HTMLElement} element
* @returns {boolean}
*/
hasValidTabIndex: function(element) {
if (!element.hasAttribute('tabindex') || element.tabIndex === undefined) {
return false;
}
var tabIndex = element.getAttribute('tabindex');
// IE11 parses tabindex="" as the value "-32768"
if (tabIndex == '-32768') {
return false;
}
return !!(tabIndex && !isNaN(parseInt(tabIndex, 10)));
},
/**
* Checks whether the specified element has any geometry / rectangles.
* @param {HTMLElement} element
* @returns {boolean}
*/
hasGeometry: function(element) {
// Use logic from jQuery to check for an invisible element.
// See https://github.com/jquery/jquery/blob/8969732518470a7f8e654d5bc5be0b0076cb0b87/src/css/hiddenVisibleSelectors.js#L9
return !!(element.offsetWidth || element.offsetHeight ||
(typeof element.getClientRects === 'function' && element.getClientRects().length));
},
/**
* Returns the frame element from a window object. Since browsers like MS Edge throw errors if
* the frameElement property is being accessed from a different host address, this property
* should be accessed carefully.
* @param {Window} window
* @returns {null|HTMLElement}
*/
getFrameElement: function(window) {
try {
return window.frameElement;
} catch (error) {
return null;
}
},
/**
* Gets the parent window of a DOM node with regards of being inside of an iframe.
* @param {HTMLElement} node
* @returns {Window}
*/
getWindow: function(node) {
// ownerDocument is null if `node` itself *is* a document.
return node.ownerDocument && node.ownerDocument.defaultView || window;
},
/**
* Gets whether an element's
* @param {Node} element
* @returns {boolean}
*/
isNativeFormElement: function(element) {
var nodeName = element.nodeName.toLowerCase();
return nodeName === 'input' ||
nodeName === 'select' ||
nodeName === 'button' ||
nodeName === 'textarea';
},
/**
* Gets whether an element is an ``.
* @param {HTMLElement} element
* @returns {boolean}
*/
isHiddenInput: function(element) {
return $mdUtil.isInputElement(element) && element.type == 'hidden';
},
/**
* Gets whether an element is an anchor that has an href attribute.
* @param {HTMLElement} element
* @returns {boolean}
*/
isAnchorWithHref: function(element) {
return $mdUtil.isAnchorElement(element) && element.hasAttribute('href');
},
/**
* Gets whether an element is an input element.
* @param {HTMLElement} element
* @returns {boolean}
*/
isInputElement: function(element) {
return element.nodeName.toLowerCase() == 'input';
},
/**
* Gets whether an element is an anchor element.
* @param {HTMLElement} element
* @returns {boolean}
*/
isAnchorElement: function(element) {
return element.nodeName.toLowerCase() == 'a';
},
/**********************************************************************************************
* The following two functions were sourced from
* https://github.com/angular/components/blob/3c37e4b1c1cb74a3d0a90d173240fc730d21d9d4/src/cdk/a11y/focus-trap/focus-trap.ts#L268-L311
**********************************************************************************************/
/**
* Get the first tabbable element from a DOM subtree (inclusive).
* @param {HTMLElement} root
* @returns {HTMLElement|null}
*/
getFirstTabbableElement: function(root) {
if ($mdUtil.isFocusable(root) && $mdUtil.isTabbable(root)) {
return root;
}
// Iterate in DOM order. Note that IE doesn't have `children` for SVG so we fall
// back to `childNodes` which includes text nodes, comments etc.
var children = root.children || root.childNodes;
for (var i = 0; i < children.length; i++) {
var tabbableChild = children[i].nodeType === $document[0].ELEMENT_NODE ?
$mdUtil.getFirstTabbableElement(children[i]) : null;
if (tabbableChild) {
return tabbableChild;
}
}
return null;
},
/**
* Get the last tabbable element from a DOM subtree (inclusive).
* @param {HTMLElement} root
* @returns {HTMLElement|null}
*/
getLastTabbableElement: function(root) {
if ($mdUtil.isFocusable(root) && $mdUtil.isTabbable(root)) {
return root;
}
// Iterate in reverse DOM order.
var children = root.children || root.childNodes;
for (var i = children.length - 1; i >= 0; i--) {
var tabbableChild = children[i].nodeType === $document[0].ELEMENT_NODE ?
$mdUtil.getLastTabbableElement(children[i]) : null;
if (tabbableChild) {
return tabbableChild;
}
}
return null;
}
};
// 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";
// 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.
* @returns {null|DOMRect}
*/
function currentBounds() {
var container = element ? element.parent() : null;
var parent = container ? container.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).
* @param {string} transform
* @param {boolean=} addTransition
* @param {string=} transition
* @return {Object} object containing CSS translate 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.
* @param {DOMRect} source
* @param {DOMRect=} destination
* @returns {null|DOMRect}
*/
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.
* @param {Element|string} element
* @returns {null|DOMRect}
*/
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
* @param {DOMRect} targetRect
* @returns {{x: number, y: number}}
*/
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.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 {string|JQLite} element
* @param {string} attrName
* @param {string=} 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);
}
}
/**
* @param {Element|JQLite} element
* @returns {string}
*/
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() || '';
/**
* @param {Node} node
* @returns {boolean}
*/
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 {string|JQLite|Node & ParentNode} element
* @param {number=} 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 block-list 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 block-list 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);
MdCompilerProvider.$inject = ['$compileProvider'];
function MdCompilerProvider() {
this.$get = ["$q", "$templateRequest", "$injector", "$compile", "$controller",
function($q, $templateRequest, $injector, $compile, $controller) {
return new MdCompilerService($q, $templateRequest, $injector, $compile, $controller);
}];
/**
* @ngdoc service
* @name $mdCompiler
* @module material.core.compiler
* @description
* The $mdCompiler service is an abstraction of AngularJS's compiler, that allows developers
* to 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` services.
*
* @usage
*
* Basic Usage with a template
*
*
* $mdCompiler.compile({
* templateUrl: 'modal.html',
* controller: 'ModalCtrl',
* locals: {
* modal: myModalInstance;
* }
* }).then(function (compileData) {
* compileData.element; // Compiled DOM element
* compileData.link(myScope); // Instantiate controller and link element to scope.
* });
*
*
* Example with a content element
*
*
*
* // Create a virtual element and link it manually.
* // The compiler doesn't need to recompile the element each time.
* var myElement = $compile('Test')(myScope);
*
* $mdCompiler.compile({
* contentElement: myElement
* }).then(function (compileData) {
* compileData.element // Content Element (same as above)
* compileData.link // This does nothing when using a contentElement.
* });
*
*
* > Content Element is a significant performance improvement when the developer already knows
* > that the compiled element will be always the same and the scope will not change either.
*
* The `contentElement` option also supports DOM elements which will be temporary removed and
* restored at its old position.
*
*
* var domElement = document.querySelector('#myElement');
*
* $mdCompiler.compile({
* contentElement: myElement
* }).then(function (compileData) {
* compileData.element // Content Element (same as above)
* compileData.link // This does nothing when using a contentElement.
* });
*
*
* The `$mdCompiler` can also query for the element in the DOM itself.
*
*
* $mdCompiler.compile({
* contentElement: '#myElement'
* }).then(function (compileData) {
* compileData.element // Content Element (same as above)
* compileData.link // This does nothing when using a contentElement.
* });
*
*
*/
function MdCompilerService($q, $templateRequest, $injector, $compile, $controller) {
/**
* @private @const
* @type {!IQService}
*/
this.$q = $q;
/**
* @private @const
* @type {!ITemplateRequestService}
*/
this.$templateRequest = $templateRequest;
/**
* @private @const
* @type {!IInjectorService}
*/
this.$injector = $injector;
/**
* @private @const
* @type{!ICompileService}
*/
this.$compile = $compile;
/**
* @private @const
* @type {!IControllerService}
*/
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 {Q.Promise<{element: JQLite, link: Function, locals: Object, cleanup: any,
* controller: Object=}>} promise A promise, which will be resolved with a `compileData` object.
* `compileData` has the following properties:
*
* - `element` - `{JQLite}`: 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 copied 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.
* @returns {Q.Promise<{element: JQLite, link: Function, locals: Object, cleanup: any}>}
*/
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 {!Q.Promise<{element: JQLite, link: Function, locals: Object, cleanup: any}>} 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 {!{element: JQLite, link: Function, locals: Object, cleanup: any, controller: 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);
// Registering extra $destroy listeners should be avoided.
// Only register the listener if the controller implements a $onDestroy hook.
if (angular.isFunction(ctrl.$onDestroy)) {
scope.$on('$destroy', function() {
// Call the $onDestroy hook if it's present on the controller.
angular.isFunction(ctrl.$onDestroy) && ctrl.$onDestroy();
});
}
// 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 function or string.
* @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 ctrl = this.$controller(options.controller, injectLocals);
if (options.bindToController) {
angular.extend(ctrl, locals);
}
if (options.controllerAs) {
injectLocals.$scope[options.controllerAs] = ctrl;
}
// 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;
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", "$mdUtil"];
attachToDocument.$inject = ["$mdGesture", "$$MdGestureHandler", "$mdUtil"];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, maxClickDistance = 6;
var forceSkipClickHijack = false, disableAllGestures = false;
/**
* The position of the most recent click if that click was on a label element.
* @type {{x: number, y: number}|null}
*/
var lastLabelClickPos = null;
/**
* Used to attach event listeners once when multiple ng-apps are running.
* @type {boolean}
*/
var isInitialized = false;
/**
* @ngdoc module
* @name material.core.gestures
* @description
* AngularJS Material Gesture handling for touch devices.
* This module replaced the usage of the HammerJS library.
*/
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 the 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 hijacking clicks, you may want to change the default click distance
* $mdGestureProvider.setMaxClickDistance(12);
* });
*
*
*/
function MdGestureProvider() { }
MdGestureProvider.prototype = {
/**
* @ngdoc method
* @name $mdGestureProvider#disableAll
*
* @description
* Disable all gesture detection. This can be beneficial to application performance
* and memory usage.
*/
disableAll: function () {
disableAllGestures = true;
},
// Publish access to setter to configure a variable BEFORE the
// $mdGesture service is instantiated...
/**
* @ngdoc method
* @name $mdGestureProvider#skipClickHijack
*
* @description
* Tell the AngularJS Material Gesture module to skip (or ignore) click hijacking on mobile devices.
*/
skipClickHijack: function() {
return forceSkipClickHijack = true;
},
/**
* @ngdoc method
* @name $mdGestureProvider#setMaxClickDistance
* @param clickDistance {string} Distance in pixels. I.e. `12px`.
* @description
* Set the max distance from the origin of the touch event to trigger touch handlers.
*/
setMaxClickDistance: function(clickDistance) {
maxClickDistance = parseInt(clickDistance);
},
/**
* $get is used to build an instance of $mdGesture
* @ngInject
*/
$get : ["$$MdGestureHandler", "$$rAF", "$timeout", "$mdUtil", function($$MdGestureHandler, $$rAF, $timeout, $mdUtil) {
return new MdGesture($$MdGestureHandler, $$rAF, $timeout, $mdUtil);
}]
};
/**
* MdGesture factory construction function
* @ngInject
*/
function MdGesture($$MdGestureHandler, $$rAF, $timeout, $mdUtil) {
var touchActionProperty = $mdUtil.getTouchAction();
var hasJQuery = (typeof window.jQuery !== 'undefined') && (angular.element === window.jQuery);
var self = {
handler: addHandler,
register: register,
isAndroid: $mdUtil.isAndroid,
isIos: $mdUtil.isIos,
// On mobile w/out jQuery, we normally intercept clicks. Should we skip that?
isHijackingClicks: ($mdUtil.isIos || $mdUtil.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, horizontal: 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
},
/**
* @param {angular.JQLite} element where touch action styles need to be adjusted
* @param {{horizontal: boolean}=} options object whose horizontal property can specify to
* apply 'pan-y' or 'pan-x' touch actions.
*/
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';
}
},
/**
* @param {angular.JQLite} element where styles need to be cleaned up
*/
onCleanup: function(element) {
if (this.oldTouchAction) {
element[0].style[touchActionProperty] = this.oldTouchAction;
} else {
element[0].style[touchActionProperty] = null;
}
},
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);
}
}
});
}
/**
* 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.state.isRunning = false;
this.onEnd(ev, pointer);
},
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') {
if (typeof window.MouseEvent === "function") {
eventObj = new MouseEvent(eventType, {
bubbles: true,
cancelable: true,
screenX: Number(srcEvent.screenX),
screenY: Number(srcEvent.screenY),
clientX: Number(eventPointer.x),
clientY: Number(eventPointer.y),
ctrlKey: srcEvent.ctrlKey,
altKey: srcEvent.altKey,
shiftKey: srcEvent.shiftKey,
metaKey: srcEvent.metaKey,
button: srcEvent.button,
buttons: srcEvent.buttons,
relatedTarget: srcEvent.relatedTarget || null
});
} else {
eventObj = document.createEvent('MouseEvents');
// This has been deprecated
// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/initMouseEvent
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 {
if (typeof window.CustomEvent === "function") {
eventObj = new CustomEvent(eventType, {
bubbles: true,
cancelable: true,
detail: {}
});
} else {
// This has been deprecated
// https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/initCustomEvent
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, $mdUtil) {
if (disableAllGestures) {
return;
}
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();
}
}
/**
* Ignore click events that don't come from AngularJS Material, Ionic, Input Label clicks,
* or key presses that generate click events. This helps to ignore the ghost tap events on
* older mobile browsers that get sent after a 300-400ms delay.
* @param ev MouseEvent or modified MouseEvent with $material, pointer, and other fields
*/
function clickHijacker(ev) {
var isKeyClick;
if ($mdUtil.isIos) {
isKeyClick = angular.isDefined(ev.webkitForce) && ev.webkitForce === 0;
} else {
isKeyClick = ev.clientX === 0 && ev.clientY === 0;
}
if (!isKeyClick && !ev.$material && !ev.isIonicTap && !isInputEventFromLabelClick(ev)) {
ev.preventDefault();
ev.stopPropagation();
lastLabelClickPos = null;
} else {
lastLabelClickPos = null;
if (ev.target.tagName.toLowerCase() === 'label') {
lastLabelClickPos = {x: ev.x, y: ev.y};
}
}
}
// Listen to all events to cover all platforms.
var START_EVENTS = 'mousedown touchstart pointerdown';
var MOVE_EVENTS = 'mousemove touchmove pointermove';
var END_EVENTS = 'mouseup mouseleave touchend touchcancel pointerup pointercancel';
angular.element(document)
.on(START_EVENTS, gestureStart)
.on(MOVE_EVENTS, gestureMove)
.on(END_EVENTS, gestureEnd)
// For testing
.on('$$mdGestureReset', function gestureClearCache () {
lastPointer = pointer = null;
});
/**
* When a DOM event happens, run all registered gesture handlers' lifecycle
* methods which match the DOM event.
* Eg. when a 'touchstart' event happens, runHandlers('start') will call and
* run `handler.cancel()` and `handler.start()` on all registered handlers.
*/
function runHandlers(handlerEvent, event) {
var handler;
for (var name in HANDLERS) {
handler = HANDLERS[name];
if (handler instanceof $$MdGestureHandler) {
if (handlerEvent === 'start') {
// Run cancel to reset any handlers' state
handler.cancel();
}
handler[handlerEvent](event, pointer);
}
}
}
/*
* gestureStart vets if a start event is legitimate (and not part of a 'ghost click' from iOS/Android)
* If it is legitimate, we initiate the pointer state and mark the current pointer's type
* For example, for a touchstart event, mark the current pointer as a 'touch' pointer, so mouse events
* won't effect it.
*/
function gestureStart(ev) {
// If we're already touched down, abort
if (pointer) return;
var now = +Date.now();
// iOS & old android bug: after a touch event, a click event is sent 350 ms later.
// If <400ms have passed, don't allow an event of a different type than the previous event
if (lastPointer && !typesMatch(ev, lastPointer) && (now - lastPointer.endTime < 1500)) {
return;
}
pointer = makeStartPointer(ev);
runHandlers('start', ev);
}
/**
* If a move event happens of the right type, update the pointer and run all the move handlers.
* "of the right type": if a mousemove happens but our pointer started with a touch event, do
* nothing.
*/
function gestureMove(ev) {
if (!pointer || !typesMatch(ev, pointer)) return;
updatePointerState(ev, pointer);
runHandlers('move', ev);
}
/**
* If an end event happens of the right type, update the pointer, run endHandlers, and save the
* pointer as 'lastPointer'.
*/
function gestureEnd(ev) {
if (!pointer || !typesMatch(ev, pointer)) return;
updatePointerState(ev, pointer);
pointer.endTime = +Date.now();
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", "$rootScope"];
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, $rootScope) {
this.$timeout = $timeout;
this.$mdUtil = $mdUtil;
this.$rootScope = $rootScope;
// IE browsers can also trigger pointer events, which also leads to an interaction.
this.pointerEvent = 'MSPointerEvent' in window ? 'MSPointerDown' : 'PointerEvent' in window ? 'pointerdown' : null;
this.bodyElement = angular.element(document.body);
this.isBuffering = false;
this.bufferTimeout = null;
this.lastInteractionType = null;
this.lastInteractionTime = null;
this.inputHandler = this.onInputEvent.bind(this);
this.bufferedInputHandler = this.onBufferInputEvent.bind(this);
// 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();
this.$rootScope.$on('$destroy', this.deregister.bind(this));
}
/**
* Removes all event listeners created by $mdInteration on the
* body element.
*/
MdInteractionService.prototype.deregister = function() {
this.bodyElement.off('keydown mousedown', this.inputHandler);
if ('ontouchstart' in document.documentElement) {
this.bodyElement.off('touchstart', this.bufferedInputHandler);
}
if (this.pointerEvent) {
this.bodyElement.off(this.pointerEvent, this.inputHandler);
}
};
/**
* Initializes the interaction service, by registering all interaction events to the
* body element.
*/
MdInteractionService.prototype.initializeEvents = function() {
this.bodyElement.on('keydown mousedown', this.inputHandler);
if ('ontouchstart' in document.documentElement) {
this.bodyElement.on('touchstart', this.bufferedInputHandler);
}
if (this.pointerEvent) {
this.bodyElement.on(this.pointerEvent, this.inputHandler);
}
};
/**
* 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 $$interimElementProvider
* @module material.core.interimElement
*
* @description
*
* Factory that constructs `$$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.
*
*
* 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 $$interimElementProvider.$service
*
* @description
* A service used to control inserting and removing of an element from the DOM.
* It is used by $mdBottomSheet, $mdDialog, $mdToast, $mdMenu, $mdPanel, and $mdSelect.
*/
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;
return service = {
show: show,
hide: waitForInterim(hide),
cancel: waitForInterim(cancel),
destroy : destroy,
$injector_: $injector
};
/**
* @ngdoc method
* @name $$interimElementProvider.$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.
*
* @param {Object} options map of options and values
* @returns {Promise} a Promise that will be resolved when hide() is called or rejected when
* cancel() is called.
*/
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()
.then(function () {
showingInterims.push(interimElement);
})
.catch(function (reason) {
return reason;
})
.finally(function() {
showPromises.splice(showPromises.indexOf(showAction), 1);
});
});
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 $$interimElementProvider.$service#hide
* @kind function
*
* @description
* Removes the `$interimElement` from the DOM and resolves the Promise returned from `show()`.
*
* @param {*} reason Data used to resolve the Promise
* @param {object} options map of options and values
* @returns {Promise} a Promise that will be resolved after the element has been removed
* from the DOM.
*/
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]);
/**
* @param {InterimElement} interim element to close
* @returns {Promise}
*/
function closeElement(interim) {
if (!interim) {
return $q.when(reason);
}
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 $$interimElementProvider.$service#cancel
* @kind function
*
* @description
* Removes the `$interimElement` from the DOM and rejects the Promise returned from `show()`.
*
* @param {*} reason Data used to resolve the Promise
* @param {object} options map of options and values
* @returns {Promise} Promise that will be resolved after the element has been removed
* from the DOM.
*/
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);
};
}
/**
* @ngdoc method
* @name $$interimElementProvider.$service#destroy
* @kind function
*
* Special method to quick-remove the interim element without running animations. This is
* useful when the parent component has been or is being destroyed.
*
* 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 autoHide and transition-in
* effects.
* @return {Q.Promise}
*/
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
* @param {Object} options
* @return {Q.Promise<{element: JQLite=, link: Function, locals: Object, cleanup: any=,
* controller: Object=}>}
*/
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
* @param {{element: JQLite=, link: Function, locals: Object, controller: Object=}} compileData
* @param {Object} options
* @return {JQLite}
*/
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.
* @param {JQLite} element
* @param {Object} options
* @return {JQLite}
*/
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 hiding
* timer.
* @param {JQLite} element
* @param {Object} options
* @param {Object} controller
* @return {Q.Promise}
*/
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 {
// This fourth controller parameter is used by $mdDialog in beforeShow().
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))
.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'))
// 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;
}
};
}];
/**
* Observe deprecated layout attributes and update the element's layout classes to match.
*/
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 transformed class 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);
}
};
}
/**
* 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 + ">" + nodeName + ">";
url = "https://github.com/philipwalton/flexbugs#9-some-html-elements-cant-be-flex-containers";
message = "Markup '{0}' may not work as expected in IE Browsers. Consult '{1}' for details.";
$log.warn($mdUtil.supplant(message, [usage, url]));
}
}
}
/**
* For the Layout attribute value, validate or replace with default fallback value.
*/
function validateAttributeValue(className, value, updateFn) {
var origValue = value;
if (!needsInterpolation(value)) {
switch (className.replace(SUFFIXES,"")) {
case 'layout' :
if (!findIn(value, LAYOUT_OPTIONS)) {
value = LAYOUT_OPTIONS[0]; // 'row';
}
break;
case 'flex' :
if (!findIn(value, FLEX_OPTIONS)) {
if (isNaN(value)) {
value = '';
}
}
break;
case 'flex-offset' :
case 'flex-order' :
if (!value || isNaN(+value)) {
value = '0';
}
break;
case 'layout-align' :
var axis = extractAlignAxis(value);
value = $mdUtil.supplant("{main}-{cross}",axis);
break;
case 'layout-padding' :
case 'layout-margin' :
case 'layout-fill' :
case 'layout-wrap' :
case 'layout-nowrap' :
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 });
* }
* });
*
*/
/**
* @ngdoc service
* @name $mdInkRippleProvider
* @module material.core.ripple
*
* @description
* If you want to disable ink ripples globally, for all components, you can call the
* `disableInkRipple` method in your app's config.
*
*
* @usage
*
* 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 $mdInkRippleProvider#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 {string} color
* @param {number} multiplier
* @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 () {
this.$timeout(function () {
autoCleanup(this, this.clearRipples);
}.bind(this));
};
/**
* 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 400 500 600 A100 A200 A400',
'contrastStrongLightColors': '700 800 900 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 300 400 A100 A200 A400',
// White on 500 does not meet the minimum 4.5 contrast ratio (at 4.34),
// but it's worse with a dark foreground (3.61).
'contrastStrongLightColors': '500 600 700 800 900 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 300 A100 A200 A400',
'contrastStrongLightColors': '400 500 600 700 800 900 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 300 A100',
'contrastStrongLightColors': '400 500 600 700 800 900 A200 A400 A700'
},
'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 300 A100 A200',
'contrastStrongLightColors': '400 500 600 700 800 900 A400 A700'
},
'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',
// White on A400 does not meet the minimum 4.5 contrast ratio (at 3.98),
// but it's worse with a dark foreground (3.94).
'contrastDarkColors': '50 100 200 300 400 500 600 A100 A200',
'contrastStrongLightColors': '700 800 900 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',
// Dark on 700 does not meet the minimum 4.5 contrast ratio (at 4.07),
// but it's worse with a white foreground (3.85).
'contrastStrongLightColors': '800 900 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',
// Dark on 700 does not meet the minimum 4.5 contrast ratio (at 4.47),
// but it's worse with a white foreground (3.5).
'contrastStrongLightColors': '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',
// Dark on 500 does not meet the minimum 4.5 contrast ratio (at 4.27),
// but it's worse with a white foreground (3.67).
// White on 600 does not meet the minimum 4.5 contrast ratio (at 4.31),
// but it's worse with a dark foreground (3.64).
'contrastStrongLightColors': '600 700 800 900'
},
'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',
// White on 700 does not meet the minimum 4.5 contrast ratio (at 4.11),
// but it's worse with a dark foreground (3.81).
'contrastStrongLightColors': '700 800 900'
},
'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',
'contrastStrongLightColors': '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',
'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',
'contrastStrongLightColors': '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': 'dark',
// Dark on 700 does not meet the minimum 4.5 contrast ratio (at 4.01),
// but it's worse with a white foreground (3.91).
// White on 800 does not meet the minimum 4.5 contrast ratio (at 4.43),
// but it's worse with a dark foreground (3.54).
'contrastStrongLightColors': '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 300 A100 A200',
'contrastStrongLightColors': '400 500 600 700 800 900 A400 A700'
},
'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': '700 800 900 A200 A400 A700',
'contrastStrongLightColors': '600'
},
'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 400 A100 A200 A400',
// White on 500 does not meet the minimum 4.5 contrast ratio (at 4.37),
// but it's worse with a dark foreground.
'contrastStrongLightColors': '500 600 700 800 900 A700'
}
});
})();
(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'
* });
* });
*
*/
/**
* 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
* {{primary-contrast-divider}} - Apply divider opacity to contrast color
*
* Foreground expansion: Applies rgba to black/white foreground text
*
* Old Foreground Expressions:
* {{foreground-1}} - used for primary text
* {{foreground-2}} - used for secondary text/divider
* {{foreground-3}} - used for disabled text
* {{foreground-4}} - used for dividers
*
* New Foreground Expressions:
*
* Apply primary text color for contrasting with default background
* {{background-default-contrast}} - default opacity
* {{background-default-contrast-secondary}} - opacity for secondary text
* {{background-default-contrast-hint}} - opacity for hints and placeholders
* {{background-default-contrast-disabled}} - opacity for disabled text
* {{background-default-contrast-divider}} - opacity for dividers
*
* Apply contrast color for specific shades
* {{background-50-contrast-icon}} - Apply contrast color for icon on background's shade 50 hue
*/
// 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 are automatically generated based on background color when not specified
// Custom palettes can provide override colors
// @see https://material.io/archive/guidelines/style/color.html#color-usability
var DARK_FOREGROUND = {
name: 'dark',
};
var LIGHT_FOREGROUND = {
name: 'light',
};
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'
}
};
// Icon opacity values (active/inactive) from
// https://material.io/archive/guidelines/style/color.html#color-usability
var DARK_CONTRAST_OPACITY = {
'icon': 0.54,
'secondary': 0.54,
'disabled': 0.38,
'hint': 0.38,
'divider': 0.12,
};
var LIGHT_CONTRAST_OPACITY = {
'icon': 0.87,
'secondary': 0.7,
'disabled': 0.5,
'hint': 0.5,
'divider': 0.12
};
// Icon opacity values (active/inactive) from
// https://material.io/archive/guidelines/style/color.html#color-usability
var STRONG_LIGHT_CONTRAST_OPACITY = {
'icon': 1.0,
'secondary': 0.7,
'disabled': 0.5,
'hint': 0.5,
'divider': 0.12
};
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();
};
};
/**
* @ngdoc method
* @name $mdThemingProvider#enableBrowserColor
* @description
* Enables browser header coloring. For more info please visit
*
* Web Fundamentals.
* @param {object=} options Options for the browser color, which include:
* - `theme` - `{string}`: A defined theme via `$mdThemeProvider` to use the palettes from. Default is `default` theme.
* - `palette` - `{string}`: Can be any one of the basic material design palettes, extended defined palettes, or `primary`,
* `accent`, `background`, and `warn`. Default is `primary`.
* - `hue` - `{string}`: The hue from the selected palette. Default is `800`.
* @returns {function} Function that removes the browser coloring when called.
*/
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];
if (color.substr(0, 1) !== '#') color = '#' + color;
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)
});
},
/**
* @ngdoc method
* @name $mdThemingProvider#disableTheming
* @description
* An easier way to disable theming without having to use `.constant("$MD_THEME_CSS","");`.
* This disables all dynamic theme style sheet generations and injections.
* @param {boolean=} isDisabled Disable all dynamic theme style sheet generations and injections
* if `true` or `undefined`.
*/
disableTheming: function(isDisabled) {
themeConfig.disableTheming = angular.isUndefined(isDisabled) || !!isDisabled;
},
/**
* @ngdoc method
* @name $mdThemingProvider#registerStyles
* @param {string} styles The styles to be appended to AngularJS Material's built in theme CSS.
*/
registerStyles: function(styles) {
themeConfig.registeredStyles.push(styles);
},
/**
* @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.
* The string must already be base64 encoded. You can use `btoa(string)` to do this encoding.
* In your CSP's `style-src`, you would then add an entry for `'nonce-nonceValue'`.
*/
setNonce: function(nonceValue) {
themeConfig.nonce = nonceValue;
},
generateThemesOnDemand: function(onDemand) {
themeConfig.generateOnDemand = onDemand;
},
/**
* @ngdoc method
* @name $mdThemingProvider#setDefaultTheme
* @param {string} theme Default theme name to be applied to elements.
* Default value is `default`.
*/
setDefaultTheme: function(theme) {
defaultTheme = theme;
},
/**
* @ngdoc method
* @name $mdThemingProvider#alwaysWatchTheme
* @param {boolean} alwaysWatch Whether or not to always watch themes for changes and re-apply
* classes when they change. Default is `false`. Enabling can reduce performance.
*/
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
};
/**
* @ngdoc method
* @name $mdThemingProvider#definePalette
* @description
* In the event that you need to define a custom color palette, you can use this function to
* make it available to your theme for use in its intention groups.
* Note that you must specify all hues in the definition map.
* @param {string} name Name of palette being defined
* @param {object} map Palette definition that includes hue definitions and contrast colors:
* - `'50'` - `{string}`: HEX color
* - `'100'` - `{string}`: HEX color
* - `'200'` - `{string}`: HEX color
* - `'300'` - `{string}`: HEX color
* - `'400'` - `{string}`: HEX color
* - `'500'` - `{string}`: HEX color
* - `'600'` - `{string}`: HEX color
* - `'700'` - `{string}`: HEX color
* - `'800'` - `{string}`: HEX color
* - `'900'` - `{string}`: HEX color
* - `'A100'` - `{string}`: HEX color
* - `'A200'` - `{string}`: HEX color
* - `'A400'` - `{string}`: HEX color
* - `'A700'` - `{string}`: HEX color
* - `'contrastDefaultColor'` - `{string}`: `light` or `dark`
* - `'contrastDarkColors'` - `{string[]}`: Hues which should use dark contrast colors (i.e. raised button text).
* For example: `['50', '100', '200', '300', '400', 'A100']`.
* - `'contrastLightColors'` - `{string[]}`: Hues which should use light contrast colors (i.e. raised button text).
* For example: `['500', '600', '700', '800', '900', 'A200', 'A400', 'A700']`.
*/
function definePalette(name, map) {
map = map || {};
PALETTES[name] = checkPaletteValid(name, map);
return themingProvider;
}
/**
* @ngdoc method
* @name $mdThemingProvider#extendPalette
* @description
* Sometimes it is easier to extend an existing color palette and then change a few properties,
* rather than defining a whole new palette.
* @param {string} name Name of palette being extended
* @param {object} map Palette definition that includes optional hue definitions and contrast colors:
* - `'50'` - `{string}`: HEX color
* - `'100'` - `{string}`: HEX color
* - `'200'` - `{string}`: HEX color
* - `'300'` - `{string}`: HEX color
* - `'400'` - `{string}`: HEX color
* - `'500'` - `{string}`: HEX color
* - `'600'` - `{string}`: HEX color
* - `'700'` - `{string}`: HEX color
* - `'800'` - `{string}`: HEX color
* - `'900'` - `{string}`: HEX color
* - `'A100'` - `{string}`: HEX color
* - `'A200'` - `{string}`: HEX color
* - `'A400'` - `{string}`: HEX color
* - `'A700'` - `{string}`: HEX color
* - `'contrastDefaultColor'` - `{string}`: `light` or `dark`
* - `'contrastDarkColors'` - `{string[]}`: Hues which should use dark contrast colors (i.e. raised button text).
* For example: `['50', '100', '200', '300', '400', 'A100']`.
* - `'contrastLightColors'` - `{string[]}`: Hues which should use light contrast colors (i.e. raised button text).
* For example: `['500', '600', '700', '800', '900', 'A200', 'A400', 'A700']`.
* @returns {object} A new object which is a copy of the given palette, `name`,
* with variables from `map` overwritten.
*/
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;
}
/**
* @ngdoc method
* @name $mdThemingProvider#theme
* @description
* Register a theme (which is a collection of color palettes); i.e. `warn`, `accent`,
* `background`, and `primary`.
* Optionally inherit from an existing theme.
* @param {string} name Name of theme being registered
* @param {string=} inheritFrom Existing theme name to inherit from
*/
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;
};
});
}
/**
* @ngdoc service
* @name $mdTheming
* @module material.core.theming
* @description
* Service that makes an element apply theming related classes to itself.
*
* For more information on the hue objects, their default values, as well as valid hue values, please visit the custom hues section of Configuring a Theme.
*
*
* // Example component directive that we want to apply theming classes to.
* app.directive('myFancyDirective', function($mdTheming) {
* return {
* restrict: 'AE',
* link: function(scope, element, attrs) {
* // Initialize the service using our directive's element
* $mdTheming(element);
*
* $mdTheming.defineTheme('myTheme', {
* primary: 'blue',
* primaryHues: {
* default: '500',
* hue-1: '300',
* hue-2: '900',
* hue-3: 'A100'
* },
* accent: 'pink',
* accentHues: {
* default: '600',
* hue-1: '300',
* hue-2: '200',
* hue-3: 'A500'
* },
* warn: 'red',
* // It's not necessary to specify all hues in the object.
* warnHues: {
* default: '200',
* hue-3: 'A100'
* },
* // It's not necessary to specify custom hues at all.
* background: 'grey',
* dark: true
* });
* // Your directive's custom code here.
* }
* };
* });
*
* @param {element=} element Element that will have theming classes applied to it.
*/
/**
* @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 {string} name theme name to generate
*/
/**
* @ngdoc method
* @name $mdTheming#setBrowserColor
* @description
* Enables browser header coloring. For more info please visit
*
* Web Fundamentals.
* @param {object=} options Options for the browser color, which include:
* - `theme` - `{string}`: A defined theme via `$mdThemeProvider` to use the palettes from.
* Default is `default` theme.
* - `palette` - `{string}`: Can be any one of the basic material design palettes, extended
* defined palettes, or `primary`, `accent`, `background`, and `warn`. Default is `primary`.
*
* - `hue` - `{string}`: The hue from the selected palette. Default is `800`.
* @returns {function} Function that removes the browser coloring when called.
*/
/**
* @ngdoc method
* @name $mdTheming#defineTheme
* @description
* Dynamically define a theme by using an options object that contains palette names.
*
* @param {string} name Theme name to define
* @param {object} options Theme definition options
*
* Options are:
* - `primary` - `{string}`: The name of the primary palette to use in the theme.
* - `primaryHues` - `{object=}`: Override hues for primary palette.
* - `accent` - `{string}`: The name of the accent palette to use in the theme.
* - `accentHues` - `{object=}`: Override hues for accent palette.
* - `warn` - `{string}`: The name of the warn palette to use in the theme.
* - `warnHues` - `{object=}`: Override hues for warn palette.
* - `background` - `{string}`: The name of the background palette to use in the theme.
* - `backgroundHues` - `{object=}`: Override hues for background palette.
* - `dark` - `{boolean}`: Indicates if it's a dark theme.
* @returns {Promise} A resolved promise with the new 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, options.primaryHues);
}
if (options.accent) {
theme.accentPalette(options.accent, options.accentHues);
}
if (options.warn) {
theme.warnPalette(options.warn, options.warnHues);
}
if (options.background) {
theme.backgroundPalette(options.background, options.backgroundHues);
}
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');
var scope = el.scope();
updateThemeClass(lookupThemeName());
if (ctrl) {
var watchTheme = alwaysWatchTheme ||
ctrl.$shouldWatch ||
$mdUtil.parseAttributeBoolean(el.attr('md-theme-watch'));
if (watchTheme || ctrl.isAsyncTheme) {
var clearNameWatcher = function () {
if (unwatch) {
unwatch();
unwatch = undefined;
}
};
var unwatch = ctrl.registerChanges(function(name) {
updateThemeClass(name);
if (!watchTheme) {
clearNameWatcher();
}
});
if (scope) {
scope.$on('$destroy', clearNameWatcher);
} else {
el.on('$destroy', clearNameWatcher);
}
}
}
/**
* 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 getTheme = function () {
var interpolation = $interpolate(attrs.mdTheme)(scope);
return $parse(interpolation)(scope) || interpolation;
};
var ctrl = {
isAsyncTheme: angular.isFunction(getTheme()) || angular.isFunction(getTheme().then),
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 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 themeNameRegex = new RegExp('\\.md-' + theme.name + '-theme', 'g');
// Matches '{{ primary-color }}', etc
var hueRegex = new RegExp('([\'"])?{{\\s*([a-zA-Z]+)-?(color|default)?-?(contrast)?-?((?:\\d\\.?\\d*)|(?:[a-zA-Z]+))?\\s*}}(["\'])?','g');
var simpleVariableRegex = /'?"?{{\s*([a-zA-Z]+)-(A?\d+|hue-[0-3]|shadow|default)-?(contrast)?-?((?:\d\.?\d*)|(?:[a-zA-Z]+))?\s*}}'?"?/g;
var defaultBgHue = theme.colors['background'].hues['default'];
var defaultBgContrastType = PALETTES[theme.colors['background'].name][defaultBgHue].contrastType;
// 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, contrast, opacity) {
var regexColorType = colorType;
if (colorType === 'foreground') {
if (hue === 'shadow') {
return theme.foregroundShadow;
} else if (theme.foregroundPalette[hue]) {
// Use user defined palette number (ie: foreground-2)
return rgba(colorToRgbaArray(theme.foregroundPalette[hue]));
} else if (theme.foregroundPalette['1']){
return rgba(colorToRgbaArray(theme.foregroundPalette['1']));
}
// Default to background-default-contrast-{opacity}
colorType = 'background';
contrast = 'contrast';
if (!opacity && hue) {
// Convert references to legacy hues to opacities (i.e. foreground-4 to *-divider)
switch (hue) {
// hue-1 uses default opacity
case '2':
opacity = 'secondary';
break;
case '3':
opacity = 'disabled';
break;
case '4':
opacity = 'divider';
}
}
hue = 'default';
}
// `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];
}
var colorDetails = (PALETTES[ theme.colors[colorType].name ][hue] || '');
// If user has specified a foreground color, use those
if (colorType === 'background' && contrast && regexColorType !== 'foreground' &&
colorDetails.contrastType === defaultBgContrastType) {
// Don't process if colorType was changed
switch (opacity) {
case 'secondary':
case 'icon':
if (theme.foregroundPalette['2']) {
return rgba(colorToRgbaArray(theme.foregroundPalette['2']));
}
break;
case 'disabled':
case 'hint':
if (theme.foregroundPalette['3']) {
return rgba(colorToRgbaArray(theme.foregroundPalette['3']));
}
break;
case 'divider':
if (theme.foregroundPalette['4']) {
return rgba(colorToRgbaArray(theme.foregroundPalette['4']));
}
break;
default:
if (theme.foregroundPalette['1']) {
return rgba(colorToRgbaArray(theme.foregroundPalette['1']));
}
break;
}
}
if (contrast && opacity) {
opacity = colorDetails.opacity[opacity] || opacity;
}
return rgba(colorDetails[contrast ? 'contrast' : 'value'], opacity);
});
var generatedRules = [];
// For each type, generate rules for each hue (ie. default, md-hue-1, md-hue-2, md-hue-3)
angular.forEach(['default', 'hue-1', 'hue-2', 'hue-3'], function(hueName) {
var newRule = rules
.replace(hueRegex, function(match, _, matchedColorType, hueType, contrast, opacity) {
var color = theme.colors[matchedColorType];
var palette = PALETTES[color.name];
var hueValue = color.hues[hueName];
if (contrast && opacity) {
opacity = palette[hueValue].opacity[opacity] || 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 themeable
// components as templates
// Break the CSS into individual rules
var rules = splitCss(themeCss).map(function(rule) {
return rule.trim();
});
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) {
// 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).
* @param {!object} palette to sanitize
*/
function sanitizePalette(palette) {
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;
/**
* @param {string} hueName
* @return {'dark'|'light'|'strongLight'}
*/
function getContrastType(hueName) {
if (defaultContrast === 'light' ? darkColors.indexOf(hueName) !== -1 :
(lightColors.indexOf(hueName) === -1 && strongLightColors.indexOf(hueName) === -1)) {
return 'dark';
}
if (strongLightColors.indexOf(hueName) !== -1) {
return 'strongLight';
}
return 'light';
}
/**
* @param {'dark'|'light'|'strongLight'} contrastType
* @return {[number, number, number]} [red, green, blue] array
*/
function getContrastColor(contrastType) {
switch (contrastType) {
default:
case 'strongLight':
return STRONG_LIGHT_CONTRAST_COLOR;
case 'light':
return LIGHT_CONTRAST_COLOR;
case 'dark':
return DARK_CONTRAST_COLOR;
}
}
/**
* @param {'dark'|'light'|'strongLight'} contrastType
* @return {{secondary: number, divider: number, hint: number, icon: number, disabled: number}}
*/
function getOpacityValues(contrastType) {
switch (contrastType) {
default:
case 'strongLight':
return STRONG_LIGHT_CONTRAST_OPACITY;
case 'light':
return LIGHT_CONTRAST_OPACITY;
case 'dark':
return DARK_CONTRAST_OPACITY;
}
}
// 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));
}
var contrastType = getContrastType(hueName);
palette[hueName] = {
hex: palette[hueName],
value: rgbValue,
contrastType: contrastType,
contrast: getContrastColor(contrastType),
opacity: getOpacityValues(contrastType)
};
});
}
/**
* @param {string} themeCss
* @returns {[]} a string representing a CSS file that is split, producing an array with a rule
* at each index.
*/
function splitCss(themeCss) {
var result = [];
var currentRule = '';
var openedCurlyBrackets = 0;
var closedCurlyBrackets = 0;
for (var i = 0; i < themeCss.length; i++) {
var character = themeCss.charAt(i);
// Check for content in quotes
if (character === '\'' || character === '"') {
// Append text in quotes to current rule
var textInQuotes = themeCss.substring(i, themeCss.indexOf(character, i + 1));
currentRule += textInQuotes;
// Jump to the closing quote char
i += textInQuotes.length;
} else {
currentRule += character;
if (character === '}') {
closedCurlyBrackets++;
if (closedCurlyBrackets === openedCurlyBrackets) {
closedCurlyBrackets = 0;
openedCurlyBrackets = 0;
result.push(currentRule);
currentRule = '';
}
} else if (character === '{') {
openedCurlyBrackets++;
}
}
}
// Add comments added after last valid rule.
if (currentRule !== '') {
result.push(currentRule);
}
return result;
}
}
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(', '))
);
}
}
/**
* @param {string} clr rbg or rgba color
* @return {number[]|undefined} [red, green, blue] array if it can be computed
*/
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) : 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";
/**
* @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";
MdAutocompleteCtrl.$inject = ["$scope", "$element", "$mdUtil", "$mdConstant", "$mdTheming", "$window", "$animate", "$rootElement", "$attrs", "$q", "$log", "$mdLiveAnnouncer"];angular
.module('material.components.autocomplete')
.controller('MdAutocompleteCtrl', MdAutocompleteCtrl);
var ITEM_HEIGHT = 48,
MAX_ITEMS = 5,
MENU_PADDING = 8,
INPUT_PADDING = 2, // Padding provided by `md-input-container`
MODE_STANDARD = 'standard',
MODE_VIRTUAL = 'virtual';
function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, $window,
$animate, $rootElement, $attrs, $q, $log, $mdLiveAnnouncer) {
// Internal Variables.
var ctrl = this,
itemParts = $scope.itemsExpr.split(/ in /i),
itemExpr = itemParts[ 1 ],
elements = null,
cache = {},
noBlur = false,
selectedItemWatchers = [],
hasFocus = false,
fetchesInProgress = 0,
enableWrapScroll = null,
inputModelCtrl = null,
debouncedOnResize = $mdUtil.debounce(onWindowResize),
mode = MODE_VIRTUAL; // default
/**
* The root document element. This is used for attaching a top-level click handler to
* close the options panel when a click outside said panel occurs. We use `documentElement`
* instead of body because, when scrolling is disabled, some browsers consider the body element
* to be completely off the screen and propagate events directly to the html element.
* @type {!Object} angular.JQLite
*/
ctrl.documentElement = angular.element(document.documentElement);
// Public Exported Variables with handlers
defineProperty('hidden', handleHiddenChange, true);
// Public Exported Variables
ctrl.scope = $scope;
ctrl.parent = $scope.$parent;
ctrl.itemName = itemParts[0];
ctrl.matches = [];
ctrl.loading = false;
ctrl.hidden = true;
ctrl.index = -1;
ctrl.activeOption = null;
ctrl.id = $mdUtil.nextUid();
ctrl.isDisabled = null;
ctrl.isRequired = null;
ctrl.isReadonly = null;
ctrl.hasNotFound = false;
ctrl.selectedMessage = $scope.selectedMessage || 'selected';
ctrl.noMatchMessage = $scope.noMatchMessage || 'There are no matches available.';
ctrl.singleMatchMessage = $scope.singleMatchMessage || 'There is 1 match available.';
ctrl.multipleMatchStartMessage = $scope.multipleMatchStartMessage || 'There are ';
ctrl.multipleMatchEndMessage = $scope.multipleMatchEndMessage || ' matches available.';
ctrl.defaultEscapeOptions = 'clear';
// Public Exported Methods
ctrl.keydown = keydown;
ctrl.blur = blur;
ctrl.focus = focus;
ctrl.clear = clearValue;
ctrl.select = select;
ctrl.listEnter = onListEnter;
ctrl.listLeave = onListLeave;
ctrl.focusInput = focusInputElement;
ctrl.getCurrentDisplayValue = getCurrentDisplayValue;
ctrl.registerSelectedItemWatcher = registerSelectedItemWatcher;
ctrl.unregisterSelectedItemWatcher = unregisterSelectedItemWatcher;
ctrl.notFoundVisible = notFoundVisible;
ctrl.loadingIsVisible = loadingIsVisible;
ctrl.positionDropdown = positionDropdown;
/**
* Report types to be used for the $mdLiveAnnouncer
* @enum {number} Unique flag id.
*/
var ReportType = {
Count: 1,
Selected: 2
};
return init();
// initialization methods
/**
* Initialize the controller, setup watchers, gather elements
*/
function init () {
$mdUtil.initOptionalProperties($scope, $attrs, {
searchText: '',
selectedItem: null,
clearButton: false,
disableVirtualRepeat: false,
});
$mdTheming($element);
configureWatchers();
$mdUtil.nextTick(function () {
gatherElements();
moveDropdown();
// Touch devices often do not send a click event on tap. We still want to focus the input
// and open the options pop-up in these cases.
$element.on('touchstart', focusInputElement);
// Forward all focus events to the input element when autofocus is enabled
if ($scope.autofocus) {
$element.on('focus', focusInputElement);
}
if ($scope.inputAriaDescribedBy) {
elements.input.setAttribute('aria-describedby', $scope.inputAriaDescribedBy);
}
if (!$scope.floatingLabel) {
if ($scope.inputAriaLabel) {
elements.input.setAttribute('aria-label', $scope.inputAriaLabel);
} else if ($scope.inputAriaLabelledBy) {
elements.input.setAttribute('aria-labelledby', $scope.inputAriaLabelledBy);
} else if ($scope.placeholder) {
// If no aria-label or aria-labelledby references are defined, then just label using the
// placeholder.
elements.input.setAttribute('aria-label', $scope.placeholder);
}
}
});
}
function updateModelValidators() {
if (!$scope.requireMatch || !inputModelCtrl) return;
inputModelCtrl.$setValidity('md-require-match', !!$scope.selectedItem || !$scope.searchText);
}
/**
* Calculates the dropdown's position and applies the new styles to the menu element
* @returns {*}
*/
function positionDropdown () {
if (!elements) {
return $mdUtil.nextTick(positionDropdown, false, $scope);
}
var dropdownHeight = ($scope.dropdownItems || MAX_ITEMS) * ITEM_HEIGHT;
var hrect = elements.wrap.getBoundingClientRect(),
vrect = elements.snap.getBoundingClientRect(),
root = elements.root.getBoundingClientRect(),
top = vrect.bottom - root.top,
bot = root.bottom - vrect.top,
left = hrect.left - root.left,
width = hrect.width,
offset = getVerticalOffset(),
position = $scope.dropdownPosition,
styles, enoughBottomSpace, enoughTopSpace;
var bottomSpace = root.bottom - vrect.bottom - MENU_PADDING + $mdUtil.getViewportTop();
var topSpace = vrect.top - MENU_PADDING;
// Automatically determine dropdown placement based on available space in viewport.
if (!position) {
enoughTopSpace = topSpace > dropdownHeight;
enoughBottomSpace = bottomSpace > dropdownHeight;
if (enoughBottomSpace) {
position = 'bottom';
} else if (enoughTopSpace) {
position = 'top';
} else {
position = topSpace > bottomSpace ? 'top' : 'bottom';
}
}
// Adjust the width to account for the padding provided by `md-input-container`
if ($attrs.mdFloatingLabel) {
left += INPUT_PADDING;
width -= INPUT_PADDING * 2;
}
styles = {
left: left + 'px',
minWidth: width + 'px',
maxWidth: Math.max(hrect.right - root.left, root.right - hrect.left) - MENU_PADDING + 'px'
};
if (position === 'top') {
styles.top = 'auto';
styles.bottom = bot + 'px';
styles.maxHeight = Math.min(dropdownHeight, topSpace) + 'px';
} else {
bottomSpace = root.bottom - hrect.bottom - MENU_PADDING + $mdUtil.getViewportTop();
styles.top = (top - offset) + 'px';
styles.bottom = 'auto';
styles.maxHeight = Math.min(dropdownHeight, bottomSpace) + 'px';
}
elements.$.scrollContainer.css(styles);
$mdUtil.nextTick(correctHorizontalAlignment, false, $scope);
/**
* Calculates the vertical offset for floating label examples to account for ngMessages
* @returns {number}
*/
function getVerticalOffset () {
var offset = 0;
var inputContainer = $element.find('md-input-container');
if (inputContainer.length) {
var input = inputContainer.find('input');
offset = inputContainer.prop('offsetHeight');
offset -= input.prop('offsetTop');
offset -= input.prop('offsetHeight');
// add in the height left up top for the floating label text
offset += inputContainer.prop('offsetTop');
}
return offset;
}
/**
* Makes sure that the menu doesn't go off of the screen on either side.
*/
function correctHorizontalAlignment () {
var dropdown = elements.scrollContainer.getBoundingClientRect(),
styles = {};
if (dropdown.right > root.right) {
styles.left = (hrect.right - dropdown.width) + 'px';
}
elements.$.scrollContainer.css(styles);
}
}
/**
* Moves the dropdown menu to the body tag in order to avoid z-index and overflow issues.
*/
function moveDropdown () {
if (!elements.$.root.length) return;
$mdTheming(elements.$.scrollContainer);
elements.$.scrollContainer.detach();
elements.$.root.append(elements.$.scrollContainer);
if ($animate.pin) $animate.pin(elements.$.scrollContainer, $rootElement);
}
/**
* Sends focus to the input element.
*/
function focusInputElement () {
elements.input.focus();
}
/**
* Update the activeOption based on the selected item in the listbox.
* The activeOption is used in the template to set the aria-activedescendant attribute, which
* enables screen readers to properly handle visual focus within the listbox and announce the
* item's place in the list. I.e. "List item 3 of 50". Anytime that `ctrl.index` changes, this
* function needs to be called to update the activeOption.
*/
function updateActiveOption() {
var selectedOption = elements.scroller.querySelector('.selected');
if (selectedOption) {
ctrl.activeOption = selectedOption.id;
} else {
ctrl.activeOption = null;
}
}
/**
* Sets up any watchers used by autocomplete
*/
function configureWatchers () {
var wait = parseInt($scope.delay, 10) || 0;
$attrs.$observe('disabled', function (value) { ctrl.isDisabled = $mdUtil.parseAttributeBoolean(value, false); });
$attrs.$observe('required', function (value) { ctrl.isRequired = $mdUtil.parseAttributeBoolean(value, false); });
$attrs.$observe('readonly', function (value) { ctrl.isReadonly = $mdUtil.parseAttributeBoolean(value, false); });
$scope.$watch('searchText', wait ? $mdUtil.debounce(handleSearchText, wait) : handleSearchText);
$scope.$watch('selectedItem', selectedItemChange);
angular.element($window).on('resize', debouncedOnResize);
$scope.$on('$destroy', cleanup);
}
/**
* Removes any events or leftover elements created by this controller
*/
function cleanup () {
if (!ctrl.hidden) {
$mdUtil.enableScrolling();
}
angular.element($window).off('resize', debouncedOnResize);
if (elements){
var items = ['ul', 'scroller', 'scrollContainer', 'input'];
angular.forEach(items, function(key){
elements.$[key].remove();
});
}
}
/**
* Event handler to be called whenever the window resizes.
*/
function onWindowResize() {
if (!ctrl.hidden) {
positionDropdown();
}
}
/**
* Gathers all of the elements needed for this controller
*/
function gatherElements () {
var snapWrap = gatherSnapWrap();
elements = {
main: $element[0],
scrollContainer: $element[0].querySelector('.md-virtual-repeat-container, .md-standard-list-container'),
scroller: $element[0].querySelector('.md-virtual-repeat-scroller, .md-standard-list-scroller'),
ul: $element.find('ul')[0],
input: $element.find('input')[0],
wrap: snapWrap.wrap,
snap: snapWrap.snap,
root: document.body,
};
elements.li = elements.ul.getElementsByTagName('li');
elements.$ = getAngularElements(elements);
mode = elements.scrollContainer.classList.contains('md-standard-list-container') ? MODE_STANDARD : MODE_VIRTUAL;
inputModelCtrl = elements.$.input.controller('ngModel');
}
/**
* Gathers the snap and wrap elements
*
*/
function gatherSnapWrap() {
var element;
var value;
for (element = $element; element.length; element = element.parent()) {
value = element.attr('md-autocomplete-snap');
if (angular.isDefined(value)) break;
}
if (element.length) {
return {
snap: element[0],
wrap: (value.toLowerCase() === 'width') ? element[0] : $element.find('md-autocomplete-wrap')[0]
};
}
var wrap = $element.find('md-autocomplete-wrap')[0];
return {
snap: wrap,
wrap: wrap
};
}
/**
* Gathers angular-wrapped versions of each element
* @param elements
* @returns {{}}
*/
function getAngularElements (elements) {
var obj = {};
for (var key in elements) {
if (elements.hasOwnProperty(key)) obj[ key ] = angular.element(elements[ key ]);
}
return obj;
}
// event/change handlers
/**
* @param {Event} $event
*/
function preventDefault($event) {
$event.preventDefault();
}
/**
* @param {Event} $event
*/
function stopPropagation($event) {
$event.stopPropagation();
}
/**
* Handles changes to the `hidden` property.
* @param {boolean} hidden true to hide the options pop-up, false to show it.
* @param {boolean} oldHidden the previous value of hidden
*/
function handleHiddenChange (hidden, oldHidden) {
var scrollContainerElement;
if (elements) {
scrollContainerElement = angular.element(elements.scrollContainer);
}
if (!hidden && oldHidden) {
positionDropdown();
// Report in polite mode, because the screen reader should finish the default description of
// the input element.
reportMessages(true, ReportType.Count | ReportType.Selected);
if (elements) {
$mdUtil.disableScrollAround(elements.scrollContainer);
enableWrapScroll = disableElementScrollEvents(elements.wrap);
if ($mdUtil.isIos) {
ctrl.documentElement.on('touchend', handleTouchOutsidePanel);
if (scrollContainerElement) {
scrollContainerElement.on('touchstart touchmove touchend', stopPropagation);
}
}
ctrl.index = getDefaultIndex();
$mdUtil.nextTick(function() {
updateActiveOption();
updateScroll();
});
}
} else if (hidden && !oldHidden) {
if ($mdUtil.isIos) {
ctrl.documentElement.off('touchend', handleTouchOutsidePanel);
if (scrollContainerElement) {
scrollContainerElement.off('touchstart touchmove touchend', stopPropagation);
}
}
$mdUtil.enableScrolling();
if (enableWrapScroll) {
enableWrapScroll();
enableWrapScroll = null;
}
}
}
/**
* Handling touch events that bubble up to the document is required for closing the dropdown
* panel on touch outside of the options pop-up panel on iOS.
* @param {Event} $event
*/
function handleTouchOutsidePanel($event) {
ctrl.hidden = true;
// iOS does not blur the pop-up for touches on the scroll mask, so we have to do it.
doBlur(true);
}
/**
* Disables scrolling for a specific element.
* @param {!string|!DOMElement} element to disable scrolling
* @return {Function} function to call to re-enable scrolling for the element
*/
function disableElementScrollEvents(element) {
var elementToDisable = angular.element(element);
elementToDisable.on('wheel touchmove', preventDefault);
return function() {
elementToDisable.off('wheel touchmove', preventDefault);
};
}
/**
* When the user mouses over the dropdown menu, ignore blur events.
*/
function onListEnter () {
noBlur = true;
}
/**
* When the user's mouse leaves the menu, blur events may hide the menu again.
*/
function onListLeave () {
if (!hasFocus && !ctrl.hidden) elements.input.focus();
noBlur = false;
ctrl.hidden = shouldHide();
}
/**
* Handles changes to the selected item.
* @param selectedItem
* @param previousSelectedItem
*/
function selectedItemChange (selectedItem, previousSelectedItem) {
updateModelValidators();
if (selectedItem) {
getDisplayValue(selectedItem).then(function (val) {
$scope.searchText = val;
handleSelectedItemChange(selectedItem, previousSelectedItem);
});
} else if (previousSelectedItem && $scope.searchText) {
getDisplayValue(previousSelectedItem).then(function(displayValue) {
// Clear the searchText, when the selectedItem is set to null.
// Do not clear the searchText, when the searchText isn't matching with the previous
// selected item.
if (angular.isString($scope.searchText)
&& displayValue.toString().toLowerCase() === $scope.searchText.toLowerCase()) {
$scope.searchText = '';
}
});
}
if (selectedItem !== previousSelectedItem) {
announceItemChange();
}
}
/**
* Use the user-defined expression to announce changes each time a new item is selected
*/
function announceItemChange () {
angular.isFunction($scope.itemChange) &&
$scope.itemChange(getItemAsNameVal($scope.selectedItem));
}
/**
* Use the user-defined expression to announce changes each time the search text is changed
*/
function announceTextChange () {
angular.isFunction($scope.textChange) && $scope.textChange();
}
/**
* Calls any external watchers listening for the selected item. Used in conjunction with
* `registerSelectedItemWatcher`.
* @param selectedItem
* @param previousSelectedItem
*/
function handleSelectedItemChange (selectedItem, previousSelectedItem) {
selectedItemWatchers.forEach(function (watcher) {
watcher(selectedItem, previousSelectedItem);
});
}
/**
* Register a function to be called when the selected item changes.
* @param cb
*/
function registerSelectedItemWatcher (cb) {
if (selectedItemWatchers.indexOf(cb) === -1) {
selectedItemWatchers.push(cb);
}
}
/**
* Unregister a function previously registered for selected item changes.
* @param cb
*/
function unregisterSelectedItemWatcher (cb) {
var i = selectedItemWatchers.indexOf(cb);
if (i !== -1) {
selectedItemWatchers.splice(i, 1);
}
}
/**
* Handles changes to the searchText property.
* @param {string} searchText
* @param {string} previousSearchText
*/
function handleSearchText (searchText, previousSearchText) {
ctrl.index = getDefaultIndex();
// do nothing on init
if (searchText === previousSearchText) return;
updateModelValidators();
getDisplayValue($scope.selectedItem).then(function (val) {
// clear selected item if search text no longer matches it
if (searchText !== val) {
$scope.selectedItem = null;
// trigger change event if available
if (searchText !== previousSearchText) {
announceTextChange();
}
// cancel results if search text is not long enough
if (!isMinLengthMet()) {
ctrl.matches = [];
setLoading(false);
reportMessages(true, ReportType.Count);
} else {
handleQuery();
}
}
});
}
/**
* Handles input blur event, determines if the dropdown should hide.
* @param {Event=} $event
*/
function blur($event) {
hasFocus = false;
if (!noBlur) {
ctrl.hidden = shouldHide();
evalAttr('ngBlur', { $event: $event });
} else if (angular.isObject($event)) {
$event.stopImmediatePropagation();
}
}
/**
* Force blur on input element
* @param {boolean} forceBlur
*/
function doBlur(forceBlur) {
if (forceBlur) {
noBlur = false;
hasFocus = false;
}
elements.input.blur();
}
/**
* Handles input focus event, determines if the dropdown should show.
*/
function focus($event) {
hasFocus = true;
if (isSearchable() && isMinLengthMet()) {
handleQuery();
}
ctrl.hidden = shouldHide();
evalAttr('ngFocus', { $event: $event });
}
/**
* Handles keyboard input.
* @param event
*/
function keydown (event) {
switch (event.keyCode) {
case $mdConstant.KEY_CODE.DOWN_ARROW:
if (ctrl.loading || hasSelection()) return;
event.stopPropagation();
event.preventDefault();
ctrl.index = ctrl.index + 1 > ctrl.matches.length - 1 ? 0 : Math.min(ctrl.index + 1, ctrl.matches.length - 1);
$mdUtil.nextTick(updateActiveOption);
updateScroll();
break;
case $mdConstant.KEY_CODE.UP_ARROW:
if (ctrl.loading || hasSelection()) return;
event.stopPropagation();
event.preventDefault();
ctrl.index = ctrl.index - 1 < 0 ? ctrl.matches.length - 1 : Math.max(0, ctrl.index - 1);
$mdUtil.nextTick(updateActiveOption);
updateScroll();
break;
case $mdConstant.KEY_CODE.TAB:
// If we hit tab, assume that we've left the list so it will close
onListLeave();
if (ctrl.hidden || ctrl.loading || ctrl.index < 0 || ctrl.matches.length < 1) return;
select(ctrl.index);
break;
case $mdConstant.KEY_CODE.ENTER:
if (ctrl.hidden || ctrl.loading || ctrl.index < 0 || ctrl.matches.length < 1) return;
if (hasSelection()) return;
event.stopImmediatePropagation();
event.preventDefault();
select(ctrl.index);
break;
case $mdConstant.KEY_CODE.ESCAPE:
event.preventDefault(); // Prevent browser from always clearing input
if (!shouldProcessEscape()) return;
event.stopPropagation();
clearSelectedItem();
if ($scope.searchText && hasEscapeOption('clear')) {
clearSearchText();
}
// Manually hide (needed for mdNotFound support)
ctrl.hidden = true;
if (hasEscapeOption('blur')) {
// Force the component to blur if they hit escape
doBlur(true);
}
break;
default:
}
}
// getters
/**
* Returns the minimum length needed to display the dropdown.
* @returns {*}
*/
function getMinLength () {
return angular.isNumber($scope.minLength) ? $scope.minLength : 1;
}
/**
* Returns the display value for an item.
* @param {*} item
* @returns {*}
*/
function getDisplayValue (item) {
return $q.when(getItemText(item) || item).then(function(itemText) {
if (itemText && !angular.isString(itemText)) {
$log.warn('md-autocomplete: Could not resolve display value to a string. ' +
'Please check the `md-item-text` attribute.');
}
return itemText;
});
/**
* Getter function to invoke user-defined expression (in the directive)
* to convert your object to a single string.
* @param {*} item
* @returns {string|null}
*/
function getItemText (item) {
return (item && $scope.itemText) ? $scope.itemText(getItemAsNameVal(item)) : null;
}
}
/**
* Returns the locals object for compiling item templates.
* @param {*} item
* @returns {Object|undefined}
*/
function getItemAsNameVal (item) {
if (!item) {
return undefined;
}
var locals = {};
if (ctrl.itemName) {
locals[ ctrl.itemName ] = item;
}
return locals;
}
/**
* Returns the default index based on whether or not autoselect is enabled.
* @returns {number} 0 if autoselect is enabled, -1 if not.
*/
function getDefaultIndex () {
return $scope.autoselect ? 0 : -1;
}
/**
* Sets the loading parameter and updates the hidden state.
* @param value {boolean} Whether or not the component is currently loading.
*/
function setLoading(value) {
if (ctrl.loading !== value) {
ctrl.loading = value;
}
// Always refresh the hidden variable as something else might have changed
ctrl.hidden = shouldHide();
}
/**
* Determines if the menu should be hidden.
* @returns {boolean} true if the menu should be hidden
*/
function shouldHide () {
return !shouldShow();
}
/**
* Determines whether the autocomplete is able to query within the current state.
* @returns {boolean} true if the query can be run
*/
function isSearchable() {
if (ctrl.loading && !hasMatches()) {
// No query when query is in progress.
return false;
} else if (hasSelection()) {
// No query if there is already a selection
return false;
}
else if (!hasFocus) {
// No query if the input does not have focus
return false;
}
return true;
}
/**
* @returns {boolean} if the escape keydown should be processed, return true.
* Otherwise return false.
*/
function shouldProcessEscape() {
return hasEscapeOption('blur') || !ctrl.hidden || ctrl.loading || hasEscapeOption('clear') && $scope.searchText;
}
/**
* @param {string} option check if this option is set
* @returns {boolean} if the specified escape option is set, return true. Return false otherwise.
*/
function hasEscapeOption(option) {
if (!angular.isString($scope.escapeOptions)) {
return ctrl.defaultEscapeOptions.indexOf(option) !== -1;
} else {
return $scope.escapeOptions.toLowerCase().indexOf(option) !== -1;
}
}
/**
* Determines if the menu should be shown.
* @returns {boolean} true if the menu should be shown
*/
function shouldShow() {
if (ctrl.isReadonly) {
// Don't show if read only is set
return false;
} else if (!isSearchable()) {
// Don't show if a query is in progress, there is already a selection,
// or the input is not focused.
return false;
}
return (isMinLengthMet() && hasMatches()) || notFoundVisible();
}
/**
* @returns {boolean} true if the search text has matches.
*/
function hasMatches() {
return ctrl.matches.length ? true : false;
}
/**
* @returns {boolean} true if the autocomplete has a valid selection.
*/
function hasSelection() {
return ctrl.scope.selectedItem ? true : false;
}
/**
* @returns {boolean} true if the loading indicator is, or should be, visible.
*/
function loadingIsVisible() {
return ctrl.loading && !hasSelection();
}
/**
* @returns {*} the display value of the current item.
*/
function getCurrentDisplayValue () {
return getDisplayValue(ctrl.matches[ ctrl.index ]);
}
/**
* Determines if the minimum length is met by the search text.
* @returns {*} true if the minimum length is met by the search text
*/
function isMinLengthMet () {
return ($scope.searchText || '').length >= getMinLength();
}
// actions
/**
* Defines a public property with a handler and a default value.
* @param {string} key
* @param {Function} handler function
* @param {*} defaultValue default value
*/
function defineProperty (key, handler, defaultValue) {
Object.defineProperty(ctrl, key, {
get: function () { return defaultValue; },
set: function (newValue) {
var oldValue = defaultValue;
defaultValue = newValue;
handler(newValue, oldValue);
}
});
}
/**
* Selects the item at the given index.
* @param {number} index to select
*/
function select (index) {
// force form to update state for validation
$mdUtil.nextTick(function () {
getDisplayValue(ctrl.matches[ index ]).then(function (val) {
var ngModel = elements.$.input.controller('ngModel');
$mdLiveAnnouncer.announce(val + ' ' + ctrl.selectedMessage, 'assertive');
ngModel.$setViewValue(val);
ngModel.$render();
}).finally(function () {
$scope.selectedItem = ctrl.matches[ index ];
setLoading(false);
});
}, false);
}
/**
* Clears the searchText value and selected item.
* @param {Event} $event
*/
function clearValue ($event) {
if ($event) {
$event.stopPropagation();
}
clearSelectedItem();
clearSearchText();
}
/**
* Clears the selected item
*/
function clearSelectedItem () {
// Reset our variables
ctrl.index = -1;
$mdUtil.nextTick(updateActiveOption);
ctrl.matches = [];
}
/**
* Clears the searchText value
*/
function clearSearchText () {
// Set the loading to true so we don't see flashes of content.
// The flashing will only occur when an async request is running.
// So the loading process will stop when the results had been retrieved.
setLoading(true);
$scope.searchText = '';
// Normally, triggering the change / input event is unnecessary, because the browser detects it properly.
// But some browsers are not detecting it properly, which means that we have to trigger the event.
// Using the `input` is not working properly, because for example IE11 is not supporting the `input` event.
// The `change` event is a good alternative and is supported by all supported browsers.
var eventObj = document.createEvent('CustomEvent');
eventObj.initCustomEvent('change', true, true, { value: '' });
elements.input.dispatchEvent(eventObj);
// For some reason, firing the above event resets the value of $scope.searchText if
// $scope.searchText has a space character at the end, so we blank it one more time and then
// focus.
elements.input.blur();
$scope.searchText = '';
elements.input.focus();
}
/**
* Fetches the results for the provided search text.
* @param searchText
*/
function fetchResults (searchText) {
var items = $scope.$parent.$eval(itemExpr),
term = searchText.toLowerCase(),
isList = angular.isArray(items),
isPromise = !!items.then; // Every promise should contain a `then` property
if (isList) onResultsRetrieved(items);
else if (isPromise) handleAsyncResults(items);
function handleAsyncResults(items) {
if (!items) return;
items = $q.when(items);
fetchesInProgress++;
setLoading(true);
$mdUtil.nextTick(function () {
items
.then(onResultsRetrieved)
.finally(function(){
if (--fetchesInProgress === 0) {
setLoading(false);
}
});
},true, $scope);
}
function onResultsRetrieved(matches) {
cache[term] = matches;
// Just cache the results if the request is now outdated.
// The request becomes outdated, when the new searchText has changed during the result fetching.
if ((searchText || '') !== ($scope.searchText || '')) {
return;
}
handleResults(matches);
}
}
/**
* Reports given message types to supported screen readers.
* @param {boolean} isPolite Whether the announcement should be polite.
* @param {!number} types Message flags to be reported to the screen reader.
*/
function reportMessages(isPolite, types) {
var politeness = isPolite ? 'polite' : 'assertive';
var messages = [];
if (types & ReportType.Selected && ctrl.index !== -1) {
messages.push(getCurrentDisplayValue());
}
if (types & ReportType.Count) {
messages.push($q.resolve(getCountMessage()));
}
$q.all(messages).then(function(data) {
$mdLiveAnnouncer.announce(data.join(' '), politeness);
});
}
/**
* @returns {string} the ARIA message for how many results match the current query.
*/
function getCountMessage () {
switch (ctrl.matches.length) {
case 0:
return ctrl.noMatchMessage;
case 1:
return ctrl.singleMatchMessage;
default:
return ctrl.multipleMatchStartMessage + ctrl.matches.length + ctrl.multipleMatchEndMessage;
}
}
/**
* Makes sure that the focused element is within view.
*/
function updateScroll () {
if (!elements.li[0]) return;
if (mode === MODE_STANDARD) {
updateStandardScroll();
} else {
updateVirtualScroll();
}
}
function updateVirtualScroll() {
// elements in virtual scroll have consistent heights
var optionHeight = elements.li[0].offsetHeight,
top = optionHeight * Math.max(0, ctrl.index),
bottom = top + optionHeight,
containerHeight = elements.scroller.clientHeight,
scrollTop = elements.scroller.scrollTop;
if (top < scrollTop) {
scrollTo(top);
} else if (bottom > scrollTop + containerHeight) {
scrollTo(bottom - containerHeight);
}
}
function updateStandardScroll() {
// elements in standard scroll have variable heights
var selected = elements.li[Math.max(0, ctrl.index)];
var containerHeight = elements.scrollContainer.offsetHeight,
top = selected && selected.offsetTop || 0,
bottom = top + selected.clientHeight,
scrollTop = elements.scrollContainer.scrollTop;
if (top < scrollTop) {
scrollTo(top);
} else if (bottom > scrollTop + containerHeight) {
scrollTo(bottom - containerHeight);
}
}
function isPromiseFetching() {
return fetchesInProgress !== 0;
}
function scrollTo (offset) {
if (mode === MODE_STANDARD) {
elements.scrollContainer.scrollTop = offset;
} else {
elements.$.scrollContainer.controller('mdVirtualRepeatContainer').scrollTo(offset);
}
}
function notFoundVisible () {
var textLength = (ctrl.scope.searchText || '').length;
return ctrl.hasNotFound && !hasMatches() && (!ctrl.loading || isPromiseFetching()) && textLength >= getMinLength() && (hasFocus || noBlur) && !hasSelection();
}
/**
* Starts the query to gather the results for the current searchText. Attempts to return cached
* results first, then forwards the process to `fetchResults` if necessary.
*/
function handleQuery () {
var searchText = $scope.searchText || '';
var term = searchText.toLowerCase();
// If caching is enabled and the current searchText is stored in the cache
if (!$scope.noCache && cache[term]) {
// The results should be handled as same as a normal un-cached request does.
handleResults(cache[term]);
} else {
fetchResults(searchText);
}
ctrl.hidden = shouldHide();
}
/**
* Handles the retrieved results by showing them in the autocompletes dropdown.
* @param results Retrieved results
*/
function handleResults(results) {
ctrl.matches = results;
ctrl.hidden = shouldHide();
// If loading is in progress, then we'll end the progress. This is needed for example,
// when the `clear` button was clicked, because there we always show the loading process, to prevent flashing.
if (ctrl.loading) setLoading(false);
if ($scope.selectOnMatch) selectItemOnMatch();
positionDropdown();
reportMessages(true, ReportType.Count);
}
/**
* If there is only one matching item and the search text matches its display value exactly,
* automatically select that item. Note: This function is only called if the user uses the
* `md-select-on-match` flag.
*/
function selectItemOnMatch () {
var searchText = $scope.searchText,
matches = ctrl.matches,
item = matches[ 0 ];
if (matches.length === 1) getDisplayValue(item).then(function (displayValue) {
var isMatching = searchText === displayValue;
if ($scope.matchInsensitive && !isMatching) {
isMatching = searchText.toLowerCase() === displayValue.toLowerCase();
}
if (isMatching) {
select(0);
}
});
}
/**
* Evaluates an attribute expression against the parent scope.
* @param {String} attr Name of the attribute to be evaluated.
* @param {Object?} locals Properties to be injected into the evaluation context.
*/
function evalAttr(attr, locals) {
if ($attrs[attr]) {
$scope.$parent.$eval($attrs[attr], locals || {});
}
}
}
})();
(function(){
"use strict";
MdAutocomplete.$inject = ["$$mdSvgRegistry"];angular
.module('material.components.autocomplete')
.directive('mdAutocomplete', MdAutocomplete);
/**
* @ngdoc directive
* @name mdAutocomplete
* @module material.components.autocomplete
*
* @description
* `` is a special input component with a drop-down of all possible matches to a
* custom query. This component allows you to provide real-time suggestions as the user types
* in the input area.
*
* To start, you will need to specify the required parameters and provide a template for your
* results. The content inside `md-autocomplete` will be treated as a template.
*
* In more complex cases, you may want to include other content such as a message to display when
* no matches were found. You can do this by wrapping your template in `md-item-template` and
* adding a tag for `md-not-found`. An example of this is shown below.
*
* To reset the displayed value you must clear both values for `md-search-text` and
* `md-selected-item`.
*
* ### Validation
*
* You can use `ng-messages` to include validation the same way that you would normally validate;
* however, if you want to replicate a standard input with a floating label, you will have to
* do the following:
*
* - Make sure that your template is wrapped in `md-item-template`
* - Add your `ng-messages` code inside of `md-autocomplete`
* - Add your validation properties to `md-autocomplete` (ie. `required`)
* - Add a `name` to `md-autocomplete` (to be used on the generated `input`)
*
* There is an example below of how this should look.
*
* ### Snapping Drop-Down
*
* You can cause the autocomplete drop-down to snap to an ancestor element by applying the
* `md-autocomplete-snap` attribute to that element. You can also snap to the width of
* the `md-autocomplete-snap` element by setting the attribute's value to `width`
* (ie. `md-autocomplete-snap="width"`).
*
* ### Notes
*
* **Autocomplete Dropdown Items Rendering**
*
* The `md-autocomplete` uses the the
* mdVirtualRepeat directive for displaying the results inside of the dropdown.
*
* > When encountering issues regarding the item template please take a look at the
* VirtualRepeatContainer documentation.
*
* **Autocomplete inside of a Virtual Repeat**
*
* When using the `md-autocomplete` directive inside of a
* VirtualRepeatContainer the dropdown items
* might not update properly, because caching of the results is enabled by default.
*
* The autocomplete will then show invalid dropdown items, because the Virtual Repeat only updates
* the scope bindings rather than re-creating the `md-autocomplete`. This means that the previous
* cached results will be used.
*
* > To avoid such problems, ensure that the autocomplete does not cache any results via
* `md-no-cache="true"`:
*
*
*
* {{ item.display }}
*
*
*
*
* @param {expression} md-items An expression in the format of `item in results` to iterate over
* matches for your search.
* The `results` expression can be also a function, which returns the results synchronously
* or asynchronously (per Promise).
* @param {expression=} md-selected-item-change An expression to be run each time a new item is
* selected.
* @param {expression=} md-search-text-change An expression to be run each time the search text
* updates.
* @param {expression=} md-search-text A model to bind the search query text to.
* @param {object=} md-selected-item A model to bind the selected item to.
* @param {expression=} md-item-text An expression that will convert your object to a single string.
* @param {string=} placeholder Placeholder text that will be forwarded to the input.
* @param {boolean=} md-no-cache Disables the internal caching that happens in autocomplete.
* @param {boolean=} ng-disabled Determines whether or not to disable the input field.
* @param {boolean=} md-require-match When set to true, the autocomplete will add a validator,
* which will evaluate to false, when no item is currently selected.
* @param {number=} md-min-length Specifies the minimum length of text before autocomplete will
* make suggestions.
* @param {number=} md-delay Specifies the amount of time (in milliseconds) to wait before looking
* for results.
* @param {boolean=} md-clear-button Whether the clear button for the autocomplete input should show
* up or not. When `md-floating-label` is set, defaults to false, defaults to true otherwise.
* @param {boolean=} md-autofocus If true, the autocomplete will be automatically focused when a
* `$mdDialog`, `$mdBottomsheet` or `$mdSidenav`, which contains the autocomplete, is opening.
*
* Also the autocomplete will immediately focus the input element.
* @param {boolean=} md-no-asterisk When present, asterisk will not be appended to the floating
* label.
* @param {boolean=} md-autoselect If set to true, the first item will be automatically selected
* in the dropdown upon open.
* @param {string=} md-input-name The name attribute given to the input element to be used with
* FormController.
* @param {string=} md-menu-class This class will be applied to the dropdown menu for styling.
* @param {string=} md-menu-container-class This class will be applied to the parent container
* of the dropdown panel.
* @param {string=} md-input-class This will be applied to the input for styling. This attribute
* is only valid when a `md-floating-label` is defined.
* @param {string=} md-floating-label This will add a floating label to autocomplete and wrap it in
* `md-input-container`.
* @param {string=} md-select-on-focus When present the input's text will be automatically selected
* on focus.
* @param {string=} md-input-id An ID to be added to the input element.
* @param {number=} md-input-minlength The minimum length for the input's value for validation.
* @param {number=} md-input-maxlength The maximum length for the input's value for validation.
* @param {boolean=} md-select-on-match When set, autocomplete will automatically select
* the item if the search text is an exact match.
* An exact match is when only one match is displayed.
* @param {boolean=} md-match-case-insensitive When set and using `md-select-on-match`, autocomplete
* will select on case-insensitive match.
* @param {string=} md-escape-options Override escape key logic. Default is `clear`.
* Options: `blur`, `clear`, `none`.
* @param {string=} md-dropdown-items Specifies the maximum amount of items to be shown in
* the dropdown.
* When the dropdown doesn't fit into the viewport, the dropdown will shrink
* as much as possible.
* @param {string=} md-dropdown-position Overrides the default dropdown position. Options: `top`,
* `bottom`.
* @param {string=} input-aria-describedby A space-separated list of element IDs. This should
* contain the IDs of any elements that describe this autocomplete. Screen readers will read the
* content of these elements at the end of announcing that the autocomplete has been selected
* and describing its current state. The descriptive elements do not need to be visible on the
* page.
* @param {string=} input-aria-labelledby A space-separated list of element IDs. The ideal use case
* is that this would contain the ID of a `
' : '';
}
function getRepeatType(repeatMode) {
return isVirtualRepeatDisabled(repeatMode) ?
'ng-repeat' : 'md-virtual-repeat';
}
function isVirtualRepeatDisabled(repeatMode) {
// ensure we have a valid repeat mode
var correctedRepeatMode = getRepeatMode(repeatMode);
return correctedRepeatMode !== REPEAT_VIRTUAL;
}
function getInputElement () {
if (attr.mdFloatingLabel) {
return '\
\
{{floatingLabel}}\
\
' + leftover + '
\
';
} else {
return '\
';
}
}
function getClearButton() {
return '' +
'';
}
}
};
}
})();
(function(){
"use strict";
MdAutocompleteItemScopeDirective.$inject = ["$compile", "$mdUtil"];angular
.module('material.components.autocomplete')
.directive('mdAutocompleteParentScope', MdAutocompleteItemScopeDirective);
function MdAutocompleteItemScopeDirective($compile, $mdUtil) {
return {
restrict: 'AE',
compile: compile,
terminal: true,
transclude: 'element'
};
function compile(tElement, tAttr, transclude) {
return function postLink(scope, element, attr) {
var ctrl = scope.$mdAutocompleteCtrl;
var newScope = ctrl.parent.$new();
var itemName = ctrl.itemName;
// Watch for changes to our scope's variables and copy them to the new scope
watchVariable('$index', '$index');
watchVariable('item', itemName);
// Ensure that $digest calls on our scope trigger $digest on newScope.
connectScopes();
// Link the element against newScope.
transclude(newScope, function(clone) {
element.after(clone);
});
/**
* Creates a watcher for variables that are copied from the parent scope
* @param variable
* @param alias
*/
function watchVariable(variable, alias) {
newScope[alias] = scope[variable];
scope.$watch(variable, function(value) {
$mdUtil.nextTick(function() {
newScope[alias] = value;
});
});
}
/**
* Creates watchers on scope and newScope that ensure that for any
* $digest of scope, newScope is also $digested.
*/
function connectScopes() {
var scopeDigesting = false;
var newScopeDigesting = false;
scope.$watch(function() {
if (newScopeDigesting || scopeDigesting) {
return;
}
scopeDigesting = true;
scope.$$postDigest(function() {
if (!newScopeDigesting) {
newScope.$digest();
}
scopeDigesting = newScopeDigesting = false;
});
});
newScope.$watch(function() {
newScopeDigesting = true;
});
}
};
}
}
})();
(function(){
"use strict";
MdHighlightCtrl.$inject = ["$scope", "$element", "$attrs", "$mdUtil"];angular
.module('material.components.autocomplete')
.controller('MdHighlightCtrl', MdHighlightCtrl);
function MdHighlightCtrl ($scope, $element, $attrs, $mdUtil) {
this.$scope = $scope;
this.$element = $element;
this.$attrs = $attrs;
this.$mdUtil = $mdUtil;
// Cache the Regex to avoid rebuilding each time.
this.regex = null;
}
MdHighlightCtrl.prototype.init = function(unsafeTermFn, unsafeContentFn) {
this.flags = this.$attrs.mdHighlightFlags || '';
this.unregisterFn = this.$scope.$watch(function($scope) {
return {
term: unsafeTermFn($scope),
contentText: unsafeContentFn($scope)
};
}.bind(this), this.onRender.bind(this), true);
this.$element.on('$destroy', this.unregisterFn);
};
/**
* Triggered once a new change has been recognized and the highlighted
* text needs to be updated.
*/
MdHighlightCtrl.prototype.onRender = function(state, prevState) {
var contentText = state.contentText;
/* Update the regex if it's outdated, because we don't want to rebuilt it constantly. */
if (this.regex === null || state.term !== prevState.term) {
this.regex = this.createRegex(state.term, this.flags);
}
/* If a term is available apply the regex to the content */
if (state.term) {
this.applyRegex(contentText);
} else {
this.$element.text(contentText);
}
};
/**
* Decomposes the specified text into different tokens (whether match or not).
* Breaking down the string guarantees proper XSS protection due to the native browser
* escaping of unsafe text.
*/
MdHighlightCtrl.prototype.applyRegex = function(text) {
var tokens = this.resolveTokens(text);
this.$element.empty();
tokens.forEach(function (token) {
if (token.isMatch) {
var tokenEl = angular.element('').text(token.text);
this.$element.append(tokenEl);
} else {
this.$element.append(document.createTextNode(token));
}
}.bind(this));
};
/**
* Decomposes the specified text into different tokens by running the regex against the text.
*/
MdHighlightCtrl.prototype.resolveTokens = function(string) {
var tokens = [];
var lastIndex = 0;
// Use replace here, because it supports global and single regular expressions at same time.
string.replace(this.regex, function(match, index) {
appendToken(lastIndex, index);
tokens.push({
text: match,
isMatch: true
});
lastIndex = index + match.length;
});
// Append the missing text as a token.
appendToken(lastIndex);
return tokens;
function appendToken(from, to) {
var targetText = string.slice(from, to);
targetText && tokens.push(targetText);
}
};
/** Creates a regex for the specified text with the given flags. */
MdHighlightCtrl.prototype.createRegex = function(term, flags) {
var startFlag = '', endFlag = '';
var regexTerm = this.$mdUtil.sanitize(term);
if (flags.indexOf('^') >= 0) startFlag = '^';
if (flags.indexOf('$') >= 0) endFlag = '$';
return new RegExp(startFlag + regexTerm + endFlag, flags.replace(/[$^]/g, ''));
};
})();
(function(){
"use strict";
MdHighlight.$inject = ["$interpolate", "$parse"];angular
.module('material.components.autocomplete')
.directive('mdHighlightText', MdHighlight);
/**
* @ngdoc directive
* @name mdHighlightText
* @module material.components.autocomplete
*
* @description
* The `md-highlight-text` directive allows you to specify text that should be highlighted within
* an element. Highlighted text will be wrapped in `` which can
* be styled through CSS. Please note that child elements may not be used with this directive.
*
* @param {string} md-highlight-text A model to be searched for
* @param {string=} md-highlight-flags A list of flags (loosely based on JavaScript RexExp flags).
* #### **Supported flags**:
* - `g`: Find all matches within the provided text
* - `i`: Ignore case when searching for matches
* - `$`: Only match if the text ends with the search term
* - `^`: Only match if the text begins with the search term
*
* @usage
*
*
*
*
* {{result.text}}
*
*
*
*/
function MdHighlight ($interpolate, $parse) {
return {
terminal: true,
controller: 'MdHighlightCtrl',
compile: function mdHighlightCompile(tElement, tAttr) {
var termExpr = $parse(tAttr.mdHighlightText);
var unsafeContentExpr = $interpolate(tElement.html());
return function mdHighlightLink(scope, element, attr, ctrl) {
ctrl.init(termExpr, unsafeContentExpr);
};
}
};
}
})();
(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();
* }
*
* });
*
*
* ### Custom Presets
* Developers are also able to create their own preset, which can be easily used without repeating
* their options each time.
*
*
* $mdBottomSheetProvider.addPreset('testPreset', {
* options: function() {
* return {
* template:
* '' +
* 'This is a custom preset' +
* '',
* controllerAs: 'bottomSheet',
* bindToController: true,
* clickOutsideToClose: true,
* escapeToClose: true
* };
* }
* });
*
*
* After you create your preset during the config phase, you can easily access it.
*
*
* $mdBottomSheet.show(
* $mdBottomSheet.testPreset()
* );
*
*/
/**
* @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} optionsOrPreset Either provide an `$mdBottomSheetPreset` defined during the
* config phase or 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 and available in it's $onInit function.
* - `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.
* - `isLockedOpen` - `{boolean=}`: Disables all default ways of closing the bottom sheet.
* **Note:** this will override the `clickOutsideToClose` and `escapeToClose` options, leaving
* only the `hide` and `cancel` methods as ways of closing the bottom sheet. Defaults to false.
* - `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 bottom sheet (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,
isLockedOpen: false
};
function onShow(scope, element, options) {
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.isLockedOpen) {
options.clickOutsideToClose = false;
options.escapeToClose = false;
} else {
options.cleanupGestures = registerGestures(element, options.parent);
}
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 programmatic focus allowed.
// This allows clicks on backdrop to propagate 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);
}
$mdTheming.inherit(element, options.parent);
if (options.disableParentScroll) {
options.restoreScroll = $mdUtil.disableScrollAround(element, options.parent);
}
return $animate.enter(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) {
if (!options.disableBackdrop) $animate.leave(backdrop);
return $animate.leave(element).then(function() {
if (options.disableParentScroll) {
options.restoreScroll();
delete options.restoreScroll;
}
options.cleanupGestures && options.cleanupGestures();
});
}
/**
* Adds the drag gestures to the bottom sheet.
* @param {JQLite} element where CSS transitions will be applied
* @param {JQLite} parent used for registering gesture listeners
* @return {Function} function that removes gesture listeners that were set up by
* registerGestures()
*/
function registerGestures(element, parent) {
var deregister = $mdGesture.register(parent, 'drag', { horizontal: false });
parent.on('$md.dragstart', onDragStart)
.on('$md.drag', onDrag)
.on('$md.dragend', onDragEnd);
return function cleanupGestures() {
deregister();
parent.off('$md.dragstart', onDragStart);
parent.off('$md.drag', onDrag);
parent.off('$md.dragend', onDragEnd);
};
function onDragStart() {
// 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 `