diff --git a/third_party/ui/bower_components/angular-animate/.bower.json b/third_party/ui/bower_components/angular-animate/.bower.json
new file mode 100644
index 0000000000..5694f6ad77
--- /dev/null
+++ b/third_party/ui/bower_components/angular-animate/.bower.json
@@ -0,0 +1,19 @@
+{
+ "name": "angular-animate",
+ "version": "1.3.15",
+ "main": "./angular-animate.js",
+ "ignore": [],
+ "dependencies": {
+ "angular": "1.3.15"
+ },
+ "homepage": "https://github.com/angular/bower-angular-animate",
+ "_release": "1.3.15",
+ "_resolution": {
+ "type": "version",
+ "tag": "v1.3.15",
+ "commit": "30fb369974560dbeb8a5311861c124e094944dab"
+ },
+ "_source": "git://github.com/angular/bower-angular-animate.git",
+ "_target": "1.3.x",
+ "_originalSource": "angular-animate"
+}
\ No newline at end of file
diff --git a/third_party/ui/bower_components/angular-animate/README.md b/third_party/ui/bower_components/angular-animate/README.md
new file mode 100644
index 0000000000..8313da67c3
--- /dev/null
+++ b/third_party/ui/bower_components/angular-animate/README.md
@@ -0,0 +1,68 @@
+# packaged angular-animate
+
+This repo is for distribution on `npm` and `bower`. The source for this module is in the
+[main AngularJS repo](https://github.com/angular/angular.js/tree/master/src/ngAnimate).
+Please file issues and pull requests against that repo.
+
+## Install
+
+You can install this package either with `npm` or with `bower`.
+
+### npm
+
+```shell
+npm install angular-animate
+```
+
+Then add `ngAnimate` as a dependency for your app:
+
+```javascript
+angular.module('myApp', [require('angular-animate')]);
+```
+
+### bower
+
+```shell
+bower install angular-animate
+```
+
+Then add a `
+```
+
+Then add `ngAnimate` as a dependency for your app:
+
+```javascript
+angular.module('myApp', ['ngAnimate']);
+```
+
+## Documentation
+
+Documentation is available on the
+[AngularJS docs site](http://docs.angularjs.org/api/ngAnimate).
+
+## License
+
+The MIT License
+
+Copyright (c) 2010-2015 Google, Inc. http://angularjs.org
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/third_party/ui/bower_components/angular-animate/angular-animate.js b/third_party/ui/bower_components/angular-animate/angular-animate.js
new file mode 100644
index 0000000000..7bd0d7a3a6
--- /dev/null
+++ b/third_party/ui/bower_components/angular-animate/angular-animate.js
@@ -0,0 +1,2137 @@
+/**
+ * @license AngularJS v1.3.15
+ * (c) 2010-2014 Google, Inc. http://angularjs.org
+ * License: MIT
+ */
+(function(window, angular, undefined) {'use strict';
+
+/* jshint maxlen: false */
+
+/**
+ * @ngdoc module
+ * @name ngAnimate
+ * @description
+ *
+ * The `ngAnimate` module provides support for JavaScript, CSS3 transition and CSS3 keyframe animation hooks within existing core and custom directives.
+ *
+ *
+ *
+ * # Usage
+ *
+ * To see animations in action, all that is required is to define the appropriate CSS classes
+ * or to register a JavaScript animation via the `myModule.animation()` function. The directives that support animation automatically are:
+ * `ngRepeat`, `ngInclude`, `ngIf`, `ngSwitch`, `ngShow`, `ngHide`, `ngView` and `ngClass`. Custom directives can take advantage of animation
+ * by using the `$animate` service.
+ *
+ * Below is a more detailed breakdown of the supported animation events provided by pre-existing ng directives:
+ *
+ * | Directive | Supported Animations |
+ * |----------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------|
+ * | {@link ng.directive:ngRepeat#animations ngRepeat} | enter, leave and move |
+ * | {@link ngRoute.directive:ngView#animations ngView} | enter and leave |
+ * | {@link ng.directive:ngInclude#animations ngInclude} | enter and leave |
+ * | {@link ng.directive:ngSwitch#animations ngSwitch} | enter and leave |
+ * | {@link ng.directive:ngIf#animations ngIf} | enter and leave |
+ * | {@link ng.directive:ngClass#animations ngClass} | add and remove (the CSS class(es) present) |
+ * | {@link ng.directive:ngShow#animations ngShow} & {@link ng.directive:ngHide#animations ngHide} | add and remove (the ng-hide class value) |
+ * | {@link ng.directive:form#animation-hooks form} & {@link ng.directive:ngModel#animation-hooks ngModel} | add and remove (dirty, pristine, valid, invalid & all other validations) |
+ * | {@link module:ngMessages#animations ngMessages} | add and remove (ng-active & ng-inactive) |
+ * | {@link module:ngMessages#animations ngMessage} | enter and leave |
+ *
+ * You can find out more information about animations upon visiting each directive page.
+ *
+ * Below is an example of how to apply animations to a directive that supports animation hooks:
+ *
+ * ```html
+ *
+ *
+ *
+ *
+ * ```
+ *
+ * Keep in mind that, by default, if an animation is running, any child elements cannot be animated
+ * until the parent element's animation has completed. This blocking feature can be overridden by
+ * placing the `ng-animate-children` attribute on a parent container tag.
+ *
+ * ```html
+ *
+ *
+ *
+ * ...
+ *
+ *
+ *
+ * ```
+ *
+ * When the `on` expression value changes and an animation is triggered then each of the elements within
+ * will all animate without the block being applied to child elements.
+ *
+ * ## Are animations run when the application starts?
+ * No they are not. When an application is bootstrapped Angular will disable animations from running to avoid
+ * a frenzy of animations from being triggered as soon as the browser has rendered the screen. For this to work,
+ * Angular will wait for two digest cycles until enabling animations. From there on, any animation-triggering
+ * layout changes in the application will trigger animations as normal.
+ *
+ * In addition, upon bootstrap, if the routing system or any directives or load remote data (via $http) then Angular
+ * will automatically extend the wait time to enable animations once **all** of the outbound HTTP requests
+ * are complete.
+ *
+ * ## CSS-defined Animations
+ * The animate service will automatically apply two CSS classes to the animated element and these two CSS classes
+ * are designed to contain the start and end CSS styling. Both CSS transitions and keyframe animations are supported
+ * and can be used to play along with this naming structure.
+ *
+ * The following code below demonstrates how to perform animations using **CSS transitions** with Angular:
+ *
+ * ```html
+ *
+ *
+ *
+ *
+ *
+ * ```
+ *
+ * The following code below demonstrates how to perform animations using **CSS animations** with Angular:
+ *
+ * ```html
+ *
+ *
+ *
+ *
+ *
+ * ```
+ *
+ * Both CSS3 animations and transitions can be used together and the animate service will figure out the correct duration and delay timing.
+ *
+ * Upon DOM mutation, the event class is added first (something like `ng-enter`), then the browser prepares itself to add
+ * the active class (in this case `ng-enter-active`) which then triggers the animation. The animation module will automatically
+ * detect the CSS code to determine when the animation ends. Once the animation is over then both CSS classes will be
+ * removed from the DOM. If a browser does not support CSS transitions or CSS animations then the animation will start and end
+ * immediately resulting in a DOM element that is at its final state. This final state is when the DOM element
+ * has no CSS transition/animation classes applied to it.
+ *
+ * ### Structural transition animations
+ *
+ * Structural transitions (such as enter, leave and move) will always apply a `0s none` transition
+ * value to force the browser into rendering the styles defined in the setup (`.ng-enter`, `.ng-leave`
+ * or `.ng-move`) class. This means that any active transition animations operating on the element
+ * will be cut off to make way for the enter, leave or move animation.
+ *
+ * ### Class-based transition animations
+ *
+ * Class-based transitions refer to transition animations that are triggered when a CSS class is
+ * added to or removed from the element (via `$animate.addClass`, `$animate.removeClass`,
+ * `$animate.setClass`, or by directives such as `ngClass`, `ngModel` and `form`).
+ * They are different when compared to structural animations since they **do not cancel existing
+ * animations** nor do they **block successive transitions** from rendering on the same element.
+ * This distinction allows for **multiple class-based transitions** to be performed on the same element.
+ *
+ * In addition to ngAnimate supporting the default (natural) functionality of class-based transition
+ * animations, ngAnimate also decorates the element with starting and ending CSS classes to aid the
+ * developer in further styling the element throughout the transition animation. Earlier versions
+ * of ngAnimate may have caused natural CSS transitions to break and not render properly due to
+ * $animate temporarily blocking transitions using `0s none` in order to allow the setup CSS class
+ * (the `-add` or `-remove` class) to be applied without triggering an animation. However, as of
+ * **version 1.3**, this workaround has been removed with ngAnimate and all non-ngAnimate CSS
+ * class transitions are compatible with ngAnimate.
+ *
+ * There is, however, one special case when dealing with class-based transitions in ngAnimate.
+ * When rendering class-based transitions that make use of the setup and active CSS classes
+ * (e.g. `.fade-add` and `.fade-add-active` for when `.fade` is added) be sure to define
+ * the transition value **on the active CSS class** and not the setup class.
+ *
+ * ```css
+ * .fade-add {
+ * /* remember to place a 0s transition here
+ * to ensure that the styles are applied instantly
+ * even if the element already has a transition style */
+ * transition:0s linear all;
+ *
+ * /* starting CSS styles */
+ * opacity:1;
+ * }
+ * .fade-add.fade-add-active {
+ * /* this will be the length of the animation */
+ * transition:1s linear all;
+ * opacity:0;
+ * }
+ * ```
+ *
+ * The setup CSS class (in this case `.fade-add`) also has a transition style property, however, it
+ * has a duration of zero. This may not be required, however, incase the browser is unable to render
+ * the styling present in this CSS class instantly then it could be that the browser is attempting
+ * to perform an unnecessary transition.
+ *
+ * This workaround, however, does not apply to standard class-based transitions that are rendered
+ * when a CSS class containing a transition is applied to an element:
+ *
+ * ```css
+ * /* this works as expected */
+ * .fade {
+ * transition:1s linear all;
+ * opacity:0;
+ * }
+ * ```
+ *
+ * Please keep this in mind when coding the CSS markup that will be used within class-based transitions.
+ * Also, try not to mix the two class-based animation flavors together since the CSS code may become
+ * overly complex.
+ *
+ *
+ * ### Preventing Collisions With Third Party Libraries
+ *
+ * Some third-party frameworks place animation duration defaults across many element or className
+ * selectors in order to make their code small and reuseable. This can lead to issues with ngAnimate, which
+ * is expecting actual animations on these elements and has to wait for their completion.
+ *
+ * You can prevent this unwanted behavior by using a prefix on all your animation classes:
+ *
+ * ```css
+ * /* prefixed with animate- */
+ * .animate-fade-add.animate-fade-add-active {
+ * transition:1s linear all;
+ * opacity:0;
+ * }
+ * ```
+ *
+ * You then configure `$animate` to enforce this prefix:
+ *
+ * ```js
+ * $animateProvider.classNameFilter(/animate-/);
+ * ```
+ *
+ *
+ * ### CSS Staggering Animations
+ * A Staggering animation is a collection of animations that are issued with a slight delay in between each successive operation resulting in a
+ * curtain-like effect. The ngAnimate module (versions >=1.2) supports staggering animations and the stagger effect can be
+ * performed by creating a **ng-EVENT-stagger** CSS class and attaching that class to the base CSS class used for
+ * the animation. The style property expected within the stagger class can either be a **transition-delay** or an
+ * **animation-delay** property (or both if your animation contains both transitions and keyframe animations).
+ *
+ * ```css
+ * .my-animation.ng-enter {
+ * /* standard transition code */
+ * -webkit-transition: 1s linear all;
+ * transition: 1s linear all;
+ * opacity:0;
+ * }
+ * .my-animation.ng-enter-stagger {
+ * /* this will have a 100ms delay between each successive leave animation */
+ * -webkit-transition-delay: 0.1s;
+ * transition-delay: 0.1s;
+ *
+ * /* in case the stagger doesn't work then these two values
+ * must be set to 0 to avoid an accidental CSS inheritance */
+ * -webkit-transition-duration: 0s;
+ * transition-duration: 0s;
+ * }
+ * .my-animation.ng-enter.ng-enter-active {
+ * /* standard transition styles */
+ * opacity:1;
+ * }
+ * ```
+ *
+ * Staggering animations work by default in ngRepeat (so long as the CSS class is defined). Outside of ngRepeat, to use staggering animations
+ * on your own, they can be triggered by firing multiple calls to the same event on $animate. However, the restrictions surrounding this
+ * are that each of the elements must have the same CSS className value as well as the same parent element. A stagger operation
+ * will also be reset if more than 10ms has passed after the last animation has been fired.
+ *
+ * The following code will issue the **ng-leave-stagger** event on the element provided:
+ *
+ * ```js
+ * var kids = parent.children();
+ *
+ * $animate.leave(kids[0]); //stagger index=0
+ * $animate.leave(kids[1]); //stagger index=1
+ * $animate.leave(kids[2]); //stagger index=2
+ * $animate.leave(kids[3]); //stagger index=3
+ * $animate.leave(kids[4]); //stagger index=4
+ *
+ * $timeout(function() {
+ * //stagger has reset itself
+ * $animate.leave(kids[5]); //stagger index=0
+ * $animate.leave(kids[6]); //stagger index=1
+ * }, 100, false);
+ * ```
+ *
+ * Stagger animations are currently only supported within CSS-defined animations.
+ *
+ * ## JavaScript-defined Animations
+ * In the event that you do not want to use CSS3 transitions or CSS3 animations or if you wish to offer animations on browsers that do not
+ * yet support CSS transitions/animations, then you can make use of JavaScript animations defined inside of your AngularJS module.
+ *
+ * ```js
+ * //!annotate="YourApp" Your AngularJS Module|Replace this or ngModule with the module that you used to define your application.
+ * var ngModule = angular.module('YourApp', ['ngAnimate']);
+ * ngModule.animation('.my-crazy-animation', function() {
+ * return {
+ * enter: function(element, done) {
+ * //run the animation here and call done when the animation is complete
+ * return function(cancelled) {
+ * //this (optional) function will be called when the animation
+ * //completes or when the animation is cancelled (the cancelled
+ * //flag will be set to true if cancelled).
+ * };
+ * },
+ * leave: function(element, done) { },
+ * move: function(element, done) { },
+ *
+ * //animation that can be triggered before the class is added
+ * beforeAddClass: function(element, className, done) { },
+ *
+ * //animation that can be triggered after the class is added
+ * addClass: function(element, className, done) { },
+ *
+ * //animation that can be triggered before the class is removed
+ * beforeRemoveClass: function(element, className, done) { },
+ *
+ * //animation that can be triggered after the class is removed
+ * removeClass: function(element, className, done) { }
+ * };
+ * });
+ * ```
+ *
+ * JavaScript-defined animations are created with a CSS-like class selector and a collection of events which are set to run
+ * a javascript callback function. When an animation is triggered, $animate will look for a matching animation which fits
+ * the element's CSS class attribute value and then run the matching animation event function (if found).
+ * In other words, if the CSS classes present on the animated element match any of the JavaScript animations then the callback function will
+ * be executed. It should be also noted that only simple, single class selectors are allowed (compound class selectors are not supported).
+ *
+ * Within a JavaScript animation, an object containing various event callback animation functions is expected to be returned.
+ * As explained above, these callbacks are triggered based on the animation event. Therefore if an enter animation is run,
+ * and the JavaScript animation is found, then the enter callback will handle that animation (in addition to the CSS keyframe animation
+ * or transition code that is defined via a stylesheet).
+ *
+ *
+ * ### Applying Directive-specific Styles to an Animation
+ * In some cases a directive or service may want to provide `$animate` with extra details that the animation will
+ * include into its animation. Let's say for example we wanted to render an animation that animates an element
+ * towards the mouse coordinates as to where the user clicked last. By collecting the X/Y coordinates of the click
+ * (via the event parameter) we can set the `top` and `left` styles into an object and pass that into our function
+ * call to `$animate.addClass`.
+ *
+ * ```js
+ * canvas.on('click', function(e) {
+ * $animate.addClass(element, 'on', {
+ * to: {
+ * left : e.client.x + 'px',
+ * top : e.client.y + 'px'
+ * }
+ * }):
+ * });
+ * ```
+ *
+ * Now when the animation runs, and a transition or keyframe animation is picked up, then the animation itself will
+ * also include and transition the styling of the `left` and `top` properties into its running animation. If we want
+ * to provide some starting animation values then we can do so by placing the starting animations styles into an object
+ * called `from` in the same object as the `to` animations.
+ *
+ * ```js
+ * canvas.on('click', function(e) {
+ * $animate.addClass(element, 'on', {
+ * from: {
+ * position: 'absolute',
+ * left: '0px',
+ * top: '0px'
+ * },
+ * to: {
+ * left : e.client.x + 'px',
+ * top : e.client.y + 'px'
+ * }
+ * }):
+ * });
+ * ```
+ *
+ * Once the animation is complete or cancelled then the union of both the before and after styles are applied to the
+ * element. If `ngAnimate` is not present then the styles will be applied immediately.
+ *
+ */
+
+angular.module('ngAnimate', ['ng'])
+
+ /**
+ * @ngdoc provider
+ * @name $animateProvider
+ * @description
+ *
+ * The `$animateProvider` allows developers to register JavaScript animation event handlers directly inside of a module.
+ * When an animation is triggered, the $animate service will query the $animate service to find any animations that match
+ * the provided name value.
+ *
+ * Requires the {@link ngAnimate `ngAnimate`} module to be installed.
+ *
+ * Please visit the {@link ngAnimate `ngAnimate`} module overview page learn more about how to use animations in your application.
+ *
+ */
+ .directive('ngAnimateChildren', function() {
+ var NG_ANIMATE_CHILDREN = '$$ngAnimateChildren';
+ return function(scope, element, attrs) {
+ var val = attrs.ngAnimateChildren;
+ if (angular.isString(val) && val.length === 0) { //empty attribute
+ element.data(NG_ANIMATE_CHILDREN, true);
+ } else {
+ scope.$watch(val, function(value) {
+ element.data(NG_ANIMATE_CHILDREN, !!value);
+ });
+ }
+ };
+ })
+
+ //this private service is only used within CSS-enabled animations
+ //IE8 + IE9 do not support rAF natively, but that is fine since they
+ //also don't support transitions and keyframes which means that the code
+ //below will never be used by the two browsers.
+ .factory('$$animateReflow', ['$$rAF', '$document', function($$rAF, $document) {
+ var bod = $document[0].body;
+ return function(fn) {
+ //the returned function acts as the cancellation function
+ return $$rAF(function() {
+ //the line below will force the browser to perform a repaint
+ //so that all the animated elements within the animation frame
+ //will be properly updated and drawn on screen. This is
+ //required to perform multi-class CSS based animations with
+ //Firefox. DO NOT REMOVE THIS LINE.
+ var a = bod.offsetWidth + 1;
+ fn();
+ });
+ };
+ }])
+
+ .config(['$provide', '$animateProvider', function($provide, $animateProvider) {
+ var noop = angular.noop;
+ var forEach = angular.forEach;
+ var selectors = $animateProvider.$$selectors;
+ var isArray = angular.isArray;
+ var isString = angular.isString;
+ var isObject = angular.isObject;
+
+ var ELEMENT_NODE = 1;
+ var NG_ANIMATE_STATE = '$$ngAnimateState';
+ var NG_ANIMATE_CHILDREN = '$$ngAnimateChildren';
+ var NG_ANIMATE_CLASS_NAME = 'ng-animate';
+ var rootAnimateState = {running: true};
+
+ function extractElementNode(element) {
+ for (var i = 0; i < element.length; i++) {
+ var elm = element[i];
+ if (elm.nodeType == ELEMENT_NODE) {
+ return elm;
+ }
+ }
+ }
+
+ function prepareElement(element) {
+ return element && angular.element(element);
+ }
+
+ function stripCommentsFromElement(element) {
+ return angular.element(extractElementNode(element));
+ }
+
+ function isMatchingElement(elm1, elm2) {
+ return extractElementNode(elm1) == extractElementNode(elm2);
+ }
+ var $$jqLite;
+ $provide.decorator('$animate',
+ ['$delegate', '$$q', '$injector', '$sniffer', '$rootElement', '$$asyncCallback', '$rootScope', '$document', '$templateRequest', '$$jqLite',
+ function($delegate, $$q, $injector, $sniffer, $rootElement, $$asyncCallback, $rootScope, $document, $templateRequest, $$$jqLite) {
+
+ $$jqLite = $$$jqLite;
+ $rootElement.data(NG_ANIMATE_STATE, rootAnimateState);
+
+ // Wait until all directive and route-related templates are downloaded and
+ // compiled. The $templateRequest.totalPendingRequests variable keeps track of
+ // all of the remote templates being currently downloaded. If there are no
+ // templates currently downloading then the watcher will still fire anyway.
+ var deregisterWatch = $rootScope.$watch(
+ function() { return $templateRequest.totalPendingRequests; },
+ function(val, oldVal) {
+ if (val !== 0) return;
+ deregisterWatch();
+
+ // Now that all templates have been downloaded, $animate will wait until
+ // the post digest queue is empty before enabling animations. By having two
+ // calls to $postDigest calls we can ensure that the flag is enabled at the
+ // very end of the post digest queue. Since all of the animations in $animate
+ // use $postDigest, it's important that the code below executes at the end.
+ // This basically means that the page is fully downloaded and compiled before
+ // any animations are triggered.
+ $rootScope.$$postDigest(function() {
+ $rootScope.$$postDigest(function() {
+ rootAnimateState.running = false;
+ });
+ });
+ }
+ );
+
+ var globalAnimationCounter = 0;
+ var classNameFilter = $animateProvider.classNameFilter();
+ var isAnimatableClassName = !classNameFilter
+ ? function() { return true; }
+ : function(className) {
+ return classNameFilter.test(className);
+ };
+
+ function classBasedAnimationsBlocked(element, setter) {
+ var data = element.data(NG_ANIMATE_STATE) || {};
+ if (setter) {
+ data.running = true;
+ data.structural = true;
+ element.data(NG_ANIMATE_STATE, data);
+ }
+ return data.disabled || (data.running && data.structural);
+ }
+
+ function runAnimationPostDigest(fn) {
+ var cancelFn, defer = $$q.defer();
+ defer.promise.$$cancelFn = function() {
+ cancelFn && cancelFn();
+ };
+ $rootScope.$$postDigest(function() {
+ cancelFn = fn(function() {
+ defer.resolve();
+ });
+ });
+ return defer.promise;
+ }
+
+ function parseAnimateOptions(options) {
+ // some plugin code may still be passing in the callback
+ // function as the last param for the $animate methods so
+ // it's best to only allow string or array values for now
+ if (isObject(options)) {
+ if (options.tempClasses && isString(options.tempClasses)) {
+ options.tempClasses = options.tempClasses.split(/\s+/);
+ }
+ return options;
+ }
+ }
+
+ function resolveElementClasses(element, cache, runningAnimations) {
+ runningAnimations = runningAnimations || {};
+
+ var lookup = {};
+ forEach(runningAnimations, function(data, selector) {
+ forEach(selector.split(' '), function(s) {
+ lookup[s]=data;
+ });
+ });
+
+ var hasClasses = Object.create(null);
+ forEach((element.attr('class') || '').split(/\s+/), function(className) {
+ hasClasses[className] = true;
+ });
+
+ var toAdd = [], toRemove = [];
+ forEach((cache && cache.classes) || [], function(status, className) {
+ var hasClass = hasClasses[className];
+ var matchingAnimation = lookup[className] || {};
+
+ // When addClass and removeClass is called then $animate will check to
+ // see if addClass and removeClass cancel each other out. When there are
+ // more calls to removeClass than addClass then the count falls below 0
+ // and then the removeClass animation will be allowed. Otherwise if the
+ // count is above 0 then that means an addClass animation will commence.
+ // Once an animation is allowed then the code will also check to see if
+ // there exists any on-going animation that is already adding or remvoing
+ // the matching CSS class.
+ if (status === false) {
+ //does it have the class or will it have the class
+ if (hasClass || matchingAnimation.event == 'addClass') {
+ toRemove.push(className);
+ }
+ } else if (status === true) {
+ //is the class missing or will it be removed?
+ if (!hasClass || matchingAnimation.event == 'removeClass') {
+ toAdd.push(className);
+ }
+ }
+ });
+
+ return (toAdd.length + toRemove.length) > 0 && [toAdd.join(' '), toRemove.join(' ')];
+ }
+
+ function lookup(name) {
+ if (name) {
+ var matches = [],
+ flagMap = {},
+ classes = name.substr(1).split('.');
+
+ //the empty string value is the default animation
+ //operation which performs CSS transition and keyframe
+ //animations sniffing. This is always included for each
+ //element animation procedure if the browser supports
+ //transitions and/or keyframe animations. The default
+ //animation is added to the top of the list to prevent
+ //any previous animations from affecting the element styling
+ //prior to the element being animated.
+ if ($sniffer.transitions || $sniffer.animations) {
+ matches.push($injector.get(selectors['']));
+ }
+
+ for (var i=0; i < classes.length; i++) {
+ var klass = classes[i],
+ selectorFactoryName = selectors[klass];
+ if (selectorFactoryName && !flagMap[klass]) {
+ matches.push($injector.get(selectorFactoryName));
+ flagMap[klass] = true;
+ }
+ }
+ return matches;
+ }
+ }
+
+ function animationRunner(element, animationEvent, className, options) {
+ //transcluded directives may sometimes fire an animation using only comment nodes
+ //best to catch this early on to prevent any animation operations from occurring
+ var node = element[0];
+ if (!node) {
+ return;
+ }
+
+ if (options) {
+ options.to = options.to || {};
+ options.from = options.from || {};
+ }
+
+ var classNameAdd;
+ var classNameRemove;
+ if (isArray(className)) {
+ classNameAdd = className[0];
+ classNameRemove = className[1];
+ if (!classNameAdd) {
+ className = classNameRemove;
+ animationEvent = 'removeClass';
+ } else if (!classNameRemove) {
+ className = classNameAdd;
+ animationEvent = 'addClass';
+ } else {
+ className = classNameAdd + ' ' + classNameRemove;
+ }
+ }
+
+ var isSetClassOperation = animationEvent == 'setClass';
+ var isClassBased = isSetClassOperation
+ || animationEvent == 'addClass'
+ || animationEvent == 'removeClass'
+ || animationEvent == 'animate';
+
+ var currentClassName = element.attr('class');
+ var classes = currentClassName + ' ' + className;
+ if (!isAnimatableClassName(classes)) {
+ return;
+ }
+
+ var beforeComplete = noop,
+ beforeCancel = [],
+ before = [],
+ afterComplete = noop,
+ afterCancel = [],
+ after = [];
+
+ var animationLookup = (' ' + classes).replace(/\s+/g,'.');
+ forEach(lookup(animationLookup), function(animationFactory) {
+ var created = registerAnimation(animationFactory, animationEvent);
+ if (!created && isSetClassOperation) {
+ registerAnimation(animationFactory, 'addClass');
+ registerAnimation(animationFactory, 'removeClass');
+ }
+ });
+
+ function registerAnimation(animationFactory, event) {
+ var afterFn = animationFactory[event];
+ var beforeFn = animationFactory['before' + event.charAt(0).toUpperCase() + event.substr(1)];
+ if (afterFn || beforeFn) {
+ if (event == 'leave') {
+ beforeFn = afterFn;
+ //when set as null then animation knows to skip this phase
+ afterFn = null;
+ }
+ after.push({
+ event: event, fn: afterFn
+ });
+ before.push({
+ event: event, fn: beforeFn
+ });
+ return true;
+ }
+ }
+
+ function run(fns, cancellations, allCompleteFn) {
+ var animations = [];
+ forEach(fns, function(animation) {
+ animation.fn && animations.push(animation);
+ });
+
+ var count = 0;
+ function afterAnimationComplete(index) {
+ if (cancellations) {
+ (cancellations[index] || noop)();
+ if (++count < animations.length) return;
+ cancellations = null;
+ }
+ allCompleteFn();
+ }
+
+ //The code below adds directly to the array in order to work with
+ //both sync and async animations. Sync animations are when the done()
+ //operation is called right away. DO NOT REFACTOR!
+ forEach(animations, function(animation, index) {
+ var progress = function() {
+ afterAnimationComplete(index);
+ };
+ switch (animation.event) {
+ case 'setClass':
+ cancellations.push(animation.fn(element, classNameAdd, classNameRemove, progress, options));
+ break;
+ case 'animate':
+ cancellations.push(animation.fn(element, className, options.from, options.to, progress));
+ break;
+ case 'addClass':
+ cancellations.push(animation.fn(element, classNameAdd || className, progress, options));
+ break;
+ case 'removeClass':
+ cancellations.push(animation.fn(element, classNameRemove || className, progress, options));
+ break;
+ default:
+ cancellations.push(animation.fn(element, progress, options));
+ break;
+ }
+ });
+
+ if (cancellations && cancellations.length === 0) {
+ allCompleteFn();
+ }
+ }
+
+ return {
+ node: node,
+ event: animationEvent,
+ className: className,
+ isClassBased: isClassBased,
+ isSetClassOperation: isSetClassOperation,
+ applyStyles: function() {
+ if (options) {
+ element.css(angular.extend(options.from || {}, options.to || {}));
+ }
+ },
+ before: function(allCompleteFn) {
+ beforeComplete = allCompleteFn;
+ run(before, beforeCancel, function() {
+ beforeComplete = noop;
+ allCompleteFn();
+ });
+ },
+ after: function(allCompleteFn) {
+ afterComplete = allCompleteFn;
+ run(after, afterCancel, function() {
+ afterComplete = noop;
+ allCompleteFn();
+ });
+ },
+ cancel: function() {
+ if (beforeCancel) {
+ forEach(beforeCancel, function(cancelFn) {
+ (cancelFn || noop)(true);
+ });
+ beforeComplete(true);
+ }
+ if (afterCancel) {
+ forEach(afterCancel, function(cancelFn) {
+ (cancelFn || noop)(true);
+ });
+ afterComplete(true);
+ }
+ }
+ };
+ }
+
+ /**
+ * @ngdoc service
+ * @name $animate
+ * @kind object
+ *
+ * @description
+ * The `$animate` service provides animation detection support while performing DOM operations (enter, leave and move) as well as during addClass and removeClass operations.
+ * When any of these operations are run, the $animate service
+ * will examine any JavaScript-defined animations (which are defined by using the $animateProvider provider object)
+ * as well as any CSS-defined animations against the CSS classes present on the element once the DOM operation is run.
+ *
+ * The `$animate` service is used behind the scenes with pre-existing directives and animation with these directives
+ * will work out of the box without any extra configuration.
+ *
+ * Requires the {@link ngAnimate `ngAnimate`} module to be installed.
+ *
+ * Please visit the {@link ngAnimate `ngAnimate`} module overview page learn more about how to use animations in your application.
+ * ## Callback Promises
+ * With AngularJS 1.3, each of the animation methods, on the `$animate` service, return a promise when called. The
+ * promise itself is then resolved once the animation has completed itself, has been cancelled or has been
+ * skipped due to animations being disabled. (Note that even if the animation is cancelled it will still
+ * call the resolve function of the animation.)
+ *
+ * ```js
+ * $animate.enter(element, container).then(function() {
+ * //...this is called once the animation is complete...
+ * });
+ * ```
+ *
+ * Also note that, due to the nature of the callback promise, if any Angular-specific code (like changing the scope,
+ * location of the page, etc...) is executed within the callback promise then be sure to wrap the code using
+ * `$scope.$apply(...)`;
+ *
+ * ```js
+ * $animate.leave(element).then(function() {
+ * $scope.$apply(function() {
+ * $location.path('/new-page');
+ * });
+ * });
+ * ```
+ *
+ * An animation can also be cancelled by calling the `$animate.cancel(promise)` method with the provided
+ * promise that was returned when the animation was started.
+ *
+ * ```js
+ * var promise = $animate.addClass(element, 'super-long-animation');
+ * promise.then(function() {
+ * //this will still be called even if cancelled
+ * });
+ *
+ * element.on('click', function() {
+ * //tooo lazy to wait for the animation to end
+ * $animate.cancel(promise);
+ * });
+ * ```
+ *
+ * (Keep in mind that the promise cancellation is unique to `$animate` since promises in
+ * general cannot be cancelled.)
+ *
+ */
+ return {
+ /**
+ * @ngdoc method
+ * @name $animate#animate
+ * @kind function
+ *
+ * @description
+ * Performs an inline animation on the element which applies the provided `to` and `from` CSS styles to the element.
+ * If any detected CSS transition, keyframe or JavaScript matches the provided `className` value then the animation
+ * will take on the provided styles. For example, if a transition animation is set for the given className then the
+ * provided `from` and `to` styles will be applied alongside the given transition. If a JavaScript animation is
+ * detected then the provided styles will be given in as function paramters.
+ *
+ * ```js
+ * ngModule.animation('.my-inline-animation', function() {
+ * return {
+ * animate : function(element, className, from, to, done) {
+ * //styles
+ * }
+ * }
+ * });
+ * ```
+ *
+ * Below is a breakdown of each step that occurs during the `animate` animation:
+ *
+ * | Animation Step | What the element class attribute looks like |
+ * |-----------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------|
+ * | 1. `$animate.animate(...)` is called | `class="my-animation"` |
+ * | 2. `$animate` waits for the next digest to start the animation | `class="my-animation ng-animate"` |
+ * | 3. `$animate` runs the JavaScript-defined animations detected on the element | `class="my-animation ng-animate"` |
+ * | 4. the `className` class value is added to the element | `class="my-animation ng-animate className"` |
+ * | 5. `$animate` scans the element styles to get the CSS transition/animation duration and delay | `class="my-animation ng-animate className"` |
+ * | 6. `$animate` blocks all CSS transitions on the element to ensure the `.className` class styling is applied right away| `class="my-animation ng-animate className"` |
+ * | 7. `$animate` applies the provided collection of `from` CSS styles to the element | `class="my-animation ng-animate className"` |
+ * | 8. `$animate` waits for a single animation frame (this performs a reflow) | `class="my-animation ng-animate className"` |
+ * | 9. `$animate` removes the CSS transition block placed on the element | `class="my-animation ng-animate className"` |
+ * | 10. the `className-active` class is added (this triggers the CSS transition/animation) | `class="my-animation ng-animate className className-active"` |
+ * | 11. `$animate` applies the collection of `to` CSS styles to the element which are then handled by the transition | `class="my-animation ng-animate className className-active"` |
+ * | 12. `$animate` waits for the animation to complete (via events and timeout) | `class="my-animation ng-animate className className-active"` |
+ * | 13. The animation ends and all generated CSS classes are removed from the element | `class="my-animation"` |
+ * | 14. The returned promise is resolved. | `class="my-animation"` |
+ *
+ * @param {DOMElement} element the element that will be the focus of the enter animation
+ * @param {object} from a collection of CSS styles that will be applied to the element at the start of the animation
+ * @param {object} to a collection of CSS styles that the element will animate towards
+ * @param {string=} className an optional CSS class that will be added to the element for the duration of the animation (the default class is `ng-inline-animate`)
+ * @param {object=} options an optional collection of options that will be picked up by the CSS transition/animation
+ * @return {Promise} the animation callback promise
+ */
+ animate: function(element, from, to, className, options) {
+ className = className || 'ng-inline-animate';
+ options = parseAnimateOptions(options) || {};
+ options.from = to ? from : null;
+ options.to = to ? to : from;
+
+ return runAnimationPostDigest(function(done) {
+ return performAnimation('animate', className, stripCommentsFromElement(element), null, null, noop, options, done);
+ });
+ },
+
+ /**
+ * @ngdoc method
+ * @name $animate#enter
+ * @kind function
+ *
+ * @description
+ * Appends the element to the parentElement element that resides in the document and then runs the enter animation. Once
+ * the animation is started, the following CSS classes will be present on the element for the duration of the animation:
+ *
+ * Below is a breakdown of each step that occurs during enter animation:
+ *
+ * | Animation Step | What the element class attribute looks like |
+ * |-----------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------|
+ * | 1. `$animate.enter(...)` is called | `class="my-animation"` |
+ * | 2. element is inserted into the `parentElement` element or beside the `afterElement` element | `class="my-animation"` |
+ * | 3. `$animate` waits for the next digest to start the animation | `class="my-animation ng-animate"` |
+ * | 4. `$animate` runs the JavaScript-defined animations detected on the element | `class="my-animation ng-animate"` |
+ * | 5. the `.ng-enter` class is added to the element | `class="my-animation ng-animate ng-enter"` |
+ * | 6. `$animate` scans the element styles to get the CSS transition/animation duration and delay | `class="my-animation ng-animate ng-enter"` |
+ * | 7. `$animate` blocks all CSS transitions on the element to ensure the `.ng-enter` class styling is applied right away | `class="my-animation ng-animate ng-enter"` |
+ * | 8. `$animate` waits for a single animation frame (this performs a reflow) | `class="my-animation ng-animate ng-enter"` |
+ * | 9. `$animate` removes the CSS transition block placed on the element | `class="my-animation ng-animate ng-enter"` |
+ * | 10. the `.ng-enter-active` class is added (this triggers the CSS transition/animation) | `class="my-animation ng-animate ng-enter ng-enter-active"` |
+ * | 11. `$animate` waits for the animation to complete (via events and timeout) | `class="my-animation ng-animate ng-enter ng-enter-active"` |
+ * | 12. The animation ends and all generated CSS classes are removed from the element | `class="my-animation"` |
+ * | 13. The returned promise is resolved. | `class="my-animation"` |
+ *
+ * @param {DOMElement} element the element that will be the focus of the enter animation
+ * @param {DOMElement} parentElement the parent element of the element that will be the focus of the enter animation
+ * @param {DOMElement} afterElement the sibling element (which is the previous element) of the element that will be the focus of the enter animation
+ * @param {object=} options an optional collection of options that will be picked up by the CSS transition/animation
+ * @return {Promise} the animation callback promise
+ */
+ enter: function(element, parentElement, afterElement, options) {
+ options = parseAnimateOptions(options);
+ element = angular.element(element);
+ parentElement = prepareElement(parentElement);
+ afterElement = prepareElement(afterElement);
+
+ classBasedAnimationsBlocked(element, true);
+ $delegate.enter(element, parentElement, afterElement);
+ return runAnimationPostDigest(function(done) {
+ return performAnimation('enter', 'ng-enter', stripCommentsFromElement(element), parentElement, afterElement, noop, options, done);
+ });
+ },
+
+ /**
+ * @ngdoc method
+ * @name $animate#leave
+ * @kind function
+ *
+ * @description
+ * Runs the leave animation operation and, upon completion, removes the element from the DOM. Once
+ * the animation is started, the following CSS classes will be added for the duration of the animation:
+ *
+ * Below is a breakdown of each step that occurs during leave animation:
+ *
+ * | Animation Step | What the element class attribute looks like |
+ * |-----------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------|
+ * | 1. `$animate.leave(...)` is called | `class="my-animation"` |
+ * | 2. `$animate` runs the JavaScript-defined animations detected on the element | `class="my-animation ng-animate"` |
+ * | 3. `$animate` waits for the next digest to start the animation | `class="my-animation ng-animate"` |
+ * | 4. the `.ng-leave` class is added to the element | `class="my-animation ng-animate ng-leave"` |
+ * | 5. `$animate` scans the element styles to get the CSS transition/animation duration and delay | `class="my-animation ng-animate ng-leave"` |
+ * | 6. `$animate` blocks all CSS transitions on the element to ensure the `.ng-leave` class styling is applied right away | `class="my-animation ng-animate ng-leave"` |
+ * | 7. `$animate` waits for a single animation frame (this performs a reflow) | `class="my-animation ng-animate ng-leave"` |
+ * | 8. `$animate` removes the CSS transition block placed on the element | `class="my-animation ng-animate ng-leave"` |
+ * | 9. the `.ng-leave-active` class is added (this triggers the CSS transition/animation) | `class="my-animation ng-animate ng-leave ng-leave-active"` |
+ * | 10. `$animate` waits for the animation to complete (via events and timeout) | `class="my-animation ng-animate ng-leave ng-leave-active"` |
+ * | 11. The animation ends and all generated CSS classes are removed from the element | `class="my-animation"` |
+ * | 12. The element is removed from the DOM | ... |
+ * | 13. The returned promise is resolved. | ... |
+ *
+ * @param {DOMElement} element the element that will be the focus of the leave animation
+ * @param {object=} options an optional collection of styles that will be picked up by the CSS transition/animation
+ * @return {Promise} the animation callback promise
+ */
+ leave: function(element, options) {
+ options = parseAnimateOptions(options);
+ element = angular.element(element);
+
+ cancelChildAnimations(element);
+ classBasedAnimationsBlocked(element, true);
+ return runAnimationPostDigest(function(done) {
+ return performAnimation('leave', 'ng-leave', stripCommentsFromElement(element), null, null, function() {
+ $delegate.leave(element);
+ }, options, done);
+ });
+ },
+
+ /**
+ * @ngdoc method
+ * @name $animate#move
+ * @kind function
+ *
+ * @description
+ * Fires the move DOM operation. Just before the animation starts, the animate service will either append it into the parentElement container or
+ * add the element directly after the afterElement element if present. Then the move animation will be run. Once
+ * the animation is started, the following CSS classes will be added for the duration of the animation:
+ *
+ * Below is a breakdown of each step that occurs during move animation:
+ *
+ * | Animation Step | What the element class attribute looks like |
+ * |----------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------|
+ * | 1. `$animate.move(...)` is called | `class="my-animation"` |
+ * | 2. element is moved into the parentElement element or beside the afterElement element | `class="my-animation"` |
+ * | 3. `$animate` waits for the next digest to start the animation | `class="my-animation ng-animate"` |
+ * | 4. `$animate` runs the JavaScript-defined animations detected on the element | `class="my-animation ng-animate"` |
+ * | 5. the `.ng-move` class is added to the element | `class="my-animation ng-animate ng-move"` |
+ * | 6. `$animate` scans the element styles to get the CSS transition/animation duration and delay | `class="my-animation ng-animate ng-move"` |
+ * | 7. `$animate` blocks all CSS transitions on the element to ensure the `.ng-move` class styling is applied right away | `class="my-animation ng-animate ng-move"` |
+ * | 8. `$animate` waits for a single animation frame (this performs a reflow) | `class="my-animation ng-animate ng-move"` |
+ * | 9. `$animate` removes the CSS transition block placed on the element | `class="my-animation ng-animate ng-move"` |
+ * | 10. the `.ng-move-active` class is added (this triggers the CSS transition/animation) | `class="my-animation ng-animate ng-move ng-move-active"` |
+ * | 11. `$animate` waits for the animation to complete (via events and timeout) | `class="my-animation ng-animate ng-move ng-move-active"` |
+ * | 12. The animation ends and all generated CSS classes are removed from the element | `class="my-animation"` |
+ * | 13. The returned promise is resolved. | `class="my-animation"` |
+ *
+ * @param {DOMElement} element the element that will be the focus of the move animation
+ * @param {DOMElement} parentElement the parentElement element of the element that will be the focus of the move animation
+ * @param {DOMElement} afterElement the sibling element (which is the previous element) of the element that will be the focus of the move animation
+ * @param {object=} options an optional collection of styles that will be picked up by the CSS transition/animation
+ * @return {Promise} the animation callback promise
+ */
+ move: function(element, parentElement, afterElement, options) {
+ options = parseAnimateOptions(options);
+ element = angular.element(element);
+ parentElement = prepareElement(parentElement);
+ afterElement = prepareElement(afterElement);
+
+ cancelChildAnimations(element);
+ classBasedAnimationsBlocked(element, true);
+ $delegate.move(element, parentElement, afterElement);
+ return runAnimationPostDigest(function(done) {
+ return performAnimation('move', 'ng-move', stripCommentsFromElement(element), parentElement, afterElement, noop, options, done);
+ });
+ },
+
+ /**
+ * @ngdoc method
+ * @name $animate#addClass
+ *
+ * @description
+ * Triggers a custom animation event based off the className variable and then attaches the className value to the element as a CSS class.
+ * Unlike the other animation methods, the animate service will suffix the className value with {@type -add} in order to provide
+ * the animate service the setup and active CSS classes in order to trigger the animation (this will be skipped if no CSS transitions
+ * or keyframes are defined on the -add-active or base CSS class).
+ *
+ * Below is a breakdown of each step that occurs during addClass animation:
+ *
+ * | Animation Step | What the element class attribute looks like |
+ * |--------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------|
+ * | 1. `$animate.addClass(element, 'super')` is called | `class="my-animation"` |
+ * | 2. `$animate` runs the JavaScript-defined animations detected on the element | `class="my-animation ng-animate"` |
+ * | 3. the `.super-add` class is added to the element | `class="my-animation ng-animate super-add"` |
+ * | 4. `$animate` waits for a single animation frame (this performs a reflow) | `class="my-animation ng-animate super-add"` |
+ * | 5. the `.super` and `.super-add-active` classes are added (this triggers the CSS transition/animation) | `class="my-animation ng-animate super super-add super-add-active"` |
+ * | 6. `$animate` scans the element styles to get the CSS transition/animation duration and delay | `class="my-animation ng-animate super super-add super-add-active"` |
+ * | 7. `$animate` waits for the animation to complete (via events and timeout) | `class="my-animation ng-animate super super-add super-add-active"` |
+ * | 8. The animation ends and all generated CSS classes are removed from the element | `class="my-animation super"` |
+ * | 9. The super class is kept on the element | `class="my-animation super"` |
+ * | 10. The returned promise is resolved. | `class="my-animation super"` |
+ *
+ * @param {DOMElement} element the element that will be animated
+ * @param {string} className the CSS class that will be added to the element and then animated
+ * @param {object=} options an optional collection of styles that will be picked up by the CSS transition/animation
+ * @return {Promise} the animation callback promise
+ */
+ addClass: function(element, className, options) {
+ return this.setClass(element, className, [], options);
+ },
+
+ /**
+ * @ngdoc method
+ * @name $animate#removeClass
+ *
+ * @description
+ * Triggers a custom animation event based off the className variable and then removes the CSS class provided by the className value
+ * from the element. Unlike the other animation methods, the animate service will suffix the className value with {@type -remove} in
+ * order to provide the animate service the setup and active CSS classes in order to trigger the animation (this will be skipped if
+ * no CSS transitions or keyframes are defined on the -remove or base CSS classes).
+ *
+ * Below is a breakdown of each step that occurs during removeClass animation:
+ *
+ * | Animation Step | What the element class attribute looks like |
+ * |----------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------|
+ * | 1. `$animate.removeClass(element, 'super')` is called | `class="my-animation super"` |
+ * | 2. `$animate` runs the JavaScript-defined animations detected on the element | `class="my-animation super ng-animate"` |
+ * | 3. the `.super-remove` class is added to the element | `class="my-animation super ng-animate super-remove"` |
+ * | 4. `$animate` waits for a single animation frame (this performs a reflow) | `class="my-animation super ng-animate super-remove"` |
+ * | 5. the `.super-remove-active` classes are added and `.super` is removed (this triggers the CSS transition/animation) | `class="my-animation ng-animate super-remove super-remove-active"` |
+ * | 6. `$animate` scans the element styles to get the CSS transition/animation duration and delay | `class="my-animation ng-animate super-remove super-remove-active"` |
+ * | 7. `$animate` waits for the animation to complete (via events and timeout) | `class="my-animation ng-animate super-remove super-remove-active"` |
+ * | 8. The animation ends and all generated CSS classes are removed from the element | `class="my-animation"` |
+ * | 9. The returned promise is resolved. | `class="my-animation"` |
+ *
+ *
+ * @param {DOMElement} element the element that will be animated
+ * @param {string} className the CSS class that will be animated and then removed from the element
+ * @param {object=} options an optional collection of styles that will be picked up by the CSS transition/animation
+ * @return {Promise} the animation callback promise
+ */
+ removeClass: function(element, className, options) {
+ return this.setClass(element, [], className, options);
+ },
+
+ /**
+ *
+ * @ngdoc method
+ * @name $animate#setClass
+ *
+ * @description Adds and/or removes the given CSS classes to and from the element.
+ * Once complete, the `done()` callback will be fired (if provided).
+ *
+ * | Animation Step | What the element class attribute looks like |
+ * |----------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------|
+ * | 1. `$animate.setClass(element, 'on', 'off')` is called | `class="my-animation off"` |
+ * | 2. `$animate` runs the JavaScript-defined animations detected on the element | `class="my-animation ng-animate off"` |
+ * | 3. the `.on-add` and `.off-remove` classes are added to the element | `class="my-animation ng-animate on-add off-remove off"` |
+ * | 4. `$animate` waits for a single animation frame (this performs a reflow) | `class="my-animation ng-animate on-add off-remove off"` |
+ * | 5. the `.on`, `.on-add-active` and `.off-remove-active` classes are added and `.off` is removed (this triggers the CSS transition/animation) | `class="my-animation ng-animate on on-add on-add-active off-remove off-remove-active"` |
+ * | 6. `$animate` scans the element styles to get the CSS transition/animation duration and delay | `class="my-animation ng-animate on on-add on-add-active off-remove off-remove-active"` |
+ * | 7. `$animate` waits for the animation to complete (via events and timeout) | `class="my-animation ng-animate on on-add on-add-active off-remove off-remove-active"` |
+ * | 8. The animation ends and all generated CSS classes are removed from the element | `class="my-animation on"` |
+ * | 9. The returned promise is resolved. | `class="my-animation on"` |
+ *
+ * @param {DOMElement} element the element which will have its CSS classes changed
+ * removed from it
+ * @param {string} add the CSS classes which will be added to the element
+ * @param {string} remove the CSS class which will be removed from the element
+ * CSS classes have been set on the element
+ * @param {object=} options an optional collection of styles that will be picked up by the CSS transition/animation
+ * @return {Promise} the animation callback promise
+ */
+ setClass: function(element, add, remove, options) {
+ options = parseAnimateOptions(options);
+
+ var STORAGE_KEY = '$$animateClasses';
+ element = angular.element(element);
+ element = stripCommentsFromElement(element);
+
+ if (classBasedAnimationsBlocked(element)) {
+ return $delegate.$$setClassImmediately(element, add, remove, options);
+ }
+
+ // we're using a combined array for both the add and remove
+ // operations since the ORDER OF addClass and removeClass matters
+ var classes, cache = element.data(STORAGE_KEY);
+ var hasCache = !!cache;
+ if (!cache) {
+ cache = {};
+ cache.classes = {};
+ }
+ classes = cache.classes;
+
+ add = isArray(add) ? add : add.split(' ');
+ forEach(add, function(c) {
+ if (c && c.length) {
+ classes[c] = true;
+ }
+ });
+
+ remove = isArray(remove) ? remove : remove.split(' ');
+ forEach(remove, function(c) {
+ if (c && c.length) {
+ classes[c] = false;
+ }
+ });
+
+ if (hasCache) {
+ if (options && cache.options) {
+ cache.options = angular.extend(cache.options || {}, options);
+ }
+
+ //the digest cycle will combine all the animations into one function
+ return cache.promise;
+ } else {
+ element.data(STORAGE_KEY, cache = {
+ classes: classes,
+ options: options
+ });
+ }
+
+ return cache.promise = runAnimationPostDigest(function(done) {
+ var parentElement = element.parent();
+ var elementNode = extractElementNode(element);
+ var parentNode = elementNode.parentNode;
+ // TODO(matsko): move this code into the animationsDisabled() function once #8092 is fixed
+ if (!parentNode || parentNode['$$NG_REMOVED'] || elementNode['$$NG_REMOVED']) {
+ done();
+ return;
+ }
+
+ var cache = element.data(STORAGE_KEY);
+ element.removeData(STORAGE_KEY);
+
+ var state = element.data(NG_ANIMATE_STATE) || {};
+ var classes = resolveElementClasses(element, cache, state.active);
+ return !classes
+ ? done()
+ : performAnimation('setClass', classes, element, parentElement, null, function() {
+ if (classes[0]) $delegate.$$addClassImmediately(element, classes[0]);
+ if (classes[1]) $delegate.$$removeClassImmediately(element, classes[1]);
+ }, cache.options, done);
+ });
+ },
+
+ /**
+ * @ngdoc method
+ * @name $animate#cancel
+ * @kind function
+ *
+ * @param {Promise} animationPromise The animation promise that is returned when an animation is started.
+ *
+ * @description
+ * Cancels the provided animation.
+ */
+ cancel: function(promise) {
+ promise.$$cancelFn();
+ },
+
+ /**
+ * @ngdoc method
+ * @name $animate#enabled
+ * @kind function
+ *
+ * @param {boolean=} value If provided then set the animation on or off.
+ * @param {DOMElement=} element If provided then the element will be used to represent the enable/disable operation
+ * @return {boolean} Current animation state.
+ *
+ * @description
+ * Globally enables/disables animations.
+ *
+ */
+ enabled: function(value, element) {
+ switch (arguments.length) {
+ case 2:
+ if (value) {
+ cleanup(element);
+ } else {
+ var data = element.data(NG_ANIMATE_STATE) || {};
+ data.disabled = true;
+ element.data(NG_ANIMATE_STATE, data);
+ }
+ break;
+
+ case 1:
+ rootAnimateState.disabled = !value;
+ break;
+
+ default:
+ value = !rootAnimateState.disabled;
+ break;
+ }
+ return !!value;
+ }
+ };
+
+ /*
+ all animations call this shared animation triggering function internally.
+ The animationEvent variable refers to the JavaScript animation event that will be triggered
+ and the className value is the name of the animation that will be applied within the
+ CSS code. Element, `parentElement` and `afterElement` are provided DOM elements for the animation
+ and the onComplete callback will be fired once the animation is fully complete.
+ */
+ function performAnimation(animationEvent, className, element, parentElement, afterElement, domOperation, options, doneCallback) {
+ var noopCancel = noop;
+ var runner = animationRunner(element, animationEvent, className, options);
+ if (!runner) {
+ fireDOMOperation();
+ fireBeforeCallbackAsync();
+ fireAfterCallbackAsync();
+ closeAnimation();
+ return noopCancel;
+ }
+
+ animationEvent = runner.event;
+ className = runner.className;
+ var elementEvents = angular.element._data(runner.node);
+ elementEvents = elementEvents && elementEvents.events;
+
+ if (!parentElement) {
+ parentElement = afterElement ? afterElement.parent() : element.parent();
+ }
+
+ //skip the animation if animations are disabled, a parent is already being animated,
+ //the element is not currently attached to the document body or then completely close
+ //the animation if any matching animations are not found at all.
+ //NOTE: IE8 + IE9 should close properly (run closeAnimation()) in case an animation was found.
+ if (animationsDisabled(element, parentElement)) {
+ fireDOMOperation();
+ fireBeforeCallbackAsync();
+ fireAfterCallbackAsync();
+ closeAnimation();
+ return noopCancel;
+ }
+
+ var ngAnimateState = element.data(NG_ANIMATE_STATE) || {};
+ var runningAnimations = ngAnimateState.active || {};
+ var totalActiveAnimations = ngAnimateState.totalActive || 0;
+ var lastAnimation = ngAnimateState.last;
+ var skipAnimation = false;
+
+ if (totalActiveAnimations > 0) {
+ var animationsToCancel = [];
+ if (!runner.isClassBased) {
+ if (animationEvent == 'leave' && runningAnimations['ng-leave']) {
+ skipAnimation = true;
+ } else {
+ //cancel all animations when a structural animation takes place
+ for (var klass in runningAnimations) {
+ animationsToCancel.push(runningAnimations[klass]);
+ }
+ ngAnimateState = {};
+ cleanup(element, true);
+ }
+ } else if (lastAnimation.event == 'setClass') {
+ animationsToCancel.push(lastAnimation);
+ cleanup(element, className);
+ } else if (runningAnimations[className]) {
+ var current = runningAnimations[className];
+ if (current.event == animationEvent) {
+ skipAnimation = true;
+ } else {
+ animationsToCancel.push(current);
+ cleanup(element, className);
+ }
+ }
+
+ if (animationsToCancel.length > 0) {
+ forEach(animationsToCancel, function(operation) {
+ operation.cancel();
+ });
+ }
+ }
+
+ if (runner.isClassBased
+ && !runner.isSetClassOperation
+ && animationEvent != 'animate'
+ && !skipAnimation) {
+ skipAnimation = (animationEvent == 'addClass') == element.hasClass(className); //opposite of XOR
+ }
+
+ if (skipAnimation) {
+ fireDOMOperation();
+ fireBeforeCallbackAsync();
+ fireAfterCallbackAsync();
+ fireDoneCallbackAsync();
+ return noopCancel;
+ }
+
+ runningAnimations = ngAnimateState.active || {};
+ totalActiveAnimations = ngAnimateState.totalActive || 0;
+
+ if (animationEvent == 'leave') {
+ //there's no need to ever remove the listener since the element
+ //will be removed (destroyed) after the leave animation ends or
+ //is cancelled midway
+ element.one('$destroy', function(e) {
+ var element = angular.element(this);
+ var state = element.data(NG_ANIMATE_STATE);
+ if (state) {
+ var activeLeaveAnimation = state.active['ng-leave'];
+ if (activeLeaveAnimation) {
+ activeLeaveAnimation.cancel();
+ cleanup(element, 'ng-leave');
+ }
+ }
+ });
+ }
+
+ //the ng-animate class does nothing, but it's here to allow for
+ //parent animations to find and cancel child animations when needed
+ $$jqLite.addClass(element, NG_ANIMATE_CLASS_NAME);
+ if (options && options.tempClasses) {
+ forEach(options.tempClasses, function(className) {
+ $$jqLite.addClass(element, className);
+ });
+ }
+
+ var localAnimationCount = globalAnimationCounter++;
+ totalActiveAnimations++;
+ runningAnimations[className] = runner;
+
+ element.data(NG_ANIMATE_STATE, {
+ last: runner,
+ active: runningAnimations,
+ index: localAnimationCount,
+ totalActive: totalActiveAnimations
+ });
+
+ //first we run the before animations and when all of those are complete
+ //then we perform the DOM operation and run the next set of animations
+ fireBeforeCallbackAsync();
+ runner.before(function(cancelled) {
+ var data = element.data(NG_ANIMATE_STATE);
+ cancelled = cancelled ||
+ !data || !data.active[className] ||
+ (runner.isClassBased && data.active[className].event != animationEvent);
+
+ fireDOMOperation();
+ if (cancelled === true) {
+ closeAnimation();
+ } else {
+ fireAfterCallbackAsync();
+ runner.after(closeAnimation);
+ }
+ });
+
+ return runner.cancel;
+
+ function fireDOMCallback(animationPhase) {
+ var eventName = '$animate:' + animationPhase;
+ if (elementEvents && elementEvents[eventName] && elementEvents[eventName].length > 0) {
+ $$asyncCallback(function() {
+ element.triggerHandler(eventName, {
+ event: animationEvent,
+ className: className
+ });
+ });
+ }
+ }
+
+ function fireBeforeCallbackAsync() {
+ fireDOMCallback('before');
+ }
+
+ function fireAfterCallbackAsync() {
+ fireDOMCallback('after');
+ }
+
+ function fireDoneCallbackAsync() {
+ fireDOMCallback('close');
+ doneCallback();
+ }
+
+ //it is less complicated to use a flag than managing and canceling
+ //timeouts containing multiple callbacks.
+ function fireDOMOperation() {
+ if (!fireDOMOperation.hasBeenRun) {
+ fireDOMOperation.hasBeenRun = true;
+ domOperation();
+ }
+ }
+
+ function closeAnimation() {
+ if (!closeAnimation.hasBeenRun) {
+ if (runner) { //the runner doesn't exist if it fails to instantiate
+ runner.applyStyles();
+ }
+
+ closeAnimation.hasBeenRun = true;
+ if (options && options.tempClasses) {
+ forEach(options.tempClasses, function(className) {
+ $$jqLite.removeClass(element, className);
+ });
+ }
+
+ var data = element.data(NG_ANIMATE_STATE);
+ if (data) {
+
+ /* only structural animations wait for reflow before removing an
+ animation, but class-based animations don't. An example of this
+ failing would be when a parent HTML tag has a ng-class attribute
+ causing ALL directives below to skip animations during the digest */
+ if (runner && runner.isClassBased) {
+ cleanup(element, className);
+ } else {
+ $$asyncCallback(function() {
+ var data = element.data(NG_ANIMATE_STATE) || {};
+ if (localAnimationCount == data.index) {
+ cleanup(element, className, animationEvent);
+ }
+ });
+ element.data(NG_ANIMATE_STATE, data);
+ }
+ }
+ fireDoneCallbackAsync();
+ }
+ }
+ }
+
+ function cancelChildAnimations(element) {
+ var node = extractElementNode(element);
+ if (node) {
+ var nodes = angular.isFunction(node.getElementsByClassName) ?
+ node.getElementsByClassName(NG_ANIMATE_CLASS_NAME) :
+ node.querySelectorAll('.' + NG_ANIMATE_CLASS_NAME);
+ forEach(nodes, function(element) {
+ element = angular.element(element);
+ var data = element.data(NG_ANIMATE_STATE);
+ if (data && data.active) {
+ forEach(data.active, function(runner) {
+ runner.cancel();
+ });
+ }
+ });
+ }
+ }
+
+ function cleanup(element, className) {
+ if (isMatchingElement(element, $rootElement)) {
+ if (!rootAnimateState.disabled) {
+ rootAnimateState.running = false;
+ rootAnimateState.structural = false;
+ }
+ } else if (className) {
+ var data = element.data(NG_ANIMATE_STATE) || {};
+
+ var removeAnimations = className === true;
+ if (!removeAnimations && data.active && data.active[className]) {
+ data.totalActive--;
+ delete data.active[className];
+ }
+
+ if (removeAnimations || !data.totalActive) {
+ $$jqLite.removeClass(element, NG_ANIMATE_CLASS_NAME);
+ element.removeData(NG_ANIMATE_STATE);
+ }
+ }
+ }
+
+ function animationsDisabled(element, parentElement) {
+ if (rootAnimateState.disabled) {
+ return true;
+ }
+
+ if (isMatchingElement(element, $rootElement)) {
+ return rootAnimateState.running;
+ }
+
+ var allowChildAnimations, parentRunningAnimation, hasParent;
+ do {
+ //the element did not reach the root element which means that it
+ //is not apart of the DOM. Therefore there is no reason to do
+ //any animations on it
+ if (parentElement.length === 0) break;
+
+ var isRoot = isMatchingElement(parentElement, $rootElement);
+ var state = isRoot ? rootAnimateState : (parentElement.data(NG_ANIMATE_STATE) || {});
+ if (state.disabled) {
+ return true;
+ }
+
+ //no matter what, for an animation to work it must reach the root element
+ //this implies that the element is attached to the DOM when the animation is run
+ if (isRoot) {
+ hasParent = true;
+ }
+
+ //once a flag is found that is strictly false then everything before
+ //it will be discarded and all child animations will be restricted
+ if (allowChildAnimations !== false) {
+ var animateChildrenFlag = parentElement.data(NG_ANIMATE_CHILDREN);
+ if (angular.isDefined(animateChildrenFlag)) {
+ allowChildAnimations = animateChildrenFlag;
+ }
+ }
+
+ parentRunningAnimation = parentRunningAnimation ||
+ state.running ||
+ (state.last && !state.last.isClassBased);
+ }
+ while (parentElement = parentElement.parent());
+
+ return !hasParent || (!allowChildAnimations && parentRunningAnimation);
+ }
+ }]);
+
+ $animateProvider.register('', ['$window', '$sniffer', '$timeout', '$$animateReflow',
+ function($window, $sniffer, $timeout, $$animateReflow) {
+ // Detect proper transitionend/animationend event names.
+ var CSS_PREFIX = '', TRANSITION_PROP, TRANSITIONEND_EVENT, ANIMATION_PROP, ANIMATIONEND_EVENT;
+
+ // If unprefixed events are not supported but webkit-prefixed are, use the latter.
+ // Otherwise, just use W3C names, browsers not supporting them at all will just ignore them.
+ // Note: Chrome implements `window.onwebkitanimationend` and doesn't implement `window.onanimationend`
+ // but at the same time dispatches the `animationend` event and not `webkitAnimationEnd`.
+ // Register both events in case `window.onanimationend` is not supported because of that,
+ // do the same for `transitionend` as Safari is likely to exhibit similar behavior.
+ // Also, the only modern browser that uses vendor prefixes for transitions/keyframes is webkit
+ // therefore there is no reason to test anymore for other vendor prefixes: http://caniuse.com/#search=transition
+ if (window.ontransitionend === undefined && window.onwebkittransitionend !== undefined) {
+ CSS_PREFIX = '-webkit-';
+ TRANSITION_PROP = 'WebkitTransition';
+ TRANSITIONEND_EVENT = 'webkitTransitionEnd transitionend';
+ } else {
+ TRANSITION_PROP = 'transition';
+ TRANSITIONEND_EVENT = 'transitionend';
+ }
+
+ if (window.onanimationend === undefined && window.onwebkitanimationend !== undefined) {
+ CSS_PREFIX = '-webkit-';
+ ANIMATION_PROP = 'WebkitAnimation';
+ ANIMATIONEND_EVENT = 'webkitAnimationEnd animationend';
+ } else {
+ ANIMATION_PROP = 'animation';
+ ANIMATIONEND_EVENT = 'animationend';
+ }
+
+ var DURATION_KEY = 'Duration';
+ var PROPERTY_KEY = 'Property';
+ var DELAY_KEY = 'Delay';
+ var ANIMATION_ITERATION_COUNT_KEY = 'IterationCount';
+ var ANIMATION_PLAYSTATE_KEY = 'PlayState';
+ var NG_ANIMATE_PARENT_KEY = '$$ngAnimateKey';
+ var NG_ANIMATE_CSS_DATA_KEY = '$$ngAnimateCSS3Data';
+ var ELAPSED_TIME_MAX_DECIMAL_PLACES = 3;
+ var CLOSING_TIME_BUFFER = 1.5;
+ var ONE_SECOND = 1000;
+
+ var lookupCache = {};
+ var parentCounter = 0;
+ var animationReflowQueue = [];
+ var cancelAnimationReflow;
+ function clearCacheAfterReflow() {
+ if (!cancelAnimationReflow) {
+ cancelAnimationReflow = $$animateReflow(function() {
+ animationReflowQueue = [];
+ cancelAnimationReflow = null;
+ lookupCache = {};
+ });
+ }
+ }
+
+ function afterReflow(element, callback) {
+ if (cancelAnimationReflow) {
+ cancelAnimationReflow();
+ }
+ animationReflowQueue.push(callback);
+ cancelAnimationReflow = $$animateReflow(function() {
+ forEach(animationReflowQueue, function(fn) {
+ fn();
+ });
+
+ animationReflowQueue = [];
+ cancelAnimationReflow = null;
+ lookupCache = {};
+ });
+ }
+
+ var closingTimer = null;
+ var closingTimestamp = 0;
+ var animationElementQueue = [];
+ function animationCloseHandler(element, totalTime) {
+ var node = extractElementNode(element);
+ element = angular.element(node);
+
+ //this item will be garbage collected by the closing
+ //animation timeout
+ animationElementQueue.push(element);
+
+ //but it may not need to cancel out the existing timeout
+ //if the timestamp is less than the previous one
+ var futureTimestamp = Date.now() + totalTime;
+ if (futureTimestamp <= closingTimestamp) {
+ return;
+ }
+
+ $timeout.cancel(closingTimer);
+
+ closingTimestamp = futureTimestamp;
+ closingTimer = $timeout(function() {
+ closeAllAnimations(animationElementQueue);
+ animationElementQueue = [];
+ }, totalTime, false);
+ }
+
+ function closeAllAnimations(elements) {
+ forEach(elements, function(element) {
+ var elementData = element.data(NG_ANIMATE_CSS_DATA_KEY);
+ if (elementData) {
+ forEach(elementData.closeAnimationFns, function(fn) {
+ fn();
+ });
+ }
+ });
+ }
+
+ function getElementAnimationDetails(element, cacheKey) {
+ var data = cacheKey ? lookupCache[cacheKey] : null;
+ if (!data) {
+ var transitionDuration = 0;
+ var transitionDelay = 0;
+ var animationDuration = 0;
+ var animationDelay = 0;
+
+ //we want all the styles defined before and after
+ forEach(element, function(element) {
+ if (element.nodeType == ELEMENT_NODE) {
+ var elementStyles = $window.getComputedStyle(element) || {};
+
+ var transitionDurationStyle = elementStyles[TRANSITION_PROP + DURATION_KEY];
+ transitionDuration = Math.max(parseMaxTime(transitionDurationStyle), transitionDuration);
+
+ var transitionDelayStyle = elementStyles[TRANSITION_PROP + DELAY_KEY];
+ transitionDelay = Math.max(parseMaxTime(transitionDelayStyle), transitionDelay);
+
+ var animationDelayStyle = elementStyles[ANIMATION_PROP + DELAY_KEY];
+ animationDelay = Math.max(parseMaxTime(elementStyles[ANIMATION_PROP + DELAY_KEY]), animationDelay);
+
+ var aDuration = parseMaxTime(elementStyles[ANIMATION_PROP + DURATION_KEY]);
+
+ if (aDuration > 0) {
+ aDuration *= parseInt(elementStyles[ANIMATION_PROP + ANIMATION_ITERATION_COUNT_KEY], 10) || 1;
+ }
+ animationDuration = Math.max(aDuration, animationDuration);
+ }
+ });
+ data = {
+ total: 0,
+ transitionDelay: transitionDelay,
+ transitionDuration: transitionDuration,
+ animationDelay: animationDelay,
+ animationDuration: animationDuration
+ };
+ if (cacheKey) {
+ lookupCache[cacheKey] = data;
+ }
+ }
+ return data;
+ }
+
+ function parseMaxTime(str) {
+ var maxValue = 0;
+ var values = isString(str) ?
+ str.split(/\s*,\s*/) :
+ [];
+ forEach(values, function(value) {
+ maxValue = Math.max(parseFloat(value) || 0, maxValue);
+ });
+ return maxValue;
+ }
+
+ function getCacheKey(element) {
+ var parentElement = element.parent();
+ var parentID = parentElement.data(NG_ANIMATE_PARENT_KEY);
+ if (!parentID) {
+ parentElement.data(NG_ANIMATE_PARENT_KEY, ++parentCounter);
+ parentID = parentCounter;
+ }
+ return parentID + '-' + extractElementNode(element).getAttribute('class');
+ }
+
+ function animateSetup(animationEvent, element, className, styles) {
+ var structural = ['ng-enter','ng-leave','ng-move'].indexOf(className) >= 0;
+
+ var cacheKey = getCacheKey(element);
+ var eventCacheKey = cacheKey + ' ' + className;
+ var itemIndex = lookupCache[eventCacheKey] ? ++lookupCache[eventCacheKey].total : 0;
+
+ var stagger = {};
+ if (itemIndex > 0) {
+ var staggerClassName = className + '-stagger';
+ var staggerCacheKey = cacheKey + ' ' + staggerClassName;
+ var applyClasses = !lookupCache[staggerCacheKey];
+
+ applyClasses && $$jqLite.addClass(element, staggerClassName);
+
+ stagger = getElementAnimationDetails(element, staggerCacheKey);
+
+ applyClasses && $$jqLite.removeClass(element, staggerClassName);
+ }
+
+ $$jqLite.addClass(element, className);
+
+ var formerData = element.data(NG_ANIMATE_CSS_DATA_KEY) || {};
+ var timings = getElementAnimationDetails(element, eventCacheKey);
+ var transitionDuration = timings.transitionDuration;
+ var animationDuration = timings.animationDuration;
+
+ if (structural && transitionDuration === 0 && animationDuration === 0) {
+ $$jqLite.removeClass(element, className);
+ return false;
+ }
+
+ var blockTransition = styles || (structural && transitionDuration > 0);
+ var blockAnimation = animationDuration > 0 &&
+ stagger.animationDelay > 0 &&
+ stagger.animationDuration === 0;
+
+ var closeAnimationFns = formerData.closeAnimationFns || [];
+ element.data(NG_ANIMATE_CSS_DATA_KEY, {
+ stagger: stagger,
+ cacheKey: eventCacheKey,
+ running: formerData.running || 0,
+ itemIndex: itemIndex,
+ blockTransition: blockTransition,
+ closeAnimationFns: closeAnimationFns
+ });
+
+ var node = extractElementNode(element);
+
+ if (blockTransition) {
+ blockTransitions(node, true);
+ if (styles) {
+ element.css(styles);
+ }
+ }
+
+ if (blockAnimation) {
+ blockAnimations(node, true);
+ }
+
+ return true;
+ }
+
+ function animateRun(animationEvent, element, className, activeAnimationComplete, styles) {
+ var node = extractElementNode(element);
+ var elementData = element.data(NG_ANIMATE_CSS_DATA_KEY);
+ if (node.getAttribute('class').indexOf(className) == -1 || !elementData) {
+ activeAnimationComplete();
+ return;
+ }
+
+ var activeClassName = '';
+ var pendingClassName = '';
+ forEach(className.split(' '), function(klass, i) {
+ var prefix = (i > 0 ? ' ' : '') + klass;
+ activeClassName += prefix + '-active';
+ pendingClassName += prefix + '-pending';
+ });
+
+ var style = '';
+ var appliedStyles = [];
+ var itemIndex = elementData.itemIndex;
+ var stagger = elementData.stagger;
+ var staggerTime = 0;
+ if (itemIndex > 0) {
+ var transitionStaggerDelay = 0;
+ if (stagger.transitionDelay > 0 && stagger.transitionDuration === 0) {
+ transitionStaggerDelay = stagger.transitionDelay * itemIndex;
+ }
+
+ var animationStaggerDelay = 0;
+ if (stagger.animationDelay > 0 && stagger.animationDuration === 0) {
+ animationStaggerDelay = stagger.animationDelay * itemIndex;
+ appliedStyles.push(CSS_PREFIX + 'animation-play-state');
+ }
+
+ staggerTime = Math.round(Math.max(transitionStaggerDelay, animationStaggerDelay) * 100) / 100;
+ }
+
+ if (!staggerTime) {
+ $$jqLite.addClass(element, activeClassName);
+ if (elementData.blockTransition) {
+ blockTransitions(node, false);
+ }
+ }
+
+ var eventCacheKey = elementData.cacheKey + ' ' + activeClassName;
+ var timings = getElementAnimationDetails(element, eventCacheKey);
+ var maxDuration = Math.max(timings.transitionDuration, timings.animationDuration);
+ if (maxDuration === 0) {
+ $$jqLite.removeClass(element, activeClassName);
+ animateClose(element, className);
+ activeAnimationComplete();
+ return;
+ }
+
+ if (!staggerTime && styles && Object.keys(styles).length > 0) {
+ if (!timings.transitionDuration) {
+ element.css('transition', timings.animationDuration + 's linear all');
+ appliedStyles.push('transition');
+ }
+ element.css(styles);
+ }
+
+ var maxDelay = Math.max(timings.transitionDelay, timings.animationDelay);
+ var maxDelayTime = maxDelay * ONE_SECOND;
+
+ if (appliedStyles.length > 0) {
+ //the element being animated may sometimes contain comment nodes in
+ //the jqLite object, so we're safe to use a single variable to house
+ //the styles since there is always only one element being animated
+ var oldStyle = node.getAttribute('style') || '';
+ if (oldStyle.charAt(oldStyle.length - 1) !== ';') {
+ oldStyle += ';';
+ }
+ node.setAttribute('style', oldStyle + ' ' + style);
+ }
+
+ var startTime = Date.now();
+ var css3AnimationEvents = ANIMATIONEND_EVENT + ' ' + TRANSITIONEND_EVENT;
+ var animationTime = (maxDelay + maxDuration) * CLOSING_TIME_BUFFER;
+ var totalTime = (staggerTime + animationTime) * ONE_SECOND;
+
+ var staggerTimeout;
+ if (staggerTime > 0) {
+ $$jqLite.addClass(element, pendingClassName);
+ staggerTimeout = $timeout(function() {
+ staggerTimeout = null;
+
+ if (timings.transitionDuration > 0) {
+ blockTransitions(node, false);
+ }
+ if (timings.animationDuration > 0) {
+ blockAnimations(node, false);
+ }
+
+ $$jqLite.addClass(element, activeClassName);
+ $$jqLite.removeClass(element, pendingClassName);
+
+ if (styles) {
+ if (timings.transitionDuration === 0) {
+ element.css('transition', timings.animationDuration + 's linear all');
+ }
+ element.css(styles);
+ appliedStyles.push('transition');
+ }
+ }, staggerTime * ONE_SECOND, false);
+ }
+
+ element.on(css3AnimationEvents, onAnimationProgress);
+ elementData.closeAnimationFns.push(function() {
+ onEnd();
+ activeAnimationComplete();
+ });
+
+ elementData.running++;
+ animationCloseHandler(element, totalTime);
+ return onEnd;
+
+ // This will automatically be called by $animate so
+ // there is no need to attach this internally to the
+ // timeout done method.
+ function onEnd() {
+ element.off(css3AnimationEvents, onAnimationProgress);
+ $$jqLite.removeClass(element, activeClassName);
+ $$jqLite.removeClass(element, pendingClassName);
+ if (staggerTimeout) {
+ $timeout.cancel(staggerTimeout);
+ }
+ animateClose(element, className);
+ var node = extractElementNode(element);
+ for (var i in appliedStyles) {
+ node.style.removeProperty(appliedStyles[i]);
+ }
+ }
+
+ function onAnimationProgress(event) {
+ event.stopPropagation();
+ var ev = event.originalEvent || event;
+ var timeStamp = ev.$manualTimeStamp || ev.timeStamp || Date.now();
+
+ /* Firefox (or possibly just Gecko) likes to not round values up
+ * when a ms measurement is used for the animation */
+ var elapsedTime = parseFloat(ev.elapsedTime.toFixed(ELAPSED_TIME_MAX_DECIMAL_PLACES));
+
+ /* $manualTimeStamp is a mocked timeStamp value which is set
+ * within browserTrigger(). This is only here so that tests can
+ * mock animations properly. Real events fallback to event.timeStamp,
+ * or, if they don't, then a timeStamp is automatically created for them.
+ * We're checking to see if the timeStamp surpasses the expected delay,
+ * but we're using elapsedTime instead of the timeStamp on the 2nd
+ * pre-condition since animations sometimes close off early */
+ if (Math.max(timeStamp - startTime, 0) >= maxDelayTime && elapsedTime >= maxDuration) {
+ activeAnimationComplete();
+ }
+ }
+ }
+
+ function blockTransitions(node, bool) {
+ node.style[TRANSITION_PROP + PROPERTY_KEY] = bool ? 'none' : '';
+ }
+
+ function blockAnimations(node, bool) {
+ node.style[ANIMATION_PROP + ANIMATION_PLAYSTATE_KEY] = bool ? 'paused' : '';
+ }
+
+ function animateBefore(animationEvent, element, className, styles) {
+ if (animateSetup(animationEvent, element, className, styles)) {
+ return function(cancelled) {
+ cancelled && animateClose(element, className);
+ };
+ }
+ }
+
+ function animateAfter(animationEvent, element, className, afterAnimationComplete, styles) {
+ if (element.data(NG_ANIMATE_CSS_DATA_KEY)) {
+ return animateRun(animationEvent, element, className, afterAnimationComplete, styles);
+ } else {
+ animateClose(element, className);
+ afterAnimationComplete();
+ }
+ }
+
+ function animate(animationEvent, element, className, animationComplete, options) {
+ //If the animateSetup function doesn't bother returning a
+ //cancellation function then it means that there is no animation
+ //to perform at all
+ var preReflowCancellation = animateBefore(animationEvent, element, className, options.from);
+ if (!preReflowCancellation) {
+ clearCacheAfterReflow();
+ animationComplete();
+ return;
+ }
+
+ //There are two cancellation functions: one is before the first
+ //reflow animation and the second is during the active state
+ //animation. The first function will take care of removing the
+ //data from the element which will not make the 2nd animation
+ //happen in the first place
+ var cancel = preReflowCancellation;
+ afterReflow(element, function() {
+ //once the reflow is complete then we point cancel to
+ //the new cancellation function which will remove all of the
+ //animation properties from the active animation
+ cancel = animateAfter(animationEvent, element, className, animationComplete, options.to);
+ });
+
+ return function(cancelled) {
+ (cancel || noop)(cancelled);
+ };
+ }
+
+ function animateClose(element, className) {
+ $$jqLite.removeClass(element, className);
+ var data = element.data(NG_ANIMATE_CSS_DATA_KEY);
+ if (data) {
+ if (data.running) {
+ data.running--;
+ }
+ if (!data.running || data.running === 0) {
+ element.removeData(NG_ANIMATE_CSS_DATA_KEY);
+ }
+ }
+ }
+
+ return {
+ animate: function(element, className, from, to, animationCompleted, options) {
+ options = options || {};
+ options.from = from;
+ options.to = to;
+ return animate('animate', element, className, animationCompleted, options);
+ },
+
+ enter: function(element, animationCompleted, options) {
+ options = options || {};
+ return animate('enter', element, 'ng-enter', animationCompleted, options);
+ },
+
+ leave: function(element, animationCompleted, options) {
+ options = options || {};
+ return animate('leave', element, 'ng-leave', animationCompleted, options);
+ },
+
+ move: function(element, animationCompleted, options) {
+ options = options || {};
+ return animate('move', element, 'ng-move', animationCompleted, options);
+ },
+
+ beforeSetClass: function(element, add, remove, animationCompleted, options) {
+ options = options || {};
+ var className = suffixClasses(remove, '-remove') + ' ' +
+ suffixClasses(add, '-add');
+ var cancellationMethod = animateBefore('setClass', element, className, options.from);
+ if (cancellationMethod) {
+ afterReflow(element, animationCompleted);
+ return cancellationMethod;
+ }
+ clearCacheAfterReflow();
+ animationCompleted();
+ },
+
+ beforeAddClass: function(element, className, animationCompleted, options) {
+ options = options || {};
+ var cancellationMethod = animateBefore('addClass', element, suffixClasses(className, '-add'), options.from);
+ if (cancellationMethod) {
+ afterReflow(element, animationCompleted);
+ return cancellationMethod;
+ }
+ clearCacheAfterReflow();
+ animationCompleted();
+ },
+
+ beforeRemoveClass: function(element, className, animationCompleted, options) {
+ options = options || {};
+ var cancellationMethod = animateBefore('removeClass', element, suffixClasses(className, '-remove'), options.from);
+ if (cancellationMethod) {
+ afterReflow(element, animationCompleted);
+ return cancellationMethod;
+ }
+ clearCacheAfterReflow();
+ animationCompleted();
+ },
+
+ setClass: function(element, add, remove, animationCompleted, options) {
+ options = options || {};
+ remove = suffixClasses(remove, '-remove');
+ add = suffixClasses(add, '-add');
+ var className = remove + ' ' + add;
+ return animateAfter('setClass', element, className, animationCompleted, options.to);
+ },
+
+ addClass: function(element, className, animationCompleted, options) {
+ options = options || {};
+ return animateAfter('addClass', element, suffixClasses(className, '-add'), animationCompleted, options.to);
+ },
+
+ removeClass: function(element, className, animationCompleted, options) {
+ options = options || {};
+ return animateAfter('removeClass', element, suffixClasses(className, '-remove'), animationCompleted, options.to);
+ }
+ };
+
+ function suffixClasses(classes, suffix) {
+ var className = '';
+ classes = isArray(classes) ? classes : classes.split(/\s+/);
+ forEach(classes, function(klass, i) {
+ if (klass && klass.length > 0) {
+ className += (i > 0 ? ' ' : '') + klass + suffix;
+ }
+ });
+ return className;
+ }
+ }]);
+ }]);
+
+
+})(window, window.angular);
diff --git a/third_party/ui/bower_components/angular-animate/angular-animate.min.js b/third_party/ui/bower_components/angular-animate/angular-animate.min.js
new file mode 100644
index 0000000000..c0f361b402
--- /dev/null
+++ b/third_party/ui/bower_components/angular-animate/angular-animate.min.js
@@ -0,0 +1,33 @@
+/*
+ AngularJS v1.3.15
+ (c) 2010-2014 Google, Inc. http://angularjs.org
+ License: MIT
+*/
+(function(N,f,W){'use strict';f.module("ngAnimate",["ng"]).directive("ngAnimateChildren",function(){return function(X,C,g){g=g.ngAnimateChildren;f.isString(g)&&0===g.length?C.data("$$ngAnimateChildren",!0):X.$watch(g,function(f){C.data("$$ngAnimateChildren",!!f)})}}).factory("$$animateReflow",["$$rAF","$document",function(f,C){return function(g){return f(function(){g()})}}]).config(["$provide","$animateProvider",function(X,C){function g(f){for(var n=0;n=C&&b>=x&&c()}var m=g(e);a=e.data("$$ngAnimateCSS3Data");if(-1!=m.getAttribute("class").indexOf(b)&&a){var k="",t="";n(b.split(" "),function(a,
+b){var e=(0",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/angular/angular.js/issues"
+ },
+ "homepage": "http://angularjs.org"
+}
diff --git a/third_party/ui/bower_components/angular-aria/.bower.json b/third_party/ui/bower_components/angular-aria/.bower.json
new file mode 100644
index 0000000000..8eace1bd66
--- /dev/null
+++ b/third_party/ui/bower_components/angular-aria/.bower.json
@@ -0,0 +1,19 @@
+{
+ "name": "angular-aria",
+ "version": "1.3.15",
+ "main": "./angular-aria.js",
+ "ignore": [],
+ "dependencies": {
+ "angular": "1.3.15"
+ },
+ "homepage": "https://github.com/angular/bower-angular-aria",
+ "_release": "1.3.15",
+ "_resolution": {
+ "type": "version",
+ "tag": "v1.3.15",
+ "commit": "d838769dec2546c1c80f7ccfa96f9d9b0989ea90"
+ },
+ "_source": "git://github.com/angular/bower-angular-aria.git",
+ "_target": "1.3.x",
+ "_originalSource": "angular-aria"
+}
\ No newline at end of file
diff --git a/third_party/ui/bower_components/angular-aria/README.md b/third_party/ui/bower_components/angular-aria/README.md
new file mode 100644
index 0000000000..04c5a8f949
--- /dev/null
+++ b/third_party/ui/bower_components/angular-aria/README.md
@@ -0,0 +1,67 @@
+# packaged angular-aria
+
+This repo is for distribution on `npm` and `bower`. The source for this module is in the
+[main AngularJS repo](https://github.com/angular/angular.js/tree/master/src/ngAria).
+Please file issues and pull requests against that repo.
+
+## Install
+
+You can install this package either with `npm` or with `bower`.
+
+### npm
+
+```shell
+npm install angular-aria
+```
+Then add `ngAria` as a dependency for your app:
+
+```javascript
+angular.module('myApp', [require('angular-aria')]);
+```
+
+### bower
+
+```shell
+bower install angular-aria
+```
+
+Add a `
+```
+
+Then add `ngAria` as a dependency for your app:
+
+```javascript
+angular.module('myApp', ['ngAria']);
+```
+
+## Documentation
+
+Documentation is available on the
+[AngularJS docs site](http://docs.angularjs.org/api/ngAria).
+
+## License
+
+The MIT License
+
+Copyright (c) 2010-2015 Google, Inc. http://angularjs.org
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/third_party/ui/bower_components/angular-aria/angular-aria.js b/third_party/ui/bower_components/angular-aria/angular-aria.js
new file mode 100644
index 0000000000..8e7aa9faa9
--- /dev/null
+++ b/third_party/ui/bower_components/angular-aria/angular-aria.js
@@ -0,0 +1,364 @@
+/**
+ * @license AngularJS v1.3.15
+ * (c) 2010-2014 Google, Inc. http://angularjs.org
+ * License: MIT
+ */
+(function(window, angular, undefined) {'use strict';
+
+/**
+ * @ngdoc module
+ * @name ngAria
+ * @description
+ *
+ * The `ngAria` module provides support for common
+ * [ARIA](http://www.w3.org/TR/wai-aria/)
+ * attributes that convey state or semantic information about the application for users
+ * of assistive technologies, such as screen readers.
+ *
+ *
+ *
+ * ## Usage
+ *
+ * For ngAria to do its magic, simply include the module as a dependency. The directives supported
+ * by ngAria are:
+ * `ngModel`, `ngDisabled`, `ngShow`, `ngHide`, `ngClick`, `ngDblClick`, and `ngMessages`.
+ *
+ * Below is a more detailed breakdown of the attributes handled by ngAria:
+ *
+ * | Directive | Supported Attributes |
+ * |---------------------------------------------|----------------------------------------------------------------------------------------|
+ * | {@link ng.directive:ngDisabled ngDisabled} | aria-disabled |
+ * | {@link ng.directive:ngShow ngShow} | aria-hidden |
+ * | {@link ng.directive:ngHide ngHide} | aria-hidden |
+ * | {@link ng.directive:ngDblclick ngDblclick} | tabindex |
+ * | {@link module:ngMessages ngMessages} | aria-live |
+ * | {@link ng.directive:ngModel ngModel} | aria-checked, aria-valuemin, aria-valuemax, aria-valuenow, aria-invalid, aria-required, input roles |
+ * | {@link ng.directive:ngClick ngClick} | tabindex, keypress event, button role |
+ *
+ * Find out more information about each directive by reading the
+ * {@link guide/accessibility ngAria Developer Guide}.
+ *
+ * ##Example
+ * Using ngDisabled with ngAria:
+ * ```html
+ *
+ * ```
+ * Becomes:
+ * ```html
+ *
+ * ```
+ *
+ * ##Disabling Attributes
+ * It's possible to disable individual attributes added by ngAria with the
+ * {@link ngAria.$ariaProvider#config config} method. For more details, see the
+ * {@link guide/accessibility Developer Guide}.
+ */
+ /* global -ngAriaModule */
+var ngAriaModule = angular.module('ngAria', ['ng']).
+ provider('$aria', $AriaProvider);
+
+/**
+ * @ngdoc provider
+ * @name $ariaProvider
+ *
+ * @description
+ *
+ * Used for configuring the ARIA attributes injected and managed by ngAria.
+ *
+ * ```js
+ * angular.module('myApp', ['ngAria'], function config($ariaProvider) {
+ * $ariaProvider.config({
+ * ariaValue: true,
+ * tabindex: false
+ * });
+ * });
+ *```
+ *
+ * ## Dependencies
+ * Requires the {@link ngAria} module to be installed.
+ *
+ */
+function $AriaProvider() {
+ var config = {
+ ariaHidden: true,
+ ariaChecked: true,
+ ariaDisabled: true,
+ ariaRequired: true,
+ ariaInvalid: true,
+ ariaMultiline: true,
+ ariaValue: true,
+ tabindex: true,
+ bindKeypress: true
+ };
+
+ /**
+ * @ngdoc method
+ * @name $ariaProvider#config
+ *
+ * @param {object} config object to enable/disable specific ARIA attributes
+ *
+ * - **ariaHidden** – `{boolean}` – Enables/disables aria-hidden tags
+ * - **ariaChecked** – `{boolean}` – Enables/disables aria-checked tags
+ * - **ariaDisabled** – `{boolean}` – Enables/disables aria-disabled tags
+ * - **ariaRequired** – `{boolean}` – Enables/disables aria-required tags
+ * - **ariaInvalid** – `{boolean}` – Enables/disables aria-invalid tags
+ * - **ariaMultiline** – `{boolean}` – Enables/disables aria-multiline tags
+ * - **ariaValue** – `{boolean}` – Enables/disables aria-valuemin, aria-valuemax and aria-valuenow tags
+ * - **tabindex** – `{boolean}` – Enables/disables tabindex tags
+ * - **bindKeypress** – `{boolean}` – Enables/disables keypress event binding on `<div>` and
+ * `<li>` elements with ng-click
+ *
+ * @description
+ * Enables/disables various ARIA attributes
+ */
+ this.config = function(newConfig) {
+ config = angular.extend(config, newConfig);
+ };
+
+ function watchExpr(attrName, ariaAttr, negate) {
+ return function(scope, elem, attr) {
+ var ariaCamelName = attr.$normalize(ariaAttr);
+ if (config[ariaCamelName] && !attr[ariaCamelName]) {
+ scope.$watch(attr[attrName], function(boolVal) {
+ if (negate) {
+ boolVal = !boolVal;
+ }
+ elem.attr(ariaAttr, boolVal);
+ });
+ }
+ };
+ }
+
+ /**
+ * @ngdoc service
+ * @name $aria
+ *
+ * @description
+ * @priority 200
+ *
+ * The $aria service contains helper methods for applying common
+ * [ARIA](http://www.w3.org/TR/wai-aria/) attributes to HTML directives.
+ *
+ * ngAria injects common accessibility attributes that tell assistive technologies when HTML
+ * elements are enabled, selected, hidden, and more. To see how this is performed with ngAria,
+ * let's review a code snippet from ngAria itself:
+ *
+ *```js
+ * ngAriaModule.directive('ngDisabled', ['$aria', function($aria) {
+ * return $aria.$$watchExpr('ngDisabled', 'aria-disabled');
+ * }])
+ *```
+ * Shown above, the ngAria module creates a directive with the same signature as the
+ * traditional `ng-disabled` directive. But this ngAria version is dedicated to
+ * solely managing accessibility attributes. The internal `$aria` service is used to watch the
+ * boolean attribute `ngDisabled`. If it has not been explicitly set by the developer,
+ * `aria-disabled` is injected as an attribute with its value synchronized to the value in
+ * `ngDisabled`.
+ *
+ * Because ngAria hooks into the `ng-disabled` directive, developers do not have to do
+ * anything to enable this feature. The `aria-disabled` attribute is automatically managed
+ * simply as a silent side-effect of using `ng-disabled` with the ngAria module.
+ *
+ * The full list of directives that interface with ngAria:
+ * * **ngModel**
+ * * **ngShow**
+ * * **ngHide**
+ * * **ngClick**
+ * * **ngDblclick**
+ * * **ngMessages**
+ * * **ngDisabled**
+ *
+ * Read the {@link guide/accessibility ngAria Developer Guide} for a thorough explanation of each
+ * directive.
+ *
+ *
+ * ## Dependencies
+ * Requires the {@link ngAria} module to be installed.
+ */
+ this.$get = function() {
+ return {
+ config: function(key) {
+ return config[key];
+ },
+ $$watchExpr: watchExpr
+ };
+ };
+}
+
+
+ngAriaModule.directive('ngShow', ['$aria', function($aria) {
+ return $aria.$$watchExpr('ngShow', 'aria-hidden', true);
+}])
+.directive('ngHide', ['$aria', function($aria) {
+ return $aria.$$watchExpr('ngHide', 'aria-hidden', false);
+}])
+.directive('ngModel', ['$aria', function($aria) {
+
+ function shouldAttachAttr(attr, normalizedAttr, elem) {
+ return $aria.config(normalizedAttr) && !elem.attr(attr);
+ }
+
+ function shouldAttachRole(role, elem) {
+ return !elem.attr('role') && (elem.attr('type') === role) && (elem[0].nodeName !== 'INPUT');
+ }
+
+ function getShape(attr, elem) {
+ var type = attr.type,
+ role = attr.role;
+
+ return ((type || role) === 'checkbox' || role === 'menuitemcheckbox') ? 'checkbox' :
+ ((type || role) === 'radio' || role === 'menuitemradio') ? 'radio' :
+ (type === 'range' || role === 'progressbar' || role === 'slider') ? 'range' :
+ (type || role) === 'textbox' || elem[0].nodeName === 'TEXTAREA' ? 'multiline' : '';
+ }
+
+ return {
+ restrict: 'A',
+ require: '?ngModel',
+ priority: 200, //Make sure watches are fired after any other directives that affect the ngModel value
+ link: function(scope, elem, attr, ngModel) {
+ var shape = getShape(attr, elem);
+ var needsTabIndex = shouldAttachAttr('tabindex', 'tabindex', elem);
+
+ function ngAriaWatchModelValue() {
+ return ngModel.$modelValue;
+ }
+
+ function getRadioReaction() {
+ if (needsTabIndex) {
+ needsTabIndex = false;
+ return function ngAriaRadioReaction(newVal) {
+ var boolVal = (attr.value == ngModel.$viewValue);
+ elem.attr('aria-checked', boolVal);
+ elem.attr('tabindex', 0 - !boolVal);
+ };
+ } else {
+ return function ngAriaRadioReaction(newVal) {
+ elem.attr('aria-checked', (attr.value == ngModel.$viewValue));
+ };
+ }
+ }
+
+ function ngAriaCheckboxReaction(newVal) {
+ elem.attr('aria-checked', !ngModel.$isEmpty(ngModel.$viewValue));
+ }
+
+ switch (shape) {
+ case 'radio':
+ case 'checkbox':
+ if (shouldAttachRole(shape, elem)) {
+ elem.attr('role', shape);
+ }
+ if (shouldAttachAttr('aria-checked', 'ariaChecked', elem)) {
+ scope.$watch(ngAriaWatchModelValue, shape === 'radio' ?
+ getRadioReaction() : ngAriaCheckboxReaction);
+ }
+ break;
+ case 'range':
+ if (shouldAttachRole(shape, elem)) {
+ elem.attr('role', 'slider');
+ }
+ if ($aria.config('ariaValue')) {
+ if (attr.min && !elem.attr('aria-valuemin')) {
+ elem.attr('aria-valuemin', attr.min);
+ }
+ if (attr.max && !elem.attr('aria-valuemax')) {
+ elem.attr('aria-valuemax', attr.max);
+ }
+ if (!elem.attr('aria-valuenow')) {
+ scope.$watch(ngAriaWatchModelValue, function ngAriaValueNowReaction(newVal) {
+ elem.attr('aria-valuenow', newVal);
+ });
+ }
+ }
+ break;
+ case 'multiline':
+ if (shouldAttachAttr('aria-multiline', 'ariaMultiline', elem)) {
+ elem.attr('aria-multiline', true);
+ }
+ break;
+ }
+
+ if (needsTabIndex) {
+ elem.attr('tabindex', 0);
+ }
+
+ if (ngModel.$validators.required && shouldAttachAttr('aria-required', 'ariaRequired', elem)) {
+ scope.$watch(function ngAriaRequiredWatch() {
+ return ngModel.$error.required;
+ }, function ngAriaRequiredReaction(newVal) {
+ elem.attr('aria-required', !!newVal);
+ });
+ }
+
+ if (shouldAttachAttr('aria-invalid', 'ariaInvalid', elem)) {
+ scope.$watch(function ngAriaInvalidWatch() {
+ return ngModel.$invalid;
+ }, function ngAriaInvalidReaction(newVal) {
+ elem.attr('aria-invalid', !!newVal);
+ });
+ }
+ }
+ };
+}])
+.directive('ngDisabled', ['$aria', function($aria) {
+ return $aria.$$watchExpr('ngDisabled', 'aria-disabled');
+}])
+.directive('ngMessages', function() {
+ return {
+ restrict: 'A',
+ require: '?ngMessages',
+ link: function(scope, elem, attr, ngMessages) {
+ if (!elem.attr('aria-live')) {
+ elem.attr('aria-live', 'assertive');
+ }
+ }
+ };
+})
+.directive('ngClick',['$aria', '$parse', function($aria, $parse) {
+ return {
+ restrict: 'A',
+ compile: function(elem, attr) {
+ var fn = $parse(attr.ngClick, /* interceptorFn */ null, /* expensiveChecks */ true);
+ return function(scope, elem, attr) {
+
+ var nodeBlackList = ['BUTTON', 'A', 'INPUT', 'TEXTAREA'];
+
+ function isNodeOneOf(elem, nodeTypeArray) {
+ if (nodeTypeArray.indexOf(elem[0].nodeName) !== -1) {
+ return true;
+ }
+ }
+ if (!elem.attr('role') && !isNodeOneOf(elem, nodeBlackList)) {
+ elem.attr('role', 'button');
+ }
+
+ if ($aria.config('tabindex') && !elem.attr('tabindex')) {
+ elem.attr('tabindex', 0);
+ }
+
+ if ($aria.config('bindKeypress') && !attr.ngKeypress && !isNodeOneOf(elem, nodeBlackList)) {
+ elem.on('keypress', function(event) {
+ if (event.keyCode === 32 || event.keyCode === 13) {
+ scope.$apply(callback);
+ }
+
+ function callback() {
+ fn(scope, { $event: event });
+ }
+ });
+ }
+ };
+ }
+ };
+}])
+.directive('ngDblclick', ['$aria', function($aria) {
+ return function(scope, elem, attr) {
+ if ($aria.config('tabindex') && !elem.attr('tabindex')) {
+ elem.attr('tabindex', 0);
+ }
+ };
+}]);
+
+
+})(window, window.angular);
diff --git a/third_party/ui/bower_components/angular-aria/angular-aria.min.js b/third_party/ui/bower_components/angular-aria/angular-aria.min.js
new file mode 100644
index 0000000000..8a91e61b6c
--- /dev/null
+++ b/third_party/ui/bower_components/angular-aria/angular-aria.min.js
@@ -0,0 +1,13 @@
+/*
+ AngularJS v1.3.15
+ (c) 2010-2014 Google, Inc. http://angularjs.org
+ License: MIT
+*/
+(function(r,n,s){'use strict';n.module("ngAria",["ng"]).provider("$aria",function(){function a(a,f,g){return function(b,c,d){var k=d.$normalize(f);e[k]&&!d[k]&&b.$watch(d[a],function(b){g&&(b=!b);c.attr(f,b)})}}var e={ariaHidden:!0,ariaChecked:!0,ariaDisabled:!0,ariaRequired:!0,ariaInvalid:!0,ariaMultiline:!0,ariaValue:!0,tabindex:!0,bindKeypress:!0};this.config=function(a){e=n.extend(e,a)};this.$get=function(){return{config:function(a){return e[a]},$$watchExpr:a}}}).directive("ngShow",["$aria",function(a){return a.$$watchExpr("ngShow",
+"aria-hidden",!0)}]).directive("ngHide",["$aria",function(a){return a.$$watchExpr("ngHide","aria-hidden",!1)}]).directive("ngModel",["$aria",function(a){function e(e,b,c){return a.config(b)&&!c.attr(e)}function h(a,b){return!b.attr("role")&&b.attr("type")===a&&"INPUT"!==b[0].nodeName}function f(a,b){var c=a.type,d=a.role;return"checkbox"===(c||d)||"menuitemcheckbox"===d?"checkbox":"radio"===(c||d)||"menuitemradio"===d?"radio":"range"===c||"progressbar"===d||"slider"===d?"range":"textbox"===(c||d)||
+"TEXTAREA"===b[0].nodeName?"multiline":""}return{restrict:"A",require:"?ngModel",priority:200,link:function(g,b,c,d){function k(){return d.$modelValue}function p(){return m?(m=!1,function(a){a=c.value==d.$viewValue;b.attr("aria-checked",a);b.attr("tabindex",0-!a)}):function(a){b.attr("aria-checked",c.value==d.$viewValue)}}function q(a){b.attr("aria-checked",!d.$isEmpty(d.$viewValue))}var l=f(c,b),m=e("tabindex","tabindex",b);switch(l){case "radio":case "checkbox":h(l,b)&&b.attr("role",l);e("aria-checked",
+"ariaChecked",b)&&g.$watch(k,"radio"===l?p():q);break;case "range":h(l,b)&&b.attr("role","slider");a.config("ariaValue")&&(c.min&&!b.attr("aria-valuemin")&&b.attr("aria-valuemin",c.min),c.max&&!b.attr("aria-valuemax")&&b.attr("aria-valuemax",c.max),b.attr("aria-valuenow")||g.$watch(k,function(a){b.attr("aria-valuenow",a)}));break;case "multiline":e("aria-multiline","ariaMultiline",b)&&b.attr("aria-multiline",!0)}m&&b.attr("tabindex",0);d.$validators.required&&e("aria-required","ariaRequired",b)&&
+g.$watch(function(){return d.$error.required},function(a){b.attr("aria-required",!!a)});e("aria-invalid","ariaInvalid",b)&&g.$watch(function(){return d.$invalid},function(a){b.attr("aria-invalid",!!a)})}}}]).directive("ngDisabled",["$aria",function(a){return a.$$watchExpr("ngDisabled","aria-disabled")}]).directive("ngMessages",function(){return{restrict:"A",require:"?ngMessages",link:function(a,e,h,f){e.attr("aria-live")||e.attr("aria-live","assertive")}}}).directive("ngClick",["$aria","$parse",function(a,
+e){return{restrict:"A",compile:function(h,f){var g=e(f.ngClick,null,!0);return function(b,c,d){function e(b,a){if(-1!==a.indexOf(b[0].nodeName))return!0}var f=["BUTTON","A","INPUT","TEXTAREA"];c.attr("role")||e(c,f)||c.attr("role","button");a.config("tabindex")&&!c.attr("tabindex")&&c.attr("tabindex",0);if(a.config("bindKeypress")&&!d.ngKeypress&&!e(c,f))c.on("keypress",function(a){function c(){g(b,{$event:a})}32!==a.keyCode&&13!==a.keyCode||b.$apply(c)})}}}}]).directive("ngDblclick",["$aria",function(a){return function(e,
+h,f){a.config("tabindex")&&!h.attr("tabindex")&&h.attr("tabindex",0)}}])})(window,window.angular);
+//# sourceMappingURL=angular-aria.min.js.map
diff --git a/third_party/ui/bower_components/angular-aria/angular-aria.min.js.map b/third_party/ui/bower_components/angular-aria/angular-aria.min.js.map
new file mode 100644
index 0000000000..95139eb790
--- /dev/null
+++ b/third_party/ui/bower_components/angular-aria/angular-aria.min.js.map
@@ -0,0 +1,8 @@
+{
+"version":3,
+"file":"angular-aria.min.js",
+"lineCount":12,
+"mappings":"A;;;;;aAKC,SAAQ,CAACA,CAAD,CAASC,CAAT,CAAkBC,CAAlB,CAA6B,CAmDnBD,CAAAE,OAAA,CAAe,QAAf,CAAyB,CAAC,IAAD,CAAzB,CAAAC,SAAAC,CACc,OADdA,CAwBnBC,QAAsB,EAAG,CAqCvBC,QAASA,EAAS,CAACC,CAAD,CAAWC,CAAX,CAAqBC,CAArB,CAA6B,CAC7C,MAAO,SAAQ,CAACC,CAAD,CAAQC,CAAR,CAAcC,CAAd,CAAoB,CACjC,IAAIC,EAAgBD,CAAAE,WAAA,CAAgBN,CAAhB,CAChBO,EAAA,CAAOF,CAAP,CAAJ,EAA8B,CAAAD,CAAA,CAAKC,CAAL,CAA9B,EACEH,CAAAM,OAAA,CAAaJ,CAAA,CAAKL,CAAL,CAAb,CAA6B,QAAQ,CAACU,CAAD,CAAU,CACzCR,CAAJ,GACEQ,CADF,CACY,CAACA,CADb,CAGAN,EAAAC,KAAA,CAAUJ,CAAV,CAAoBS,CAApB,CAJ6C,CAA/C,CAH+B,CADU,CApC/C,IAAIF,EAAS,CACXG,WAAY,CAAA,CADD,CAEXC,YAAa,CAAA,CAFF,CAGXC,aAAc,CAAA,CAHH,CAIXC,aAAc,CAAA,CAJH,CAKXC,YAAa,CAAA,CALF,CAMXC,cAAe,CAAA,CANJ,CAOXC,UAAW,CAAA,CAPA,CAQXC,SAAU,CAAA,CARC,CASXC,aAAc,CAAA,CATH,CAgCb,KAAAX,OAAA,CAAcY,QAAQ,CAACC,CAAD,CAAY,CAChCb,CAAA,CAASf,CAAA6B,OAAA,CAAed,CAAf,CAAuBa,CAAvB,CADuB,CAgElC,KAAAE,KAAA,CAAYC,QAAQ,EAAG,CACrB,MAAO,CACLhB,OAAQA,QAAQ,CAACiB,CAAD,CAAM,CACpB,MAAOjB,EAAA,CAAOiB,CAAP,CADa,CADjB,CAILC,YAAa3B,CAJR,CADc,CAjGA,CAxBNF,CAoInB8B,UAAA,CAAuB,QAAvB,CAAiC,CAAC,OAAD,CAAU,QAAQ,CAACC,CAAD,CAAQ,CACzD,MAAOA,EAAAF,YAAA,CAAkB,QAAlB;AAA4B,aAA5B,CAA2C,CAAA,CAA3C,CADkD,CAA1B,CAAjC,CAAAC,UAAA,CAGW,QAHX,CAGqB,CAAC,OAAD,CAAU,QAAQ,CAACC,CAAD,CAAQ,CAC7C,MAAOA,EAAAF,YAAA,CAAkB,QAAlB,CAA4B,aAA5B,CAA2C,CAAA,CAA3C,CADsC,CAA1B,CAHrB,CAAAC,UAAA,CAMW,SANX,CAMsB,CAAC,OAAD,CAAU,QAAQ,CAACC,CAAD,CAAQ,CAE9CC,QAASA,EAAgB,CAACxB,CAAD,CAAOyB,CAAP,CAAuB1B,CAAvB,CAA6B,CACpD,MAAOwB,EAAApB,OAAA,CAAasB,CAAb,CAAP,EAAuC,CAAC1B,CAAAC,KAAA,CAAUA,CAAV,CADY,CAItD0B,QAASA,EAAgB,CAACC,CAAD,CAAO5B,CAAP,CAAa,CACpC,MAAO,CAACA,CAAAC,KAAA,CAAU,MAAV,CAAR,EAA8BD,CAAAC,KAAA,CAAU,MAAV,CAA9B,GAAoD2B,CAApD,EAAmF,OAAnF,GAA8D5B,CAAA,CAAK,CAAL,CAAA6B,SAD1B,CAItCC,QAASA,EAAQ,CAAC7B,CAAD,CAAOD,CAAP,CAAa,CAAA,IACxB+B,EAAO9B,CAAA8B,KADiB,CAExBH,EAAO3B,CAAA2B,KAEX,OAA2B,UAApB,IAAEG,CAAF,EAAUH,CAAV,GAA2C,kBAA3C,GAAkCA,CAAlC,CAAiE,UAAjE,CACoB,OAApB,IAAEG,CAAF,EAAUH,CAAV,GAA2C,eAA3C,GAAkCA,CAAlC,CAA8D,OAA9D,CACU,OAAV,GAACG,CAAD,EAA2C,aAA3C,GAAkCH,CAAlC,EAAqE,QAArE,GAA4DA,CAA5D,CAAiF,OAAjF,CACmB,SAAnB,IAACG,CAAD,EAASH,CAAT;AAAuD,UAAvD,GAAkC5B,CAAA,CAAK,CAAL,CAAA6B,SAAlC,CAAoE,WAApE,CAAkF,EAP7D,CAU9B,MAAO,CACLG,SAAU,GADL,CAELC,QAAS,UAFJ,CAGLC,SAAU,GAHL,CAILC,KAAMA,QAAQ,CAACpC,CAAD,CAAQC,CAAR,CAAcC,CAAd,CAAoBmC,CAApB,CAA6B,CAIzCC,QAASA,EAAqB,EAAG,CAC/B,MAAOD,EAAAE,YADwB,CAIjCC,QAASA,EAAgB,EAAG,CAC1B,MAAIC,EAAJ,EACEA,CACOC,CADS,CAAA,CACTA,CAAAA,QAA4B,CAACC,CAAD,CAAS,CACtCpC,CAAAA,CAAWL,CAAA0C,MAAXrC,EAAyB8B,CAAAQ,WAC7B5C,EAAAC,KAAA,CAAU,cAAV,CAA0BK,CAA1B,CACAN,EAAAC,KAAA,CAAU,UAAV,CAAsB,CAAtB,CAA0B,CAACK,CAA3B,CAH0C,CAF9C,EAQSmC,QAA4B,CAACC,CAAD,CAAS,CAC1C1C,CAAAC,KAAA,CAAU,cAAV,CAA2BA,CAAA0C,MAA3B,EAAyCP,CAAAQ,WAAzC,CAD0C,CATpB,CAe5BC,QAASA,EAAsB,CAACH,CAAD,CAAS,CACtC1C,CAAAC,KAAA,CAAU,cAAV,CAA0B,CAACmC,CAAAU,SAAA,CAAiBV,CAAAQ,WAAjB,CAA3B,CADsC,CAtBxC,IAAIG,EAAQjB,CAAA,CAAS7B,CAAT,CAAeD,CAAf,CAAZ,CACIwC,EAAgBf,CAAA,CAAiB,UAAjB,CAA6B,UAA7B,CAAyCzB,CAAzC,CAyBpB,QAAQ+C,CAAR,EACE,KAAK,OAAL,CACA,KAAK,UAAL,CACMpB,CAAA,CAAiBoB,CAAjB,CAAwB/C,CAAxB,CAAJ,EACEA,CAAAC,KAAA,CAAU,MAAV,CAAkB8C,CAAlB,CAEEtB,EAAA,CAAiB,cAAjB;AAAiC,aAAjC,CAAgDzB,CAAhD,CAAJ,EACED,CAAAM,OAAA,CAAagC,CAAb,CAA8C,OAAV,GAAAU,CAAA,CAChCR,CAAA,EADgC,CACXM,CADzB,CAGF,MACF,MAAK,OAAL,CACMlB,CAAA,CAAiBoB,CAAjB,CAAwB/C,CAAxB,CAAJ,EACEA,CAAAC,KAAA,CAAU,MAAV,CAAkB,QAAlB,CAEEuB,EAAApB,OAAA,CAAa,WAAb,CAAJ,GACMH,CAAA+C,IAMJ,EANiB,CAAAhD,CAAAC,KAAA,CAAU,eAAV,CAMjB,EALED,CAAAC,KAAA,CAAU,eAAV,CAA2BA,CAAA+C,IAA3B,CAKF,CAHI/C,CAAAgD,IAGJ,EAHiB,CAAAjD,CAAAC,KAAA,CAAU,eAAV,CAGjB,EAFED,CAAAC,KAAA,CAAU,eAAV,CAA2BA,CAAAgD,IAA3B,CAEF,CAAKjD,CAAAC,KAAA,CAAU,eAAV,CAAL,EACEF,CAAAM,OAAA,CAAagC,CAAb,CAAoCa,QAA+B,CAACR,CAAD,CAAS,CAC1E1C,CAAAC,KAAA,CAAU,eAAV,CAA2ByC,CAA3B,CAD0E,CAA5E,CARJ,CAaA,MACF,MAAK,WAAL,CACMjB,CAAA,CAAiB,gBAAjB,CAAmC,eAAnC,CAAoDzB,CAApD,CAAJ,EACEA,CAAAC,KAAA,CAAU,gBAAV,CAA4B,CAAA,CAA5B,CA/BN,CAoCIuC,CAAJ,EACExC,CAAAC,KAAA,CAAU,UAAV,CAAsB,CAAtB,CAGEmC,EAAAe,YAAAC,SAAJ,EAAoC3B,CAAA,CAAiB,eAAjB,CAAkC,cAAlC,CAAkDzB,CAAlD,CAApC;AACED,CAAAM,OAAA,CAAagD,QAA4B,EAAG,CAC1C,MAAOjB,EAAAkB,OAAAF,SADmC,CAA5C,CAEGG,QAA+B,CAACb,CAAD,CAAS,CACzC1C,CAAAC,KAAA,CAAU,eAAV,CAA2B,CAAEyC,CAAAA,CAA7B,CADyC,CAF3C,CAOEjB,EAAA,CAAiB,cAAjB,CAAiC,aAAjC,CAAgDzB,CAAhD,CAAJ,EACED,CAAAM,OAAA,CAAamD,QAA2B,EAAG,CACzC,MAAOpB,EAAAqB,SADkC,CAA3C,CAEGC,QAA8B,CAAChB,CAAD,CAAS,CACxC1C,CAAAC,KAAA,CAAU,cAAV,CAA0B,CAAEyC,CAAAA,CAA5B,CADwC,CAF1C,CA5EuC,CAJtC,CApBuC,CAA1B,CANtB,CAAAnB,UAAA,CAmHW,YAnHX,CAmHyB,CAAC,OAAD,CAAU,QAAQ,CAACC,CAAD,CAAQ,CACjD,MAAOA,EAAAF,YAAA,CAAkB,YAAlB,CAAgC,eAAhC,CAD0C,CAA1B,CAnHzB,CAAAC,UAAA,CAsHW,YAtHX,CAsHyB,QAAQ,EAAG,CAClC,MAAO,CACLS,SAAU,GADL,CAELC,QAAS,aAFJ,CAGLE,KAAMA,QAAQ,CAACpC,CAAD,CAAQC,CAAR,CAAcC,CAAd,CAAoB0D,CAApB,CAAgC,CACvC3D,CAAAC,KAAA,CAAU,WAAV,CAAL,EACED,CAAAC,KAAA,CAAU,WAAV,CAAuB,WAAvB,CAF0C,CAHzC,CAD2B,CAtHpC,CAAAsB,UAAA,CAiIW,SAjIX,CAiIqB,CAAC,OAAD,CAAU,QAAV,CAAoB,QAAQ,CAACC,CAAD;AAAQoC,CAAR,CAAgB,CAC/D,MAAO,CACL5B,SAAU,GADL,CAEL6B,QAASA,QAAQ,CAAC7D,CAAD,CAAOC,CAAP,CAAa,CAC5B,IAAI6D,EAAKF,CAAA,CAAO3D,CAAA8D,QAAP,CAAyC,IAAzC,CAAqE,CAAA,CAArE,CACT,OAAO,SAAQ,CAAChE,CAAD,CAAQC,CAAR,CAAcC,CAAd,CAAoB,CAIjC+D,QAASA,EAAW,CAAChE,CAAD,CAAOiE,CAAP,CAAsB,CACxC,GAAiD,EAAjD,GAAIA,CAAAC,QAAA,CAAsBlE,CAAA,CAAK,CAAL,CAAA6B,SAAtB,CAAJ,CACE,MAAO,CAAA,CAF+B,CAF1C,IAAIsC,EAAgB,CAAC,QAAD,CAAW,GAAX,CAAgB,OAAhB,CAAyB,UAAzB,CAOfnE,EAAAC,KAAA,CAAU,MAAV,CAAL,EAA2B+D,CAAA,CAAYhE,CAAZ,CAAkBmE,CAAlB,CAA3B,EACEnE,CAAAC,KAAA,CAAU,MAAV,CAAkB,QAAlB,CAGEuB,EAAApB,OAAA,CAAa,UAAb,CAAJ,EAAiC,CAAAJ,CAAAC,KAAA,CAAU,UAAV,CAAjC,EACED,CAAAC,KAAA,CAAU,UAAV,CAAsB,CAAtB,CAGF,IAAIuB,CAAApB,OAAA,CAAa,cAAb,CAAJ,EAAqCgE,CAAAnE,CAAAmE,WAArC,EAAyD,CAAAJ,CAAA,CAAYhE,CAAZ,CAAkBmE,CAAlB,CAAzD,CACEnE,CAAAqE,GAAA,CAAQ,UAAR,CAAoB,QAAQ,CAACC,CAAD,CAAQ,CAKlCC,QAASA,EAAQ,EAAG,CAClBT,CAAA,CAAG/D,CAAH,CAAU,CAAEyE,OAAQF,CAAV,CAAV,CADkB,CAJE,EAAtB,GAAIA,CAAAG,QAAJ,EAA8C,EAA9C,GAA4BH,CAAAG,QAA5B,EACE1E,CAAA2E,OAAA,CAAaH,CAAb,CAFgC,CAApC,CAlB+B,CAFP,CAFzB,CADwD,CAA5C,CAjIrB,CAAAhD,UAAA,CAsKW,YAtKX,CAsKyB,CAAC,OAAD,CAAU,QAAQ,CAACC,CAAD,CAAQ,CACjD,MAAO,SAAQ,CAACzB,CAAD;AAAQC,CAAR,CAAcC,CAAd,CAAoB,CAC7BuB,CAAApB,OAAA,CAAa,UAAb,CAAJ,EAAiC,CAAAJ,CAAAC,KAAA,CAAU,UAAV,CAAjC,EACED,CAAAC,KAAA,CAAU,UAAV,CAAsB,CAAtB,CAF+B,CADc,CAA1B,CAtKzB,CAvLsC,CAArC,CAAD,CAsWGb,MAtWH,CAsWWA,MAAAC,QAtWX;",
+"sources":["angular-aria.js"],
+"names":["window","angular","undefined","module","provider","ngAriaModule","$AriaProvider","watchExpr","attrName","ariaAttr","negate","scope","elem","attr","ariaCamelName","$normalize","config","$watch","boolVal","ariaHidden","ariaChecked","ariaDisabled","ariaRequired","ariaInvalid","ariaMultiline","ariaValue","tabindex","bindKeypress","this.config","newConfig","extend","$get","this.$get","key","$$watchExpr","directive","$aria","shouldAttachAttr","normalizedAttr","shouldAttachRole","role","nodeName","getShape","type","restrict","require","priority","link","ngModel","ngAriaWatchModelValue","$modelValue","getRadioReaction","needsTabIndex","ngAriaRadioReaction","newVal","value","$viewValue","ngAriaCheckboxReaction","$isEmpty","shape","min","max","ngAriaValueNowReaction","$validators","required","ngAriaRequiredWatch","$error","ngAriaRequiredReaction","ngAriaInvalidWatch","$invalid","ngAriaInvalidReaction","ngMessages","$parse","compile","fn","ngClick","isNodeOneOf","nodeTypeArray","indexOf","nodeBlackList","ngKeypress","on","event","callback","$event","keyCode","$apply"]
+}
diff --git a/third_party/ui/bower_components/angular-aria/bower.json b/third_party/ui/bower_components/angular-aria/bower.json
new file mode 100644
index 0000000000..3a9b3f9f10
--- /dev/null
+++ b/third_party/ui/bower_components/angular-aria/bower.json
@@ -0,0 +1,9 @@
+{
+ "name": "angular-aria",
+ "version": "1.3.15",
+ "main": "./angular-aria.js",
+ "ignore": [],
+ "dependencies": {
+ "angular": "1.3.15"
+ }
+}
diff --git a/third_party/ui/bower_components/angular-aria/index.js b/third_party/ui/bower_components/angular-aria/index.js
new file mode 100644
index 0000000000..0a8f0d9b05
--- /dev/null
+++ b/third_party/ui/bower_components/angular-aria/index.js
@@ -0,0 +1,2 @@
+require('./angular-aria');
+module.exports = 'ngAria';
diff --git a/third_party/ui/bower_components/angular-aria/package.json b/third_party/ui/bower_components/angular-aria/package.json
new file mode 100644
index 0000000000..c870967783
--- /dev/null
+++ b/third_party/ui/bower_components/angular-aria/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "angular-aria",
+ "version": "1.3.15",
+ "description": "AngularJS module for making accessibility easy",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/angular/angular.js.git"
+ },
+ "keywords": [
+ "angular",
+ "framework",
+ "browser",
+ "accessibility",
+ "a11y",
+ "client-side"
+ ],
+ "author": "Angular Core Team ",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/angular/angular.js/issues"
+ },
+ "homepage": "http://angularjs.org"
+}
diff --git a/third_party/ui/bower_components/angular-cookies/.bower.json b/third_party/ui/bower_components/angular-cookies/.bower.json
new file mode 100644
index 0000000000..f15df021ad
--- /dev/null
+++ b/third_party/ui/bower_components/angular-cookies/.bower.json
@@ -0,0 +1,19 @@
+{
+ "name": "angular-cookies",
+ "version": "1.3.15",
+ "main": "./angular-cookies.js",
+ "ignore": [],
+ "dependencies": {
+ "angular": "1.3.15"
+ },
+ "homepage": "https://github.com/angular/bower-angular-cookies",
+ "_release": "1.3.15",
+ "_resolution": {
+ "type": "version",
+ "tag": "v1.3.15",
+ "commit": "846700fd8e45eefa1a43cf1865bcb7aaf95a68e0"
+ },
+ "_source": "git://github.com/angular/bower-angular-cookies.git",
+ "_target": "1.3.x",
+ "_originalSource": "angular-cookies"
+}
\ No newline at end of file
diff --git a/third_party/ui/bower_components/angular-cookies/README.md b/third_party/ui/bower_components/angular-cookies/README.md
new file mode 100644
index 0000000000..7b190d3461
--- /dev/null
+++ b/third_party/ui/bower_components/angular-cookies/README.md
@@ -0,0 +1,68 @@
+# packaged angular-cookies
+
+This repo is for distribution on `npm` and `bower`. The source for this module is in the
+[main AngularJS repo](https://github.com/angular/angular.js/tree/master/src/ngCookies).
+Please file issues and pull requests against that repo.
+
+## Install
+
+You can install this package either with `npm` or with `bower`.
+
+### npm
+
+```shell
+npm install angular-cookies
+```
+
+Then add `ngCookies` as a dependency for your app:
+
+```javascript
+angular.module('myApp', [require('angular-cookies')]);
+```
+
+### bower
+
+```shell
+bower install angular-cookies
+```
+
+Add a `
+```
+
+Then add `ngCookies` as a dependency for your app:
+
+```javascript
+angular.module('myApp', ['ngCookies']);
+```
+
+## Documentation
+
+Documentation is available on the
+[AngularJS docs site](http://docs.angularjs.org/api/ngCookies).
+
+## License
+
+The MIT License
+
+Copyright (c) 2010-2015 Google, Inc. http://angularjs.org
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/third_party/ui/bower_components/angular-cookies/angular-cookies.js b/third_party/ui/bower_components/angular-cookies/angular-cookies.js
new file mode 100644
index 0000000000..03b352ba30
--- /dev/null
+++ b/third_party/ui/bower_components/angular-cookies/angular-cookies.js
@@ -0,0 +1,206 @@
+/**
+ * @license AngularJS v1.3.15
+ * (c) 2010-2014 Google, Inc. http://angularjs.org
+ * License: MIT
+ */
+(function(window, angular, undefined) {'use strict';
+
+/**
+ * @ngdoc module
+ * @name ngCookies
+ * @description
+ *
+ * # ngCookies
+ *
+ * The `ngCookies` module provides a convenient wrapper for reading and writing browser cookies.
+ *
+ *
+ *
+ *
+ * See {@link ngCookies.$cookies `$cookies`} and
+ * {@link ngCookies.$cookieStore `$cookieStore`} for usage.
+ */
+
+
+angular.module('ngCookies', ['ng']).
+ /**
+ * @ngdoc service
+ * @name $cookies
+ *
+ * @description
+ * Provides read/write access to browser's cookies.
+ *
+ * Only a simple Object is exposed and by adding or removing properties to/from this object, new
+ * cookies are created/deleted at the end of current $eval.
+ * The object's properties can only be strings.
+ *
+ * Requires the {@link ngCookies `ngCookies`} module to be installed.
+ *
+ * @example
+ *
+ * ```js
+ * angular.module('cookiesExample', ['ngCookies'])
+ * .controller('ExampleController', ['$cookies', function($cookies) {
+ * // Retrieving a cookie
+ * var favoriteCookie = $cookies.myFavorite;
+ * // Setting a cookie
+ * $cookies.myFavorite = 'oatmeal';
+ * }]);
+ * ```
+ */
+ factory('$cookies', ['$rootScope', '$browser', function($rootScope, $browser) {
+ var cookies = {},
+ lastCookies = {},
+ lastBrowserCookies,
+ runEval = false,
+ copy = angular.copy,
+ isUndefined = angular.isUndefined;
+
+ //creates a poller fn that copies all cookies from the $browser to service & inits the service
+ $browser.addPollFn(function() {
+ var currentCookies = $browser.cookies();
+ if (lastBrowserCookies != currentCookies) { //relies on browser.cookies() impl
+ lastBrowserCookies = currentCookies;
+ copy(currentCookies, lastCookies);
+ copy(currentCookies, cookies);
+ if (runEval) $rootScope.$apply();
+ }
+ })();
+
+ runEval = true;
+
+ //at the end of each eval, push cookies
+ //TODO: this should happen before the "delayed" watches fire, because if some cookies are not
+ // strings or browser refuses to store some cookies, we update the model in the push fn.
+ $rootScope.$watch(push);
+
+ return cookies;
+
+
+ /**
+ * Pushes all the cookies from the service to the browser and verifies if all cookies were
+ * stored.
+ */
+ function push() {
+ var name,
+ value,
+ browserCookies,
+ updated;
+
+ //delete any cookies deleted in $cookies
+ for (name in lastCookies) {
+ if (isUndefined(cookies[name])) {
+ $browser.cookies(name, undefined);
+ }
+ }
+
+ //update all cookies updated in $cookies
+ for (name in cookies) {
+ value = cookies[name];
+ if (!angular.isString(value)) {
+ value = '' + value;
+ cookies[name] = value;
+ }
+ if (value !== lastCookies[name]) {
+ $browser.cookies(name, value);
+ updated = true;
+ }
+ }
+
+ //verify what was actually stored
+ if (updated) {
+ updated = false;
+ browserCookies = $browser.cookies();
+
+ for (name in cookies) {
+ if (cookies[name] !== browserCookies[name]) {
+ //delete or reset all cookies that the browser dropped from $cookies
+ if (isUndefined(browserCookies[name])) {
+ delete cookies[name];
+ } else {
+ cookies[name] = browserCookies[name];
+ }
+ updated = true;
+ }
+ }
+ }
+ }
+ }]).
+
+
+ /**
+ * @ngdoc service
+ * @name $cookieStore
+ * @requires $cookies
+ *
+ * @description
+ * Provides a key-value (string-object) storage, that is backed by session cookies.
+ * Objects put or retrieved from this storage are automatically serialized or
+ * deserialized by angular's toJson/fromJson.
+ *
+ * Requires the {@link ngCookies `ngCookies`} module to be installed.
+ *
+ * @example
+ *
+ * ```js
+ * angular.module('cookieStoreExample', ['ngCookies'])
+ * .controller('ExampleController', ['$cookieStore', function($cookieStore) {
+ * // Put cookie
+ * $cookieStore.put('myFavorite','oatmeal');
+ * // Get cookie
+ * var favoriteCookie = $cookieStore.get('myFavorite');
+ * // Removing a cookie
+ * $cookieStore.remove('myFavorite');
+ * }]);
+ * ```
+ */
+ factory('$cookieStore', ['$cookies', function($cookies) {
+
+ return {
+ /**
+ * @ngdoc method
+ * @name $cookieStore#get
+ *
+ * @description
+ * Returns the value of given cookie key
+ *
+ * @param {string} key Id to use for lookup.
+ * @returns {Object} Deserialized cookie value.
+ */
+ get: function(key) {
+ var value = $cookies[key];
+ return value ? angular.fromJson(value) : value;
+ },
+
+ /**
+ * @ngdoc method
+ * @name $cookieStore#put
+ *
+ * @description
+ * Sets a value for given cookie key
+ *
+ * @param {string} key Id for the `value`.
+ * @param {Object} value Value to be stored.
+ */
+ put: function(key, value) {
+ $cookies[key] = angular.toJson(value);
+ },
+
+ /**
+ * @ngdoc method
+ * @name $cookieStore#remove
+ *
+ * @description
+ * Remove given cookie
+ *
+ * @param {string} key Id of the key-value pair to delete.
+ */
+ remove: function(key) {
+ delete $cookies[key];
+ }
+ };
+
+ }]);
+
+
+})(window, window.angular);
diff --git a/third_party/ui/bower_components/angular-cookies/angular-cookies.min.js b/third_party/ui/bower_components/angular-cookies/angular-cookies.min.js
new file mode 100644
index 0000000000..bbfc72dab2
--- /dev/null
+++ b/third_party/ui/bower_components/angular-cookies/angular-cookies.min.js
@@ -0,0 +1,8 @@
+/*
+ AngularJS v1.3.15
+ (c) 2010-2014 Google, Inc. http://angularjs.org
+ License: MIT
+*/
+(function(p,f,n){'use strict';f.module("ngCookies",["ng"]).factory("$cookies",["$rootScope","$browser",function(e,b){var c={},g={},h,k=!1,l=f.copy,m=f.isUndefined;b.addPollFn(function(){var a=b.cookies();h!=a&&(h=a,l(a,g),l(a,c),k&&e.$apply())})();k=!0;e.$watch(function(){var a,d,e;for(a in g)m(c[a])&&b.cookies(a,n);for(a in c)d=c[a],f.isString(d)||(d=""+d,c[a]=d),d!==g[a]&&(b.cookies(a,d),e=!0);if(e)for(a in d=b.cookies(),c)c[a]!==d[a]&&(m(d[a])?delete c[a]:c[a]=d[a])});return c}]).factory("$cookieStore",
+["$cookies",function(e){return{get:function(b){return(b=e[b])?f.fromJson(b):b},put:function(b,c){e[b]=f.toJson(c)},remove:function(b){delete e[b]}}}])})(window,window.angular);
+//# sourceMappingURL=angular-cookies.min.js.map
diff --git a/third_party/ui/bower_components/angular-cookies/angular-cookies.min.js.map b/third_party/ui/bower_components/angular-cookies/angular-cookies.min.js.map
new file mode 100644
index 0000000000..677960c595
--- /dev/null
+++ b/third_party/ui/bower_components/angular-cookies/angular-cookies.min.js.map
@@ -0,0 +1,8 @@
+{
+"version":3,
+"file":"angular-cookies.min.js",
+"lineCount":7,
+"mappings":"A;;;;;aAKC,SAAQ,CAACA,CAAD,CAASC,CAAT,CAAkBC,CAAlB,CAA6B,CAmBtCD,CAAAE,OAAA,CAAe,WAAf,CAA4B,CAAC,IAAD,CAA5B,CAAAC,QAAA,CA0BW,UA1BX,CA0BuB,CAAC,YAAD,CAAe,UAAf,CAA2B,QAAQ,CAACC,CAAD,CAAaC,CAAb,CAAuB,CAAA,IACvEC,EAAU,EAD6D,CAEvEC,EAAc,EAFyD,CAGvEC,CAHuE,CAIvEC,EAAU,CAAA,CAJ6D,CAKvEC,EAAOV,CAAAU,KALgE,CAMvEC,EAAcX,CAAAW,YAGlBN,EAAAO,UAAA,CAAmB,QAAQ,EAAG,CAC5B,IAAIC,EAAiBR,CAAAC,QAAA,EACjBE,EAAJ,EAA0BK,CAA1B,GACEL,CAGA,CAHqBK,CAGrB,CAFAH,CAAA,CAAKG,CAAL,CAAqBN,CAArB,CAEA,CADAG,CAAA,CAAKG,CAAL,CAAqBP,CAArB,CACA,CAAIG,CAAJ,EAAaL,CAAAU,OAAA,EAJf,CAF4B,CAA9B,CAAA,EAUAL,EAAA,CAAU,CAAA,CAKVL,EAAAW,OAAA,CASAC,QAAa,EAAG,CAAA,IACVC,CADU,CAEVC,CAFU,CAIVC,CAGJ,KAAKF,CAAL,GAAaV,EAAb,CACMI,CAAA,CAAYL,CAAA,CAAQW,CAAR,CAAZ,CAAJ,EACEZ,CAAAC,QAAA,CAAiBW,CAAjB,CAAuBhB,CAAvB,CAKJ,KAAKgB,CAAL,GAAaX,EAAb,CACEY,CAKA,CALQZ,CAAA,CAAQW,CAAR,CAKR,CAJKjB,CAAAoB,SAAA,CAAiBF,CAAjB,CAIL,GAHEA,CACA,CADQ,EACR,CADaA,CACb,CAAAZ,CAAA,CAAQW,CAAR,CAAA,CAAgBC,CAElB,EAAIA,CAAJ,GAAcX,CAAA,CAAYU,CAAZ,CAAd,GACEZ,CAAAC,QAAA,CAAiBW,CAAjB,CAAuBC,CAAvB,CACA,CAAAC,CAAA,CAAU,CAAA,CAFZ,CAOF,IAAIA,CAAJ,CAIE,IAAKF,CAAL,GAFAI,EAEaf,CAFID,CAAAC,QAAA,EAEJA,CAAAA,CAAb,CACMA,CAAA,CAAQW,CAAR,CAAJ,GAAsBI,CAAA,CAAeJ,CAAf,CAAtB,GAEMN,CAAA,CAAYU,CAAA,CAAeJ,CAAf,CAAZ,CAAJ,CACE,OAAOX,CAAA,CAAQW,CAAR,CADT,CAGEX,CAAA,CAAQW,CAAR,CAHF,CAGkBI,CAAA,CAAeJ,CAAf,CALpB,CAhCU,CAThB,CAEA,OAAOX,EA1BoE,CAA1D,CA1BvB,CAAAH,QAAA,CAoIW,cApIX;AAoI2B,CAAC,UAAD,CAAa,QAAQ,CAACmB,CAAD,CAAW,CAErD,MAAO,CAWLC,IAAKA,QAAQ,CAACC,CAAD,CAAM,CAEjB,MAAO,CADHN,CACG,CADKI,CAAA,CAASE,CAAT,CACL,EAAQxB,CAAAyB,SAAA,CAAiBP,CAAjB,CAAR,CAAkCA,CAFxB,CAXd,CA0BLQ,IAAKA,QAAQ,CAACF,CAAD,CAAMN,CAAN,CAAa,CACxBI,CAAA,CAASE,CAAT,CAAA,CAAgBxB,CAAA2B,OAAA,CAAeT,CAAf,CADQ,CA1BrB,CAuCLU,OAAQA,QAAQ,CAACJ,CAAD,CAAM,CACpB,OAAOF,CAAA,CAASE,CAAT,CADa,CAvCjB,CAF8C,CAAhC,CApI3B,CAnBsC,CAArC,CAAD,CAwMGzB,MAxMH,CAwMWA,MAAAC,QAxMX;",
+"sources":["angular-cookies.js"],
+"names":["window","angular","undefined","module","factory","$rootScope","$browser","cookies","lastCookies","lastBrowserCookies","runEval","copy","isUndefined","addPollFn","currentCookies","$apply","$watch","push","name","value","updated","isString","browserCookies","$cookies","get","key","fromJson","put","toJson","remove"]
+}
diff --git a/third_party/ui/bower_components/angular-cookies/bower.json b/third_party/ui/bower_components/angular-cookies/bower.json
new file mode 100644
index 0000000000..326d037199
--- /dev/null
+++ b/third_party/ui/bower_components/angular-cookies/bower.json
@@ -0,0 +1,9 @@
+{
+ "name": "angular-cookies",
+ "version": "1.3.15",
+ "main": "./angular-cookies.js",
+ "ignore": [],
+ "dependencies": {
+ "angular": "1.3.15"
+ }
+}
diff --git a/third_party/ui/bower_components/angular-cookies/index.js b/third_party/ui/bower_components/angular-cookies/index.js
new file mode 100644
index 0000000000..657667549a
--- /dev/null
+++ b/third_party/ui/bower_components/angular-cookies/index.js
@@ -0,0 +1,2 @@
+require('./angular-cookies');
+module.exports = 'ngCookies';
diff --git a/third_party/ui/bower_components/angular-cookies/package.json b/third_party/ui/bower_components/angular-cookies/package.json
new file mode 100644
index 0000000000..acddef7111
--- /dev/null
+++ b/third_party/ui/bower_components/angular-cookies/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "angular-cookies",
+ "version": "1.3.15",
+ "description": "AngularJS module for cookies",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/angular/angular.js.git"
+ },
+ "keywords": [
+ "angular",
+ "framework",
+ "browser",
+ "cookies",
+ "client-side"
+ ],
+ "author": "Angular Core Team ",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/angular/angular.js/issues"
+ },
+ "homepage": "http://angularjs.org"
+}
diff --git a/third_party/ui/bower_components/angular-css/.bower.json b/third_party/ui/bower_components/angular-css/.bower.json
new file mode 100644
index 0000000000..f4009ee14a
--- /dev/null
+++ b/third_party/ui/bower_components/angular-css/.bower.json
@@ -0,0 +1,45 @@
+{
+ "name": "angular-css",
+ "main": "./angular-css.js",
+ "version": "1.0.7",
+ "homepage": "http://door3.github.io/angular-css",
+ "authors": [
+ "Alex Castillo "
+ ],
+ "description": "CSS on-demand for AngularJS",
+ "keywords": [
+ "angularjs",
+ "ng-route",
+ "ui-router",
+ "css"
+ ],
+ "dependencies": {
+ "angular": ">= 1.3.0"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git://github.com/door3/angular-css"
+ },
+ "license": "MIT",
+ "ignore": [
+ "**/.*",
+ "node_modules",
+ "bower_components",
+ "component.json",
+ "package.json",
+ "lib",
+ "config",
+ "demo",
+ "test",
+ "tests"
+ ],
+ "_release": "1.0.7",
+ "_resolution": {
+ "type": "version",
+ "tag": "v1.0.7",
+ "commit": "06232156c8c21aeee0626a3a6bf4d0499516c7e8"
+ },
+ "_source": "git://github.com/door3/angular-css.git",
+ "_target": "1.0.x",
+ "_originalSource": "angular-css"
+}
\ No newline at end of file
diff --git a/third_party/ui/bower_components/angular-css/Gruntfile.js b/third_party/ui/bower_components/angular-css/Gruntfile.js
new file mode 100644
index 0000000000..8f13608919
--- /dev/null
+++ b/third_party/ui/bower_components/angular-css/Gruntfile.js
@@ -0,0 +1,52 @@
+'use strict';
+
+module.exports = function(grunt) {
+ grunt.initConfig({
+ pkg: grunt.file.readJSON('package.json'),
+
+ karma: {
+ unit: {
+ options: {
+ files: [
+ 'node_modules/angular/angular.js',
+ 'node_modules/angular-mocks/angular-mocks.js',
+ 'node_modules/chai/chai.js',
+ 'angular-css.js',
+ 'test/spec.js'
+ ]
+ },
+
+ frameworks: ['mocha'],
+
+ browsers: [
+ 'Chrome',
+ 'PhantomJS',
+ 'Firefox'
+ ],
+
+ singleRun: true
+ }
+ },
+
+ uglify: {
+ options: {
+ banner: '/*! <%= pkg.name %> <%= pkg.version %> | Copyright (c) <%= grunt.template.today("yyyy") %> DOOR3, Alex Castillo | MIT License */'
+ },
+
+ build: {
+ src: '<%= pkg.name %>.js',
+ dest: '<%= pkg.name %>.min.js'
+ }
+ }
+ });
+
+ grunt.loadNpmTasks('grunt-contrib-uglify');
+ grunt.loadNpmTasks('grunt-karma');
+
+ grunt.registerTask('test', ['karma']);
+
+ grunt.registerTask('default', [
+ 'test',
+ 'uglify'
+ ]);
+};
diff --git a/third_party/ui/bower_components/angular-css/LICENSE.txt b/third_party/ui/bower_components/angular-css/LICENSE.txt
new file mode 100644
index 0000000000..5c5d7b4f5b
--- /dev/null
+++ b/third_party/ui/bower_components/angular-css/LICENSE.txt
@@ -0,0 +1,21 @@
+The MIT License
+
+Copyright (c) 2014 DOOR3, Alex Castillo
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
\ No newline at end of file
diff --git a/third_party/ui/bower_components/angular-css/README.md b/third_party/ui/bower_components/angular-css/README.md
new file mode 100644
index 0000000000..913e1d5d04
--- /dev/null
+++ b/third_party/ui/bower_components/angular-css/README.md
@@ -0,0 +1,371 @@
+# AngularCSS
+
+##### CSS on-demand for AngularJS
+Optimize the presentation layer of your single-page apps by dynamically injecting stylesheets as needed.
+
+AngularCSS listens for [route](https://github.com/angular/bower-angular-route) (or [states](https://github.com/angular-ui/ui-router)) change events, adds the CSS defined on the current route and removes the CSS from the previous route. It also works with directives in the same fashion with the compile and scope destroy events. See the code samples below for more details.
+
+##### Read the Article
+
+[Introducing AngularCSS: CSS On-Demand for AngularJS](http://door3.com/insights/introducing-angularcss-css-demand-angularjs)
+
+### Demos
+
+[Angular's ngRoute Demo](http://door3.github.io/angular-css) ([source](../gh-pages/app.routes.js))
+
+[UI Router Demo](http://door3.github.io/angular-css/states.html) ([source](../gh-pages/app.states.js))
+
+
+### Quick Start
+
+Install and manage with [Bower](http://bower.io). A [CDN](http://cdnjs.com/libraries/angular-css) is also provided by cdnjs.com
+
+``` bash
+$ bower install angular-css
+```
+
+
+1) Include the required JavaScript libraries in your `index.html` (ngRoute and UI Router are optional).
+
+``` html
+
+
+
+```
+
+2) Add `door3.css` as a dependency for your app.
+
+``` js
+var myApp = angular.module('myApp', ['ngRoute','door3.css']);
+```
+
+### Examples
+
+This module can be used by adding a css property in your routes values, directives or by calling the `$css` service methods from controllers and services.
+
+The css property supports a string, an array of strings, object notation or an array of objects.
+
+See examples below for more information.
+
+
+#### In Directives
+
+``` js
+myApp.directive('myDirective', function () {
+ return {
+ restrict: 'E',
+ templateUrl: 'my-directive/my-directive.html',
+ /* Binding css to directives */
+ css: 'my-directive/my-directive.css'
+ }
+});
+```
+
+
+#### In Controllers
+
+``` js
+myApp.controller('pageCtrl', function ($scope, $css) {
+
+ // Binds stylesheet(s) to scope create/destroy events (recommended over add/remove)
+ $css.bind({
+ href: 'my-page/my-page.css'
+ }, $scope);
+
+ // Simply add stylesheet(s)
+ $css.add('my-page/my-page.css');
+
+ // Simply remove stylesheet(s)
+ $css.remove(['my-page/my-page.css','my-page/my-page2.css']);
+
+ // Remove all stylesheets
+ $css.removeAll();
+
+});
+```
+
+
+#### For Routes (Angular's ngRoute)
+
+Requires [ngRoute](https://github.com/angular/bower-angular-route) as a dependency
+
+
+``` js
+myApp.config(function($routeProvider) {
+
+ $routeProvider
+ .when('/page1', {
+ templateUrl: 'page1/page1.html',
+ controller: 'page1Ctrl',
+ /* Now you can bind css to routes */
+ css: 'page1/page1.css'
+ })
+ .when('/page2', {
+ templateUrl: 'page2/page2.html',
+ controller: 'page2Ctrl',
+ /* You can also enable features like bust cache, persist and preload */
+ css: {
+ href: 'page2/page2.css',
+ bustCache: true
+ }
+ })
+ .when('/page3', {
+ templateUrl: 'page3/page3.html',
+ controller: 'page3Ctrl',
+ /* This is how you can include multiple stylesheets */
+ css: ['page3/page3.css','page3/page3-2.css']
+ })
+ .when('/page4', {
+ templateUrl: 'page4/page4.html',
+ controller: 'page4Ctrl',
+ css: [
+ {
+ href: 'page4/page4.css',
+ persist: true
+ }, {
+ href: 'page4/page4.mobile.css',
+ /* Media Query support via window.matchMedia API
+ * This will only add the stylesheet if the breakpoint matches */
+ media: 'screen and (max-width : 768px)'
+ }, {
+ href: 'page4/page4.print.css',
+ media: 'print'
+ }
+ ]
+ });
+
+});
+```
+
+#### For States (UI Router)
+
+Requires [ui.router](https://github.com/angular-ui/ui-router) as a dependency
+
+
+``` js
+myApp.config(function($stateProvider) {
+
+ $stateProvider
+ .state('page1', {
+ url: '/page1',
+ templateUrl: 'page1/page1.html',
+ css: 'page1/page1.css'
+ })
+ .state('page2', {
+ url: '/page2',
+ templateUrl: 'page2/page2.html',
+ css: {
+ href: 'page2/page2.css',
+ preload: true,
+ persist: true
+ }
+ })
+ .state('page3', {
+ url: '/page3',
+ templateUrl: 'page3/page3.html',
+ css: ['page3/page3.css','page3/page3-2.css'],
+ views: {
+ 'state1': {
+ templateUrl: 'page3/states/page3-state1.html',
+ css: 'page3/states/page3-state1.css'
+ },
+ 'state2': {
+ templateUrl: 'page3/states/page3-state2.html',
+ css: ['page3/states/page3-state2.css']
+ },
+ 'state3': {
+ templateUrl: 'page3/states/page3-state3.html',
+ css: {
+ href: 'page3/states/page3-state3.css'
+ }
+ }
+ }
+ })
+ .state('page4', {
+ url: '/page4',
+ templateUrl: 'page4/page4.html',
+ views: {
+ 'state1': {
+ templateUrl: 'states/page4/page4-state1.html',
+ css: 'states/page4/page4-state1.css'
+ },
+ 'state2': {
+ templateUrl: 'states/page4/page4-state2.html',
+ css: ['states/page4/page4-state2.css']
+ },
+ 'state3': {
+ templateUrl: 'states/page4/page4-state3.html',
+ css: {
+ href: 'states/page4/page4-state3.css'
+ }
+ }
+ },
+ css: [
+ {
+ href: 'page4/page4.css',
+ }, {
+ href: 'page4/page4.mobile.css',
+ media: 'screen and (max-width : 768px)'
+ }, {
+ href: 'page4/page4.print.css',
+ media: 'print'
+ }
+ ]
+ });
+
+});
+```
+
+### Responsive Design
+
+AngularCSS supports "smart media queries". This means that stylesheets with media queries will be only added when the breakpoint matches.
+This will significantly optimize the load time of your apps.
+
+```js
+$routeProvider
+ .when('/my-page', {
+ templateUrl: 'my-page/my-page.html',
+ css: [
+ {
+ href: 'my-page/my-page.mobile.css',
+ media: '(max-width: 480px)'
+ }, {
+ href: 'my-page/my-page.tablet.css',
+ media: '(min-width: 768px) and (max-width: 1024px)'
+ }, {
+ href: 'my-page/my-page.desktop.css',
+ media: '(min-width: 1224px)'
+ }
+ ]
+ });
+```
+
+Even though you can use the `media` property to specify media queries, the best way to manage your breakpoins is by settings them in the provider's defaults. For example:
+
+```js
+myApp.config(function($routeProvider, $cssProvider) {
+
+ angular.extend($cssProvider.defaults, {
+ breakpoints: {
+ mobile: '(max-width: 480px)',
+ tablet: '(min-width: 768px) and (max-width: 1024px)',
+ desktop: '(min-width: 1224px)'
+ }
+ });
+
+ $routeProvider
+ .when('/my-page', {
+ templateUrl: 'my-page/my-page.html',
+ css: [
+ {
+ href: 'my-page/my-page.mobile.css',
+ breakpoint: 'mobile'
+ }, {
+ href: 'my-page/my-page.tablet.css',
+ breakpoint: 'tablet'
+ }, {
+ href: 'my-page/my-page.desktop.css',
+ breakpoint: 'desktop'
+ }
+ ]
+ });
+
+});
+```
+
+
+### Config
+
+You can configure AngularCSS at the global level or at the stylesheet level.
+
+#### Configuring global options
+
+These options are applied during the `config` phase of your app via `$cssProvider`.
+
+``` js
+myApp.config(function($cssProvider) {
+
+ angular.extend($cssProvider.defaults, {
+ container: 'head',
+ method: 'append',
+ persist: false,
+ preload: false,
+ bustCache: false
+ });
+
+});
+```
+
+#### Configuring CSS options
+
+These options are applied at the stylesheet level.
+
+``` js
+css: {
+ href: 'file-path.css',
+ rel: 'stylesheet',
+ type: 'text/css',
+ media: false,
+ persist: false,
+ preload: false,
+ bustCache: false,
+ weight: 0
+}
+```
+
+### Support
+
+AngularCSS is fully supported by AngularJS 1.3+
+
+There is partial support for AngularJS 1.2. It does not support `css` property via DDO (Directive Definition Object).
+The workarond is to bind (or add) the CSS in the directive's controller or link function via `$css` service.
+
+``` js
+myApp.directive('myDirective', function () {
+ return {
+ restrict: 'E',
+ templateUrl: 'my-directive/my-directive.html',
+ controller: function ($scope, $css) {
+ $css.bind('my-directive/my-directive.css', $scope);
+ }
+ }
+});
+```
+
+
+#### Browsers
+
+Chrome, Firefox, Safari, iOS Safari, Android and IE9+
+
+IE9 Does not support [matchMedia](http://caniuse.com/#feat=matchmedia) API. This means that in IE9, stylesheets with media queries will be added without checking if the breakpoint matches.
+
+
+### Contributing
+
+Please submit all pull requests the against master branch. If your pull request contains JavaScript patches or features, you should include relevant unit tests.
+
+### Copyright and license
+
+```
+The MIT License
+
+Copyright (c) 2014 DOOR3, Alex Castillo
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+```
diff --git a/third_party/ui/bower_components/angular-css/angular-css.js b/third_party/ui/bower_components/angular-css/angular-css.js
new file mode 100644
index 0000000000..26f4f61649
--- /dev/null
+++ b/third_party/ui/bower_components/angular-css/angular-css.js
@@ -0,0 +1,613 @@
+/**
+ * AngularCSS - CSS on-demand for AngularJS
+ * @version v1.0.7
+ * @author DOOR3, Alex Castillo
+ * @link http://door3.github.io/angular-css
+ * @license MIT License, http://www.opensource.org/licenses/MIT
+ */
+
+'use strict';
+
+(function (angular) {
+
+ /**
+ * AngularCSS Module
+ * Contains: config, constant, provider and run
+ **/
+ var angularCSS = angular.module('door3.css', []);
+
+ // Config
+ angularCSS.config(['$logProvider', function ($logProvider) {
+ // Turn off/on in order to see console logs during dev mode
+ $logProvider.debugEnabled(false);
+ }]);
+
+ // Provider
+ angularCSS.provider('$css', [function $cssProvider() {
+
+ // Defaults - default options that can be overridden from application config
+ var defaults = this.defaults = {
+ element: 'link',
+ rel: 'stylesheet',
+ type: 'text/css',
+ container: 'head',
+ method: 'append',
+ weight: 0
+ };
+
+ this.$get = ['$rootScope','$injector','$q','$window','$timeout','$compile','$http','$filter','$log',
+ function $get($rootScope, $injector, $q, $window, $timeout, $compile, $http, $filter, $log) {
+
+ var $css = {};
+
+ var template = '';
+
+ // Variables - default options that can be overridden from application config
+ var mediaQuery = {}, mediaQueryListener = {}, mediaQueriesToIgnore = ['print'], options = angular.extend({}, defaults),
+ container = angular.element(document.querySelector ? document.querySelector(options.container) : document.getElementsByTagName(options.container)[0]),
+ dynamicPaths = [];
+
+ // Parse all directives
+ angular.forEach($directives, function (directive, key) {
+ if (directive.hasOwnProperty('css')) {
+ $directives[key] = parse(directive.css);
+ }
+ });
+
+ /**
+ * Listen for directive add event in order to add stylesheet(s)
+ **/
+ function $directiveAddEventListener(event, directive, scope) {
+ // Binds directive's css
+ if (scope && directive.hasOwnProperty('css')) {
+ $css.bind([parse(directive.css)], scope);
+ }
+ }
+
+ /**
+ * Listen for route change event and add/remove stylesheet(s)
+ **/
+ function $routeEventListener(event, current, prev) {
+ // Removes previously added css rules
+ if (prev) {
+ $css.remove($css.getFromRoute(prev).concat(dynamicPaths));
+ // Reset dynamic paths array
+ dynamicPaths.length = 0;
+ }
+ // Adds current css rules
+ if (current) {
+ $css.add($css.getFromRoute(current));
+ }
+ }
+
+ /**
+ * Listen for state change event and add/remove stylesheet(s)
+ **/
+ function $stateEventListener(event, current, params, prev) {
+ // Removes previously added css rules
+ if (prev) {
+ $css.remove($css.getFromState(prev).concat(dynamicPaths));
+ // Reset dynamic paths array
+ dynamicPaths.length = 0;
+ }
+ // Adds current css rules
+ if (current) {
+ $css.add($css.getFromState(current));
+ }
+ }
+
+ /**
+ * Map breakpoitns defined in defaults to stylesheet media attribute
+ **/
+ function mapBreakpointToMedia(stylesheet) {
+ if (angular.isDefined(options.breakpoints)) {
+ if (stylesheet.breakpoint in options.breakpoints) {
+ stylesheet.media = options.breakpoints[stylesheet.breakpoint];
+ }
+ delete stylesheet.breakpoints;
+ }
+ }
+
+ /**
+ * Parse: returns array with full all object based on defaults
+ **/
+ function parse(obj) {
+ if (!obj) {
+ return;
+ }
+ // Function syntax
+ if (angular.isFunction(obj)) {
+ obj = angular.copy($injector.invoke(obj));
+ }
+ // String syntax
+ if (angular.isString(obj)) {
+ obj = angular.extend({
+ href: obj
+ }, options);
+ }
+ // Array of strings syntax
+ if (angular.isArray(obj) && angular.isString(obj[0])) {
+ angular.forEach(obj, function (item) {
+ obj = angular.extend({
+ href: item
+ }, options);
+ });
+ }
+ // Object syntax
+ if (angular.isObject(obj) && !angular.isArray(obj)) {
+ obj = angular.extend(obj, options);
+ }
+ // Array of objects syntax
+ if (angular.isArray(obj) && angular.isObject(obj[0])) {
+ angular.forEach(obj, function (item) {
+ obj = angular.extend(item, options);
+ });
+ }
+ // Map breakpoint to media attribute
+ mapBreakpointToMedia(obj);
+ return obj;
+ }
+
+ // Add stylesheets to scope
+ $rootScope.stylesheets = [];
+
+ // Adds compiled link tags to container element
+ container[options.method]($compile(template)($rootScope));
+
+ // Directive event listener (emulated internally)
+ $rootScope.$on('$directiveAdd', $directiveAddEventListener);
+
+ // Routes event listener ($route required)
+ $rootScope.$on('$routeChangeSuccess', $routeEventListener);
+
+ // States event listener ($state required)
+ $rootScope.$on('$stateChangeSuccess', $stateEventListener);
+
+ /**
+ * Bust Cache
+ **/
+ function bustCache(stylesheet) {
+ if (!stylesheet) {
+ return $log.error('No stylesheets provided');
+ }
+ var queryString = '?cache=';
+ // Append query string for bust cache only once
+ if (stylesheet.href.indexOf(queryString) === -1) {
+ stylesheet.href = stylesheet.href + (stylesheet.bustCache ? queryString + (new Date().getTime()) : '');
+ }
+ }
+
+ /**
+ * Filter By: returns an array of routes based on a property option
+ **/
+ function filterBy(array, prop) {
+ if (!array || !prop) {
+ return $log.error('filterBy: missing array or property');
+ }
+ return $filter('filter')(array, function (item) {
+ return item[prop];
+ });
+ }
+
+ /**
+ * Add Media Query
+ **/
+ function addViaMediaQuery(stylesheet) {
+ if (!stylesheet) {
+ return $log.error('No stylesheet provided');
+ }
+ // Media query object
+ mediaQuery[stylesheet.href] = $window.matchMedia(stylesheet.media);
+ // Media Query Listener function
+ mediaQueryListener[stylesheet.href] = function(mediaQuery) {
+ // Trigger digest
+ $timeout(function () {
+ if (mediaQuery.matches) {
+ // Add stylesheet
+ $rootScope.stylesheets.push(stylesheet);
+ } else {
+ var index = $rootScope.stylesheets.indexOf($filter('filter')($rootScope.stylesheets, {
+ href: stylesheet.href
+ })[0]);
+ // Remove stylesheet
+ if (index !== -1) {
+ $rootScope.stylesheets.splice(index, 1);
+ }
+ }
+ });
+ }
+ // Listen for media query changes
+ mediaQuery[stylesheet.href].addListener(mediaQueryListener[stylesheet.href]);
+ // Invoke first media query check
+ mediaQueryListener[stylesheet.href](mediaQuery[stylesheet.href]);
+ }
+
+ /**
+ * Remove Media Query
+ **/
+ function removeViaMediaQuery(stylesheet) {
+ if (!stylesheet) {
+ return $log.error('No stylesheet provided');
+ }
+ // Remove media query listener
+ if ($rootScope && angular.isDefined(mediaQuery)
+ && mediaQuery[stylesheet.href]
+ && angular.isDefined(mediaQueryListener)) {
+ mediaQuery[stylesheet.href].removeListener(mediaQueryListener[stylesheet.href]);
+ }
+ }
+
+ /**
+ * Is Media Query: checks for media settings, media queries to be ignore and match media support
+ **/
+ function isMediaQuery(stylesheet) {
+ if (!stylesheet) {
+ return $log.error('No stylesheet provided');
+ }
+ return !!(
+ // Check for media query setting
+ stylesheet.media
+ // Check for media queries to be ignored
+ && (mediaQueriesToIgnore.indexOf(stylesheet.media) === -1)
+ // Check for matchMedia support
+ && $window.matchMedia
+ );
+ }
+
+ /**
+ * Get From Route: returns array of css objects from single route
+ **/
+ $css.getFromRoute = function (route) {
+ if (!route) {
+ return $log.error('Get From Route: No route provided');
+ }
+ var css = null, result = [];
+ if (route.$$route && route.$$route.css) {
+ css = route.$$route.css;
+ }
+ else if (route.css) {
+ css = route.css;
+ }
+ // Adds route css rules to array
+ if (css) {
+ if (angular.isArray(css)) {
+ angular.forEach(css, function (cssItem) {
+ if (angular.isFunction(cssItem)) {
+ dynamicPaths.push(parse(cssItem));
+ }
+ result.push(parse(cssItem));
+ });
+ } else {
+ if (angular.isFunction(css)) {
+ dynamicPaths.push(parse(css));
+ }
+ result.push(parse(css));
+ }
+ }
+ return result;
+ };
+
+ /**
+ * Get From Routes: returns array of css objects from ng routes
+ **/
+ $css.getFromRoutes = function (routes) {
+ if (!routes) {
+ return $log.error('Get From Routes: No routes provided');
+ }
+ var result = [];
+ // Make array of all routes
+ angular.forEach(routes, function (route) {
+ var css = $css.getFromRoute(route);
+ if (css.length) {
+ result.push(css[0]);
+ }
+ });
+ return result;
+ };
+
+ /**
+ * Get From State: returns array of css objects from single state
+ **/
+ $css.getFromState = function (state) {
+ if (!state) {
+ return $log.error('Get From State: No state provided');
+ }
+ var result = [];
+ // State "views" notation
+ if (angular.isDefined(state.views)) {
+ angular.forEach(state.views, function (item) {
+ if (item.css) {
+ if (angular.isFunction(item.css)) {
+ dynamicPaths.push(parse(item.css));
+ }
+ result.push(parse(item.css));
+ }
+ });
+ }
+ // State "children" notation
+ if (angular.isDefined(state.children)) {
+ angular.forEach(state.children, function (child) {
+ if (child.css) {
+ if (angular.isFunction(child.css)) {
+ dynamicPaths.push(parse(child.css));
+ }
+ result.push(parse(child.css));
+ }
+ if (angular.isDefined(child.children)) {
+ angular.forEach(child.children, function (childChild) {
+ if (childChild.css) {
+ if (angular.isFunction(childChild.css)) {
+ dynamicPaths.push(parse(childChild.css));
+ }
+ result.push(parse(childChild.css));
+ }
+ });
+ }
+ });
+ }
+ // State default notation
+ if (angular.isDefined(state.css)) {
+ // For multiple stylesheets
+ if (angular.isArray(state.css)) {
+ angular.forEach(state.css, function (itemCss) {
+ if (angular.isFunction(itemCss)) {
+ dynamicPaths.push(parse(itemCss));
+ }
+ result.push(parse(itemCss));
+ });
+ // For single stylesheets
+ } else {
+ if (angular.isFunction(state.css)) {
+ dynamicPaths.push(parse(state.css));
+ }
+ result.push(parse(state.css));
+ }
+ }
+ return result;
+ };
+
+ /**
+ * Get From States: returns array of css objects from states
+ **/
+ $css.getFromStates = function (states) {
+ if (!states) {
+ return $log.error('Get From States: No states provided');
+ }
+ var result = [];
+ // Make array of all routes
+ angular.forEach(states, function (state) {
+ var css = $css.getFromState(state);
+ if (angular.isArray(css)) {
+ angular.forEach(css, function (cssItem) {
+ result.push(cssItem);
+ });
+ } else {
+ result.push(css);
+ }
+ });
+ return result;
+ };
+
+ /**
+ * Preload: preloads css via http request
+ **/
+ $css.preload = function (stylesheets, callback) {
+ // If no stylesheets provided, then preload all
+ if (!stylesheets) {
+ stylesheets = [];
+ // Add all stylesheets from custom directives to array
+ if ($directives.length) {
+ Array.prototype.push.apply(stylesheets, $directives);
+ }
+ // Add all stylesheets from ngRoute to array
+ if ($injector.has('$route')) {
+ Array.prototype.push.apply(stylesheets, $css.getFromRoutes($injector.get('$route').routes));
+ }
+ // Add all stylesheets from UI Router to array
+ if ($injector.has('$state')) {
+ Array.prototype.push.apply(stylesheets, $css.getFromStates($injector.get('$state').get()));
+ }
+ stylesheets = filterBy(stylesheets, 'preload');
+ }
+ if (!angular.isArray(stylesheets)) {
+ stylesheets = [stylesheets];
+ }
+ var stylesheetLoadPromises = [];
+ angular.forEach(stylesheets, function(stylesheet, key) {
+ stylesheet = stylesheets[key] = parse(stylesheet);
+ stylesheetLoadPromises.push(
+ // Preload via ajax request
+ $http.get(stylesheet.href).error(function (response) {
+ $log.error('AngularCSS: Incorrect path for ' + stylesheet.href);
+ })
+ );
+ });
+ if (angular.isFunction(callback)) {
+ $q.all(stylesheetLoadPromises).then(function () {
+ callback(stylesheets);
+ });
+ }
+ };
+
+ /**
+ * Bind: binds css in scope with own scope create/destroy events
+ **/
+ $css.bind = function (css, $scope) {
+ if (!css || !$scope) {
+ return $log.error('No scope or stylesheets provided');
+ }
+ var result = [];
+ // Adds route css rules to array
+ if (angular.isArray(css)) {
+ angular.forEach(css, function (cssItem) {
+ result.push(parse(cssItem));
+ });
+ } else {
+ result.push(parse(css));
+ }
+ $css.add(result);
+ $log.debug('$css.bind(): Added', result);
+ $scope.$on('$destroy', function () {
+ $css.remove(result);
+ $log.debug('$css.bind(): Removed', result);
+ });
+ };
+
+ /**
+ * Add: adds stylesheets to scope
+ **/
+ $css.add = function (stylesheets, callback) {
+ if (!stylesheets) {
+ return $log.error('No stylesheets provided');
+ }
+ if (!angular.isArray(stylesheets)) {
+ stylesheets = [stylesheets];
+ }
+ angular.forEach(stylesheets, function(stylesheet) {
+ stylesheet = parse(stylesheet);
+ // Avoid adding duplicate stylesheets
+ if (stylesheet.href && !$filter('filter')($rootScope.stylesheets, { href: stylesheet.href }).length) {
+ // Bust Cache feature
+ bustCache(stylesheet)
+ // Media Query add support check
+ if (isMediaQuery(stylesheet)) {
+ addViaMediaQuery(stylesheet);
+ }
+ else {
+ $rootScope.stylesheets.push(stylesheet);
+ }
+ $log.debug('$css.add(): ' + stylesheet.href);
+ }
+ });
+ // Broadcasts custom event for css add
+ $rootScope.$broadcast('$cssAdd', stylesheets, $rootScope.stylesheets);
+ };
+
+ /**
+ * Remove: removes stylesheets from scope
+ **/
+ $css.remove = function (stylesheets, callback) {
+ if (!stylesheets) {
+ return $log.error('No stylesheets provided');
+ }
+ if (!angular.isArray(stylesheets)) {
+ stylesheets = [stylesheets];
+ }
+ // Only proceed based on persist setting
+ stylesheets = $filter('filter')(stylesheets, function (stylesheet) {
+ return !stylesheet.persist;
+ });
+ angular.forEach(stylesheets, function(stylesheet) {
+ stylesheet = parse(stylesheet);
+ // Get index of current item to be removed based on href
+ var index = $rootScope.stylesheets.indexOf($filter('filter')($rootScope.stylesheets, {
+ href: stylesheet.href
+ })[0]);
+ // Remove stylesheet from scope (if found)
+ if (index !== -1) {
+ $rootScope.stylesheets.splice(index, 1);
+ }
+ // Remove stylesheet via media query
+ removeViaMediaQuery(stylesheet);
+ $log.debug('$css.remove(): ' + stylesheet.href);
+ });
+ // Broadcasts custom event for css remove
+ $rootScope.$broadcast('$cssRemove', stylesheets, $rootScope.stylesheets);
+ };
+
+ /**
+ * Remove All: removes all style tags from the DOM
+ **/
+ $css.removeAll = function () {
+ // Remove all stylesheets from scope
+ if ($rootScope && $rootScope.hasOwnProperty('stylesheets')) {
+ $rootScope.stylesheets.length = 0;
+ }
+ $log.debug('all stylesheets removed');
+ };
+
+ // Preload all stylesheets
+ $css.preload();
+
+ return $css;
+
+ }];
+
+ }]);
+
+ /**
+ * Links filter - renders the stylesheets array in html format
+ **/
+ angularCSS.filter('$cssLinks', function () {
+ return function (stylesheets) {
+ if (!stylesheets || !angular.isArray(stylesheets)) {
+ return stylesheets;
+ }
+ var result = '';
+ angular.forEach(stylesheets, function (stylesheet) {
+ result += '\n\n';
+ });
+ return result;
+ }
+ });
+
+ /**
+ * Run - auto instantiate the $css provider by injecting it in the run phase of this module
+ **/
+ angularCSS.run(['$css', function ($css) { } ]);
+
+ /**
+ * AngularJS hack - This way we can get and decorate all custom directives
+ * in order to broadcast a custom $directiveAdd event
+ **/
+ var $directives = [];
+ var originalModule = angular.module;
+ angular.module = function () {
+ var module = originalModule.apply(this, arguments);
+ var originalDirective = module.directive;
+ module.directive = function(directiveName, directiveFactory) {
+ var originalDirectiveFactory = angular.isFunction(directiveFactory) ?
+ directiveFactory : directiveFactory[directiveFactory.length - 1];
+ try {
+ var directive = angular.copy(originalDirectiveFactory)();
+ directive.directiveName = directiveName;
+ if (directive.hasOwnProperty('css')) {
+ $directives.push(directive);
+ }
+ } catch (e) { }
+ return originalDirective.apply(this, arguments);
+ };
+ module.config(['$provide','$injector', function ($provide, $injector) {
+ angular.forEach($directives, function ($directive) {
+ var dirProvider = $directive.directiveName + 'Directive';
+ if ($injector.has(dirProvider)) {
+ $provide.decorator(dirProvider, ['$delegate', '$rootScope', '$timeout', function ($delegate, $rootScope, $timeout) {
+ var directive = $delegate[0];
+ var compile = directive.compile;
+ if (directive.css) {
+ $directive.css = directive.css;
+ }
+ directive.compile = function() {
+ var link = compile ? compile.apply(this, arguments): false;
+ return function(scope) {
+ var linkArgs = arguments;
+ $timeout(function () {
+ if (link) {
+ link.apply(this, linkArgs);
+ }
+ });
+ $rootScope.$broadcast('$directiveAdd', directive, scope);
+ };
+ };
+ return $delegate;
+ }]);
+ }
+ });
+ }]);
+ return module;
+ };
+ /* End of hack */
+
+})(angular);
diff --git a/third_party/ui/bower_components/angular-css/angular-css.min.js b/third_party/ui/bower_components/angular-css/angular-css.min.js
new file mode 100644
index 0000000000..07f2f84a02
--- /dev/null
+++ b/third_party/ui/bower_components/angular-css/angular-css.min.js
@@ -0,0 +1 @@
+/*! angular-css 1.0.7 | Copyright (c) 2015 DOOR3, Alex Castillo | MIT License */"use strict";!function(a){var b=a.module("door3.css",[]);b.config(["$logProvider",function(a){a.debugEnabled(!1)}]),b.provider("$css",[function(){var b=this.defaults={element:"link",rel:"stylesheet",type:"text/css",container:"head",method:"append",weight:0};this.$get=["$rootScope","$injector","$q","$window","$timeout","$compile","$http","$filter","$log",function(d,e,f,g,h,i,j,k,l){function m(a,b,c){c&&b.hasOwnProperty("css")&&w.bind([q(b.css)],c)}function n(a,b,c){c&&(w.remove(w.getFromRoute(c).concat(D)),D.length=0),b&&w.add(w.getFromRoute(b))}function o(a,b,c,d){d&&(w.remove(w.getFromState(d).concat(D)),D.length=0),b&&w.add(w.getFromState(b))}function p(b){a.isDefined(B.breakpoints)&&(b.breakpoint in B.breakpoints&&(b.media=B.breakpoints[b.breakpoint]),delete b.breakpoints)}function q(b){return b?(a.isFunction(b)&&(b=a.copy(e.invoke(b))),a.isString(b)&&(b=a.extend({href:b},B)),a.isArray(b)&&a.isString(b[0])&&a.forEach(b,function(c){b=a.extend({href:c},B)}),a.isObject(b)&&!a.isArray(b)&&(b=a.extend(b,B)),a.isArray(b)&&a.isObject(b[0])&&a.forEach(b,function(c){b=a.extend(c,B)}),p(b),b):void 0}function r(a){if(!a)return l.error("No stylesheets provided");var b="?cache=";-1===a.href.indexOf(b)&&(a.href=a.href+(a.bustCache?b+(new Date).getTime():""))}function s(a,b){return a&&b?k("filter")(a,function(a){return a[b]}):l.error("filterBy: missing array or property")}function t(a){return a?(y[a.href]=g.matchMedia(a.media),z[a.href]=function(b){h(function(){if(b.matches)d.stylesheets.push(a);else{var c=d.stylesheets.indexOf(k("filter")(d.stylesheets,{href:a.href})[0]);-1!==c&&d.stylesheets.splice(c,1)}})},y[a.href].addListener(z[a.href]),void z[a.href](y[a.href])):l.error("No stylesheet provided")}function u(b){return b?void(d&&a.isDefined(y)&&y[b.href]&&a.isDefined(z)&&y[b.href].removeListener(z[b.href])):l.error("No stylesheet provided")}function v(a){return a?!(!a.media||-1!==A.indexOf(a.media)||!g.matchMedia):l.error("No stylesheet provided")}var w={},x='',y={},z={},A=["print"],B=a.extend({},b),C=a.element(document.querySelector?document.querySelector(B.container):document.getElementsByTagName(B.container)[0]),D=[];return a.forEach(c,function(a,b){a.hasOwnProperty("css")&&(c[b]=q(a.css))}),d.stylesheets=[],C[B.method](i(x)(d)),d.$on("$directiveAdd",m),d.$on("$routeChangeSuccess",n),d.$on("$stateChangeSuccess",o),w.getFromRoute=function(b){if(!b)return l.error("Get From Route: No route provided");var c=null,d=[];return b.$$route&&b.$$route.css?c=b.$$route.css:b.css&&(c=b.css),c&&(a.isArray(c)?a.forEach(c,function(b){a.isFunction(b)&&D.push(q(b)),d.push(q(b))}):(a.isFunction(c)&&D.push(q(c)),d.push(q(c)))),d},w.getFromRoutes=function(b){if(!b)return l.error("Get From Routes: No routes provided");var c=[];return a.forEach(b,function(a){var b=w.getFromRoute(a);b.length&&c.push(b[0])}),c},w.getFromState=function(b){if(!b)return l.error("Get From State: No state provided");var c=[];return a.isDefined(b.views)&&a.forEach(b.views,function(b){b.css&&(a.isFunction(b.css)&&D.push(q(b.css)),c.push(q(b.css)))}),a.isDefined(b.children)&&a.forEach(b.children,function(b){b.css&&(a.isFunction(b.css)&&D.push(q(b.css)),c.push(q(b.css))),a.isDefined(b.children)&&a.forEach(b.children,function(b){b.css&&(a.isFunction(b.css)&&D.push(q(b.css)),c.push(q(b.css)))})}),a.isDefined(b.css)&&(a.isArray(b.css)?a.forEach(b.css,function(b){a.isFunction(b)&&D.push(q(b)),c.push(q(b))}):(a.isFunction(b.css)&&D.push(q(b.css)),c.push(q(b.css)))),c},w.getFromStates=function(b){if(!b)return l.error("Get From States: No states provided");var c=[];return a.forEach(b,function(b){var d=w.getFromState(b);a.isArray(d)?a.forEach(d,function(a){c.push(a)}):c.push(d)}),c},w.preload=function(b,d){b||(b=[],c.length&&Array.prototype.push.apply(b,c),e.has("$route")&&Array.prototype.push.apply(b,w.getFromRoutes(e.get("$route").routes)),e.has("$state")&&Array.prototype.push.apply(b,w.getFromStates(e.get("$state").get())),b=s(b,"preload")),a.isArray(b)||(b=[b]);var g=[];a.forEach(b,function(a,c){a=b[c]=q(a),g.push(j.get(a.href).error(function(){l.error("AngularCSS: Incorrect path for "+a.href)}))}),a.isFunction(d)&&f.all(g).then(function(){d(b)})},w.bind=function(b,c){if(!b||!c)return l.error("No scope or stylesheets provided");var d=[];a.isArray(b)?a.forEach(b,function(a){d.push(q(a))}):d.push(q(b)),w.add(d),l.debug("$css.bind(): Added",d),c.$on("$destroy",function(){w.remove(d),l.debug("$css.bind(): Removed",d)})},w.add=function(b){return b?(a.isArray(b)||(b=[b]),a.forEach(b,function(a){a=q(a),a.href&&!k("filter")(d.stylesheets,{href:a.href}).length&&(r(a),v(a)?t(a):d.stylesheets.push(a),l.debug("$css.add(): "+a.href))}),void d.$broadcast("$cssAdd",b,d.stylesheets)):l.error("No stylesheets provided")},w.remove=function(b){return b?(a.isArray(b)||(b=[b]),b=k("filter")(b,function(a){return!a.persist}),a.forEach(b,function(a){a=q(a);var b=d.stylesheets.indexOf(k("filter")(d.stylesheets,{href:a.href})[0]);-1!==b&&d.stylesheets.splice(b,1),u(a),l.debug("$css.remove(): "+a.href)}),void d.$broadcast("$cssRemove",b,d.stylesheets)):l.error("No stylesheets provided")},w.removeAll=function(){d&&d.hasOwnProperty("stylesheets")&&(d.stylesheets.length=0),l.debug("all stylesheets removed")},w.preload(),w}]}]),b.filter("$cssLinks",function(){return function(b){if(!b||!a.isArray(b))return b;var c="";return a.forEach(b,function(a){c+='\n\n"}),c}}),b.run(["$css",function(){}]);var c=[],d=a.module;a.module=function(){var b=d.apply(this,arguments),e=b.directive;return b.directive=function(b,d){var f=a.isFunction(d)?d:d[d.length-1];try{var g=a.copy(f)();g.directiveName=b,g.hasOwnProperty("css")&&c.push(g)}catch(h){}return e.apply(this,arguments)},b.config(["$provide","$injector",function(b,d){a.forEach(c,function(a){var c=a.directiveName+"Directive";d.has(c)&&b.decorator(c,["$delegate","$rootScope","$timeout",function(b,c,d){var e=b[0],f=e.compile;return e.css&&(a.css=e.css),e.compile=function(){var a=f?f.apply(this,arguments):!1;return function(b){var f=arguments;d(function(){a&&a.apply(this,f)}),c.$broadcast("$directiveAdd",e,b)}},b}])})}]),b}}(angular);
\ No newline at end of file
diff --git a/third_party/ui/bower_components/angular-css/bower.json b/third_party/ui/bower_components/angular-css/bower.json
new file mode 100644
index 0000000000..0bb696abd9
--- /dev/null
+++ b/third_party/ui/bower_components/angular-css/bower.json
@@ -0,0 +1,36 @@
+{
+ "name": "angular-css",
+ "main": "./angular-css.js",
+ "version": "1.0.7",
+ "homepage": "http://door3.github.io/angular-css",
+ "authors": [
+ "Alex Castillo "
+ ],
+ "description": "CSS on-demand for AngularJS",
+ "keywords": [
+ "angularjs",
+ "ng-route",
+ "ui-router",
+ "css"
+ ],
+ "dependencies": {
+ "angular": ">= 1.3.0"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git://github.com/door3/angular-css"
+ },
+ "license": "MIT",
+ "ignore": [
+ "**/.*",
+ "node_modules",
+ "bower_components",
+ "component.json",
+ "package.json",
+ "lib",
+ "config",
+ "demo",
+ "test",
+ "tests"
+ ]
+}
\ No newline at end of file
diff --git a/third_party/ui/bower_components/angular-filter/.bower.json b/third_party/ui/bower_components/angular-filter/.bower.json
new file mode 100644
index 0000000000..d6522bed94
--- /dev/null
+++ b/third_party/ui/bower_components/angular-filter/.bower.json
@@ -0,0 +1,39 @@
+{
+ "name": "angular-filter",
+ "version": "0.5.4",
+ "main": "dist/angular-filter.js",
+ "description": "Bunch of useful filters for angularJS(with no external dependencies!)",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/a8m/angular-filter.git"
+ },
+ "dependencies": {
+ "angular": "*"
+ },
+ "devDependencies": {
+ "angular-mocks": "*"
+ },
+ "ignore": [
+ "node_modules",
+ "bower_components",
+ "package.json",
+ "lib",
+ "test",
+ "src",
+ "Gruntfile.js",
+ ".gitignore",
+ "README.md",
+ "travis.yml",
+ ".bowercc"
+ ],
+ "homepage": "https://github.com/a8m/angular-filter",
+ "_release": "0.5.4",
+ "_resolution": {
+ "type": "version",
+ "tag": "v0.5.4",
+ "commit": "13c430b3df656e3d0a3a78953d63bbce93f0ffe9"
+ },
+ "_source": "git://github.com/a8m/angular-filter.git",
+ "_target": "~0.5.1",
+ "_originalSource": "angular-filter"
+}
\ No newline at end of file
diff --git a/third_party/ui/bower_components/angular-filter/.bowerrc b/third_party/ui/bower_components/angular-filter/.bowerrc
new file mode 100644
index 0000000000..69fad35801
--- /dev/null
+++ b/third_party/ui/bower_components/angular-filter/.bowerrc
@@ -0,0 +1,3 @@
+{
+ "directory": "bower_components"
+}
diff --git a/third_party/ui/bower_components/angular-filter/.travis.yml b/third_party/ui/bower_components/angular-filter/.travis.yml
new file mode 100644
index 0000000000..2c573b8cba
--- /dev/null
+++ b/third_party/ui/bower_components/angular-filter/.travis.yml
@@ -0,0 +1,8 @@
+language: node_js
+node_js:
+ - '0.10'
+before_script:
+ - export DISPLAY=:99.0
+ - sh -e /etc/init.d/xvfb start
+ - 'npm install -g bower grunt-cli'
+ - 'bower install --config.interactive=false'
diff --git a/third_party/ui/bower_components/angular-filter/bower.json b/third_party/ui/bower_components/angular-filter/bower.json
new file mode 100644
index 0000000000..4e3e7dedc9
--- /dev/null
+++ b/third_party/ui/bower_components/angular-filter/bower.json
@@ -0,0 +1,29 @@
+{
+ "name": "angular-filter",
+ "version": "0.5.4",
+ "main": "dist/angular-filter.js",
+ "description": "Bunch of useful filters for angularJS(with no external dependencies!)",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/a8m/angular-filter.git"
+ },
+ "dependencies": {
+ "angular": "*"
+ },
+ "devDependencies": {
+ "angular-mocks": "*"
+ },
+ "ignore": [
+ "node_modules",
+ "bower_components",
+ "package.json",
+ "lib",
+ "test",
+ "src",
+ "Gruntfile.js",
+ ".gitignore",
+ "README.md",
+ "travis.yml",
+ ".bowercc"
+ ]
+}
diff --git a/third_party/ui/bower_components/angular-filter/dist/angular-filter.js b/third_party/ui/bower_components/angular-filter/dist/angular-filter.js
new file mode 100644
index 0000000000..7a6c6fd26f
--- /dev/null
+++ b/third_party/ui/bower_components/angular-filter/dist/angular-filter.js
@@ -0,0 +1,2239 @@
+/**
+ * Bunch of useful filters for angularJS(with no external dependencies!)
+ * @version v0.5.4 - 2015-02-20 * @link https://github.com/a8m/angular-filter
+ * @author Ariel Mashraki
+ * @license MIT License, http://www.opensource.org/licenses/MIT
+ */
+(function ( window, angular, undefined ) {
+/*jshint globalstrict:true*/
+'use strict';
+
+var isDefined = angular.isDefined,
+ isUndefined = angular.isUndefined,
+ isFunction = angular.isFunction,
+ isString = angular.isString,
+ isNumber = angular.isNumber,
+ isObject = angular.isObject,
+ isArray = angular.isArray,
+ forEach = angular.forEach,
+ extend = angular.extend,
+ copy = angular.copy,
+ equals = angular.equals;
+
+
+/**
+ * @description
+ * get an object and return array of values
+ * @param object
+ * @returns {Array}
+ */
+function toArray(object) {
+ return isArray(object) ? object :
+ Object.keys(object).map(function(key) {
+ return object[key];
+ });
+}
+
+/**
+ * @param value
+ * @returns {boolean}
+ */
+function isNull(value) {
+ return value === null;
+}
+
+/**
+ * @description
+ * return if object contains partial object
+ * @param partial{object}
+ * @param object{object}
+ * @returns {boolean}
+ */
+function objectContains(partial, object) {
+ var keys = Object.keys(partial);
+
+ return keys.map(function(el) {
+ return (object[el] !== undefined) && (object[el] == partial[el]);
+ }).indexOf(false) == -1;
+
+}
+
+/**
+ * @description
+ * search for approximate pattern in string
+ * @param word
+ * @param pattern
+ * @returns {*}
+ */
+function hasApproxPattern(word, pattern) {
+ if(pattern === '')
+ return word;
+
+ var index = word.indexOf(pattern.charAt(0));
+
+ if(index === -1)
+ return false;
+
+ return hasApproxPattern(word.substr(index+1), pattern.substr(1))
+}
+
+/**
+ * @description
+ * return the first n element of an array,
+ * if expression provided, is returns as long the expression return truthy
+ * @param array
+ * @param n {number}
+ * @param expression {$parse}
+ * @return array or single object
+ */
+function getFirstMatches(array, n, expression) {
+ var count = 0;
+
+ return array.filter(function(elm) {
+ var rest = isDefined(expression) ? (count < n && expression(elm)) : count < n;
+ count = rest ? count+1 : count;
+
+ return rest;
+ });
+}
+/**
+ * Polyfill to ECMA6 String.prototype.contains
+ */
+if (!String.prototype.contains) {
+ String.prototype.contains = function() {
+ return String.prototype.indexOf.apply(this, arguments) !== -1;
+ };
+}
+
+/**
+ * @param num {Number}
+ * @param decimal {Number}
+ * @param $math
+ * @returns {Number}
+ */
+function convertToDecimal(num, decimal, $math){
+ return $math.round(num * $math.pow(10,decimal)) / ($math.pow(10,decimal));
+}
+
+/**
+ * @description
+ * Get an object, and return an array composed of it's properties names(nested too).
+ * @param obj {Object}
+ * @param stack {Array}
+ * @param parent {String}
+ * @returns {Array}
+ * @example
+ * parseKeys({ a:1, b: { c:2, d: { e: 3 } } }) ==> ["a", "b.c", "b.d.e"]
+ */
+function deepKeys(obj, stack, parent) {
+ stack = stack || [];
+ var keys = Object.keys(obj);
+
+ keys.forEach(function(el) {
+ //if it's a nested object
+ if(isObject(obj[el]) && !isArray(obj[el])) {
+ //concatenate the new parent if exist
+ var p = parent ? parent + '.' + el : parent;
+ deepKeys(obj[el], stack, p || el);
+ } else {
+ //create and save the key
+ var key = parent ? parent + '.' + el : el;
+ stack.push(key)
+ }
+ });
+ return stack
+}
+
+/**
+ * @description
+ * Test if given object is a Scope instance
+ * @param obj
+ * @returns {Boolean}
+ */
+function isScope(obj) {
+ return obj && obj.$evalAsync && obj.$watch;
+}
+
+/**
+ * @ngdoc filter
+ * @name a8m.angular
+ * @kind function
+ *
+ * @description
+ * reference to angular function
+ */
+
+angular.module('a8m.angular', [])
+
+ .filter('isUndefined', function () {
+ return function (input) {
+ return angular.isUndefined(input);
+ }
+ })
+ .filter('isDefined', function() {
+ return function (input) {
+ return angular.isDefined(input);
+ }
+ })
+ .filter('isFunction', function() {
+ return function (input) {
+ return angular.isFunction(input);
+ }
+ })
+ .filter('isString', function() {
+ return function (input) {
+ return angular.isString(input)
+ }
+ })
+ .filter('isNumber', function() {
+ return function (input) {
+ return angular.isNumber(input);
+ }
+ })
+ .filter('isArray', function() {
+ return function (input) {
+ return angular.isArray(input);
+ }
+ })
+ .filter('isObject', function() {
+ return function (input) {
+ return angular.isObject(input);
+ }
+ })
+ .filter('isEqual', function() {
+ return function (o1, o2) {
+ return angular.equals(o1, o2);
+ }
+ });
+
+/**
+ * @ngdoc filter
+ * @name a8m.conditions
+ * @kind function
+ *
+ * @description
+ * reference to math conditions
+ */
+ angular.module('a8m.conditions', [])
+
+ .filter({
+ isGreaterThan : isGreaterThanFilter,
+ '>' : isGreaterThanFilter,
+
+ isGreaterThanOrEqualTo : isGreaterThanOrEqualToFilter,
+ '>=' : isGreaterThanOrEqualToFilter,
+
+ isLessThan : isLessThanFilter,
+ '<' : isLessThanFilter,
+
+ isLessThanOrEqualTo : isLessThanOrEqualToFilter,
+ '<=' : isLessThanOrEqualToFilter,
+
+ isEqualTo : isEqualToFilter,
+ '==' : isEqualToFilter,
+
+ isNotEqualTo : isNotEqualToFilter,
+ '!=' : isNotEqualToFilter,
+
+ isIdenticalTo : isIdenticalToFilter,
+ '===' : isIdenticalToFilter,
+
+ isNotIdenticalTo : isNotIdenticalToFilter,
+ '!==' : isNotIdenticalToFilter
+ });
+
+ function isGreaterThanFilter() {
+ return function (input, check) {
+ return input > check;
+ };
+ }
+
+ function isGreaterThanOrEqualToFilter() {
+ return function (input, check) {
+ return input >= check;
+ };
+ }
+
+ function isLessThanFilter() {
+ return function (input, check) {
+ return input < check;
+ };
+ }
+
+ function isLessThanOrEqualToFilter() {
+ return function (input, check) {
+ return input <= check;
+ };
+ }
+
+ function isEqualToFilter() {
+ return function (input, check) {
+ return input == check;
+ };
+ }
+
+ function isNotEqualToFilter() {
+ return function (input, check) {
+ return input != check;
+ };
+ }
+
+ function isIdenticalToFilter() {
+ return function (input, check) {
+ return input === check;
+ };
+ }
+
+ function isNotIdenticalToFilter() {
+ return function (input, check) {
+ return input !== check;
+ };
+ }
+/**
+ * @ngdoc filter
+ * @name isNull
+ * @kind function
+ *
+ * @description
+ * checks if value is null or not
+ * @return Boolean
+ */
+angular.module('a8m.is-null', [])
+ .filter('isNull', function () {
+ return function(input) {
+ return isNull(input);
+ }
+ });
+
+/**
+ * @ngdoc filter
+ * @name after-where
+ * @kind function
+ *
+ * @description
+ * get a collection and properties object, and returns all of the items
+ * in the collection after the first that found with the given properties.
+ *
+ */
+angular.module('a8m.after-where', [])
+ .filter('afterWhere', function() {
+ return function (collection, object) {
+
+ collection = (isObject(collection))
+ ? toArray(collection)
+ : collection;
+
+ if(!isArray(collection) || isUndefined(object))
+ return collection;
+
+ var index = collection.map( function( elm ) {
+ return objectContains(object, elm);
+ }).indexOf( true );
+
+ return collection.slice((index === -1) ? 0 : index);
+ }
+ });
+
+/**
+ * @ngdoc filter
+ * @name after
+ * @kind function
+ *
+ * @description
+ * get a collection and specified count, and returns all of the items
+ * in the collection after the specified count.
+ *
+ */
+
+angular.module('a8m.after', [])
+ .filter('after', function() {
+ return function (collection, count) {
+ collection = (isObject(collection))
+ ? toArray(collection)
+ : collection;
+
+ return (isArray(collection))
+ ? collection.slice(count)
+ : collection;
+ }
+ });
+
+/**
+ * @ngdoc filter
+ * @name before-where
+ * @kind function
+ *
+ * @description
+ * get a collection and properties object, and returns all of the items
+ * in the collection before the first that found with the given properties.
+ */
+angular.module('a8m.before-where', [])
+ .filter('beforeWhere', function() {
+ return function (collection, object) {
+
+ collection = (isObject(collection))
+ ? toArray(collection)
+ : collection;
+
+ if(!isArray(collection) || isUndefined(object))
+ return collection;
+
+ var index = collection.map( function( elm ) {
+ return objectContains(object, elm);
+ }).indexOf( true );
+
+ return collection.slice(0, (index === -1) ? collection.length : ++index);
+ }
+ });
+
+/**
+ * @ngdoc filter
+ * @name before
+ * @kind function
+ *
+ * @description
+ * get a collection and specified count, and returns all of the items
+ * in the collection before the specified count.
+ */
+angular.module('a8m.before', [])
+ .filter('before', function() {
+ return function (collection, count) {
+ collection = (isObject(collection))
+ ? toArray(collection)
+ : collection;
+
+ return (isArray(collection))
+ ? collection.slice(0, (!count) ? count : --count)
+ : collection;
+ }
+ });
+
+/**
+ * @ngdoc filter
+ * @name concat
+ * @kind function
+ *
+ * @description
+ * get (array/object, object/array) and return merged collection
+ */
+angular.module('a8m.concat', [])
+ //TODO(Ariel):unique option ? or use unique filter to filter result
+ .filter('concat', [function () {
+ return function (collection, joined) {
+
+ if (isUndefined(joined)) {
+ return collection;
+ }
+ if (isArray(collection)) {
+ return (isObject(joined))
+ ? collection.concat(toArray(joined))
+ : collection.concat(joined);
+ }
+
+ if (isObject(collection)) {
+ var array = toArray(collection);
+ return (isObject(joined))
+ ? array.concat(toArray(joined))
+ : array.concat(joined);
+ }
+ return collection;
+ };
+ }
+]);
+
+/**
+ * @ngdoc filter
+ * @name contains
+ * @kind function
+ *
+ * @description
+ * Checks if given expression is present in one or more object in the collection
+ */
+angular.module('a8m.contains', [])
+ .filter({
+ contains: ['$parse', containsFilter],
+ some: ['$parse', containsFilter]
+ });
+
+function containsFilter( $parse ) {
+ return function (collection, expression) {
+
+ collection = (isObject(collection)) ? toArray(collection) : collection;
+
+ if(!isArray(collection) || isUndefined(expression)) {
+ return false;
+ }
+
+ return collection.some(function(elm) {
+ return (isObject(elm) || isFunction(expression))
+ ? $parse(expression)(elm)
+ : elm === expression;
+ });
+
+ }
+ }
+
+/**
+ * @ngdoc filter
+ * @name countBy
+ * @kind function
+ *
+ * @description
+ * Sorts a list into groups and returns a count for the number of objects in each group.
+ */
+
+angular.module('a8m.count-by', [])
+
+ .filter('countBy', [ '$parse', function ( $parse ) {
+ return function (collection, property) {
+
+ var result = {},
+ get = $parse(property),
+ prop;
+
+ collection = (isObject(collection)) ? toArray(collection) : collection;
+
+ if(!isArray(collection) || isUndefined(property)) {
+ return collection;
+ }
+
+ collection.forEach( function( elm ) {
+ prop = get(elm);
+
+ if(!result[prop]) {
+ result[prop] = 0;
+ }
+
+ result[prop]++;
+ });
+
+ return result;
+ }
+ }]);
+
+/**
+ * @ngdoc filter
+ * @name defaults
+ * @kind function
+ *
+ * @description
+ * defaultsFilter allows to specify a default fallback value for properties that resolve to undefined.
+ */
+angular.module('a8m.defaults', [])
+ .filter('defaults', ['$parse', function( $parse ) {
+ return function(collection, defaults) {
+
+ collection = (isObject(collection)) ? toArray(collection) : collection;
+
+ if(!isArray(collection) || !isObject(defaults)) {
+ return collection;
+ }
+
+ var keys = deepKeys(defaults);
+
+ collection.forEach(function(elm) {
+ //loop through all the keys
+ keys.forEach(function(key) {
+ var getter = $parse(key);
+ var setter = getter.assign;
+ //if it's not exist
+ if(isUndefined(getter(elm))) {
+ //get from defaults, and set to the returned object
+ setter(elm, getter(defaults))
+ }
+ });
+ });
+
+ return collection;
+ }
+ }]);
+/**
+ * @ngdoc filter
+ * @name every
+ * @kind function
+ *
+ * @description
+ * Checks if given expression is present in all members in the collection
+ *
+ */
+angular.module('a8m.every', [])
+ .filter('every', ['$parse', function($parse) {
+ return function (collection, expression) {
+ collection = (isObject(collection)) ? toArray(collection) : collection;
+
+ if(!isArray(collection) || isUndefined(expression)) {
+ return true;
+ }
+
+ return collection.every( function(elm) {
+ return (isObject(elm) || isFunction(expression))
+ ? $parse(expression)(elm)
+ : elm === expression;
+ });
+ }
+ }]);
+
+/**
+ * @ngdoc filter
+ * @name filterBy
+ * @kind function
+ *
+ * @description
+ * filter by specific properties, avoid the rest
+ */
+angular.module('a8m.filter-by', [])
+ .filter('filterBy', ['$parse', function( $parse ) {
+ return function(collection, properties, search) {
+
+ var comparator;
+
+ search = (isString(search) || isNumber(search)) ?
+ String(search).toLowerCase() : undefined;
+
+ collection = (isObject(collection)) ? toArray(collection) : collection;
+
+ if(!isArray(collection) || isUndefined(search)) {
+ return collection;
+ }
+
+ return collection.filter(function(elm) {
+ return properties.some(function(prop) {
+
+ /**
+ * check if there is concatenate properties
+ * example:
+ * object: { first: 'foo', last:'bar' }
+ * filterBy: ['first + last'] => search by full name(i.e 'foo bar')
+ */
+ if(!~prop.indexOf('+')) {
+ comparator = $parse(prop)(elm)
+ } else {
+ var propList = prop.replace(new RegExp('\\s', 'g'), '').split('+');
+ comparator = propList.reduce(function(prev, cur, index) {
+ return (index === 1) ? $parse(prev)(elm) + ' ' + $parse(cur)(elm) :
+ prev + ' ' + $parse(cur)(elm);
+ });
+ }
+
+ return (isString(comparator) || isNumber(comparator))
+ ? String(comparator).toLowerCase().contains(search)
+ : false;
+ });
+ });
+ }
+ }]);
+
+/**
+ * @ngdoc filter
+ * @name first
+ * @kind function
+ *
+ * @description
+ * Gets the first element or first n elements of an array
+ * if callback is provided, is returns as long the callback return truthy
+ */
+angular.module('a8m.first', [])
+ .filter('first', ['$parse', function( $parse ) {
+ return function(collection) {
+
+ var n
+ , getter
+ , args;
+
+ collection = (isObject(collection))
+ ? toArray(collection)
+ : collection;
+
+ if(!isArray(collection)) {
+ return collection;
+ }
+
+ args = Array.prototype.slice.call(arguments, 1);
+ n = (isNumber(args[0])) ? args[0] : 1;
+ getter = (!isNumber(args[0])) ? args[0] : (!isNumber(args[1])) ? args[1] : undefined;
+
+ return (args.length) ? getFirstMatches(collection, n,(getter) ? $parse(getter) : getter) :
+ collection[0];
+ }
+ }]);
+
+/**
+ * @ngdoc filter
+ * @name flatten
+ * @kind function
+ *
+ * @description
+ * Flattens a nested array (the nesting can be to any depth).
+ * If you pass shallow, the array will only be flattened a single level
+ */
+angular.module('a8m.flatten', [])
+ .filter('flatten', function () {
+ return function(collection, shallow) {
+
+ shallow = shallow || false;
+ collection = (isObject(collection))
+ ? toArray(collection)
+ : collection;
+
+ if(!isArray(collection)) {
+ return collection;
+ }
+
+ return !shallow
+ ? flatten(collection, 0)
+ : [].concat.apply([], collection);
+ }
+ });
+
+/**
+ * flatten nested array (the nesting can be to any depth).
+ * @param array {Array}
+ * @param i {int}
+ * @returns {Array}
+ * @private
+ */
+function flatten(array, i) {
+ i = i || 0;
+
+ if(i >= array.length)
+ return array;
+
+ if(isArray(array[i])) {
+ return flatten(array.slice(0,i)
+ .concat(array[i], array.slice(i+1)), i);
+ }
+ return flatten(array, i+1);
+}
+
+/**
+ * @ngdoc filter
+ * @name fuzzyByKey
+ * @kind function
+ *
+ * @description
+ * fuzzy string searching by key
+ */
+angular.module('a8m.fuzzy-by', [])
+ .filter('fuzzyBy', ['$parse', function ( $parse ) {
+ return function (collection, property, search, csensitive) {
+
+ var sensitive = csensitive || false,
+ prop, getter;
+
+ collection = (isObject(collection)) ? toArray(collection) : collection;
+
+ if(!isArray(collection) || isUndefined(property)
+ || isUndefined(search)) {
+ return collection;
+ }
+
+ getter = $parse(property);
+
+ return collection.filter(function(elm) {
+
+ prop = getter(elm);
+ if(!isString(prop)) {
+ return false;
+ }
+
+ prop = (sensitive) ? prop : prop.toLowerCase();
+ search = (sensitive) ? search : search.toLowerCase();
+
+ return hasApproxPattern(prop, search) !== false
+ })
+ }
+
+ }]);
+/**
+ * @ngdoc filter
+ * @name fuzzy
+ * @kind function
+ *
+ * @description
+ * fuzzy string searching for array of strings, objects
+ */
+angular.module('a8m.fuzzy', [])
+ .filter('fuzzy', function () {
+ return function (collection, search, csensitive) {
+
+ var sensitive = csensitive || false;
+
+ collection = (isObject(collection)) ? toArray(collection) : collection;
+
+ if(!isArray(collection) || isUndefined(search)) {
+ return collection;
+ }
+
+ search = (sensitive) ? search : search.toLowerCase();
+
+ return collection.filter(function(elm) {
+
+ if(isString(elm)) {
+ elm = (sensitive) ? elm : elm.toLowerCase();
+ return hasApproxPattern(elm, search) !== false
+ }
+
+ return (isObject(elm)) ? _hasApproximateKey(elm, search) : false;
+
+ });
+
+ /**
+ * checks if object has key{string} that match
+ * to fuzzy search pattern
+ * @param object
+ * @param search
+ * @returns {boolean}
+ * @private
+ */
+ function _hasApproximateKey(object, search) {
+ var properties = Object.keys(object),
+ prop, flag;
+ return 0 < properties.filter(function (elm) {
+ prop = object[elm];
+
+ //avoid iteration if we found some key that equal[performance]
+ if(flag) return true;
+
+ if (isString(prop)) {
+ prop = (sensitive) ? prop : prop.toLowerCase();
+ return flag = (hasApproxPattern(prop, search) !== false);
+ }
+
+ return false;
+
+ }).length;
+ }
+
+ }
+ });
+
+/**
+ * @ngdoc filter
+ * @name groupBy
+ * @kind function
+ *
+ * @description
+ * Create an object composed of keys generated from the result of running each element of a collection,
+ * each key is an array of the elements.
+ */
+
+angular.module('a8m.group-by', [ 'a8m.filter-watcher' ])
+
+ .filter('groupBy', [ '$parse', 'filterWatcher', function ( $parse, filterWatcher ) {
+ return function (collection, property) {
+
+ if(!isObject(collection) || isUndefined(property)) {
+ return collection;
+ }
+
+ var getterFn = $parse(property);
+
+ return filterWatcher.isMemoized('groupBy', arguments) ||
+ filterWatcher.memoize('groupBy', arguments, this,
+ _groupBy(collection, getterFn));
+
+ /**
+ * groupBy function
+ * @param collection
+ * @param getter
+ * @returns {{}}
+ */
+ function _groupBy(collection, getter) {
+ var result = {};
+ var prop;
+
+ forEach( collection, function( elm ) {
+ prop = getter(elm);
+
+ if(!result[prop]) {
+ result[prop] = [];
+ }
+ result[prop].push(elm);
+ });
+ return result;
+ }
+ }
+ }]);
+
+/**
+ * @ngdoc filter
+ * @name isEmpty
+ * @kind function
+ *
+ * @description
+ * get collection or string and return if it empty
+ */
+angular.module('a8m.is-empty', [])
+ .filter('isEmpty', function () {
+ return function(collection) {
+ return (isObject(collection))
+ ? !toArray(collection).length
+ : !collection.length;
+ }
+ });
+
+/**
+ * @ngdoc filter
+ * @name join
+ * @kind function
+ *
+ * @description
+ * join a collection by a provided delimiter (space by default)
+ */
+angular.module('a8m.join', [])
+ .filter('join', function () {
+ return function (input, delimiter) {
+ if (isUndefined(input) || !isArray(input)) {
+ return input;
+ }
+ if (isUndefined(delimiter)) {
+ delimiter = ' ';
+ }
+
+ return input.join(delimiter);
+ };
+ })
+;
+
+/**
+ * @ngdoc filter
+ * @name last
+ * @kind function
+ *
+ * @description
+ * Gets the last element or last n elements of an array
+ * if callback is provided, is returns as long the callback return truthy
+ */
+angular.module('a8m.last', [])
+ .filter('last', ['$parse', function( $parse ) {
+ return function(collection) {
+ var n
+ , getter
+ , args
+ //cuz reverse change our src collection
+ //and we don't want side effects
+ , reversed = copy(collection);
+
+ reversed = (isObject(reversed))
+ ? toArray(reversed)
+ : reversed;
+
+ if(!isArray(reversed)) {
+ return reversed;
+ }
+
+ args = Array.prototype.slice.call(arguments, 1);
+ n = (isNumber(args[0])) ? args[0] : 1;
+ getter = (!isNumber(args[0])) ? args[0] : (!isNumber(args[1])) ? args[1] : undefined;
+
+ return (args.length)
+ //send reversed collection as arguments, and reverse it back as result
+ ? getFirstMatches(reversed.reverse(), n,(getter) ? $parse(getter) : getter).reverse()
+ //get the last element
+ : reversed[reversed.length-1];
+ }
+ }]);
+
+/**
+ * @ngdoc filter
+ * @name map
+ * @kind function
+ *
+ * @description
+ * Returns a new collection of the results of each expression execution.
+ */
+angular.module('a8m.map', [])
+ .filter('map', ['$parse', function($parse) {
+ return function (collection, expression) {
+
+ collection = (isObject(collection))
+ ? toArray(collection)
+ : collection;
+
+ if(!isArray(collection) || isUndefined(expression)) {
+ return collection;
+ }
+
+ return collection.map(function (elm) {
+ return $parse(expression)(elm);
+ });
+ }
+ }]);
+
+/**
+ * @ngdoc filter
+ * @name omit
+ * @kind function
+ *
+ * @description
+ * filter collection by expression
+ */
+
+angular.module('a8m.omit', [])
+
+ .filter('omit', ['$parse', function($parse) {
+ return function (collection, expression) {
+
+ collection = (isObject(collection))
+ ? toArray(collection)
+ : collection;
+
+ if(!isArray(collection) || isUndefined(expression)) {
+ return collection;
+ }
+
+ return collection.filter(function (elm) {
+ return !($parse(expression)(elm));
+ });
+ }
+ }]);
+
+/**
+ * @ngdoc filter
+ * @name omit
+ * @kind function
+ *
+ * @description
+ * filter collection by expression
+ */
+
+angular.module('a8m.pick', [])
+
+ .filter('pick', ['$parse', function($parse) {
+ return function (collection, expression) {
+
+ collection = (isObject(collection))
+ ? toArray(collection)
+ : collection;
+
+ if(!isArray(collection) || isUndefined(expression)) {
+ return collection;
+ }
+
+ return collection.filter(function (elm) {
+ return $parse(expression)(elm);
+ });
+ }
+ }]);
+
+/**
+ * @ngdoc filter
+ * @name removeWith
+ * @kind function
+ *
+ * @description
+ * get collection and properties object, and removed elements
+ * with this properties
+ */
+
+angular.module('a8m.remove-with', [])
+ .filter('removeWith', function() {
+ return function (collection, object) {
+
+ if(isUndefined(object)) {
+ return collection;
+ }
+ collection = isObject(collection)
+ ? toArray(collection)
+ : collection;
+
+ return collection.filter(function (elm) {
+ return !objectContains(object, elm);
+ });
+ }
+ });
+
+
+/**
+ * @ngdoc filter
+ * @name remove
+ * @kind function
+ *
+ * @description
+ * remove specific members from collection
+ */
+
+angular.module('a8m.remove', [])
+
+ .filter('remove', function () {
+ return function (collection) {
+
+ collection = (isObject(collection)) ? toArray(collection) : collection;
+
+ var args = Array.prototype.slice.call(arguments, 1);
+
+ if(!isArray(collection)) {
+ return collection;
+ }
+
+ return collection.filter( function( member ) {
+ return !args.some(function(nest) {
+ return equals(nest, member);
+ })
+ });
+
+ }
+ });
+
+/**
+ * @ngdoc filter
+ * @name reverse
+ * @kind function
+ *
+ * @description
+ * Reverses a string or collection
+ */
+angular.module('a8m.reverse', [])
+ .filter('reverse',[ function () {
+ return function (input) {
+ input = (isObject(input)) ? toArray(input) : input;
+
+ if(isString(input)) {
+ return input.split('').reverse().join('');
+ }
+
+ return isArray(input)
+ ? input.slice().reverse()
+ : input;
+ }
+ }]);
+
+/**
+ * @ngdoc filter
+ * @name searchField
+ * @kind function
+ *
+ * @description
+ * for each member, join several strings field and add them to
+ * new field called 'searchField' (use for search filtering)
+ */
+angular.module('a8m.search-field', [])
+ .filter('searchField', ['$parse', function ($parse) {
+ return function (collection) {
+
+ var get, field;
+
+ collection = (isObject(collection)) ? toArray(collection) : collection;
+
+ var args = Array.prototype.slice.call(arguments, 1);
+
+ if(!isArray(collection) || !args.length) {
+ return collection;
+ }
+
+ return collection.map(function(member) {
+
+ field = args.map(function(field) {
+ get = $parse(field);
+ return get(member);
+ }).join(' ');
+
+ return extend(member, { searchField: field });
+ });
+ }
+ }]);
+
+/**
+ * @ngdoc filter
+ * @name toArray
+ * @kind function
+ *
+ * @description
+ * Convert objects into stable arrays.
+ * if addKey set to true,the filter also attaches a new property
+ * $key to the value containing the original key that was used in
+ * the object we are iterating over to reference the property
+ */
+angular.module('a8m.to-array', [])
+ .filter('toArray', function() {
+ return function (collection, addKey) {
+
+ if(!isObject(collection)) {
+ return collection;
+ }
+
+ return !addKey
+ ? toArray(collection)
+ : Object.keys(collection).map(function (key) {
+ return extend(collection[key], { $key: key });
+ });
+ }
+ });
+
+/**
+ * @ngdoc filter
+ * @name unique/uniq
+ * @kind function
+ *
+ * @description
+ * get collection and filter duplicate members
+ * if uniqueFilter get a property(nested to) as argument it's
+ * filter by this property as unique identifier
+ */
+
+angular.module('a8m.unique', [])
+ .filter({
+ unique: ['$parse', uniqFilter],
+ uniq: ['$parse', uniqFilter]
+ });
+
+function uniqFilter($parse) {
+ return function (collection, property) {
+
+ collection = (isObject(collection)) ? toArray(collection) : collection;
+
+ if (!isArray(collection)) {
+ return collection;
+ }
+
+ //store all unique identifiers
+ var uniqueItems = [],
+ get = $parse(property);
+
+ return (isUndefined(property))
+ //if it's kind of primitive array
+ ? collection.filter(function (elm, pos, self) {
+ return self.indexOf(elm) === pos;
+ })
+ //else compare with equals
+ : collection.filter(function (elm) {
+ var prop = get(elm);
+ if(some(uniqueItems, prop)) {
+ return false;
+ }
+ uniqueItems.push(prop);
+ return true;
+ });
+
+ //checked if the unique identifier is already exist
+ function some(array, member) {
+ if(isUndefined(member)) {
+ return false;
+ }
+ return array.some(function(el) {
+ return equals(el, member);
+ });
+ }
+ }
+}
+
+/**
+ * @ngdoc filter
+ * @name where
+ * @kind function
+ *
+ * @description
+ * of each element in a collection to the given properties object,
+ * returning an array of all elements that have equivalent property values.
+ *
+ */
+angular.module('a8m.where', [])
+ .filter('where', function() {
+ return function (collection, object) {
+
+ if(isUndefined(object)) {
+ return collection;
+ }
+ collection = (isObject(collection))
+ ? toArray(collection)
+ : collection;
+
+ return collection.filter(function (elm) {
+ return objectContains(object, elm);
+ });
+ }
+ });
+
+/**
+ * @ngdoc filter
+ * @name xor
+ * @kind function
+ *
+ * @description
+ * Exclusive or filter by expression
+ */
+
+angular.module('a8m.xor', [])
+
+ .filter('xor', ['$parse', function($parse) {
+ return function (col1, col2, expression) {
+
+ expression = expression || false;
+
+ col1 = (isObject(col1)) ? toArray(col1) : col1;
+ col2 = (isObject(col2)) ? toArray(col2) : col2;
+
+ if(!isArray(col1) || !isArray(col2)) return col1;
+
+ return col1.concat(col2)
+ .filter(function(elm) {
+ return !(some(elm, col1) && some(elm, col2));
+ });
+
+ function some(el, col) {
+ var getter = $parse(expression);
+ return col.some(function(dElm) {
+ return expression
+ ? equals(getter(dElm), getter(el))
+ : equals(dElm, el);
+ });
+ }
+ }
+ }]);
+
+/**
+ * @ngdoc filter
+ * @name formatBytes
+ * @kind function
+ *
+ * @description
+ * Convert bytes into appropriate display
+ * 1024 bytes => 1 KB
+ */
+angular.module('a8m.math.byteFmt', ['a8m.math'])
+ .filter('byteFmt', ['$math', function ($math) {
+ return function (bytes, decimal) {
+
+ if(isNumber(decimal) && isFinite(decimal) && decimal%1===0 && decimal >= 0 &&
+ isNumber(bytes) && isFinite(bytes)) {
+
+ if(bytes < 1024) { // within 1 KB so B
+ return convertToDecimal(bytes, decimal, $math) + ' B';
+ } else if(bytes < 1048576) { // within 1 MB so KB
+ return convertToDecimal((bytes / 1024), decimal, $math) + ' KB';
+ } else if(bytes < 1073741824){ // within 1 GB so MB
+ return convertToDecimal((bytes / 1048576), decimal, $math) + ' MB';
+ } else { // GB or more
+ return convertToDecimal((bytes / 1073741824), decimal, $math) + ' GB';
+ }
+
+ }
+ return "NaN";
+ }
+ }]);
+/**
+ * @ngdoc filter
+ * @name degrees
+ * @kind function
+ *
+ * @description
+ * Convert angle from radians to degrees
+ */
+angular.module('a8m.math.degrees', ['a8m.math'])
+ .filter('degrees', ['$math', function ($math) {
+ return function (radians, decimal) {
+ // if decimal is not an integer greater than -1, we cannot do. quit with error "NaN"
+ // if degrees is not a real number, we cannot do also. quit with error "NaN"
+ if(isNumber(decimal) && isFinite(decimal) && decimal%1===0 && decimal >= 0 &&
+ isNumber(radians) && isFinite(radians)) {
+ var degrees = (radians * 180) / $math.PI;
+ return $math.round(degrees * $math.pow(10,decimal)) / ($math.pow(10,decimal));
+ } else {
+ return "NaN";
+ }
+ }
+}]);
+
+
+
+/**
+ * @ngdoc filter
+ * @name formatBytes
+ * @kind function
+ *
+ * @description
+ * Convert bytes into appropriate display
+ * 1024 kilobytes => 1 MB
+ */
+angular.module('a8m.math.kbFmt', ['a8m.math'])
+ .filter('kbFmt', ['$math', function ($math) {
+ return function (bytes, decimal) {
+
+ if(isNumber(decimal) && isFinite(decimal) && decimal%1===0 && decimal >= 0 &&
+ isNumber(bytes) && isFinite(bytes)) {
+
+ if(bytes < 1024) { // within 1 MB so KB
+ return convertToDecimal(bytes, decimal, $math) + ' KB';
+ } else if(bytes < 1048576) { // within 1 GB so MB
+ return convertToDecimal((bytes / 1024), decimal, $math) + ' MB';
+ } else {
+ return convertToDecimal((bytes / 1048576), decimal, $math) + ' GB';
+ }
+ }
+ return "NaN";
+ }
+}]);
+/**
+ * @ngdoc module
+ * @name math
+ * @description
+ * reference to global Math object
+ */
+angular.module('a8m.math', [])
+ .factory('$math', ['$window', function ($window) {
+ return $window.Math;
+ }]);
+
+/**
+ * @ngdoc filter
+ * @name max
+ * @kind function
+ *
+ * @description
+ * Math.max will get an array and return the max value. if an expression
+ * is provided, will return max value by expression.
+ */
+angular.module('a8m.math.max', ['a8m.math'])
+ .filter('max', ['$math', '$parse', function ($math, $parse) {
+ return function (input, expression) {
+
+ if(!isArray(input)) {
+ return input;
+ }
+ return isUndefined(expression)
+ ? $math.max.apply($math, input)
+ : input[indexByMax(input, expression)];
+ };
+
+ /**
+ * @private
+ * @param array
+ * @param exp
+ * @returns {number|*|Number}
+ */
+ function indexByMax(array, exp) {
+ var mappedArray = array.map(function(elm){
+ return $parse(exp)(elm);
+ });
+ return mappedArray.indexOf($math.max.apply($math, mappedArray));
+ }
+ }]);
+/**
+ * @ngdoc filter
+ * @name min
+ * @kind function
+ *
+ * @description
+ * Math.min will get an array and return the min value. if an expression
+ * is provided, will return min value by expression.
+ */
+angular.module('a8m.math.min', ['a8m.math'])
+ .filter('min', ['$math', '$parse', function ($math, $parse) {
+ return function (input, expression) {
+
+ if(!isArray(input)) {
+ return input;
+ }
+ return isUndefined(expression)
+ ? $math.min.apply($math, input)
+ : input[indexByMin(input, expression)];
+ };
+
+ /**
+ * @private
+ * @param array
+ * @param exp
+ * @returns {number|*|Number}
+ */
+ function indexByMin(array, exp) {
+ var mappedArray = array.map(function(elm){
+ return $parse(exp)(elm);
+ });
+ return mappedArray.indexOf($math.min.apply($math, mappedArray));
+ }
+ }]);
+/**
+ * @ngdoc filter
+ * @name Percent
+ * @kind function
+ *
+ * @description
+ * percentage between two numbers
+ */
+angular.module('a8m.math.percent', ['a8m.math'])
+ .filter('percent', ['$math', '$window', function ($math, $window) {
+ return function (input, divided, round) {
+
+ var divider = (isString(input)) ? $window.Number(input) : input;
+ divided = divided || 100;
+ round = round || false;
+
+ if (!isNumber(divider) || $window.isNaN(divider)) return input;
+
+ return round
+ ? $math.round((divider / divided) * 100)
+ : (divider / divided) * 100;
+ }
+ }]);
+
+/**
+ * @ngdoc filter
+ * @name toRadians
+ * @kind function
+ *
+ * @description
+ * Convert angle from degrees to radians
+ */
+angular.module('a8m.math.radians', ['a8m.math'])
+ .filter('radians', ['$math', function ($math) {
+ return function (degrees, decimal) {
+ // if decimal is not an integer greater than -1, we cannot do. quit with error "NaN"
+ // if degrees is not a real number, we cannot do also. quit with error "NaN"
+ if(isNumber(decimal) && isFinite(decimal) && decimal%1===0 && decimal >= 0 &&
+ isNumber(degrees) && isFinite(degrees)) {
+ var radians = (degrees * 3.14159265359) / 180;
+ return $math.round(radians * $math.pow(10,decimal)) / ($math.pow(10,decimal));
+ }
+ return "NaN";
+ }
+}]);
+
+
+
+/**
+ * @ngdoc filter
+ * @name Radix
+ * @kind function
+ *
+ * @description
+ * converting decimal numbers to different bases(radix)
+ */
+angular.module('a8m.math.radix', [])
+ .filter('radix', function () {
+ return function (input, radix) {
+ var RANGE = /^[2-9]$|^[1-2]\d$|^3[0-6]$/;
+
+ if(!isNumber(input) || !RANGE.test(radix)) {
+ return input;
+ }
+
+ return input.toString(radix).toUpperCase();
+ }
+ });
+
+/**
+ * @ngdoc filter
+ * @name formatBytes
+ * @kind function
+ *
+ * @description
+ * Convert number into abbreviations.
+ * i.e: K for one thousand, M for Million, B for billion
+ * e.g: number of users:235,221, decimal:1 => 235.2 K
+ */
+angular.module('a8m.math.shortFmt', ['a8m.math'])
+ .filter('shortFmt', ['$math', function ($math) {
+ return function (number, decimal) {
+ if(isNumber(decimal) && isFinite(decimal) && decimal%1===0 && decimal >= 0 &&
+ isNumber(number) && isFinite(number)){
+
+ if(number < 1e3) {
+ return number;
+ } else if(number < 1e6) {
+ return convertToDecimal((number / 1e3), decimal, $math) + ' K';
+ } else if(number < 1e9){
+ return convertToDecimal((number / 1e6), decimal, $math) + ' M';
+ } else {
+ return convertToDecimal((number / 1e9), decimal, $math) + ' B';
+ }
+
+ }
+ return "NaN";
+ }
+}]);
+/**
+ * @ngdoc filter
+ * @name sum
+ * @kind function
+ *
+ * @description
+ * Sum up all values within an array
+ */
+angular.module('a8m.math.sum', [])
+ .filter('sum', function () {
+ return function (input, initial) {
+ return !isArray(input)
+ ? input
+ : input.reduce(function(prev, curr) {
+ return prev + curr;
+ }, initial || 0);
+ }
+ });
+
+/**
+ * @ngdoc filter
+ * @name endsWith
+ * @kind function
+ *
+ * @description
+ * checks whether string ends with the ends parameter.
+ */
+angular.module('a8m.ends-with', [])
+
+ .filter('endsWith', function () {
+ return function (input, ends, csensitive) {
+
+ var sensitive = csensitive || false,
+ position;
+
+ if(!isString(input) || isUndefined(ends)) {
+ return input;
+ }
+
+ input = (sensitive) ? input : input.toLowerCase();
+ position = input.length - ends.length;
+
+ return input.indexOf((sensitive) ? ends : ends.toLowerCase(), position) !== -1;
+ }
+ });
+
+/**
+ * @ngdoc filter
+ * @name latinize
+ * @kind function
+ *
+ * @description
+ * remove accents/diacritics from a string
+ */
+angular.module('a8m.latinize', [])
+ .filter('latinize',[ function () {
+ var defaultDiacriticsRemovalap = [
+ {'base':'A', 'letters':'\u0041\u24B6\uFF21\u00C0\u00C1\u00C2\u1EA6\u1EA4\u1EAA\u1EA8\u00C3\u0100\u0102\u1EB0\u1EAE\u1EB4\u1EB2\u0226\u01E0\u00C4\u01DE\u1EA2\u00C5\u01FA\u01CD\u0200\u0202\u1EA0\u1EAC\u1EB6\u1E00\u0104\u023A\u2C6F'},
+ {'base':'AA','letters':'\uA732'},
+ {'base':'AE','letters':'\u00C6\u01FC\u01E2'},
+ {'base':'AO','letters':'\uA734'},
+ {'base':'AU','letters':'\uA736'},
+ {'base':'AV','letters':'\uA738\uA73A'},
+ {'base':'AY','letters':'\uA73C'},
+ {'base':'B', 'letters':'\u0042\u24B7\uFF22\u1E02\u1E04\u1E06\u0243\u0182\u0181'},
+ {'base':'C', 'letters':'\u0043\u24B8\uFF23\u0106\u0108\u010A\u010C\u00C7\u1E08\u0187\u023B\uA73E'},
+ {'base':'D', 'letters':'\u0044\u24B9\uFF24\u1E0A\u010E\u1E0C\u1E10\u1E12\u1E0E\u0110\u018B\u018A\u0189\uA779'},
+ {'base':'DZ','letters':'\u01F1\u01C4'},
+ {'base':'Dz','letters':'\u01F2\u01C5'},
+ {'base':'E', 'letters':'\u0045\u24BA\uFF25\u00C8\u00C9\u00CA\u1EC0\u1EBE\u1EC4\u1EC2\u1EBC\u0112\u1E14\u1E16\u0114\u0116\u00CB\u1EBA\u011A\u0204\u0206\u1EB8\u1EC6\u0228\u1E1C\u0118\u1E18\u1E1A\u0190\u018E'},
+ {'base':'F', 'letters':'\u0046\u24BB\uFF26\u1E1E\u0191\uA77B'},
+ {'base':'G', 'letters':'\u0047\u24BC\uFF27\u01F4\u011C\u1E20\u011E\u0120\u01E6\u0122\u01E4\u0193\uA7A0\uA77D\uA77E'},
+ {'base':'H', 'letters':'\u0048\u24BD\uFF28\u0124\u1E22\u1E26\u021E\u1E24\u1E28\u1E2A\u0126\u2C67\u2C75\uA78D'},
+ {'base':'I', 'letters':'\u0049\u24BE\uFF29\u00CC\u00CD\u00CE\u0128\u012A\u012C\u0130\u00CF\u1E2E\u1EC8\u01CF\u0208\u020A\u1ECA\u012E\u1E2C\u0197'},
+ {'base':'J', 'letters':'\u004A\u24BF\uFF2A\u0134\u0248'},
+ {'base':'K', 'letters':'\u004B\u24C0\uFF2B\u1E30\u01E8\u1E32\u0136\u1E34\u0198\u2C69\uA740\uA742\uA744\uA7A2'},
+ {'base':'L', 'letters':'\u004C\u24C1\uFF2C\u013F\u0139\u013D\u1E36\u1E38\u013B\u1E3C\u1E3A\u0141\u023D\u2C62\u2C60\uA748\uA746\uA780'},
+ {'base':'LJ','letters':'\u01C7'},
+ {'base':'Lj','letters':'\u01C8'},
+ {'base':'M', 'letters':'\u004D\u24C2\uFF2D\u1E3E\u1E40\u1E42\u2C6E\u019C'},
+ {'base':'N', 'letters':'\u004E\u24C3\uFF2E\u01F8\u0143\u00D1\u1E44\u0147\u1E46\u0145\u1E4A\u1E48\u0220\u019D\uA790\uA7A4'},
+ {'base':'NJ','letters':'\u01CA'},
+ {'base':'Nj','letters':'\u01CB'},
+ {'base':'O', 'letters':'\u004F\u24C4\uFF2F\u00D2\u00D3\u00D4\u1ED2\u1ED0\u1ED6\u1ED4\u00D5\u1E4C\u022C\u1E4E\u014C\u1E50\u1E52\u014E\u022E\u0230\u00D6\u022A\u1ECE\u0150\u01D1\u020C\u020E\u01A0\u1EDC\u1EDA\u1EE0\u1EDE\u1EE2\u1ECC\u1ED8\u01EA\u01EC\u00D8\u01FE\u0186\u019F\uA74A\uA74C'},
+ {'base':'OI','letters':'\u01A2'},
+ {'base':'OO','letters':'\uA74E'},
+ {'base':'OU','letters':'\u0222'},
+ {'base':'OE','letters':'\u008C\u0152'},
+ {'base':'oe','letters':'\u009C\u0153'},
+ {'base':'P', 'letters':'\u0050\u24C5\uFF30\u1E54\u1E56\u01A4\u2C63\uA750\uA752\uA754'},
+ {'base':'Q', 'letters':'\u0051\u24C6\uFF31\uA756\uA758\u024A'},
+ {'base':'R', 'letters':'\u0052\u24C7\uFF32\u0154\u1E58\u0158\u0210\u0212\u1E5A\u1E5C\u0156\u1E5E\u024C\u2C64\uA75A\uA7A6\uA782'},
+ {'base':'S', 'letters':'\u0053\u24C8\uFF33\u1E9E\u015A\u1E64\u015C\u1E60\u0160\u1E66\u1E62\u1E68\u0218\u015E\u2C7E\uA7A8\uA784'},
+ {'base':'T', 'letters':'\u0054\u24C9\uFF34\u1E6A\u0164\u1E6C\u021A\u0162\u1E70\u1E6E\u0166\u01AC\u01AE\u023E\uA786'},
+ {'base':'TZ','letters':'\uA728'},
+ {'base':'U', 'letters':'\u0055\u24CA\uFF35\u00D9\u00DA\u00DB\u0168\u1E78\u016A\u1E7A\u016C\u00DC\u01DB\u01D7\u01D5\u01D9\u1EE6\u016E\u0170\u01D3\u0214\u0216\u01AF\u1EEA\u1EE8\u1EEE\u1EEC\u1EF0\u1EE4\u1E72\u0172\u1E76\u1E74\u0244'},
+ {'base':'V', 'letters':'\u0056\u24CB\uFF36\u1E7C\u1E7E\u01B2\uA75E\u0245'},
+ {'base':'VY','letters':'\uA760'},
+ {'base':'W', 'letters':'\u0057\u24CC\uFF37\u1E80\u1E82\u0174\u1E86\u1E84\u1E88\u2C72'},
+ {'base':'X', 'letters':'\u0058\u24CD\uFF38\u1E8A\u1E8C'},
+ {'base':'Y', 'letters':'\u0059\u24CE\uFF39\u1EF2\u00DD\u0176\u1EF8\u0232\u1E8E\u0178\u1EF6\u1EF4\u01B3\u024E\u1EFE'},
+ {'base':'Z', 'letters':'\u005A\u24CF\uFF3A\u0179\u1E90\u017B\u017D\u1E92\u1E94\u01B5\u0224\u2C7F\u2C6B\uA762'},
+ {'base':'a', 'letters':'\u0061\u24D0\uFF41\u1E9A\u00E0\u00E1\u00E2\u1EA7\u1EA5\u1EAB\u1EA9\u00E3\u0101\u0103\u1EB1\u1EAF\u1EB5\u1EB3\u0227\u01E1\u00E4\u01DF\u1EA3\u00E5\u01FB\u01CE\u0201\u0203\u1EA1\u1EAD\u1EB7\u1E01\u0105\u2C65\u0250'},
+ {'base':'aa','letters':'\uA733'},
+ {'base':'ae','letters':'\u00E6\u01FD\u01E3'},
+ {'base':'ao','letters':'\uA735'},
+ {'base':'au','letters':'\uA737'},
+ {'base':'av','letters':'\uA739\uA73B'},
+ {'base':'ay','letters':'\uA73D'},
+ {'base':'b', 'letters':'\u0062\u24D1\uFF42\u1E03\u1E05\u1E07\u0180\u0183\u0253'},
+ {'base':'c', 'letters':'\u0063\u24D2\uFF43\u0107\u0109\u010B\u010D\u00E7\u1E09\u0188\u023C\uA73F\u2184'},
+ {'base':'d', 'letters':'\u0064\u24D3\uFF44\u1E0B\u010F\u1E0D\u1E11\u1E13\u1E0F\u0111\u018C\u0256\u0257\uA77A'},
+ {'base':'dz','letters':'\u01F3\u01C6'},
+ {'base':'e', 'letters':'\u0065\u24D4\uFF45\u00E8\u00E9\u00EA\u1EC1\u1EBF\u1EC5\u1EC3\u1EBD\u0113\u1E15\u1E17\u0115\u0117\u00EB\u1EBB\u011B\u0205\u0207\u1EB9\u1EC7\u0229\u1E1D\u0119\u1E19\u1E1B\u0247\u025B\u01DD'},
+ {'base':'f', 'letters':'\u0066\u24D5\uFF46\u1E1F\u0192\uA77C'},
+ {'base':'g', 'letters':'\u0067\u24D6\uFF47\u01F5\u011D\u1E21\u011F\u0121\u01E7\u0123\u01E5\u0260\uA7A1\u1D79\uA77F'},
+ {'base':'h', 'letters':'\u0068\u24D7\uFF48\u0125\u1E23\u1E27\u021F\u1E25\u1E29\u1E2B\u1E96\u0127\u2C68\u2C76\u0265'},
+ {'base':'hv','letters':'\u0195'},
+ {'base':'i', 'letters':'\u0069\u24D8\uFF49\u00EC\u00ED\u00EE\u0129\u012B\u012D\u00EF\u1E2F\u1EC9\u01D0\u0209\u020B\u1ECB\u012F\u1E2D\u0268\u0131'},
+ {'base':'j', 'letters':'\u006A\u24D9\uFF4A\u0135\u01F0\u0249'},
+ {'base':'k', 'letters':'\u006B\u24DA\uFF4B\u1E31\u01E9\u1E33\u0137\u1E35\u0199\u2C6A\uA741\uA743\uA745\uA7A3'},
+ {'base':'l', 'letters':'\u006C\u24DB\uFF4C\u0140\u013A\u013E\u1E37\u1E39\u013C\u1E3D\u1E3B\u017F\u0142\u019A\u026B\u2C61\uA749\uA781\uA747'},
+ {'base':'lj','letters':'\u01C9'},
+ {'base':'m', 'letters':'\u006D\u24DC\uFF4D\u1E3F\u1E41\u1E43\u0271\u026F'},
+ {'base':'n', 'letters':'\u006E\u24DD\uFF4E\u01F9\u0144\u00F1\u1E45\u0148\u1E47\u0146\u1E4B\u1E49\u019E\u0272\u0149\uA791\uA7A5'},
+ {'base':'nj','letters':'\u01CC'},
+ {'base':'o', 'letters':'\u006F\u24DE\uFF4F\u00F2\u00F3\u00F4\u1ED3\u1ED1\u1ED7\u1ED5\u00F5\u1E4D\u022D\u1E4F\u014D\u1E51\u1E53\u014F\u022F\u0231\u00F6\u022B\u1ECF\u0151\u01D2\u020D\u020F\u01A1\u1EDD\u1EDB\u1EE1\u1EDF\u1EE3\u1ECD\u1ED9\u01EB\u01ED\u00F8\u01FF\u0254\uA74B\uA74D\u0275'},
+ {'base':'oi','letters':'\u01A3'},
+ {'base':'ou','letters':'\u0223'},
+ {'base':'oo','letters':'\uA74F'},
+ {'base':'p','letters':'\u0070\u24DF\uFF50\u1E55\u1E57\u01A5\u1D7D\uA751\uA753\uA755'},
+ {'base':'q','letters':'\u0071\u24E0\uFF51\u024B\uA757\uA759'},
+ {'base':'r','letters':'\u0072\u24E1\uFF52\u0155\u1E59\u0159\u0211\u0213\u1E5B\u1E5D\u0157\u1E5F\u024D\u027D\uA75B\uA7A7\uA783'},
+ {'base':'s','letters':'\u0073\u24E2\uFF53\u00DF\u015B\u1E65\u015D\u1E61\u0161\u1E67\u1E63\u1E69\u0219\u015F\u023F\uA7A9\uA785\u1E9B'},
+ {'base':'t','letters':'\u0074\u24E3\uFF54\u1E6B\u1E97\u0165\u1E6D\u021B\u0163\u1E71\u1E6F\u0167\u01AD\u0288\u2C66\uA787'},
+ {'base':'tz','letters':'\uA729'},
+ {'base':'u','letters': '\u0075\u24E4\uFF55\u00F9\u00FA\u00FB\u0169\u1E79\u016B\u1E7B\u016D\u00FC\u01DC\u01D8\u01D6\u01DA\u1EE7\u016F\u0171\u01D4\u0215\u0217\u01B0\u1EEB\u1EE9\u1EEF\u1EED\u1EF1\u1EE5\u1E73\u0173\u1E77\u1E75\u0289'},
+ {'base':'v','letters':'\u0076\u24E5\uFF56\u1E7D\u1E7F\u028B\uA75F\u028C'},
+ {'base':'vy','letters':'\uA761'},
+ {'base':'w','letters':'\u0077\u24E6\uFF57\u1E81\u1E83\u0175\u1E87\u1E85\u1E98\u1E89\u2C73'},
+ {'base':'x','letters':'\u0078\u24E7\uFF58\u1E8B\u1E8D'},
+ {'base':'y','letters':'\u0079\u24E8\uFF59\u1EF3\u00FD\u0177\u1EF9\u0233\u1E8F\u00FF\u1EF7\u1E99\u1EF5\u01B4\u024F\u1EFF'},
+ {'base':'z','letters':'\u007A\u24E9\uFF5A\u017A\u1E91\u017C\u017E\u1E93\u1E95\u01B6\u0225\u0240\u2C6C\uA763'}
+ ];
+
+ var diacriticsMap = {};
+ for (var i = 0; i < defaultDiacriticsRemovalap.length; i++) {
+ var letters = defaultDiacriticsRemovalap[i].letters.split("");
+ for (var j = 0; j < letters.length ; j++){
+ diacriticsMap[letters[j]] = defaultDiacriticsRemovalap[i].base;
+ }
+ }
+
+ // "what?" version ... http://jsperf.com/diacritics/12
+ function removeDiacritics (str) {
+ return str.replace(/[^\u0000-\u007E]/g, function(a){
+ return diacriticsMap[a] || a;
+ });
+ }
+
+ return function (input) {
+
+ return isString(input)
+ ? removeDiacritics(input)
+ : input;
+ }
+ }]);
+
+/**
+ * @ngdoc filter
+ * @name ltrim
+ * @kind function
+ *
+ * @description
+ * Left trim. Similar to trimFilter, but only for left side.
+ */
+angular.module('a8m.ltrim', [])
+ .filter('ltrim', function () {
+ return function(input, chars) {
+
+ var trim = chars || '\\s';
+
+ return isString(input)
+ ? input.replace(new RegExp('^' + trim + '+'), '')
+ : input;
+ }
+ });
+
+/**
+ * @ngdoc filter
+ * @name match
+ * @kind function
+ *
+ * @description
+ * Return the matched pattern in a string.
+ */
+angular.module('a8m.match', [])
+ .filter('match', function () {
+ return function (input, pattern, flag) {
+
+ var reg = new RegExp(pattern, flag);
+
+ return isString(input)
+ ? input.match(reg)
+ : null;
+ }
+ });
+
+/**
+ * @ngdoc filter
+ * @name repeat
+ * @kind function
+ *
+ * @description
+ * Repeats a string n times
+ */
+angular.module('a8m.repeat', [])
+ .filter('repeat',[ function () {
+ return function (input, n, separator) {
+
+ var times = ~~n;
+
+ if(!isString(input)) {
+ return input;
+ }
+
+ return !times
+ ? input
+ : strRepeat(input, --n, separator || '');
+ }
+ }]);
+
+/**
+ * Repeats a string n times with given separator
+ * @param str string to repeat
+ * @param n number of times
+ * @param sep separator
+ * @returns {*}
+ */
+function strRepeat(str, n, sep) {
+ if(!n) {
+ return str;
+ }
+ return str + sep + strRepeat(str, --n, sep);
+}
+/**
+* @ngdoc filter
+* @name rtrim
+* @kind function
+*
+* @description
+* Right trim. Similar to trimFilter, but only for right side.
+*/
+angular.module('a8m.rtrim', [])
+ .filter('rtrim', function () {
+ return function(input, chars) {
+
+ var trim = chars || '\\s';
+
+ return isString(input)
+ ? input.replace(new RegExp(trim + '+$'), '')
+ : input;
+ }
+ });
+
+/**
+ * @ngdoc filter
+ * @name slugify
+ * @kind function
+ *
+ * @description
+ * remove spaces from string, replace with "-" or given argument
+ */
+angular.module('a8m.slugify', [])
+ .filter('slugify',[ function () {
+ return function (input, sub) {
+
+ var replace = (isUndefined(sub)) ? '-' : sub;
+
+ return isString(input)
+ ? input.toLowerCase().replace(/\s+/g, replace)
+ : input;
+ }
+ }]);
+
+/**
+ * @ngdoc filter
+ * @name startWith
+ * @kind function
+ *
+ * @description
+ * checks whether string starts with the starts parameter.
+ */
+angular.module('a8m.starts-with', [])
+ .filter('startsWith', function () {
+ return function (input, start, csensitive) {
+
+ var sensitive = csensitive || false;
+
+ if(!isString(input) || isUndefined(start)) {
+ return input;
+ }
+
+ input = (sensitive) ? input : input.toLowerCase();
+
+ return !input.indexOf((sensitive) ? start : start.toLowerCase());
+ }
+ });
+
+/**
+ * @ngdoc filter
+ * @name stringular
+ * @kind function
+ *
+ * @description
+ * get string with {n} and replace match with enumeration values
+ */
+angular.module('a8m.stringular', [])
+ .filter('stringular', function () {
+ return function(input) {
+
+ var args = Array.prototype.slice.call(arguments, 1);
+
+ return input.replace(/{(\d+)}/g, function (match, number) {
+ return isUndefined(args[number]) ? match : args[number];
+ });
+ }
+ });
+
+/**
+ * @ngdoc filter
+ * @name stripTags
+ * @kind function
+ *
+ * @description
+ * strip html tags from string
+ */
+angular.module('a8m.strip-tags', [])
+ .filter('stripTags', function () {
+ return function(input) {
+ return isString(input)
+ ? input.replace(/<\S[^><]*>/g, '')
+ : input;
+ }
+ });
+
+/**
+ * @ngdoc filter
+ * @name test
+ * @kind function
+ *
+ * @description
+ * test if a string match a pattern.
+ */
+angular.module('a8m.test', [])
+ .filter('test', function () {
+ return function (input, pattern, flag) {
+
+ var reg = new RegExp(pattern, flag);
+
+ return isString(input)
+ ? reg.test(input)
+ : input;
+ }
+ });
+
+/**
+ * @ngdoc filter
+ * @name trim
+ * @kind function
+ *
+ * @description
+ * Strip whitespace (or other characters) from the beginning and end of a string
+ */
+angular.module('a8m.trim', [])
+ .filter('trim', function () {
+ return function(input, chars) {
+
+ var trim = chars || '\\s';
+
+ return isString(input)
+ ? input.replace(new RegExp('^' + trim + '+|' + trim + '+$', 'g'), '')
+ : input;
+ }
+ });
+
+/**
+ * @ngdoc filter
+ * @name truncate
+ * @kind function
+ *
+ * @description
+ * truncates a string given a specified length, providing a custom string to denote an omission.
+ */
+angular.module('a8m.truncate', [])
+ .filter('truncate', function () {
+ return function(input, length, suffix, preserve) {
+
+ length = isUndefined(length) ? input.length : length;
+ preserve = preserve || false;
+ suffix = suffix || '';
+
+ if(!isString(input) || (input.length <= length)) return input;
+
+ return input.substring(0, (preserve)
+ ? ((input.indexOf(' ', length) === -1) ? input.length : input.indexOf(' ', length))
+ : length) + suffix;
+ };
+ });
+
+/**
+ * @ngdoc filter
+ * @name ucfirst
+ * @kind function
+ *
+ * @description
+ * ucfirst
+ */
+angular.module('a8m.ucfirst', [])
+ .filter('ucfirst', [function() {
+ return function(input) {
+ return isString(input)
+ ? input
+ .split(' ')
+ .map(function (ch) {
+ return ch.charAt(0).toUpperCase() + ch.substring(1);
+ })
+ .join(' ')
+ : input;
+ }
+ }]);
+
+/**
+ * @ngdoc filter
+ * @name uriComponentEncode
+ * @kind function
+ *
+ * @description
+ * get string as parameter and return encoded string
+ */
+angular.module('a8m.uri-component-encode', [])
+ .filter('uriComponentEncode',['$window', function ($window) {
+ return function (input) {
+ return isString(input)
+ ? $window.encodeURIComponent(input)
+ : input;
+ }
+ }]);
+
+/**
+ * @ngdoc filter
+ * @name uriEncode
+ * @kind function
+ *
+ * @description
+ * get string as parameter and return encoded string
+ */
+angular.module('a8m.uri-encode', [])
+ .filter('uriEncode',['$window', function ($window) {
+ return function (input) {
+ return isString(input)
+ ? $window.encodeURI(input)
+ : input;
+ }
+ }]);
+
+/**
+ * @ngdoc filter
+ * @name wrap
+ * @kind function
+ *
+ * @description
+ * Wrap a string with another string
+ */
+angular.module('a8m.wrap', [])
+ .filter('wrap', function () {
+ return function(input, wrap, ends) {
+ return isString(input) && isDefined(wrap)
+ ? [wrap, input, ends || wrap].join('')
+ : input;
+ }
+ });
+
+/**
+ * @ngdoc provider
+ * @name filterWatcher
+ * @kind function
+ *
+ * @description
+ * store specific filters result in $$cache, based on scope life time(avoid memory leak).
+ * on scope.$destroy remove it's cache from $$cache container
+ */
+
+angular.module('a8m.filter-watcher', [])
+ .provider('filterWatcher', function() {
+
+ this.$get = ['$window', '$rootScope', function($window, $rootScope) {
+
+ /**
+ * Cache storing
+ * @type {Object}
+ */
+ var $$cache = {};
+
+ /**
+ * Scope listeners container
+ * scope.$destroy => remove all cache keys
+ * bind to current scope.
+ * @type {Object}
+ */
+ var $$listeners = {};
+
+ /**
+ * $timeout without triggering the digest cycle
+ * @type {function}
+ */
+ var $$timeout = $window.setTimeout;
+
+ /**
+ * @description
+ * get `HashKey` string based on the given arguments.
+ * @param fName
+ * @param args
+ * @returns {string}
+ */
+ function getHashKey(fName, args) {
+ return [fName, angular.toJson(args)]
+ .join('#')
+ .replace(/"/g,'');
+ }
+
+ /**
+ * @description
+ * fir on $scope.$destroy,
+ * remove cache based scope from `$$cache`,
+ * and remove itself from `$$listeners`
+ * @param event
+ */
+ function removeCache(event) {
+ var id = event.targetScope.$id;
+ forEach($$listeners[id], function(key) {
+ delete $$cache[key];
+ });
+ delete $$listeners[id];
+ }
+
+ /**
+ * @description
+ * for angular version that greater than v.1.3.0
+ * if clear cache when the digest cycle end.
+ */
+ function cleanStateless() {
+ $$timeout(function() {
+ if(!$rootScope.$$phase)
+ $$cache = {};
+ });
+ }
+
+ /**
+ * @description
+ * Store hashKeys in $$listeners container
+ * on scope.$destroy, remove them all(bind an event).
+ * @param scope
+ * @param hashKey
+ * @returns {*}
+ */
+ function addListener(scope, hashKey) {
+ var id = scope.$id;
+ if(isUndefined($$listeners[id])) {
+ scope.$on('$destroy', removeCache);
+ $$listeners[id] = [];
+ }
+ return $$listeners[id].push(hashKey);
+ }
+
+ /**
+ * @description
+ * return the `cacheKey` or undefined.
+ * @param filterName
+ * @param args
+ * @returns {*}
+ */
+ function $$isMemoized(filterName, args) {
+ var hashKey = getHashKey(filterName, args);
+ return $$cache[hashKey];
+ }
+
+ /**
+ * @description
+ * store `result` in `$$cache` container, based on the hashKey.
+ * add $destroy listener and return result
+ * @param filterName
+ * @param args
+ * @param scope
+ * @param result
+ * @returns {*}
+ */
+ function $$memoize(filterName, args, scope, result) {
+ var hashKey = getHashKey(filterName, args);
+ //store result in `$$cache` container
+ $$cache[hashKey] = result;
+ // for angular versions that less than 1.3
+ // add to `$destroy` listener, a cleaner callback
+ if(isScope(scope)) {
+ addListener(scope, hashKey);
+ } else {
+ cleanStateless();
+ }
+ return result;
+ }
+
+ return {
+ isMemoized: $$isMemoized,
+ memoize: $$memoize
+ }
+
+ }];
+ });
+
+
+/**
+ * @ngdoc module
+ * @name angular.filters
+ * @description
+ * Bunch of useful filters for angularJS
+ */
+
+angular.module('angular.filter', [
+
+ 'a8m.ucfirst',
+ 'a8m.uri-encode',
+ 'a8m.uri-component-encode',
+ 'a8m.slugify',
+ 'a8m.latinize',
+ 'a8m.strip-tags',
+ 'a8m.stringular',
+ 'a8m.truncate',
+ 'a8m.starts-with',
+ 'a8m.ends-with',
+ 'a8m.wrap',
+ 'a8m.trim',
+ 'a8m.ltrim',
+ 'a8m.rtrim',
+ 'a8m.repeat',
+ 'a8m.test',
+ 'a8m.match',
+
+ 'a8m.to-array',
+ 'a8m.concat',
+ 'a8m.contains',
+ 'a8m.unique',
+ 'a8m.is-empty',
+ 'a8m.after',
+ 'a8m.after-where',
+ 'a8m.before',
+ 'a8m.before-where',
+ 'a8m.defaults',
+ 'a8m.where',
+ 'a8m.reverse',
+ 'a8m.remove',
+ 'a8m.remove-with',
+ 'a8m.group-by',
+ 'a8m.count-by',
+ 'a8m.search-field',
+ 'a8m.fuzzy-by',
+ 'a8m.fuzzy',
+ 'a8m.omit',
+ 'a8m.pick',
+ 'a8m.every',
+ 'a8m.filter-by',
+ 'a8m.xor',
+ 'a8m.map',
+ 'a8m.first',
+ 'a8m.last',
+ 'a8m.flatten',
+ 'a8m.join',
+
+ 'a8m.math',
+ 'a8m.math.max',
+ 'a8m.math.min',
+ 'a8m.math.percent',
+ 'a8m.math.radix',
+ 'a8m.math.sum',
+ 'a8m.math.degrees',
+ 'a8m.math.radians',
+ 'a8m.math.byteFmt',
+ 'a8m.math.kbFmt',
+ 'a8m.math.shortFmt',
+
+ 'a8m.angular',
+ 'a8m.conditions',
+ 'a8m.is-null',
+
+ 'a8m.filter-watcher'
+]);
+})( window, window.angular );
\ No newline at end of file
diff --git a/third_party/ui/bower_components/angular-filter/dist/angular-filter.min.js b/third_party/ui/bower_components/angular-filter/dist/angular-filter.min.js
new file mode 100644
index 0000000000..592200ac18
--- /dev/null
+++ b/third_party/ui/bower_components/angular-filter/dist/angular-filter.min.js
@@ -0,0 +1,6 @@
+/**
+ * Bunch of useful filters for angularJS(with no external dependencies!)
+ * @version v0.5.4 - 2015-02-20 * @link https://github.com/a8m/angular-filter
+ * @author Ariel Mashraki
+ * @license MIT License, http://www.opensource.org/licenses/MIT
+ */!function(a,b,c){"use strict";function d(a){return D(a)?a:Object.keys(a).map(function(b){return a[b]})}function e(a){return null===a}function f(a,b){var d=Object.keys(a);return-1==d.map(function(d){return b[d]!==c&&b[d]==a[d]}).indexOf(!1)}function g(a,b){if(""===b)return a;var c=a.indexOf(b.charAt(0));return-1===c?!1:g(a.substr(c+1),b.substr(1))}function h(a,b,c){var d=0;return a.filter(function(a){var e=x(c)?b>d&&c(a):b>d;return d=e?d+1:d,e})}function i(a,b,c){return c.round(a*c.pow(10,b))/c.pow(10,b)}function j(a,b,c){b=b||[];var d=Object.keys(a);return d.forEach(function(d){if(C(a[d])&&!D(a[d])){var e=c?c+"."+d:c;j(a[d],b,e||d)}else{var f=c?c+"."+d:d;b.push(f)}}),b}function k(a){return a&&a.$evalAsync&&a.$watch}function l(){return function(a,b){return a>b}}function m(){return function(a,b){return a>=b}}function n(){return function(a,b){return b>a}}function o(){return function(a,b){return b>=a}}function p(){return function(a,b){return a==b}}function q(){return function(a,b){return a!=b}}function r(){return function(a,b){return a===b}}function s(){return function(a,b){return a!==b}}function t(a){return function(b,c){return b=C(b)?d(b):b,!D(b)||y(c)?!1:b.some(function(b){return C(b)||z(c)?a(c)(b):b===c})}}function u(a,b){return b=b||0,b>=a.length?a:D(a[b])?u(a.slice(0,b).concat(a[b],a.slice(b+1)),b):u(a,b+1)}function v(a){return function(b,c){function e(a,b){return y(b)?!1:a.some(function(a){return H(a,b)})}if(b=C(b)?d(b):b,!D(b))return b;var f=[],g=a(c);return b.filter(y(c)?function(a,b,c){return c.indexOf(a)===b}:function(a){var b=g(a);return e(f,b)?!1:(f.push(b),!0)})}}function w(a,b,c){return b?a+c+w(a,--b,c):a}var x=b.isDefined,y=b.isUndefined,z=b.isFunction,A=b.isString,B=b.isNumber,C=b.isObject,D=b.isArray,E=b.forEach,F=b.extend,G=b.copy,H=b.equals;String.prototype.contains||(String.prototype.contains=function(){return-1!==String.prototype.indexOf.apply(this,arguments)}),b.module("a8m.angular",[]).filter("isUndefined",function(){return function(a){return b.isUndefined(a)}}).filter("isDefined",function(){return function(a){return b.isDefined(a)}}).filter("isFunction",function(){return function(a){return b.isFunction(a)}}).filter("isString",function(){return function(a){return b.isString(a)}}).filter("isNumber",function(){return function(a){return b.isNumber(a)}}).filter("isArray",function(){return function(a){return b.isArray(a)}}).filter("isObject",function(){return function(a){return b.isObject(a)}}).filter("isEqual",function(){return function(a,c){return b.equals(a,c)}}),b.module("a8m.conditions",[]).filter({isGreaterThan:l,">":l,isGreaterThanOrEqualTo:m,">=":m,isLessThan:n,"<":n,isLessThanOrEqualTo:o,"<=":o,isEqualTo:p,"==":p,isNotEqualTo:q,"!=":q,isIdenticalTo:r,"===":r,isNotIdenticalTo:s,"!==":s}),b.module("a8m.is-null",[]).filter("isNull",function(){return function(a){return e(a)}}),b.module("a8m.after-where",[]).filter("afterWhere",function(){return function(a,b){if(a=C(a)?d(a):a,!D(a)||y(b))return a;var c=a.map(function(a){return f(b,a)}).indexOf(!0);return a.slice(-1===c?0:c)}}),b.module("a8m.after",[]).filter("after",function(){return function(a,b){return a=C(a)?d(a):a,D(a)?a.slice(b):a}}),b.module("a8m.before-where",[]).filter("beforeWhere",function(){return function(a,b){if(a=C(a)?d(a):a,!D(a)||y(b))return a;var c=a.map(function(a){return f(b,a)}).indexOf(!0);return a.slice(0,-1===c?a.length:++c)}}),b.module("a8m.before",[]).filter("before",function(){return function(a,b){return a=C(a)?d(a):a,D(a)?a.slice(0,b?--b:b):a}}),b.module("a8m.concat",[]).filter("concat",[function(){return function(a,b){if(y(b))return a;if(D(a))return a.concat(C(b)?d(b):b);if(C(a)){var c=d(a);return c.concat(C(b)?d(b):b)}return a}}]),b.module("a8m.contains",[]).filter({contains:["$parse",t],some:["$parse",t]}),b.module("a8m.count-by",[]).filter("countBy",["$parse",function(a){return function(b,c){var e,f={},g=a(c);return b=C(b)?d(b):b,!D(b)||y(c)?b:(b.forEach(function(a){e=g(a),f[e]||(f[e]=0),f[e]++}),f)}}]),b.module("a8m.defaults",[]).filter("defaults",["$parse",function(a){return function(b,c){if(b=C(b)?d(b):b,!D(b)||!C(c))return b;var e=j(c);return b.forEach(function(b){e.forEach(function(d){var e=a(d),f=e.assign;y(e(b))&&f(b,e(c))})}),b}}]),b.module("a8m.every",[]).filter("every",["$parse",function(a){return function(b,c){return b=C(b)?d(b):b,!D(b)||y(c)?!0:b.every(function(b){return C(b)||z(c)?a(c)(b):b===c})}}]),b.module("a8m.filter-by",[]).filter("filterBy",["$parse",function(a){return function(b,e,f){var g;return f=A(f)||B(f)?String(f).toLowerCase():c,b=C(b)?d(b):b,!D(b)||y(f)?b:b.filter(function(b){return e.some(function(c){if(~c.indexOf("+")){var d=c.replace(new RegExp("\\s","g"),"").split("+");g=d.reduce(function(c,d,e){return 1===e?a(c)(b)+" "+a(d)(b):c+" "+a(d)(b)})}else g=a(c)(b);return A(g)||B(g)?String(g).toLowerCase().contains(f):!1})})}}]),b.module("a8m.first",[]).filter("first",["$parse",function(a){return function(b){var e,f,g;return b=C(b)?d(b):b,D(b)?(g=Array.prototype.slice.call(arguments,1),e=B(g[0])?g[0]:1,f=B(g[0])?B(g[1])?c:g[1]:g[0],g.length?h(b,e,f?a(f):f):b[0]):b}}]),b.module("a8m.flatten",[]).filter("flatten",function(){return function(a,b){return b=b||!1,a=C(a)?d(a):a,D(a)?b?[].concat.apply([],a):u(a,0):a}}),b.module("a8m.fuzzy-by",[]).filter("fuzzyBy",["$parse",function(a){return function(b,c,e,f){var h,i,j=f||!1;return b=C(b)?d(b):b,!D(b)||y(c)||y(e)?b:(i=a(c),b.filter(function(a){return h=i(a),A(h)?(h=j?h:h.toLowerCase(),e=j?e:e.toLowerCase(),g(h,e)!==!1):!1}))}}]),b.module("a8m.fuzzy",[]).filter("fuzzy",function(){return function(a,b,c){function e(a,b){var c,d,e=Object.keys(a);return 0=0&&B(b)&&isFinite(b)?1024>b?i(b,c,a)+" B":1048576>b?i(b/1024,c,a)+" KB":1073741824>b?i(b/1048576,c,a)+" MB":i(b/1073741824,c,a)+" GB":"NaN"}}]),b.module("a8m.math.degrees",["a8m.math"]).filter("degrees",["$math",function(a){return function(b,c){if(B(c)&&isFinite(c)&&c%1===0&&c>=0&&B(b)&&isFinite(b)){var d=180*b/a.PI;return a.round(d*a.pow(10,c))/a.pow(10,c)}return"NaN"}}]),b.module("a8m.math.kbFmt",["a8m.math"]).filter("kbFmt",["$math",function(a){return function(b,c){return B(c)&&isFinite(c)&&c%1===0&&c>=0&&B(b)&&isFinite(b)?1024>b?i(b,c,a)+" KB":1048576>b?i(b/1024,c,a)+" MB":i(b/1048576,c,a)+" GB":"NaN"}}]),b.module("a8m.math",[]).factory("$math",["$window",function(a){return a.Math}]),b.module("a8m.math.max",["a8m.math"]).filter("max",["$math","$parse",function(a,b){function c(c,d){var e=c.map(function(a){return b(d)(a)});return e.indexOf(a.max.apply(a,e))}return function(b,d){return D(b)?y(d)?a.max.apply(a,b):b[c(b,d)]:b}}]),b.module("a8m.math.min",["a8m.math"]).filter("min",["$math","$parse",function(a,b){function c(c,d){var e=c.map(function(a){return b(d)(a)});return e.indexOf(a.min.apply(a,e))}return function(b,d){return D(b)?y(d)?a.min.apply(a,b):b[c(b,d)]:b}}]),b.module("a8m.math.percent",["a8m.math"]).filter("percent",["$math","$window",function(a,b){return function(c,d,e){var f=A(c)?b.Number(c):c;return d=d||100,e=e||!1,!B(f)||b.isNaN(f)?c:e?a.round(f/d*100):f/d*100}}]),b.module("a8m.math.radians",["a8m.math"]).filter("radians",["$math",function(a){return function(b,c){if(B(c)&&isFinite(c)&&c%1===0&&c>=0&&B(b)&&isFinite(b)){var d=3.14159265359*b/180;return a.round(d*a.pow(10,c))/a.pow(10,c)}return"NaN"}}]),b.module("a8m.math.radix",[]).filter("radix",function(){return function(a,b){var c=/^[2-9]$|^[1-2]\d$|^3[0-6]$/;return B(a)&&c.test(b)?a.toString(b).toUpperCase():a}}),b.module("a8m.math.shortFmt",["a8m.math"]).filter("shortFmt",["$math",function(a){return function(b,c){return B(c)&&isFinite(c)&&c%1===0&&c>=0&&B(b)&&isFinite(b)?1e3>b?b:1e6>b?i(b/1e3,c,a)+" K":1e9>b?i(b/1e6,c,a)+" M":i(b/1e9,c,a)+" B":"NaN"}}]),b.module("a8m.math.sum",[]).filter("sum",function(){return function(a,b){return D(a)?a.reduce(function(a,b){return a+b},b||0):a}}),b.module("a8m.ends-with",[]).filter("endsWith",function(){return function(a,b,c){var d,e=c||!1;return!A(a)||y(b)?a:(a=e?a:a.toLowerCase(),d=a.length-b.length,-1!==a.indexOf(e?b:b.toLowerCase(),d))}}),b.module("a8m.latinize",[]).filter("latinize",[function(){function a(a){return a.replace(/[^\u0000-\u007E]/g,function(a){return c[a]||a})}for(var b=[{base:"A",letters:"AⒶAÀÁÂẦẤẪẨÃĀĂẰẮẴẲȦǠÄǞẢÅǺǍȀȂẠẬẶḀĄȺⱯ"},{base:"AA",letters:"Ꜳ"},{base:"AE",letters:"ÆǼǢ"},{base:"AO",letters:"Ꜵ"},{base:"AU",letters:"Ꜷ"},{base:"AV",letters:"ꜸꜺ"},{base:"AY",letters:"Ꜽ"},{base:"B",letters:"BⒷBḂḄḆɃƂƁ"},{base:"C",letters:"CⒸCĆĈĊČÇḈƇȻꜾ"},{base:"D",letters:"DⒹDḊĎḌḐḒḎĐƋƊƉꝹ"},{base:"DZ",letters:"DZDŽ"},{base:"Dz",letters:"DzDž"},{base:"E",letters:"EⒺEÈÉÊỀẾỄỂẼĒḔḖĔĖËẺĚȄȆẸỆȨḜĘḘḚƐƎ"},{base:"F",letters:"FⒻFḞƑꝻ"},{base:"G",letters:"GⒼGǴĜḠĞĠǦĢǤƓꞠꝽꝾ"},{base:"H",letters:"HⒽHĤḢḦȞḤḨḪĦⱧⱵꞍ"},{base:"I",letters:"IⒾIÌÍÎĨĪĬİÏḮỈǏȈȊỊĮḬƗ"},{base:"J",letters:"JⒿJĴɈ"},{base:"K",letters:"KⓀKḰǨḲĶḴƘⱩꝀꝂꝄꞢ"},{base:"L",letters:"LⓁLĿĹĽḶḸĻḼḺŁȽⱢⱠꝈꝆꞀ"},{base:"LJ",letters:"LJ"},{base:"Lj",letters:"Lj"},{base:"M",letters:"MⓂMḾṀṂⱮƜ"},{base:"N",letters:"NⓃNǸŃÑṄŇṆŅṊṈȠƝꞐꞤ"},{base:"NJ",letters:"NJ"},{base:"Nj",letters:"Nj"},{base:"O",letters:"OⓄOÒÓÔỒỐỖỔÕṌȬṎŌṐṒŎȮȰÖȪỎŐǑȌȎƠỜỚỠỞỢỌỘǪǬØǾƆƟꝊꝌ"},{base:"OI",letters:"Ƣ"},{base:"OO",letters:"Ꝏ"},{base:"OU",letters:"Ȣ"},{base:"OE",letters:"Œ"},{base:"oe",letters:"œ"},{base:"P",letters:"PⓅPṔṖƤⱣꝐꝒꝔ"},{base:"Q",letters:"QⓆQꝖꝘɊ"},{base:"R",letters:"RⓇRŔṘŘȐȒṚṜŖṞɌⱤꝚꞦꞂ"},{base:"S",letters:"SⓈSẞŚṤŜṠŠṦṢṨȘŞⱾꞨꞄ"},{base:"T",letters:"TⓉTṪŤṬȚŢṰṮŦƬƮȾꞆ"},{base:"TZ",letters:"Ꜩ"},{base:"U",letters:"UⓊUÙÚÛŨṸŪṺŬÜǛǗǕǙỦŮŰǓȔȖƯỪỨỮỬỰỤṲŲṶṴɄ"},{base:"V",letters:"VⓋVṼṾƲꝞɅ"},{base:"VY",letters:"Ꝡ"},{base:"W",letters:"WⓌWẀẂŴẆẄẈⱲ"},{base:"X",letters:"XⓍXẊẌ"},{base:"Y",letters:"YⓎYỲÝŶỸȲẎŸỶỴƳɎỾ"},{base:"Z",letters:"ZⓏZŹẐŻŽẒẔƵȤⱿⱫꝢ"},{base:"a",letters:"aⓐaẚàáâầấẫẩãāăằắẵẳȧǡäǟảåǻǎȁȃạậặḁąⱥɐ"},{base:"aa",letters:"ꜳ"},{base:"ae",letters:"æǽǣ"},{base:"ao",letters:"ꜵ"},{base:"au",letters:"ꜷ"},{base:"av",letters:"ꜹꜻ"},{base:"ay",letters:"ꜽ"},{base:"b",letters:"bⓑbḃḅḇƀƃɓ"},{base:"c",letters:"cⓒcćĉċčçḉƈȼꜿↄ"},{base:"d",letters:"dⓓdḋďḍḑḓḏđƌɖɗꝺ"},{base:"dz",letters:"dzdž"},{base:"e",letters:"eⓔeèéêềếễểẽēḕḗĕėëẻěȅȇẹệȩḝęḙḛɇɛǝ"},{base:"f",letters:"fⓕfḟƒꝼ"},{base:"g",letters:"gⓖgǵĝḡğġǧģǥɠꞡᵹꝿ"},{base:"h",letters:"hⓗhĥḣḧȟḥḩḫẖħⱨⱶɥ"},{base:"hv",letters:"ƕ"},{base:"i",letters:"iⓘiìíîĩīĭïḯỉǐȉȋịįḭɨı"},{base:"j",letters:"jⓙjĵǰɉ"},{base:"k",letters:"kⓚkḱǩḳķḵƙⱪꝁꝃꝅꞣ"},{base:"l",letters:"lⓛlŀĺľḷḹļḽḻſłƚɫⱡꝉꞁꝇ"},{base:"lj",letters:"lj"},{base:"m",letters:"mⓜmḿṁṃɱɯ"},{base:"n",letters:"nⓝnǹńñṅňṇņṋṉƞɲʼnꞑꞥ"},{base:"nj",letters:"nj"},{base:"o",letters:"oⓞoòóôồốỗổõṍȭṏōṑṓŏȯȱöȫỏőǒȍȏơờớỡởợọộǫǭøǿɔꝋꝍɵ"},{base:"oi",letters:"ƣ"},{base:"ou",letters:"ȣ"},{base:"oo",letters:"ꝏ"},{base:"p",letters:"pⓟpṕṗƥᵽꝑꝓꝕ"},{base:"q",letters:"qⓠqɋꝗꝙ"},{base:"r",letters:"rⓡrŕṙřȑȓṛṝŗṟɍɽꝛꞧꞃ"},{base:"s",letters:"sⓢsßśṥŝṡšṧṣṩșşȿꞩꞅẛ"},{base:"t",letters:"tⓣtṫẗťṭțţṱṯŧƭʈⱦꞇ"},{base:"tz",letters:"ꜩ"},{base:"u",letters:"uⓤuùúûũṹūṻŭüǜǘǖǚủůűǔȕȗưừứữửựụṳųṷṵʉ"},{base:"v",letters:"vⓥvṽṿʋꝟʌ"},{base:"vy",letters:"ꝡ"},{base:"w",letters:"wⓦwẁẃŵẇẅẘẉⱳ"},{base:"x",letters:"xⓧxẋẍ"},{base:"y",letters:"yⓨyỳýŷỹȳẏÿỷẙỵƴɏỿ"},{base:"z",letters:"zⓩzźẑżžẓẕƶȥɀⱬꝣ"}],c={},d=0;d<]*>/g,""):a}}),b.module("a8m.test",[]).filter("test",function(){return function(a,b,c){var d=new RegExp(b,c);return A(a)?d.test(a):a}}),b.module("a8m.trim",[]).filter("trim",function(){return function(a,b){var c=b||"\\s";return A(a)?a.replace(new RegExp("^"+c+"+|"+c+"+$","g"),""):a}}),b.module("a8m.truncate",[]).filter("truncate",function(){return function(a,b,c,d){return b=y(b)?a.length:b,d=d||!1,c=c||"",!A(a)||a.length<=b?a:a.substring(0,d?-1===a.indexOf(" ",b)?a.length:a.indexOf(" ",b):b)+c}}),b.module("a8m.ucfirst",[]).filter("ucfirst",[function(){return function(a){return A(a)?a.split(" ").map(function(a){return a.charAt(0).toUpperCase()+a.substring(1)}).join(" "):a}}]),b.module("a8m.uri-component-encode",[]).filter("uriComponentEncode",["$window",function(a){return function(b){return A(b)?a.encodeURIComponent(b):b}}]),b.module("a8m.uri-encode",[]).filter("uriEncode",["$window",function(a){return function(b){return A(b)?a.encodeURI(b):b}}]),b.module("a8m.wrap",[]).filter("wrap",function(){return function(a,b,c){return A(a)&&x(b)?[b,a,c||b].join(""):a}}),b.module("a8m.filter-watcher",[]).provider("filterWatcher",function(){this.$get=["$window","$rootScope",function(a,c){function d(a,c){return[a,b.toJson(c)].join("#").replace(/"/g,"")}function e(a){var b=a.targetScope.$id;E(l[b],function(a){delete j[a]}),delete l[b]}function f(){m(function(){c.$$phase||(j={})})}function g(a,b){var c=a.$id;return y(l[c])&&(a.$on("$destroy",e),l[c]=[]),l[c].push(b)}function h(a,b){var c=d(a,b);return j[c]}function i(a,b,c,e){var h=d(a,b);return j[h]=e,k(c)?g(c,h):f(),e}var j={},l={},m=a.setTimeout;return{isMemoized:h,memoize:i}}]}),b.module("angular.filter",["a8m.ucfirst","a8m.uri-encode","a8m.uri-component-encode","a8m.slugify","a8m.latinize","a8m.strip-tags","a8m.stringular","a8m.truncate","a8m.starts-with","a8m.ends-with","a8m.wrap","a8m.trim","a8m.ltrim","a8m.rtrim","a8m.repeat","a8m.test","a8m.match","a8m.to-array","a8m.concat","a8m.contains","a8m.unique","a8m.is-empty","a8m.after","a8m.after-where","a8m.before","a8m.before-where","a8m.defaults","a8m.where","a8m.reverse","a8m.remove","a8m.remove-with","a8m.group-by","a8m.count-by","a8m.search-field","a8m.fuzzy-by","a8m.fuzzy","a8m.omit","a8m.pick","a8m.every","a8m.filter-by","a8m.xor","a8m.map","a8m.first","a8m.last","a8m.flatten","a8m.join","a8m.math","a8m.math.max","a8m.math.min","a8m.math.percent","a8m.math.radix","a8m.math.sum","a8m.math.degrees","a8m.math.radians","a8m.math.byteFmt","a8m.math.kbFmt","a8m.math.shortFmt","a8m.angular","a8m.conditions","a8m.is-null","a8m.filter-watcher"])}(window,window.angular);
\ No newline at end of file
diff --git a/third_party/ui/bower_components/angular-filter/dist/angular-filter.zip b/third_party/ui/bower_components/angular-filter/dist/angular-filter.zip
new file mode 100644
index 0000000000..af0b20d5ef
Binary files /dev/null and b/third_party/ui/bower_components/angular-filter/dist/angular-filter.zip differ
diff --git a/third_party/ui/bower_components/angular-filter/license.md b/third_party/ui/bower_components/angular-filter/license.md
new file mode 100644
index 0000000000..a05e9aac6e
--- /dev/null
+++ b/third_party/ui/bower_components/angular-filter/license.md
@@ -0,0 +1,21 @@
+The MIT License
+
+Copyright (c) 2015 Ariel Mashraki
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/third_party/ui/bower_components/angular-json-human/.bower.json b/third_party/ui/bower_components/angular-json-human/.bower.json
new file mode 100644
index 0000000000..8fd595b355
--- /dev/null
+++ b/third_party/ui/bower_components/angular-json-human/.bower.json
@@ -0,0 +1,51 @@
+{
+ "author": {
+ "name": "Brian Park",
+ "email": "yaru22@gmail.com"
+ },
+ "name": "angular-json-human",
+ "description": "Angular directive to convert JSON into human readable table. Inspired by https://github.com/marianoguerra/json.human.js.",
+ "version": "1.2.1",
+ "license": "MIT",
+ "homepage": "https://github.com/yaru22/angular-json-human",
+ "repository": {
+ "type": "git",
+ "url": "git://github.com/yaru22/angular-json-human.git"
+ },
+ "main": [
+ "dist/angular-json-human.js",
+ "dist/angular-json-human.css"
+ ],
+ "ignore": [
+ ".editorconfig",
+ ".gitattributes",
+ ".gitignore",
+ ".gittrack",
+ ".jshintrc",
+ "demo/",
+ "Gruntfile.js",
+ "karma-unit.conf.js",
+ "package.json",
+ "src/",
+ "template/",
+ "test/"
+ ],
+ "dependencies": {
+ "angular": "^1.2.0",
+ "lodash": "~2.4.1"
+ },
+ "devDependencies": {
+ "angular-mocks": "^1.2.0",
+ "chai": "~1.9.0",
+ "jquery": "~2.1.0"
+ },
+ "_release": "1.2.1",
+ "_resolution": {
+ "type": "version",
+ "tag": "1.2.1",
+ "commit": "b778d1e1436c600c4d35e4f156fe931c765d559d"
+ },
+ "_source": "git://github.com/yaru22/angular-json-human.git",
+ "_target": "~1.2.1",
+ "_originalSource": "angular-json-human"
+}
\ No newline at end of file
diff --git a/third_party/ui/bower_components/angular-json-human/LICENSE b/third_party/ui/bower_components/angular-json-human/LICENSE
new file mode 100644
index 0000000000..165901dbd9
--- /dev/null
+++ b/third_party/ui/bower_components/angular-json-human/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2013-2014 Brian Park
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/third_party/ui/bower_components/angular-json-human/README.md b/third_party/ui/bower_components/angular-json-human/README.md
new file mode 100644
index 0000000000..f2c6c27284
--- /dev/null
+++ b/third_party/ui/bower_components/angular-json-human/README.md
@@ -0,0 +1,64 @@
+angular-json-human [![Analytics](https://ga-beacon.appspot.com/UA-2694988-7/angular-json-human/readme?pixel)](https://github.com/yaru22/angular-json-human)
+==================
+Angular directive to convert JSON into human readable table. Inspired by https://github.com/marianoguerra/json.human.js.
+
+Demo
+----
+Check out the demo [here](http://www.brianpark.ca/projects/angular_json_human/demo/).
+
+Dependency
+----------
+This directive requires `lodash`. I'm going to remove the dependency in the future release.
+
+How to Use
+----------
+Install it via `bower`:
+```
+$ bower install angular-json-human
+```
+
+Include `angular-json-human.(js|css)` in your project. Load the directive after loading `angular.js`
+
+```
+
+
+```
+
+Specify angular-json-human as a dependency of your Angular module.
+
+```
+var app = angular.module('ngApp', [
+ 'yaru22.jsonHuman'
+]);
+```
+
+Use it in your project.
+
+```
+
+...
+
+
+ ...
+
+
+```
+
+or check out my [Plunker](http://plnkr.co/edit/0wEPmUsw5kKbBo9RjXW4?p=preview) for the minimal setup.
+
+
+How to Contribute
+-----------------
+```
+$ git clone https://github.com/yaru22/angular-json-human.git
+$ cd angular-json-human
+$ npm install; bower install
+$ # modify the source code in src/
+$ grunt clean; grunt build
+$ # test your changes; you can modify demo/ and serve it locally to see the changes.
+$ # submit a pull request
+```
+
+TODO
+----
+- Remove the dependency on Lodash.
diff --git a/third_party/ui/bower_components/angular-json-human/bower.json b/third_party/ui/bower_components/angular-json-human/bower.json
new file mode 100644
index 0000000000..4b9cbd5d30
--- /dev/null
+++ b/third_party/ui/bower_components/angular-json-human/bower.json
@@ -0,0 +1,44 @@
+{
+ "author": {
+ "name": "Brian Park",
+ "email": "yaru22@gmail.com"
+ },
+
+ "name": "angular-json-human",
+ "description": "Angular directive to convert JSON into human readable table. Inspired by https://github.com/marianoguerra/json.human.js.",
+ "version": "1.2.1",
+ "license": "MIT",
+ "homepage": "https://github.com/yaru22/angular-json-human",
+ "repository": {
+ "type": "git",
+ "url": "git://github.com/yaru22/angular-json-human.git"
+ },
+ "main": [
+ "dist/angular-json-human.js",
+ "dist/angular-json-human.css"
+ ],
+ "ignore": [
+ ".editorconfig",
+ ".gitattributes",
+ ".gitignore",
+ ".gittrack",
+ ".jshintrc",
+ "demo/",
+ "Gruntfile.js",
+ "karma-unit.conf.js",
+ "package.json",
+ "src/",
+ "template/",
+ "test/"
+ ],
+
+ "dependencies": {
+ "angular": "^1.2.0",
+ "lodash": "~2.4.1"
+ },
+ "devDependencies": {
+ "angular-mocks": "^1.2.0",
+ "chai": "~1.9.0",
+ "jquery": "~2.1.0"
+ }
+}
diff --git a/third_party/ui/bower_components/angular-json-human/dist/angular-json-human.css b/third_party/ui/bower_components/angular-json-human/dist/angular-json-human.css
new file mode 100644
index 0000000000..880fd58f3f
--- /dev/null
+++ b/third_party/ui/bower_components/angular-json-human/dist/angular-json-human.css
@@ -0,0 +1,102 @@
+/**
+ * Angular directive to convert JSON into human readable table. Inspired by https://github.com/marianoguerra/json.human.js.
+ * @version v1.2.1 - 2014-12-22
+ * @link https://github.com/yaru22/angular-json-human
+ * @author Brian Park
+ * @license MIT License, http://www.opensource.org/licenses/MIT
+ */
+/**
+ * DISCLAIMER: This CSS is copied from https://github.com/marianoguerra/json.human.js
+ */
+
+.jh-root,
+.jh-type-object,
+.jh-type-array,
+.jh-key,
+.jh-value,
+.jh-root tr {
+ -webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */
+ -moz-box-sizing: border-box; /* Firefox, other Gecko */
+ box-sizing: border-box; /* Opera/IE 8+ */
+}
+
+.jh-key,
+.jh-value {
+ margin: 0;
+ padding: 0.2em;
+}
+
+.jh-value {
+ border-left: 1px solid #ddd;
+}
+
+.jh-type-bool,
+.jh-type-number {
+ font-weight: bold;
+ text-align: center;
+ color: #5286BC;
+}
+
+.jh-type-string {
+ font-style: italic;
+ color: #839B00;
+}
+
+.jh-array-key {
+ font-style: italic;
+ font-size: small;
+ text-align: center;
+}
+
+.jh-object-key,
+.jh-array-key {
+ color: #444;
+ vertical-align: top;
+}
+
+.jh-type-object > tbody > tr:nth-child(odd),
+.jh-type-array > tbody > tr:nth-child(odd) {
+ background-color: #f5f5f5;
+}
+
+.jh-type-object > tbody > tr:nth-child(even),
+.jh-type-array > tbody > tr:nth-child(even) {
+ background-color: #fff;
+}
+
+.jh-type-object,
+.jh-type-array {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+.jh-root {
+ border: 1px solid #ccc;
+ margin: 0.2em;
+}
+
+th.jh-key {
+ text-align: left;
+}
+
+.jh-type-object > tbody > tr,
+.jh-type-array > tbody > tr {
+ border: 1px solid #ddd;
+ border-bottom: none;
+}
+
+.jh-type-object > tbody > tr:last-child,
+.jh-type-array > tbody > tr:last-child {
+ border-bottom: 1px solid #ddd;
+}
+
+.jh-type-object > tbody > tr:hover,
+.jh-type-array > tbody > tr:hover {
+ border: 1px solid #F99927;
+}
+
+.jh-empty {
+ font-style: italic;
+ color: #999;
+ font-size: small;
+}
diff --git a/third_party/ui/bower_components/angular-json-human/dist/angular-json-human.js b/third_party/ui/bower_components/angular-json-human/dist/angular-json-human.js
new file mode 100644
index 0000000000..2be050e1e5
--- /dev/null
+++ b/third_party/ui/bower_components/angular-json-human/dist/angular-json-human.js
@@ -0,0 +1,76 @@
+/**
+ * Angular directive to convert JSON into human readable table. Inspired by https://github.com/marianoguerra/json.human.js.
+ * @version v1.2.1 - 2014-12-22
+ * @link https://github.com/yaru22/angular-json-human
+ * @author Brian Park
+ * @license MIT License, http://www.opensource.org/licenses/MIT
+ */
+/* global _, angular */
+'use strict';
+angular.module('yaru22.jsonHuman', ['yaru22.jsonHuman.tmpls']).factory('RecursionHelper', [
+ '$compile',
+ function ($compile) {
+ var RecursionHelper = {
+ compile: function (element) {
+ var contents = element.contents().remove();
+ var compiledContents;
+ return function (scope, element) {
+ if (!compiledContents) {
+ compiledContents = $compile(contents);
+ }
+ compiledContents(scope, function (clone) {
+ element.append(clone);
+ });
+ var json = scope.json;
+ scope.isBoolean = _.isBoolean(json);
+ scope.isNumber = _.isNumber(json);
+ scope.isString = _.isString(json);
+ scope.isPrimitive = scope.isBoolean || scope.isNumber || scope.isString;
+ scope.isObject = _.isPlainObject(json);
+ scope.isArray = _.isArray(json);
+ scope.isEmpty = _.isEmpty(json);
+ };
+ }
+ };
+ return RecursionHelper;
+ }
+]).directive('jsonHuman', function () {
+ return {
+ restrict: 'A',
+ scope: { data: '=jsonHuman' },
+ templateUrl: 'template/angular-json-human-root.tmpl',
+ link: function (scope) {
+ scope.$watch('data', function (json) {
+ if (typeof json === 'string') {
+ try {
+ json = JSON.parse(json);
+ } catch (e) {
+ }
+ }
+ scope.json = json;
+ scope.isObject = _.isPlainObject(json);
+ scope.isArray = _.isArray(json);
+ });
+ }
+ };
+}).directive('jsonHumanHelper', [
+ 'RecursionHelper',
+ function (RecursionHelper) {
+ return {
+ restrict: 'A',
+ scope: { json: '=jsonHumanHelper' },
+ templateUrl: 'template/angular-json-human.tmpl',
+ compile: function (tElem) {
+ return RecursionHelper.compile(tElem);
+ }
+ };
+ }
+]);
+angular.module('yaru22.jsonHuman.tmpls', []).run([
+ '$templateCache',
+ function ($templateCache) {
+ 'use strict';
+ $templateCache.put('template/angular-json-human-root.tmpl', '
")}]);
\ No newline at end of file
diff --git a/third_party/ui/bower_components/angular-material/.bower.json b/third_party/ui/bower_components/angular-material/.bower.json
new file mode 100644
index 0000000000..9270eade5c
--- /dev/null
+++ b/third_party/ui/bower_components/angular-material/.bower.json
@@ -0,0 +1,23 @@
+{
+ "name": "angular-material",
+ "version": "0.8.1",
+ "dependencies": {
+ "angular": "1.3.x",
+ "angular-animate": "1.3.x",
+ "angular-aria": "1.3.x"
+ },
+ "main": [
+ "angular-material.js",
+ "angular-material.css"
+ ],
+ "homepage": "https://github.com/angular/bower-material",
+ "_release": "0.8.1",
+ "_resolution": {
+ "type": "version",
+ "tag": "v0.8.1",
+ "commit": "53d48104a6d48b8e87bd97016acd7b5a4d7d1f3c"
+ },
+ "_source": "git://github.com/angular/bower-material.git",
+ "_target": "0.8.1",
+ "_originalSource": "angular-material"
+}
\ No newline at end of file
diff --git a/third_party/ui/bower_components/angular-material/LICENSE b/third_party/ui/bower_components/angular-material/LICENSE
new file mode 100644
index 0000000000..1a3e88cf59
--- /dev/null
+++ b/third_party/ui/bower_components/angular-material/LICENSE
@@ -0,0 +1,21 @@
+The MIT License
+
+Copyright (c) 2014 Google, Inc. http://angularjs.org
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/third_party/ui/bower_components/angular-material/README.md b/third_party/ui/bower_components/angular-material/README.md
new file mode 100644
index 0000000000..5f08071255
--- /dev/null
+++ b/third_party/ui/bower_components/angular-material/README.md
@@ -0,0 +1,169 @@
+This repo is for distribution on `npm` and `bower`. The source for this module is in the
+[main Angular Material repo](https://github.com/angular/material).
+Please file issues and pull requests against that repo.
+
+## Installing Angular Material
+
+You can install this package locally either with `npm` or with `bower`.
+
+### npm
+
+```shell
+npm install angular-material
+```
+
+Note that this package is not in CommonJS format, so doing `require('angular-material')`
+will return `undefined`. If you're using
+[Browserify](https://github.com/substack/node-browserify), you can use
+[exposify](https://github.com/thlorenz/exposify) to have `require('angular-material')`
+return the `angular-material` global.
+
+### bower
+
+```shell
+# To get the latest stable version, use bower from the command line.
+bower install angular-material
+
+# To get the most recent, last committed-to-master version use:
+bower install angular-material#master
+
+# To save the bower settings for future use:
+bower install angular-material --save
+
+# Later, you can use easily update with:
+bower update
+```
+
+> Please note that Angular Material requires **Angular 1.3.x** or higher.
+
+
+### Using the Angular Material Library
+
+Now that you have installed the Angular libraries, simply include the scripts and
+stylesheet in your main HTML file, in the order shown in the example below. Note that npm
+will install the files under `/node_modules/angular-material/` and bower will install them
+under `/bower_components/angular-material/`.
+
+### npm
+
+```html
+
+
+
+
+
+
+
+
+
');
+ $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 `