/*!
* Angular Material Design
* https://github.com/angular/material
* @license MIT
* v0.8.1
*/
angular.module('ngMaterial', ["ng","ngAnimate","ngAria","material.core","material.core.theming.palette","material.core.theming","material.components.autocomplete","material.components.backdrop","material.components.bottomSheet","material.components.card","material.components.button","material.components.checkbox","material.components.content","material.components.dialog","material.components.divider","material.components.gridList","material.components.icon","material.components.input","material.components.list","material.components.progressCircular","material.components.progressLinear","material.components.radioButton","material.components.select","material.components.sidenav","material.components.slider","material.components.sticky","material.components.subheader","material.components.swipe","material.components.switch","material.components.tabs","material.components.textField","material.components.toast","material.components.toolbar","material.components.tooltip","material.components.whiteframe"]);
/*!
* Angular Material Design
* https://github.com/angular/material
* @license MIT
* v0.8.1
*/
(function() {
'use strict';
/**
* Initialization function that validates environment
* requirements.
*/
angular
.module('material.core', [ 'material.core.theming' ])
.config( MdCoreConfigure );
function MdCoreConfigure($provide, $mdThemingProvider) {
$provide.decorator('$$rAF', ["$delegate", rAFDecorator]);
$mdThemingProvider.theme('default')
.primaryPalette('indigo')
.accentPalette('pink')
.warnPalette('red')
.backgroundPalette('grey');
}
MdCoreConfigure.$inject = ["$provide", "$mdThemingProvider"];
function rAFDecorator( $delegate ) {
/**
* Use this to throttle events that come in often.
* The throttled function will always use the *last* invocation before the
* coming frame.
*
* For example, window resize events that fire many times a second:
* If we set to use an raf-throttled callback on window resize, then
* our callback will only be fired once per frame, with the last resize
* event that happened before that frame.
*
* @param {function} callback function to debounce
*/
$delegate.throttle = function(cb) {
var queueArgs, alreadyQueued, queueCb, context;
return function debounced() {
queueArgs = arguments;
context = this;
queueCb = cb;
if (!alreadyQueued) {
alreadyQueued = true;
$delegate(function() {
queueCb.apply(context, queueArgs);
alreadyQueued = false;
});
}
};
};
return $delegate;
}
})();
/*!
* Angular Material Design
* https://github.com/angular/material
* @license MIT
* v0.8.1
*/
(function() {
'use strict';
angular.module('material.core')
.factory('$mdConstant', MdConstantFactory);
function MdConstantFactory($$rAF, $sniffer) {
var webkit = /webkit/i.test($sniffer.vendorPrefix);
function vendorProperty(name) {
return webkit ? ('webkit' + name.charAt(0).toUpperCase() + name.substring(1)) : name;
}
return {
KEY_CODE: {
ENTER: 13,
ESCAPE: 27,
SPACE: 32,
LEFT_ARROW : 37,
UP_ARROW : 38,
RIGHT_ARROW : 39,
DOWN_ARROW : 40,
TAB : 9
},
CSS: {
/* Constants */
TRANSITIONEND: 'transitionend' + (webkit ? ' webkitTransitionEnd' : ''),
ANIMATIONEND: 'animationend' + (webkit ? ' 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')
},
MEDIA: {
'sm': '(max-width: 600px)',
'gt-sm': '(min-width: 600px)',
'md': '(min-width: 600px) and (max-width: 960px)',
'gt-md': '(min-width: 960px)',
'lg': '(min-width: 960px) and (max-width: 1200px)',
'gt-lg': '(min-width: 1200px)'
},
MEDIA_PRIORITY: [
'gt-lg',
'lg',
'gt-md',
'md',
'gt-sm',
'sm'
]
};
}
MdConstantFactory.$inject = ["$$rAF", "$sniffer"];
})();
/*!
* Angular Material Design
* https://github.com/angular/material
* @license MIT
* v0.8.1
*/
(function(){
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 = Iterator;
return $delegate;
}
]);
}]);
/**
* iterator is a list facade to easily support iteration and accessors
*
* @param items Array list which this iterator will enumerate
* @param reloop Boolean enables iterator to consider the list as an endless reloop
*/
function Iterator(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;
}
}
}
})();
/*!
* Angular Material Design
* https://github.com/angular/material
* @license MIT
* v0.8.1
*/
angular.module('material.core')
.factory('$mdMedia', mdMediaFactory);
/**
* Exposes a function on the '$mdMedia' service which will return true or false,
* whether the given media query matches. Re-evaluates on resize. Allows presets
* for 'sm', 'md', 'lg'.
*
* @example $mdMedia('sm') == true if device-width <= sm
* @example $mdMedia('(min-width: 1200px)') == true if device-width >= 1200px
* @example $mdMedia('max-width: 300px') == true if device-width <= 300px (sanitizes input, adding parens)
*/
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] = $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 (attrs[normalizedName]) {
unwatchFns.push(
attrs.$observe(normalizedName, angular.bind(void 0, watchFn, null)));
}
for (var mediaName in $mdConstant.MEDIA) {
var normalizedName = getNormalizedName(attrs, attrName + '-' + mediaName);
if (!attrs[normalizedName]) {
return;
}
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));
}
}
mdMediaFactory.$inject = ["$mdConstant", "$rootScope", "$window"];
/*!
* Angular Material Design
* https://github.com/angular/material
* @license MIT
* v0.8.1
*/
(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.
*/
var nextUniqueId = ['0','0','0'];
angular.module('material.core')
.factory('$mdUtil', ["$cacheFactory", "$document", "$timeout", "$q", "$window", "$mdConstant", function($cacheFactory, $document, $timeout, $q, $window, $mdConstant) {
var Util;
function getNode(el) {
return el[0] || el;
}
return Util = {
now: window.performance ?
angular.bind(window.performance, window.performance.now) :
Date.now,
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 + offsetParent.scrollLeft,
top: nodeRect.top - offsetRect.top + offsetParent.scrollTop,
width: nodeRect.width,
height: nodeRect.height
};
},
offsetRect: function(element, offsetParent) {
return Util.clientRect(element, offsetParent, true);
},
floatingScrollbars: function() {
if (this.floatingScrollbars.cached === undefined) {
var tempNode = angular.element('
');
$document[0].body.appendChild(tempNode[0]);
this.floatingScrollbars.cached = (tempNode[0].offsetWidth == tempNode[0].childNodes[0].offsetWidth);
tempNode.remove();
}
return this.floatingScrollbars.cached;
},
// Mobile safari only allows you to set focus in click event listeners...
forceFocus: function(element) {
var node = element[0] || element;
document.addEventListener('click', function focusOnClick(ev) {
if (ev.target === node && ev.$focus) {
node.focus();
ev.stopImmediatePropagation();
ev.preventDefault();
node.removeEventListener('click', focusOnClick);
}
}, true);
var newEvent = document.createEvent('MouseEvents');
newEvent.initMouseEvent('click', false, true, window, {}, 0, 0, 0, 0,
false, false, false, false, 0, null);
newEvent.$material = true;
newEvent.$focus = true;
node.dispatchEvent(newEvent);
},
transitionEndPromise: function(element) {
var deferred = $q.defer();
element.on($mdConstant.CSS.TRANSITIONEND, finished);
function finished(ev) {
// Make sure this transitionend didn't bubble up from a child
if (ev.target === element[0]) {
element.off($mdConstant.CSS.TRANSITIONEND, finished);
deferred.resolve();
}
}
return deferred.promise;
},
fakeNgModel: function() {
return {
$fake: true,
$setTouched : angular.noop,
$setViewValue: function(value) {
this.$viewValue = value;
this.$render(value);
this.$viewChangeListeners.forEach(function(cb) { cb(); });
},
$isEmpty: function(value) {
return (''+value).length === 0;
},
$parsers: [],
$formatters: [],
$viewChangeListeners: [],
$render: angular.noop
};
},
// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds.
// @param wait Integer value of msecs to delay (since last debounce reset); default value 10 msecs
// @param invokeApply should the $timeout trigger $digest() dirty checking
debounce: function (func, wait, scope, invokeApply) {
var timer;
return function debounced() {
var context = scope,
args = Array.prototype.slice.call(arguments);
$timeout.cancel(timer);
timer = $timeout(function() {
timer = undefined;
func.apply(context, args);
}, wait || 10, invokeApply );
};
},
// Returns a function that can only be triggered every `delay` milliseconds.
// In other words, the function will not be called unless it has been more
// than `delay` milliseconds since the last call.
throttle: function throttle(func, delay) {
var recent;
return function throttled() {
var context = this;
var args = arguments;
var now = Util.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 = Util.now();
cb();
return Util.now() - start;
},
/**
* nextUid, from angular.js.
* A consistent way of creating unique IDs in angular. The ID is a sequence of alpha numeric
* characters such as '012ABC'. The reason why we are not using simply a number counter is that
* the number string gets longer over time, and it can also overflow, where as the nextId
* will grow much slower, it is a string, and it will never overflow.
*
* @returns an unique alpha-numeric string
*/
nextUid: function() {
var index = nextUniqueId.length;
var digit;
while(index) {
index--;
digit = nextUniqueId[index].charCodeAt(0);
if (digit == 57 /*'9'*/) {
nextUniqueId[index] = 'A';
return nextUniqueId.join('');
}
if (digit == 90 /*'Z'*/) {
nextUniqueId[index] = '0';
} else {
nextUniqueId[index] = String.fromCharCode(digit + 1);
return nextUniqueId.join('');
}
}
nextUniqueId.unshift('0');
return nextUniqueId.join('');
},
// Stop watchers and events from firing on a scope without destroying it,
// by disconnecting it from its parent and its siblings' linked lists.
disconnectScope: function disconnectScope(scope) {
if (!scope) return;
// we can't destroy the root scope or a scope that has been already destroyed
if (scope.$root === scope) return;
if (scope.$$destroyed ) return;
var parent = scope.$parent;
scope.$$disconnected = true;
// See Scope.$destroy
if (parent.$$childHead === scope) parent.$$childHead = scope.$$nextSibling;
if (parent.$$childTail === scope) parent.$$childTail = scope.$$prevSibling;
if (scope.$$prevSibling) scope.$$prevSibling.$$nextSibling = scope.$$nextSibling;
if (scope.$$nextSibling) scope.$$nextSibling.$$prevSibling = scope.$$prevSibling;
scope.$$nextSibling = scope.$$prevSibling = null;
},
// Undo the effects of disconnectScope above.
reconnectScope: function reconnectScope(scope) {
if (!scope) return;
// we can't disconnect the root node or scope already disconnected
if (scope.$root === scope) return;
if (!scope.$$disconnected) return;
var child = scope;
var parent = child.$parent;
child.$$disconnected = false;
// See Scope.$new for this logic...
child.$$prevSibling = parent.$$childTail;
if (parent.$$childHead) {
parent.$$childTail.$$nextSibling = child;
parent.$$childTail = child;
} else {
parent.$$childHead = parent.$$childTail = child;
}
},
/*
* getClosest replicates jQuery.closest() to walk up the DOM tree until it finds a matching nodeName
*
* @param el Element to start walking the DOM from
* @param tagName Tag name to find closest to el, such as 'form'
*/
getClosest: function getClosest(el, tagName) {
tagName = tagName.toUpperCase();
do {
if (el.nodeName === tagName) {
return el;
}
} while (el = el.parentNode);
return null;
}
};
}]);
/*
* 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`.
*
* TODO(ajoslin): This should be added in a better place later.
*/
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;
};
})();
/*!
* Angular Material Design
* https://github.com/angular/material
* @license MIT
* v0.8.1
*/
(function() {
'use strict';
angular.module('material.core')
.service('$mdAria', AriaService);
function AriaService($$rAF, $log, $window) {
return {
expect: expect,
expectAsync: expectAsync,
expectWithText: expectWithText
};
/**
* Check if expected attribute has been specified on the target element or child
* @param element
* @param attrName
* @param {optional} defaultValue What to set the attr to if no value is found
*/
function expect(element, attrName, defaultValue) {
var node = element[0];
if (!node.hasAttribute(attrName) && !childHasAttribute(node, attrName)) {
defaultValue = angular.isString(defaultValue) ? defaultValue.trim() : '';
if (defaultValue.length) {
element.attr(attrName, defaultValue);
} else {
$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) {
expectAsync(element, attrName, function() {
return getText(element);
});
}
function getText(element) {
return element.text().trim();
}
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 0 ? 'right' : pointer.distanceX < 0 ? 'left' : '';
pointer.directionY = pointer.distanceY > 0 ? 'up' : pointer.distanceY < 0 ? 'down' : '';
pointer.duration = +Date.now() - pointer.startTime;
pointer.velocityX = pointer.distanceX / pointer.duration;
pointer.velocityY = pointer.distanceY / pointer.duration;
}
function makeStartPointer(ev) {
var point = getEventPoint(ev);
var startPointer = {
startTime: +Date.now(),
target: ev.target,
// 'p' for pointer, '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;
}
angular.module('material.core')
.run(["$mdGesture", function($mdGesture) {}]) // make sure $mdGesture is always instantiated
.factory('$mdGesture', ["$$MdGestureHandler", "$$rAF", "$timeout", function($$MdGestureHandler, $$rAF, $timeout) {
HANDLERS = {};
if (shouldHijackClicks) {
addHandler('click', {
options: {
maxDistance: 6
},
onEnd: function(ev, pointer) {
if (pointer.distance < this.state.options.maxDistance) {
this.dispatchEvent(ev, 'click');
}
}
});
}
addHandler('press', {
onStart: function(ev, pointer) {
this.dispatchEvent(ev, '$md.pressdown');
},
onEnd: function(ev, pointer) {
this.dispatchEvent(ev, '$md.pressup');
}
});
addHandler('hold', {
options: {
// If the user keeps his finger within the same area for
// ms, dispatch a hold event.
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
ev.preventDefault();
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(ev, pointer) {
this.onCancel();
},
});
addHandler('drag', {
options: {
minDistance: 6,
horizontal: true,
},
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 allow touch events to scroll while we're dragging or
// deciding if this touchmove is a proper drag
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 * 1.5;
} else {
shouldStartDrag = Math.abs(pointer.distanceY) > this.state.options.minDistance;
shouldCancel = Math.abs(pointer.distanceX) > this.state.options.minDistance * 1.5;
}
if (shouldStartDrag) {
// Create a new pointer, 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 these every frame; any more is unnecessray
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);
}
}
});
addHandler('swipe', {
options: {
minVelocity: 0.65,
minDistance: 10,
},
onEnd: function(ev, pointer) {
if (Math.abs(pointer.velocityX) > this.state.options.minVelocity &&
Math.abs(pointer.distanceX) > this.state.options.minDistance) {
var eventType = pointer.directionX == 'left' ? '$md.swipeleft' : '$md.swiperight';
this.dispatchEvent(ev, eventType);
}
}
});
var self;
return self = {
handler: addHandler,
register: register
};
function addHandler(name, definition) {
var handler = new $$MdGestureHandler(name);
angular.extend(handler, definition);
HANDLERS[name] = handler;
return self;
}
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);
}
}])
.factory('$$MdGestureHandler', ["$$rAF", function($$rAF) {
function GestureHandler(name) {
this.name = name;
this.state = {};
}
GestureHandler.prototype = {
onStart: angular.noop,
onMove: angular.noop,
onEnd: angular.noop,
onCancel: angular.noop,
options: {},
dispatchEvent: typeof window.jQuery !== 'undefined' && angular.element === window.jQuery ?
jQueryDispatchEvent :
nativeDispatchEvent,
start: function(ev, pointer) {
if (this.state.isRunning) return;
var parentTarget = this.getNearestParent(ev.target);
var parentTargetOptions = parentTarget && parentTarget.$mdGesture[this.name] || {};
this.state = {
isRunning: true,
options: angular.extend({}, this.options, parentTargetOptions),
registeredParent: parentTarget
};
this.onStart(ev, pointer);
},
move: function(ev, pointer) {
if (!this.state.isRunning) return;
this.onMove(ev, pointer);
},
end: function(ev, pointer) {
if (!this.state.isRunning) return;
this.onEnd(ev, pointer);
this.state.isRunning = false;
},
cancel: function(ev, pointer) {
this.onCancel(ev, pointer);
this.state = {};
},
// Find and return the nearest parent element that has been registered via
// $mdGesture.register(element, 'handlerName').
getNearestParent: function(node) {
var current = node;
while (current) {
if ( (current.$mdGesture || {})[this.name] ) {
return current;
}
current = current.parentNode;
}
},
registerElement: function(element, options) {
var self = this;
element[0].$mdGesture = element[0].$mdGesture || {};
element[0].$mdGesture[this.name] = options || {};
element.on('$destroy', onDestroy);
return onDestroy;
function onDestroy() {
delete element[0].$mdGesture[self.name];
element.off('$destroy', onDestroy);
}
},
};
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.
*/
function nativeDispatchEvent(srcEvent, eventType, eventPointer) {
eventPointer = eventPointer || pointer;
var eventObj;
if (eventType === 'click') {
eventObj = document.createEvent('MouseEvents');
eventObj.initMouseEvent(
'click', true, true, window, srcEvent.detail,
eventPointer.x, eventPointer.y, eventPointer.x, eventPointer.y,
srcEvent.ctrlKey, srcEvent.altKey, srcEvent.shiftKey, srcEvent.metaKey,
srcEvent.button, srcEvent.relatedTarget || null
);
} else {
eventObj = document.createEvent('CustomEvent');
eventObj.initCustomEvent(eventType, true, true, {});
}
eventObj.$material = true;
eventObj.pointer = eventPointer;
eventObj.srcEvent = srcEvent;
eventPointer.target.dispatchEvent(eventObj);
}
return GestureHandler;
}]);
})();
/*!
* Angular Material Design
* https://github.com/angular/material
* @license MIT
* v0.8.1
*/
(function() {
'use strict';
angular.module('material.core')
.service('$mdCompiler', mdCompilerService);
function mdCompilerService($q, $http, $injector, $compile, $controller, $templateCache) {
/* jshint validthis: true */
/*
* @ngdoc service
* @name $mdCompiler
* @module material.core
* @description
* The $mdCompiler service is an abstraction of angular's compiler, that allows the developer
* to easily compile an element with a templateUrl, controller, and locals.
*
* @usage
*
* $mdCompiler.compile({
* templateUrl: 'modal.html',
* controller: 'ModalCtrl',
* locals: {
* modal: myModalInstance;
* }
* }).then(function(compileData) {
* compileData.element; // modal.html's template in an element
* compileData.link(myScope); //attach controller & scope to element
* });
*
*/
/*
* @ngdoc method
* @name $mdCompiler#compile
* @description A helper to compile an HTML template/templateUrl with a given controller,
* locals, and scope.
* @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.
* - `template` - `{string=}` An html template as a string.
* - `templateUrl` - `{string=}` A path to an html template.
* - `transformTemplate` - `{function(template)=}` A function which transforms the template after
* it is loaded. It will be given the template string as a parameter, and should
* return a a new string representing the transformed template.
* - `resolve` - `{Object.=}` - An optional map of dependencies which should
* be injected into the controller. If any of these dependencies are promises, the compiler
* will wait for them all to be resolved, or if one is rejected before the controller is
* instantiated `compile()` will fail..
* * `key` - `{string}`: a name of a dependency to be injected into the controller.
* * `factory` - `{string|function}`: If `string` then it is an alias for a service.
* Otherwise if function, then it is injected and the return value is treated as the
* dependency. If the result is a promise, it is resolved before its value is
* injected into the controller.
*
* @returns {object=} promise A promise, which will be resolved with a `compileData` object.
* `compileData` has the following properties:
*
* - `element` - `{element}`: an uncompiled element matching the provided template.
* - `link` - `{function(scope)}`: A link function, which, when called, will compile
* the element and instantiate the provided controller (if given).
* - `locals` - `{object}`: The locals which will be passed into the controller once `link` is
* called. If `bindToController` is true, they will be coppied to the ctrl instead
* - `bindToController` - `bool`: bind the locals to the controller, instead of passing them in. These values will not be available until after initialization.
*/
this.compile = function(options) {
var templateUrl = options.templateUrl;
var template = options.template || '';
var controller = options.controller;
var controllerAs = options.controllerAs;
var resolve = options.resolve || {};
var locals = options.locals || {};
var transformTemplate = options.transformTemplate || angular.identity;
var bindToController = options.bindToController;
// 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] = $injector.get(value);
} else {
resolve[key] = $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.$template = $http.get(templateUrl, {cache: $templateCache})
.then(function(response) {
return response.data;
});
} else {
resolve.$template = $q.when(template);
}
// Wait for all the resolves to finish if they are promises
return $q.all(resolve).then(function(locals) {
var template = transformTemplate(locals.$template);
var element = options.element || angular.element('
').html(template.trim()).contents();
var linkFn = $compile(element);
//Return a linking function that can be used later when the element is ready
return {
locals: locals,
element: element,
link: function link(scope) {
locals.$scope = scope;
//Instantiate controller if it exists, because we have scope
if (controller) {
var ctrl = $controller(controller, locals);
if (bindToController) {
angular.extend(ctrl, locals);
}
//See angular-route source for this logic
element.data('$ngControllerController', ctrl);
element.children().data('$ngControllerController', ctrl);
if (controllerAs) {
scope[controllerAs] = ctrl;
}
}
return linkFn(scope);
}
};
});
};
}
mdCompilerService.$inject = ["$q", "$http", "$injector", "$compile", "$controller", "$templateCache"];
})();
/*!
* Angular Material Design
* https://github.com/angular/material
* @license MIT
* v0.8.1
*/
(function() {
'use strict';
angular.module('material.core')
.provider('$$interimElement', InterimElementProvider);
/*
* @ngdoc service
* @name $$interimElement
* @module material.core
*
* @description
*
* Factory that contructs `$$interimElement.$service` services.
* Used internally in material design for elements that appear on screen temporarily.
* The service provides a promise-like API for interacting with the temporary
* elements.
*
* ```js
* app.service('$mdToast', function($$interimElement) {
* var $mdToast = $$interimElement(toastDefaultOptions);
* return $mdToast;
* });
* ```
* @param {object=} defaultOptions Options used by default for the `show` method on the service.
*
* @returns {$$interimElement.$service}
*
*/
function InterimElementProvider() {
createInterimElementProvider.$get = InterimElementFactory;
InterimElementFactory.$inject = ["$document", "$q", "$rootScope", "$timeout", "$rootElement", "$animate", "$interpolate", "$mdCompiler", "$mdTheming"];
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) {
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',
'template', 'templateUrl', 'themable', 'transformTemplate', 'parent']
});
factory.$inject = ["$$interimElement", "$animate", "$injector"];
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;
}
/**
* Create a factory that has the given methods & defaults implementing interimElement
*/
/* @ngInject */
function factory($$interimElement, $animate, $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
};
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 interimElemnt.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
if (opts && opts._options) opts = opts._options;
return interimElementService.show(
angular.extend({}, defaultOptions, 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,
$interpolate, $mdCompiler, $mdTheming ) {
var startSymbol = $interpolate.startSymbol(),
endSymbol = $interpolate.endSymbol(),
usesStandardSymbols = ((startSymbol === '{{') && (endSymbol === '}}')),
processTemplate = usesStandardSymbols ? angular.identity : replaceInterpolationSymbols;
return function createInterimElementService() {
/*
* @ngdoc service
* @name $$interimElement.$service
*
* @description
* A service used to control inserting and removing an element into the DOM.
*
*/
var stack = [];
var service;
return service = {
show: show,
hide: hide,
cancel: cancel
};
/*
* @ngdoc method
* @name $$interimElement.$service#show
* @kind function
*
* @description
* Adds the `$interimElement` to the DOM and returns a promise that will be resolved or rejected
* with hide or cancel, respectively.
*
* @param {*} options is hashMap of settings
* @returns a Promise
*
*/
function show(options) {
if (stack.length) {
return service.cancel().then(function() {
return show(options);
});
} else {
var interimElement = new InterimElement(options);
stack.push(interimElement);
return interimElement.show().then(function() {
return interimElement.deferred.promise;
});
}
}
/*
* @ngdoc method
* @name $$interimElement.$service#hide
* @kind function
*
* @description
* Removes the `$interimElement` from the DOM and resolves the promise returned from `show`
*
* @param {*} resolveParam Data to resolve the promise with
* @returns a Promise that will be resolved after the element has been removed.
*
*/
function hide(response) {
var interimElement = stack.shift();
return interimElement && interimElement.remove().then(function() {
interimElement.deferred.resolve(response);
});
}
/*
* @ngdoc method
* @name $$interimElement.$service#cancel
* @kind function
*
* @description
* Removes the `$interimElement` from the DOM and rejects the promise returned from `show`
*
* @param {*} reason Data to reject the promise with
* @returns Promise that will be resolved after the element has been removed.
*
*/
function cancel(reason) {
var interimElement = stack.shift();
return $q.when(interimElement && interimElement.remove().then(function() {
interimElement.deferred.reject(reason);
}));
}
/*
* Internal Interim Element Object
* Used internally to manage the DOM element and related data
*/
function InterimElement(options) {
var self;
var hideTimeout, element, showDone, removeDone;
options = options || {};
options = angular.extend({
preserveScope: false,
scope: options.scope || $rootScope.$new(options.isolateScope),
onShow: function(scope, element, options) {
return $animate.enter(element, options.parent);
},
onRemove: function(scope, element, options) {
// Element could be undefined if a new element is shown before
// the old one finishes compiling.
return element && $animate.leave(element) || $q.when();
}
}, options);
if (options.template) {
options.template = processTemplate(options.template);
}
return self = {
options: options,
deferred: $q.defer(),
show: function() {
return showDone = $mdCompiler.compile(options).then(function(compileData) {
angular.extend(compileData.locals, self.options);
element = compileData.link(options.scope);
// Search for parent at insertion time, if not specified
if (angular.isFunction(options.parent)) {
options.parent = options.parent(options.scope, element, options);
} else if (angular.isString(options.parent)) {
options.parent = angular.element($document[0].querySelector(options.parent));
}
// If parent querySelector/getter function fails, or it's just null,
// find a default.
if (!(options.parent || {}).length) {
options.parent = $rootElement.find('body');
if (!options.parent.length) options.parent = $rootElement;
}
if (options.themable) $mdTheming(element);
var ret = options.onShow(options.scope, element, options);
return $q.when(ret)
.then(function(){
// Issue onComplete callback when the `show()` finishes
(options.onComplete || angular.noop)(options.scope, element, options);
startHideTimeout();
});
function startHideTimeout() {
if (options.hideDelay) {
hideTimeout = $timeout(service.cancel, options.hideDelay) ;
}
}
}, function(reason) { showDone = true; self.deferred.reject(reason); });
},
cancelTimeout: function() {
if (hideTimeout) {
$timeout.cancel(hideTimeout);
hideTimeout = undefined;
}
},
remove: function() {
self.cancelTimeout();
return removeDone = $q.when(showDone).then(function() {
var ret = element ? options.onRemove(options.scope, element, options) : true;
return $q.when(ret).then(function() {
if (!options.preserveScope) options.scope.$destroy();
removeDone = true;
});
});
}
};
}
};
/*
* Replace `{{` and `}}` in a string (usually a template) with the actual start-/endSymbols used
* for interpolation. This allows pre-defined templates (for components such as dialog, toast etc)
* to continue to work in apps that use custom interpolation start-/endSymbols.
*
* @param {string} text The text in which to replace `{{` / `}}`
* @returns {string} The modified string using the actual interpolation start-/endSymbols
*/
function replaceInterpolationSymbols(text) {
if (!text || !angular.isString(text)) return text;
return text.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol);
}
}
}
})();
/*!
* Angular Material Design
* https://github.com/angular/material
* @license MIT
* v0.8.1
*/
(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.
*/
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) {
$log.error('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.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 {
pendings[handle] = deferred;
}
return deferred.promise;
}
return $q.reject("Invalid `md-component-id` value.");
}
};
function isValidID(handle){
return handle && (handle !== "");
}
}
ComponentRegistry.$inject = ["$log", "$q"];
})();
/*!
* Angular Material Design
* https://github.com/angular/material
* @license MIT
* v0.8.1
*/
(function() {
'use strict';
angular.module('material.core')
.factory('$mdInkRipple', InkRippleService)
.directive('mdInkRipple', InkRippleDirective)
.directive('mdNoInk', attrNoDirective())
.directive('mdNoBar', attrNoDirective())
.directive('mdNoStretch', attrNoDirective());
function InkRippleDirective($mdInkRipple) {
return {
controller: angular.noop,
link: function (scope, element, attr) {
if (attr.hasOwnProperty('mdInkRippleCheckbox')) {
$mdInkRipple.attachCheckboxBehavior(scope, element);
} else {
$mdInkRipple.attachButtonBehavior(scope, element);
}
}
};
}
InkRippleDirective.$inject = ["$mdInkRipple"];
function InkRippleService($window, $timeout) {
return {
attachButtonBehavior: attachButtonBehavior,
attachCheckboxBehavior: attachCheckboxBehavior,
attachTabBehavior: attachTabBehavior,
attach: attach
};
function attachButtonBehavior(scope, element, options) {
return attach(scope, element, angular.extend({
isFAB: element.hasClass('md-fab'),
isMenuItem: element.hasClass('md-menu-item'),
center: false,
dimBackground: true
}, options));
}
function attachCheckboxBehavior(scope, element, options) {
return attach(scope, element, angular.extend({
center: true,
dimBackground: false,
fitRipple: true
}, options));
}
function attachTabBehavior(scope, element, options) {
return attach(scope, element, angular.extend({
center: false,
dimBackground: true,
outline: true
}, options));
}
function attach(scope, element, options) {
if (element.controller('mdNoInk')) return angular.noop;
options = angular.extend({
colorElement: element,
mousedown: true,
hover: true,
focus: true,
center: false,
mousedownPauseTime: 150,
dimBackground: false,
outline: false,
isFAB: false,
isMenuItem: false,
fitRipple: false
}, options);
var rippleSize,
controller = element.controller('mdInkRipple') || {},
counter = 0,
ripples = [],
states = [],
isActiveExpr = element.attr('md-highlight'),
isActive = false,
isHeld = false,
node = element[0],
rippleSizeSetting = element.attr('md-ripple-size'),
color = parseColor(element.attr('md-ink-ripple')) || parseColor($window.getComputedStyle(options.colorElement[0]).color || 'rgb(0, 0, 0)');
switch (rippleSizeSetting) {
case 'full':
options.isFAB = true;
break;
case 'partial':
options.isFAB = false;
break;
}
// expose onInput for ripple testing
if (options.mousedown) {
element.on('$md.pressdown', onPressDown)
.on('$md.pressup', onPressUp);
}
controller.createRipple = createRipple;
if (isActiveExpr) {
scope.$watch(isActiveExpr, function watchActive(newValue) {
isActive = newValue;
if (isActive && !ripples.length) {
$timeout(function () { createRipple(0, 0); }, 0, false);
}
angular.forEach(ripples, updateElement);
});
}
// Publish self-detach method if desired...
return function detach() {
element.off('$md.pressdown', onPressDown)
.off('$md.pressup', onPressUp);
getRippleContainer().remove();
};
/**
* Gets the current ripple container
* If there is no ripple container, it creates one and returns it
*
* @returns {angular.element} ripple container element
*/
function getRippleContainer() {
var container = element.data('$mdRippleContainer');
if (container) return container;
container = angular.element('
');
element.append(container);
element.data('$mdRippleContainer', container);
return container;
}
function parseColor(color) {
if (!color) return;
if (color.indexOf('rgba') === 0) return color.replace(/\d?\.?\d*\s*\)\s*$/, '0.1)');
if (color.indexOf('rgb') === 0) return rgbToRGBA(color);
if (color.indexOf('#') === 0) return hexToRGBA(color);
/**
* Converts a hex value to an rgba string
*
* @param {string} hex value (3 or 6 digits) to be converted
*
* @returns {string} rgba color with 0.1 alpha
*/
function hexToRGBA(color) {
var hex = color.charAt(0) === '#' ? color.substr(1) : color,
dig = hex.length / 3,
red = hex.substr(0, dig),
grn = hex.substr(dig, dig),
blu = hex.substr(dig * 2);
if (dig === 1) {
red += red;
grn += grn;
blu += blu;
}
return 'rgba(' + parseInt(red, 16) + ',' + parseInt(grn, 16) + ',' + parseInt(blu, 16) + ',0.1)';
}
/**
* Converts rgb value to rgba string
*
* @param {string} rgb color string
*
* @returns {string} rgba color with 0.1 alpha
*/
function rgbToRGBA(color) {
return color.replace(')', ', 0.1)').replace('(', 'a(');
}
}
function removeElement(elem, wait) {
ripples.splice(ripples.indexOf(elem), 1);
if (ripples.length === 0) {
getRippleContainer().css({ backgroundColor: '' });
}
$timeout(function () { elem.remove(); }, wait, false);
}
function updateElement(elem) {
var index = ripples.indexOf(elem),
state = states[index] || {},
elemIsActive = ripples.length > 1 ? false : isActive,
elemIsHeld = ripples.length > 1 ? false : isHeld;
if (elemIsActive || state.animating || elemIsHeld) {
elem.addClass('md-ripple-visible');
} else if (elem) {
elem.removeClass('md-ripple-visible');
if (options.outline) {
elem.css({
width: rippleSize + 'px',
height: rippleSize + 'px',
marginLeft: (rippleSize * -1) + 'px',
marginTop: (rippleSize * -1) + 'px'
});
}
removeElement(elem, options.outline ? 450 : 650);
}
}
/**
* Creates a ripple at the provided coordinates
*
* @param {number} left cursor position
* @param {number} top cursor position
*
* @returns {angular.element} the generated ripple element
*/
function createRipple(left, top) {
color = parseColor(element.attr('md-ink-ripple')) || parseColor($window.getComputedStyle(options.colorElement[0]).color || 'rgb(0, 0, 0)');
var container = getRippleContainer(),
size = getRippleSize(left, top),
css = getRippleCss(size, left, top),
elem = getRippleElement(css),
index = ripples.indexOf(elem),
state = states[index] || {};
rippleSize = size;
state.animating = true;
$timeout(function () {
if (options.dimBackground) {
container.css({ backgroundColor: color });
}
elem.addClass('md-ripple-placed md-ripple-scaled');
if (options.outline) {
elem.css({
borderWidth: (size * 0.5) + 'px',
marginLeft: (size * -0.5) + 'px',
marginTop: (size * -0.5) + 'px'
});
} else {
elem.css({ left: '50%', top: '50%' });
}
updateElement(elem);
$timeout(function () {
state.animating = false;
updateElement(elem);
}, (options.outline ? 450 : 225), false);
}, 0, false);
return elem;
/**
* Creates the ripple element with the provided css
*
* @param {object} css properties to be applied
*
* @returns {angular.element} the generated ripple element
*/
function getRippleElement(css) {
var elem = angular.element('
');
ripples.unshift(elem);
states.unshift({ animating: true });
container.append(elem);
css && elem.css(css);
return elem;
}
/**
* Calculate the ripple size
*
* @returns {number} calculated ripple diameter
*/
function getRippleSize(left, top) {
var width = container.prop('offsetWidth'),
height = container.prop('offsetHeight'),
multiplier, size, rect;
if (options.isMenuItem) {
size = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2));
} else if (options.outline) {
rect = node.getBoundingClientRect();
left -= rect.left;
top -= rect.top;
width = Math.max(left, width - left);
height = Math.max(top, height - top);
size = 2 * Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2));
} else {
multiplier = options.isFAB ? 1.1 : 0.8;
size = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2)) * multiplier;
if (options.fitRipple) {
size = Math.min(height, width, size);
}
}
return size;
}
/**
* Generates the ripple css
*
* @param {number} the diameter of the ripple
* @param {number} the left cursor offset
* @param {number} the top cursor offset
*
* @returns {{backgroundColor: *, width: string, height: string, marginLeft: string, marginTop: string}}
*/
function getRippleCss(size, left, top) {
var rect,
css = {
backgroundColor: rgbaToRGB(color),
borderColor: rgbaToRGB(color),
width: size + 'px',
height: size + 'px'
};
if (options.outline) {
css.width = 0;
css.height = 0;
} else {
css.marginLeft = css.marginTop = (size * -0.5) + 'px';
}
if (options.center) {
css.left = css.top = '50%';
} else {
rect = node.getBoundingClientRect();
css.left = Math.round((left - rect.left) / container.prop('offsetWidth') * 100) + '%';
css.top = Math.round((top - rect.top) / container.prop('offsetHeight') * 100) + '%';
}
return css;
/**
* Converts rgba string to rgb, removing the alpha value
*
* @param {string} rgba color
*
* @returns {string} rgb color
*/
function rgbaToRGB(color) {
return color.replace('rgba', 'rgb').replace(/,[^\)\,]+\)/, ')');
}
}
}
/**
* Handles user input start and stop events
*
*/
function onPressDown(ev) {
if (!isRippleAllowed()) return;
var ripple = createRipple(ev.pointer.x, ev.pointer.y);
isHeld = true;
}
function onPressUp(ev) {
isHeld = false;
var ripple = ripples[ ripples.length - 1 ];
$timeout(function () { updateElement(ripple); }, 0, false);
}
/**
* Determines if the ripple is allowed
*
* @returns {boolean} true if the ripple is allowed, false if not
*/
function isRippleAllowed() {
var parent = node.parentNode;
var grandparent = parent && parent.parentNode;
var ancestor = grandparent && grandparent.parentNode;
return !isDisabled(node) && !isDisabled(parent) && !isDisabled(grandparent) && !isDisabled(ancestor);
function isDisabled (elem) {
return elem && elem.hasAttribute && elem.hasAttribute('disabled');
}
}
}
}
InkRippleService.$inject = ["$window", "$timeout"];
/**
* noink/nobar/nostretch directive: make any element that has one of
* these attributes be given a controller, so that other directives can
* `require:` these and see if there is a `no` parent attribute.
*
* @usage
*
*
*
*
*
*
*
*
* myApp.directive('detectNo', function() {
* return {
* require: ['^?mdNoInk', ^?mdNoBar'],
* link: function(scope, element, attr, ctrls) {
* var noinkCtrl = ctrls[0];
* var nobarCtrl = ctrls[1];
* if (noInkCtrl) {
* alert("the md-no-ink flag has been specified on an ancestor!");
* }
* if (nobarCtrl) {
* alert("the md-no-bar flag has been specified on an ancestor!");
* }
* }
* };
* });
*
*/
function attrNoDirective() {
return function() {
return {
controller: angular.noop
};
};
}
})();
/*!
* Angular Material Design
* https://github.com/angular/material
* @license MIT
* v0.8.1
*/
(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 A100',
'contrastStrongLightColors': '500 600 700 A200 A400 A700'
},
'pink': {
'50': '#fce4ec',
'100': '#f8bbd0',
'200': '#f48fb1',
'300': '#f06292',
'400': '#ec407a',
'500': '#e91e63',
'600': '#d81b60',
'700': '#c2185b',
'800': '#ad1457',
'900': '#880e4f',
'A100': '#ff80ab',
'A200': '#ff4081',
'A400': '#f50057',
'A700': '#c51162',
'contrastDefaultColor': 'light',
'contrastDarkColors': '50 100 200 300 400 A100',
'contrastStrongLightColors': '500 600 A200 A400 A700'
},
'purple': {
'50': '#f3e5f5',
'100': '#e1bee7',
'200': '#ce93d8',
'300': '#ba68c8',
'400': '#ab47bc',
'500': '#9c27b0',
'600': '#8e24aa',
'700': '#7b1fa2',
'800': '#6a1b9a',
'900': '#4a148c',
'A100': '#ea80fc',
'A200': '#e040fb',
'A400': '#d500f9',
'A700': '#aa00ff',
'contrastDefaultColor': 'light',
'contrastDarkColors': '50 100 200 A100',
'contrastStrongLightColors': '300 400 A200 A400 A700'
},
'deep-purple': {
'50': '#ede7f6',
'100': '#d1c4e9',
'200': '#b39ddb',
'300': '#9575cd',
'400': '#7e57c2',
'500': '#673ab7',
'600': '#5e35b1',
'700': '#512da8',
'800': '#4527a0',
'900': '#311b92',
'A100': '#b388ff',
'A200': '#7c4dff',
'A400': '#651fff',
'A700': '#6200ea',
'contrastDefaultColor': 'light',
'contrastDarkColors': '50 100 200 A100',
'contrastStrongLightColors': '300 400 A200'
},
'indigo': {
'50': '#e8eaf6',
'100': '#c5cae9',
'200': '#9fa8da',
'300': '#7986cb',
'400': '#5c6bc0',
'500': '#3f51b5',
'600': '#3949ab',
'700': '#303f9f',
'800': '#283593',
'900': '#1a237e',
'A100': '#8c9eff',
'A200': '#536dfe',
'A400': '#3d5afe',
'A700': '#304ffe',
'contrastDefaultColor': 'light',
'contrastDarkColors': '50 100 200 A100',
'contrastStrongLightColors': '300 400 A200 A400'
},
'blue': {
'50': '#e3f2fd',
'100': '#bbdefb',
'200': '#90caf9',
'300': '#64b5f6',
'400': '#42a5f5',
'500': '#2196f3',
'600': '#1e88e5',
'700': '#1976d2',
'800': '#1565c0',
'900': '#0d47a1',
'A100': '#82b1ff',
'A200': '#448aff',
'A400': '#2979ff',
'A700': '#2962ff',
'contrastDefaultColor': 'light',
'contrastDarkColors': '100 200 300 400 A100',
'contrastStrongLightColors': '500 600 700 A200 A400 A700'
},
'light-blue': {
'50': '#e1f5fe',
'100': '#b3e5fc',
'200': '#81d4fa',
'300': '#4fc3f7',
'400': '#29b6f6',
'500': '#03a9f4',
'600': '#039be5',
'700': '#0288d1',
'800': '#0277bd',
'900': '#01579b',
'A100': '#80d8ff',
'A200': '#40c4ff',
'A400': '#00b0ff',
'A700': '#0091ea',
'contrastDefaultColor': 'dark',
'contrastLightColors': '500 600 700 800 900 A700',
'contrastStrongLightColors': '500 600 700 800 A700'
},
'cyan': {
'50': '#e0f7fa',
'100': '#b2ebf2',
'200': '#80deea',
'300': '#4dd0e1',
'400': '#26c6da',
'500': '#00bcd4',
'600': '#00acc1',
'700': '#0097a7',
'800': '#00838f',
'900': '#006064',
'A100': '#84ffff',
'A200': '#18ffff',
'A400': '#00e5ff',
'A700': '#00b8d4',
'contrastDefaultColor': 'dark',
'contrastLightColors': '500 600 700 800 900',
'contrastStrongLightColors': '500 600 700 800'
},
'teal': {
'50': '#e0f2f1',
'100': '#b2dfdb',
'200': '#80cbc4',
'300': '#4db6ac',
'400': '#26a69a',
'500': '#009688',
'600': '#00897b',
'700': '#00796b',
'800': '#00695c',
'900': '#004d40',
'A100': '#a7ffeb',
'A200': '#64ffda',
'A400': '#1de9b6',
'A700': '#00bfa5',
'contrastDefaultColor': 'dark',
'contrastLightColors': '500 600 700 800 900',
'contrastStrongLightColors': '500 600 700'
},
'green': {
'50': '#e8f5e9',
'100': '#c8e6c9',
'200': '#a5d6a7',
'300': '#81c784',
'400': '#66bb6a',
'500': '#4caf50',
'600': '#43a047',
'700': '#388e3c',
'800': '#2e7d32',
'900': '#1b5e20',
'A100': '#b9f6ca',
'A200': '#69f0ae',
'A400': '#00e676',
'A700': '#00c853',
'contrastDefaultColor': 'dark',
'contrastLightColors': '500 600 700 800 900',
'contrastStrongLightColors': '500 600 700'
},
'light-green': {
'50': '#f1f8e9',
'100': '#dcedc8',
'200': '#c5e1a5',
'300': '#aed581',
'400': '#9ccc65',
'500': '#8bc34a',
'600': '#7cb342',
'700': '#689f38',
'800': '#558b2f',
'900': '#33691e',
'A100': '#ccff90',
'A200': '#b2ff59',
'A400': '#76ff03',
'A700': '#64dd17',
'contrastDefaultColor': 'dark',
'contrastLightColors': '800 900',
'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',
'contrastLightColors': '900',
'contrastStrongLightColors': '900'
},
'yellow': {
'50': '#fffde7',
'100': '#fff9c4',
'200': '#fff59d',
'300': '#fff176',
'400': '#ffee58',
'500': '#ffeb3b',
'600': '#fdd835',
'700': '#fbc02d',
'800': '#f9a825',
'900': '#f57f17',
'A100': '#ffff8d',
'A200': '#ffff00',
'A400': '#ffea00',
'A700': '#ffd600',
'contrastDefaultColor': 'dark'
},
'amber': {
'50': '#fff8e1',
'100': '#ffecb3',
'200': '#ffe082',
'300': '#ffd54f',
'400': '#ffca28',
'500': '#ffc107',
'600': '#ffb300',
'700': '#ffa000',
'800': '#ff8f00',
'900': '#ff6f00',
'A100': '#ffe57f',
'A200': '#ffd740',
'A400': '#ffc400',
'A700': '#ffab00',
'contrastDefaultColor': 'dark'
},
'orange': {
'50': '#fff3e0',
'100': '#ffe0b2',
'200': '#ffcc80',
'300': '#ffb74d',
'400': '#ffa726',
'500': '#ff9800',
'600': '#fb8c00',
'700': '#f57c00',
'800': '#ef6c00',
'900': '#e65100',
'A100': '#ffd180',
'A200': '#ffab40',
'A400': '#ff9100',
'A700': '#ff6d00',
'contrastDefaultColor': 'dark',
'contrastLightColors': '800 900',
'contrastStrongLightColors': '800 900'
},
'deep-orange': {
'50': '#fbe9e7',
'100': '#ffccbc',
'200': '#ffab91',
'300': '#ff8a65',
'400': '#ff7043',
'500': '#ff5722',
'600': '#f4511e',
'700': '#e64a19',
'800': '#d84315',
'900': '#bf360c',
'A100': '#ff9e80',
'A200': '#ff6e40',
'A400': '#ff3d00',
'A700': '#dd2c00',
'contrastDefaultColor': 'light',
'contrastDarkColors': '50 100 200 300 400 A100 A200',
'contrastStrongLightColors': '500 600 700 800 900 A400 A700'
},
'brown': {
'50': '#efebe9',
'100': '#d7ccc8',
'200': '#bcaaa4',
'300': '#a1887f',
'400': '#8d6e63',
'500': '#795548',
'600': '#6d4c41',
'700': '#5d4037',
'800': '#4e342e',
'900': '#3e2723',
'A100': '#d7ccc8',
'A200': '#bcaaa4',
'A400': '#8d6e63',
'A700': '#5d4037',
'contrastDefaultColor': 'light',
'contrastDarkColors': '50 100 200',
'contrastStrongLightColors': '300 400'
},
'grey': {
'0': '#ffffff',
'50': '#fafafa',
'100': '#f5f5f5',
'200': '#eeeeee',
'300': '#e0e0e0',
'400': '#bdbdbd',
'500': '#9e9e9e',
'600': '#757575',
'700': '#616161',
'800': '#424242',
'900': '#212121',
'1000': '#000000',
'A100': '#ffffff',
'A200': '#eeeeee',
'A400': '#bdbdbd',
'A700': '#616161',
'contrastDefaultColor': 'dark',
'contrastLightColors': '600 700 800 900'
},
'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',
'contrastStrongLightColors': '400 500'
}
});
})();
/*!
* Angular Material Design
* https://github.com/angular/material
* @license MIT
* v0.8.1
*/
(function() {
'use strict';
angular.module('material.core.theming', ['material.core.theming.palette'])
.directive('mdTheme', ThemingDirective)
.directive('mdThemable', ThemableDirective)
.provider('$mdTheming', ThemingProvider)
.run(generateThemes);
/**
* @ngdoc provider
* @name $mdThemingProvider
* @module material.core
*
* @description Provider to configure the `$mdTheming` service.
*/
/**
* @ngdoc method
* @name $mdThemingProvider#setDefaultTheme
* @param {string} themeName Default theme name to be applied to elements. Default value is `default`.
*/
/**
* @ngdoc method
* @name $mdThemingProvider#alwaysWatchTheme
* @param {boolean} watch Whether or not to always watch themes for changes and re-apply
* classes when they change. Default is `false`. Enabling can reduce performance.
*/
// In memory storage of defined themes and color palettes (both loaded by CSS, and user specified)
var PALETTES;
var THEMES;
var themingProvider;
var generationIsDone;
var DARK_FOREGROUND = {
name: 'dark',
'1': 'rgba(0,0,0,0.87)',
'2': 'rgba(0,0,0,0.54)',
'3': 'rgba(0,0,0,0.26)',
'4': 'rgba(0,0,0,0.12)'
};
var LIGHT_FOREGROUND = {
name: 'light',
'1': 'rgba(255,255,255,1.0)',
'2': 'rgba(255,255,255,0.7)',
'3': 'rgba(255,255,255,0.3)',
'4': 'rgba(255,255,255,0.12)'
};
var DARK_SHADOW = '1px 1px 0px rgba(0,0,0,0.4), -1px -1px 0px rgba(0,0,0,0.4)';
var LIGHT_SHADOW = '';
var DARK_CONTRAST_COLOR = colorToRgbaArray('rgba(0,0,0,0.87)');
var LIGHT_CONTRAST_COLOR = colorToRgbaArray('rgba(255,255,255,0.87');
var STRONG_LIGHT_CONTRAST_COLOR = colorToRgbaArray('rgb(255,255,255)');
var THEME_COLOR_TYPES = ['primary', 'accent', 'warn', 'background'];
var DEFAULT_COLOR_TYPE = 'primary';
// A color in a theme will use these hues by default, if not specified by user.
var LIGHT_DEFAULT_HUES = {
'accent': {
'default': 'A200',
'hue-1': 'A100',
'hue-2': 'A400',
'hue-3': 'A700'
}
};
var DARK_DEFAULT_HUES = {
'background': {
'default': '500',
'hue-1': '300',
'hue-2': '600',
'hue-3': '800'
}
};
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'
];
function ThemingProvider($mdColorPalette) {
PALETTES = {};
THEMES = {};
var defaultTheme = 'default';
var alwaysWatchTheme = false;
// Load JS Defined Palettes
angular.extend(PALETTES, $mdColorPalette);
// Default theme defined in core.js
ThemingService.$inject = ["$rootScope", "$log"];
return themingProvider = {
definePalette: definePalette,
extendPalette: extendPalette,
theme: registerTheme,
setDefaultTheme: function(theme) {
defaultTheme = theme;
},
alwaysWatchTheme: function(alwaysWatch) {
alwaysWatchTheme = alwaysWatch;
},
$get: ThemingService,
_LIGHT_DEFAULT_HUES: LIGHT_DEFAULT_HUES,
_DARK_DEFAULT_HUES: DARK_DEFAULT_HUES,
_PALETTES: PALETTES,
_THEMES: THEMES,
_parseRules: parseRules,
_rgba: rgba
};
// Example: $mdThemingProvider.definePalette('neonRed', { 50: '#f5fafa', ... });
function definePalette(name, map) {
map = map || {};
PALETTES[name] = checkPaletteValid(name, map);
return themingProvider;
}
// Returns an new object which is a copy of a given palette `name` with variables from
// `map` overwritten
// Example: var neonRedMap = $mdThemingProvider.extendPalette('red', { 50: '#f5fafafa' });
function extendPalette(name, map) {
return checkPaletteValid(name, angular.extend({}, PALETTES[name] || {}, map) );
}
// Make sure that palette has all required hues
function checkPaletteValid(name, map) {
var missingColors = VALID_HUE_VALUES.filter(function(field) {
return !map[field];
});
if (missingColors.length) {
throw new Error("Missing colors %1 in palette %2!"
.replace('%1', missingColors.join(', '))
.replace('%2', name));
}
return map;
}
// Register a theme (which is a collection of color palettes to use with various states
// ie. warn, accent, primary )
// Optionally inherit from an existing theme
// $mdThemingProvider.theme('custom-theme').primaryPalette('red');
function registerTheme(name, inheritFrom) {
inheritFrom = inheritFrom || 'default';
if (THEMES[name]) return THEMES[name];
var parentTheme = typeof inheritFrom === 'string' ? THEMES[inheritFrom] : inheritFrom;
var theme = new Theme(name);
if (parentTheme) {
angular.forEach(parentTheme.colors, function(color, colorType) {
theme.colors[colorType] = {
name: color.name,
// Make sure a COPY of the hues is given to the child color,
// not the same reference.
hues: angular.extend({}, color.hues)
};
});
}
THEMES[name] = theme;
return theme;
}
function Theme(name) {
var self = this;
self.name = name;
self.colors = {};
self.dark = setDark;
setDark(false);
function setDark(isDark) {
isDark = arguments.length === 0 ? true : !!isDark;
// If no change, abort
if (isDark === self.isDark) return;
self.isDark = isDark;
self.foregroundPalette = self.isDark ? LIGHT_FOREGROUND : DARK_FOREGROUND;
self.foregroundShadow = self.isDark ? DARK_SHADOW : LIGHT_SHADOW;
// Light and dark themes have different default hues.
// Go through each existing color type for this theme, and for every
// hue value that is still the default hue value from the previous light/dark setting,
// set it to the default hue value from the new light/dark setting.
var newDefaultHues = self.isDark ? DARK_DEFAULT_HUES : LIGHT_DEFAULT_HUES;
var oldDefaultHues = self.isDark ? LIGHT_DEFAULT_HUES : DARK_DEFAULT_HUES;
angular.forEach(newDefaultHues, function(newDefaults, colorType) {
var color = self.colors[colorType];
var oldDefaults = oldDefaultHues[colorType];
if (color) {
for (var hueName in color.hues) {
if (color.hues[hueName] === oldDefaults[hueName]) {
color.hues[hueName] = newDefaults[hueName];
}
}
}
});
return self;
}
THEME_COLOR_TYPES.forEach(function(colorType) {
var defaultHues = (self.isDark ? DARK_DEFAULT_HUES : LIGHT_DEFAULT_HUES)[colorType];
self[colorType + 'Palette'] = function setPaletteType(paletteName, hues) {
var color = self.colors[colorType] = {
name: paletteName,
hues: angular.extend({}, defaultHues, hues)
};
Object.keys(color.hues).forEach(function(name) {
if (!defaultHues[name]) {
throw new Error("Invalid hue name '%1' in theme %2's %3 color %4. Available hue names: %4"
.replace('%1', name)
.replace('%2', self.name)
.replace('%3', paletteName)
.replace('%4', Object.keys(defaultHues).join(', '))
);
}
});
Object.keys(color.hues).map(function(key) {
return color.hues[key];
}).forEach(function(hueValue) {
if (VALID_HUE_VALUES.indexOf(hueValue) == -1) {
throw new Error("Invalid hue value '%1' in theme %2's %3 color %4. Available hue values: %5"
.replace('%1', hueValue)
.replace('%2', self.name)
.replace('%3', colorType)
.replace('%4', paletteName)
.replace('%5', VALID_HUE_VALUES.join(', '))
);
}
});
return self;
};
self[colorType + 'Color'] = function() {
var args = Array.prototype.slice.call(arguments);
console.warn('$mdThemingProviderTheme.' + colorType + 'Color() has been deprecated. ' +
'Use $mdThemingProviderTheme.' + colorType + 'Palette() instead.');
return self[colorType + 'Palette'].apply(self, args);
};
});
}
/**
* @ngdoc service
* @name $mdTheming
*
* @description
*
* Service that makes an element apply theming related classes to itself.
*
* ```js
* app.directive('myFancyDirective', function($mdTheming) {
* return {
* restrict: 'e',
* link: function(scope, el, attrs) {
* $mdTheming(el);
* }
* };
* });
* ```
* @param {el=} element to apply theming to
*/
/* @ngInject */
function ThemingService($rootScope, $log) {
applyTheme.inherit = function(el, parent) {
var ctrl = parent.controller('mdTheme');
var attrThemeValue = el.attr('md-theme-watch');
if ( (alwaysWatchTheme || angular.isDefined(attrThemeValue)) && attrThemeValue != 'false') {
var deregisterWatch = $rootScope.$watch(function() {
return ctrl && ctrl.$mdTheme || defaultTheme;
}, changeTheme);
el.on('$destroy', deregisterWatch);
} else {
var theme = ctrl && ctrl.$mdTheme || defaultTheme;
changeTheme(theme);
}
function changeTheme(theme) {
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);
}
};
applyTheme.registered = registered;
applyTheme.defaultTheme = function() {
return defaultTheme;
};
return applyTheme;
function registered(theme) {
if (theme === undefined || theme === '') return true;
return THEMES[theme] !== undefined;
}
function applyTheme(scope, el) {
// Allow us to be invoked via a linking function signature.
if (el === undefined) {
el = scope;
scope = undefined;
}
if (scope === undefined) {
scope = $rootScope;
}
applyTheme.inherit(el, el);
}
}
}
ThemingProvider.$inject = ["$mdColorPalette"];
function ThemingDirective($mdTheming, $interpolate, $log) {
return {
priority: 100,
link: {
pre: function(scope, el, attrs) {
var ctrl = {
$setTheme: function(theme) {
if (!$mdTheming.registered(theme)) {
$log.warn('attempted to use unregistered theme \'' + theme + '\'');
}
ctrl.$mdTheme = theme;
}
};
el.data('$mdThemeController', ctrl);
ctrl.$setTheme($interpolate(attrs.mdTheme)(scope));
attrs.$observe('mdTheme', ctrl.$setTheme);
}
}
};
}
ThemingDirective.$inject = ["$mdTheming", "$interpolate", "$log"];
function ThemableDirective($mdTheming) {
return $mdTheming;
}
ThemableDirective.$inject = ["$mdTheming"];
function parseRules(theme, colorType, rules) {
checkValidPalette(theme, colorType);
rules = rules.replace(/THEME_NAME/g, theme.name);
var generatedRules = [];
var color = theme.colors[colorType];
var themeNameRegex = new RegExp('.md-' + theme.name + '-theme', 'g');
// Matches '{{ primary-color }}', etc
var hueRegex = new RegExp('(\'|")?{{\\s*(' + colorType + ')-(color|contrast)-?(\\d\\.?\\d*)?\\s*}}(\"|\')?','g');
var simpleVariableRegex = /'?"?\{\{\s*([a-zA-Z]+)-(A?\d+|hue\-[0-3]|shadow)-?(\d\.?\d*)?\s*\}\}'?"?/g;
var palette = PALETTES[color.name];
// find and replace simple variables where we use a specific hue, not angentire palette
// eg. "{{primary-100}}"
//\(' + THEME_COLOR_TYPES.join('\|') + '\)'
rules = rules.replace(simpleVariableRegex, function(match, colorType, hue, opacity) {
if (colorType === 'foreground') {
if (hue == 'shadow') {
return theme.foregroundShadow;
} else {
return theme.foregroundPalette[hue] || theme.foregroundPalette['1'];
}
}
if (hue.indexOf('hue') === 0) {
hue = theme.colors[colorType].hues[hue];
}
return rgba( (PALETTES[ theme.colors[colorType].name ][hue] || '').value, opacity );
});
// For each type, generate rules for each hue (ie. default, md-hue-1, md-hue-2, md-hue-3)
angular.forEach(color.hues, function(hueValue, hueName) {
var newRule = rules
.replace(hueRegex, function(match, _, colorType, hueType, opacity) {
return rgba(palette[hueValue][hueType === 'color' ? 'value' : 'contrast'], opacity);
});
if (hueName !== 'default') {
newRule = newRule.replace(themeNameRegex, '.md-' + theme.name + '-theme.md-' + hueName);
}
generatedRules.push(newRule);
});
return generatedRules.join('');
}
// Generate our themes at run time given the state of THEMES and PALETTES
function generateThemes($injector) {
var themeCss = $injector.has('$MD_THEME_CSS') ? $injector.get('$MD_THEME_CSS') : '';
// MD_THEME_CSS is a string generated by the build process that includes all the themable
// components as templates
// Expose contrast colors for palettes to ensure that text is always readable
angular.forEach(PALETTES, sanitizePalette);
// Break the CSS into individual rules
var rules = themeCss.split(/\}(?!(\}|'|"|;))/)
.filter(function(rule) { return rule && rule.length; })
.map(function(rule) { return rule.trim() + '}'; });
var rulesByType = {};
THEME_COLOR_TYPES.forEach(function(type) {
rulesByType[type] = '';
});
var ruleMatchRegex = new RegExp('md-(' + THEME_COLOR_TYPES.join('|') + ')', 'g');
// Sort the rules based on type, allowing us to do color substitution on a per-type basis
rules.forEach(function(rule) {
var match = rule.match(ruleMatchRegex);
// First: test that if the rule has '.md-accent', it goes into the accent set of rules
for (var i = 0, type; type = THEME_COLOR_TYPES[i]; i++) {
if (rule.indexOf('.md-' + type) > -1) {
return rulesByType[type] += rule;
}
}
// If no eg 'md-accent' class is found, try to just find 'accent' in the rule and guess from
// there
for (i = 0; type = THEME_COLOR_TYPES[i]; i++) {
if (rule.indexOf(type) > -1) {
return rulesByType[type] += rule;
}
}
// Default to the primary array
return rulesByType[DEFAULT_COLOR_TYPE] += rule;
});
var styleString = '';
// For each theme, use the color palettes specified for `primary`, `warn` and `accent`
// to generate CSS rules.
angular.forEach(THEMES, function(theme) {
THEME_COLOR_TYPES.forEach(function(colorType) {
styleString += parseRules(theme, colorType, rulesByType[colorType] + '');
});
if (theme.colors.primary.name == theme.colors.accent.name) {
console.warn("$mdThemingProvider: Using the same palette for primary and" +
" accent. This violates the material design spec.");
}
});
// Insert our newly minted styles into the DOM
if (!generationIsDone) {
var style = document.createElement('style');
style.innerHTML = styleString;
var head = document.getElementsByTagName('head')[0];
head.insertBefore(style, head.firstElementChild);
generationIsDone = true;
}
// The user specifies a 'default' contrast color as either light or dark,
// then explicitly lists which hues are the opposite contrast (eg. A100 has dark, A200 has light)
function sanitizePalette(palette) {
var defaultContrast = palette.contrastDefaultColor;
var lightColors = palette.contrastLightColors || [];
var strongLightColors = palette.contrastStrongLightColors || [];
var darkColors = palette.contrastDarkColors || [];
// These colors are provided as space-separated lists
if (typeof lightColors === 'string') lightColors = lightColors.split(' ');
if (typeof strongLightColors === 'string') strongLightColors = strongLightColors.split(' ');
if (typeof darkColors === 'string') darkColors = darkColors.split(' ');
// Cleanup after ourselves
delete palette.contrastDefaultColor;
delete palette.contrastLightColors;
delete palette.contrastStrongLightColors;
delete palette.contrastDarkColors;
// Change { 'A100': '#fffeee' } to { 'A100': { value: '#fffeee', contrast:DARK_CONTRAST_COLOR }
angular.forEach(palette, function(hueValue, hueName) {
if (angular.isObject(hueValue)) return; // Already converted
// Map everything to rgb colors
var rgbValue = colorToRgbaArray(hueValue);
if (!rgbValue) {
throw new Error("Color %1, in palette %2's hue %3, is invalid. Hex or rgb(a) color expected."
.replace('%1', hueValue)
.replace('%2', palette.name)
.replace('%3', hueName));
}
palette[hueName] = {
value: rgbValue,
contrast: getContrastColor()
};
function getContrastColor() {
if (defaultContrast === 'light') {
if (darkColors.indexOf(hueName) > -1) {
return DARK_CONTRAST_COLOR;
} else {
return strongLightColors.indexOf(hueName) > -1 ? STRONG_LIGHT_CONTRAST_COLOR
: LIGHT_CONTRAST_COLOR;
}
} else {
if (lightColors.indexOf(hueName) > -1) {
return strongLightColors.indexOf(hueName) > -1 ? STRONG_LIGHT_CONTRAST_COLOR
: LIGHT_CONTRAST_COLOR;
} else {
return DARK_CONTRAST_COLOR;
}
}
}
});
}
}
generateThemes.$inject = ["$injector"];
function checkValidPalette(theme, colorType) {
// If theme attempts to use a palette that doesnt exist, throw error
if (!PALETTES[ (theme.colors[colorType] || {}).name ]) {
throw new Error(
"You supplied an invalid color palette for theme %1's %2 palette. Available palettes: %3"
.replace('%1', theme.name)
.replace('%2', colorType)
.replace('%3', Object.keys(PALETTES).join(', '))
);
}
}
function colorToRgbaArray(clr) {
if (angular.isArray(clr) && clr.length == 3) return clr;
if (/^rgb/.test(clr)) {
return clr.replace(/(^\s*rgba?\(|\)\s*$)/g, '').split(',').map(function(value, i) {
return i == 3 ? parseFloat(value, 10) : parseInt(value, 10);
});
}
if (clr.charAt(0) == '#') clr = clr.substring(1);
if (!/^([a-fA-F0-9]{3}){1,2}$/g.test(clr)) return;
var dig = clr.length / 3;
var red = clr.substr(0, dig);
var grn = clr.substr(dig, dig);
var blu = clr.substr(dig * 2);
if (dig === 1) {
red += red;
grn += grn;
blu += blu;
}
return [parseInt(red, 16), parseInt(grn, 16), parseInt(blu, 16)];
}
function rgba(rgbArray, opacity) {
if (rgbArray.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(',') + ')';
}
})();
/*!
* Angular Material Design
* https://github.com/angular/material
* @license MIT
* v0.8.1
*/
(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'
]);
})();
/*!
* Angular Material Design
* https://github.com/angular/material
* @license MIT
* v0.8.1
*/
(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 coponents, 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', BackdropDirective);
function BackdropDirective($mdTheming) {
return $mdTheming;
}
BackdropDirective.$inject = ["$mdTheming"];
})();
/*!
* Angular Material Design
* https://github.com/angular/material
* @license MIT
* v0.8.1
*/
(function() {
'use strict';
/**
* @ngdoc module
* @name material.components.bottomSheet
* @description
* BottomSheet
*/
angular.module('material.components.bottomSheet', [
'material.core',
'material.components.backdrop'
])
.directive('mdBottomSheet', MdBottomSheetDirective)
.provider('$mdBottomSheet', MdBottomSheetProvider);
function MdBottomSheetDirective() {
return {
restrict: 'E'
};
}
/**
* @ngdoc service
* @name $mdBottomSheet
* @module material.components.bottomSheet
*
* @description
* `$mdBottomSheet` opens a bottom sheet over the app and provides a simple promise API.
*
* ## Restrictions
*
* - The bottom sheet's template must have an outer `` element.
* - Add the `md-grid` class to the bottom sheet for a grid layout.
* - Add the `md-list` class to the bottom sheet for a list layout.
*
* @usage
*
*
*
* Open a Bottom Sheet!
*
*
*
*
* var app = angular.module('app', ['ngMaterial']);
* app.controller('MyController', function($scope, $mdBottomSheet) {
* $scope.openBottomSheet = function() {
* $mdBottomSheet.show({
* template: 'Hello!'
* });
* };
* });
*
*/
/**
* @ngdoc method
* @name $mdBottomSheet#show
*
* @description
* Show a bottom sheet with the specified options.
*
* @param {object} options An options object, with the following properties:
*
* - `templateUrl` - `{string=}`: The url of an html template file that will
* be used as the content of the bottom sheet. Restrictions: the template must
* have an outer `md-bottom-sheet` element.
* - `template` - `{string=}`: Same as templateUrl, except this is an actual
* template string.
* - `scope` - `{object=}`: the scope to link the template / controller to. If none is specified, it will create a new child scope.
* This scope will be destroyed when the bottom sheet is removed unless `preserveScope` is set to true.
* - `preserveScope` - `{boolean=}`: whether to preserve the scope when the element is removed. Default is false
* - `controller` - `{string=}`: The controller to associate with this bottom sheet.
* - `locals` - `{string=}`: An object containing key/value pairs. The keys will
* be used as names of values to inject into the controller. For example,
* `locals: {three: 3}` would inject `three` into the controller with the value
* of 3.
* - `targetEvent` - `{DOMClickEvent=}`: A click's event object. When passed in as an option,
* the location of the click will be used as the starting point for the opening animation
* of the the dialog.
* - `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. Defaults to appending
* to the root element of the application.
* - `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()`.
*
* @param {*=} response An argument for the resolved promise.
*
*/
/**
* @ngdoc method
* @name $mdBottomSheet#cancel
*
* @description
* Hide the existing bottom sheet and reject the promise returned from
* `$mdBottomSheet.show()`.
*
* @param {*=} response An argument for the rejected promise.
*
*/
function MdBottomSheetProvider($$interimElementProvider) {
// how fast we need to flick down to close the sheet, pixels/ms
var CLOSING_VELOCITY = 0.5;
var PADDING = 80; // same as css
bottomSheetDefaults.$inject = ["$animate", "$mdConstant", "$timeout", "$$rAF", "$compile", "$mdTheming", "$mdBottomSheet", "$rootElement", "$rootScope", "$mdGesture"];
return $$interimElementProvider('$mdBottomSheet')
.setDefaults({
methods: ['disableParentScroll', 'escapeToClose', 'targetEvent'],
options: bottomSheetDefaults
});
/* @ngInject */
function bottomSheetDefaults($animate, $mdConstant, $timeout, $$rAF, $compile, $mdTheming, $mdBottomSheet, $rootElement, $rootScope, $mdGesture) {
var backdrop;
return {
themable: true,
targetEvent: null,
onShow: onShow,
onRemove: onRemove,
escapeToClose: true,
disableParentScroll: true
};
function onShow(scope, element, options) {
// Add a backdrop that will close on click
backdrop = $compile('')(scope);
backdrop.on('click', function() {
$timeout($mdBottomSheet.cancel);
});
$mdTheming.inherit(backdrop, options.parent);
$animate.enter(backdrop, options.parent, null);
var bottomSheet = new BottomSheet(element, options.parent);
options.bottomSheet = bottomSheet;
// Give up focus on calling item
options.targetEvent && angular.element(options.targetEvent.target).blur();
$mdTheming.inherit(bottomSheet.element, options.parent);
if (options.disableParentScroll) {
options.lastOverflow = options.parent.css('overflow');
options.parent.css('overflow', 'hidden');
}
return $animate.enter(bottomSheet.element, options.parent)
.then(function() {
var focusable = angular.element(
element[0].querySelector('button') ||
element[0].querySelector('a') ||
element[0].querySelector('[ng-click]')
);
focusable.focus();
if (options.escapeToClose) {
options.rootElementKeyupCallback = function(e) {
if (e.keyCode === $mdConstant.KEY_CODE.ESCAPE) {
$timeout($mdBottomSheet.cancel);
}
};
$rootElement.on('keyup', options.rootElementKeyupCallback);
}
});
}
function onRemove(scope, element, options) {
var bottomSheet = options.bottomSheet;
$animate.leave(backdrop);
return $animate.leave(bottomSheet.element).then(function() {
if (options.disableParentScroll) {
options.parent.css('overflow', options.lastOverflow);
delete options.lastOverflow;
}
bottomSheet.cleanup();
// Restore focus
options.targetEvent && angular.element(options.targetEvent.target).focus();
});
}
/**
* BottomSheet class to apply bottom-sheet behavior to an element
*/
function BottomSheet(element, parent) {
var deregister = $mdGesture.register(parent, 'drag', { horizontal: false });
parent.on('$md.dragstart', onDragStart)
.on('$md.drag', onDrag)
.on('$md.dragend', onDragEnd);
return {
element: element,
cleanup: function cleanup() {
deregister();
parent.off('$md.dragstart', onDragStart)
.off('$md.drag', onDrag)
.off('$md.dragend', onDragEnd);
}
};
function onDragStart(ev) {
// Disable transitions on transform so that it feels fast
element.css($mdConstant.CSS.TRANSITION_DURATION, '0ms');
}
function onDrag(ev) {
var transform = ev.pointer.distanceY;
if (transform < 5) {
// Slow down drag when trying to drag up, and stop after PADDING
transform = Math.max(-PADDING, transform / 2);
}
element.css($mdConstant.CSS.TRANSFORM, 'translate3d(0,' + (PADDING + transform) + 'px,0)');
}
function onDragEnd(ev) {
if (ev.pointer.distanceY > 0 &&
(ev.pointer.distanceY > 20 || Math.abs(ev.pointer.velocityY) > CLOSING_VELOCITY)) {
var distanceRemaining = element.prop('offsetHeight') - ev.pointer.distanceY;
var transitionDuration = Math.min(distanceRemaining / ev.pointer.velocityY * 0.75, 500);
element.css($mdConstant.CSS.TRANSITION_DURATION, transitionDuration + 'ms');
$timeout($mdBottomSheet.cancel);
} else {
element.css($mdConstant.CSS.TRANSITION_DURATION, '');
element.css($mdConstant.CSS.TRANSFORM, '');
}
}
}
}
}
MdBottomSheetProvider.$inject = ["$$interimElementProvider"];
})();
/*!
* Angular Material Design
* https://github.com/angular/material
* @license MIT
* v0.8.1
*/
(function() {
'use strict';
/**
* @ngdoc module
* @name material.components.card
*
* @description
* Card components.
*/
angular.module('material.components.card', [
'material.core'
])
.directive('mdCard', mdCardDirective);
/**
* @ngdoc directive
* @name mdCard
* @module material.components.card
*
* @restrict E
*
* @description
* The `` directive is a container element used within `` containers.
*
* Cards have constant width and variable heights; where the maximum height is limited to what can
* fit within a single view on a platform, but it can temporarily expand as needed
*
* @usage
*
*
*
*
Paracosm
*
* The titles of Washed Out's breakthrough song and the first single from Paracosm share the * two most important words in Ernest Greene's musical language: feel it. It's a simple request, as well...
*
*
*
*
*/
function mdCardDirective($mdTheming) {
return {
restrict: 'E',
link: function($scope, $element, $attr) {
$mdTheming($element);
}
};
}
mdCardDirective.$inject = ["$mdTheming"];
})();
/*!
* Angular Material Design
* https://github.com/angular/material
* @license MIT
* v0.8.1
*/
(function() {
'use strict';
/**
* @ngdoc module
* @name material.components.button
* @description
*
* Button
*/
angular.module('material.components.button', [
'material.core'
])
.directive('mdButton', MdButtonDirective);
/**
* @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 `