+
+
+
+ .container.ng-enter,
+ .container.ng-leave {
+ transition: all ease 1.5s;
+ }
+
+ .container.ng-enter,
+ .container.ng-leave-active {
+ opacity: 0;
+ }
+
+ .container.ng-leave,
+ .container.ng-enter-active {
+ opacity: 1;
+ }
+
+ .item {
+ background: firebrick;
+ color: #FFF;
+ margin-bottom: 10px;
+ }
+
+ .item.ng-enter,
+ .item.ng-leave {
+ transition: transform 1.5s ease;
+ }
+
+ .item.ng-enter {
+ transform: translateX(50px);
+ }
+
+ .item.ng-enter-active {
+ transform: translateX(0);
+ }
+
+
+ angular.module('ngAnimateChildren', ['ngAnimate'])
+ .controller('mainController', function() {
+ this.animateChildren = false;
+ this.enterElement = false;
+ });
+
+
+ */
+var $$AnimateChildrenDirective = ['$interpolate', function($interpolate) {
+ return {
+ link: function(scope, element, attrs) {
+ var val = attrs.ngAnimateChildren;
+ if (angular.isString(val) && val.length === 0) { //empty attribute
+ element.data(NG_ANIMATE_CHILDREN_DATA, true);
+ } else {
+ // Interpolate and set the value, so that it is available to
+ // animations that run right after compilation
+ setData($interpolate(val)(scope));
+ attrs.$observe('ngAnimateChildren', setData);
+ }
+
+ function setData(value) {
+ value = value === 'on' || value === 'true';
+ element.data(NG_ANIMATE_CHILDREN_DATA, value);
+ }
+ }
+ };
+}];
+
+var ANIMATE_TIMER_KEY = '$$animateCss';
+
+/**
+ * @ngdoc service
+ * @name $animateCss
+ * @kind object
+ *
+ * @description
+ * The `$animateCss` service is a useful utility to trigger customized CSS-based transitions/keyframes
+ * from a JavaScript-based animation or directly from a directive. The purpose of `$animateCss` is NOT
+ * to side-step how `$animate` and ngAnimate work, but the goal is to allow pre-existing animations or
+ * directives to create more complex animations that can be purely driven using CSS code.
+ *
+ * Note that only browsers that support CSS transitions and/or keyframe animations are capable of
+ * rendering animations triggered via `$animateCss` (bad news for IE9 and lower).
+ *
+ * ## Usage
+ * Once again, `$animateCss` is designed to be used inside of a registered JavaScript animation that
+ * is powered by ngAnimate. It is possible to use `$animateCss` directly inside of a directive, however,
+ * any automatic control over cancelling animations and/or preventing animations from being run on
+ * child elements will not be handled by Angular. For this to work as expected, please use `$animate` to
+ * trigger the animation and then setup a JavaScript animation that injects `$animateCss` to trigger
+ * the CSS animation.
+ *
+ * The example below shows how we can create a folding animation on an element using `ng-if`:
+ *
+ * ```html
+ *
+ *
+ * This element will go BOOM
+ *
+ *
+ * ```
+ *
+ * Now we create the **JavaScript animation** that will trigger the CSS transition:
+ *
+ * ```js
+ * ngModule.animation('.fold-animation', ['$animateCss', function($animateCss) {
+ * return {
+ * enter: function(element, doneFn) {
+ * var height = element[0].offsetHeight;
+ * return $animateCss(element, {
+ * from: { height:'0px' },
+ * to: { height:height + 'px' },
+ * duration: 1 // one second
+ * });
+ * }
+ * }
+ * }]);
+ * ```
+ *
+ * ## More Advanced Uses
+ *
+ * `$animateCss` is the underlying code that ngAnimate uses to power **CSS-based animations** behind the scenes. Therefore CSS hooks
+ * like `.ng-EVENT`, `.ng-EVENT-active`, `.ng-EVENT-stagger` are all features that can be triggered using `$animateCss` via JavaScript code.
+ *
+ * This also means that just about any combination of adding classes, removing classes, setting styles, dynamically setting a keyframe animation,
+ * applying a hardcoded duration or delay value, changing the animation easing or applying a stagger animation are all options that work with
+ * `$animateCss`. The service itself is smart enough to figure out the combination of options and examine the element styling properties in order
+ * to provide a working animation that will run in CSS.
+ *
+ * The example below showcases a more advanced version of the `.fold-animation` from the example above:
+ *
+ * ```js
+ * ngModule.animation('.fold-animation', ['$animateCss', function($animateCss) {
+ * return {
+ * enter: function(element, doneFn) {
+ * var height = element[0].offsetHeight;
+ * return $animateCss(element, {
+ * addClass: 'red large-text pulse-twice',
+ * easing: 'ease-out',
+ * from: { height:'0px' },
+ * to: { height:height + 'px' },
+ * duration: 1 // one second
+ * });
+ * }
+ * }
+ * }]);
+ * ```
+ *
+ * Since we're adding/removing CSS classes then the CSS transition will also pick those up:
+ *
+ * ```css
+ * /* since a hardcoded duration value of 1 was provided in the JavaScript animation code,
+ * the CSS classes below will be transitioned despite them being defined as regular CSS classes */
+ * .red { background:red; }
+ * .large-text { font-size:20px; }
+ *
+ * /* we can also use a keyframe animation and $animateCss will make it work alongside the transition */
+ * .pulse-twice {
+ * animation: 0.5s pulse linear 2;
+ * -webkit-animation: 0.5s pulse linear 2;
+ * }
+ *
+ * @keyframes pulse {
+ * from { transform: scale(0.5); }
+ * to { transform: scale(1.5); }
+ * }
+ *
+ * @-webkit-keyframes pulse {
+ * from { -webkit-transform: scale(0.5); }
+ * to { -webkit-transform: scale(1.5); }
+ * }
+ * ```
+ *
+ * Given this complex combination of CSS classes, styles and options, `$animateCss` will figure everything out and make the animation happen.
+ *
+ * ## How the Options are handled
+ *
+ * `$animateCss` is very versatile and intelligent when it comes to figuring out what configurations to apply to the element to ensure the animation
+ * works with the options provided. Say for example we were adding a class that contained a keyframe value and we wanted to also animate some inline
+ * styles using the `from` and `to` properties.
+ *
+ * ```js
+ * var animator = $animateCss(element, {
+ * from: { background:'red' },
+ * to: { background:'blue' }
+ * });
+ * animator.start();
+ * ```
+ *
+ * ```css
+ * .rotating-animation {
+ * animation:0.5s rotate linear;
+ * -webkit-animation:0.5s rotate linear;
+ * }
+ *
+ * @keyframes rotate {
+ * from { transform: rotate(0deg); }
+ * to { transform: rotate(360deg); }
+ * }
+ *
+ * @-webkit-keyframes rotate {
+ * from { -webkit-transform: rotate(0deg); }
+ * to { -webkit-transform: rotate(360deg); }
+ * }
+ * ```
+ *
+ * The missing pieces here are that we do not have a transition set (within the CSS code nor within the `$animateCss` options) and the duration of the animation is
+ * going to be detected from what the keyframe styles on the CSS class are. In this event, `$animateCss` will automatically create an inline transition
+ * style matching the duration detected from the keyframe style (which is present in the CSS class that is being added) and then prepare both the transition
+ * and keyframe animations to run in parallel on the element. Then when the animation is underway the provided `from` and `to` CSS styles will be applied
+ * and spread across the transition and keyframe animation.
+ *
+ * ## What is returned
+ *
+ * `$animateCss` works in two stages: a preparation phase and an animation phase. Therefore when `$animateCss` is first called it will NOT actually
+ * start the animation. All that is going on here is that the element is being prepared for the animation (which means that the generated CSS classes are
+ * added and removed on the element). Once `$animateCss` is called it will return an object with the following properties:
+ *
+ * ```js
+ * var animator = $animateCss(element, { ... });
+ * ```
+ *
+ * Now what do the contents of our `animator` variable look like:
+ *
+ * ```js
+ * {
+ * // starts the animation
+ * start: Function,
+ *
+ * // ends (aborts) the animation
+ * end: Function
+ * }
+ * ```
+ *
+ * To actually start the animation we need to run `animation.start()` which will then return a promise that we can hook into to detect when the animation ends.
+ * If we choose not to run the animation then we MUST run `animation.end()` to perform a cleanup on the element (since some CSS classes and stlyes may have been
+ * applied to the element during the preparation phase). Note that all other properties such as duration, delay, transitions and keyframes are just properties
+ * and that changing them will not reconfigure the parameters of the animation.
+ *
+ * ### runner.done() vs runner.then()
+ * It is documented that `animation.start()` will return a promise object and this is true, however, there is also an additional method available on the
+ * runner called `.done(callbackFn)`. The done method works the same as `.finally(callbackFn)`, however, it does **not trigger a digest to occur**.
+ * Therefore, for performance reasons, it's always best to use `runner.done(callback)` instead of `runner.then()`, `runner.catch()` or `runner.finally()`
+ * unless you really need a digest to kick off afterwards.
+ *
+ * Keep in mind that, to make this easier, ngAnimate has tweaked the JS animations API to recognize when a runner instance is returned from $animateCss
+ * (so there is no need to call `runner.done(doneFn)` inside of your JavaScript animation code).
+ * Check the {@link ngAnimate.$animateCss#usage animation code above} to see how this works.
+ *
+ * @param {DOMElement} element the element that will be animated
+ * @param {object} options the animation-related options that will be applied during the animation
+ *
+ * * `event` - The DOM event (e.g. enter, leave, move). When used, a generated CSS class of `ng-EVENT` and `ng-EVENT-active` will be applied
+ * to the element during the animation. Multiple events can be provided when spaces are used as a separator. (Note that this will not perform any DOM operation.)
+ * * `structural` - Indicates that the `ng-` prefix will be added to the event class. Setting to `false` or omitting will turn `ng-EVENT` and
+ * `ng-EVENT-active` in `EVENT` and `EVENT-active`. Unused if `event` is omitted.
+ * * `easing` - The CSS easing value that will be applied to the transition or keyframe animation (or both).
+ * * `transitionStyle` - The raw CSS transition style that will be used (e.g. `1s linear all`).
+ * * `keyframeStyle` - The raw CSS keyframe animation style that will be used (e.g. `1s my_animation linear`).
+ * * `from` - The starting CSS styles (a key/value object) that will be applied at the start of the animation.
+ * * `to` - The ending CSS styles (a key/value object) that will be applied across the animation via a CSS transition.
+ * * `addClass` - A space separated list of CSS classes that will be added to the element and spread across the animation.
+ * * `removeClass` - A space separated list of CSS classes that will be removed from the element and spread across the animation.
+ * * `duration` - A number value representing the total duration of the transition and/or keyframe (note that a value of 1 is 1000ms). If a value of `0`
+ * is provided then the animation will be skipped entirely.
+ * * `delay` - A number value representing the total delay of the transition and/or keyframe (note that a value of 1 is 1000ms). If a value of `true` is
+ * used then whatever delay value is detected from the CSS classes will be mirrored on the elements styles (e.g. by setting delay true then the style value
+ * of the element will be `transition-delay: DETECTED_VALUE`). Using `true` is useful when you want the CSS classes and inline styles to all share the same
+ * CSS delay value.
+ * * `stagger` - A numeric time value representing the delay between successively animated elements
+ * ({@link ngAnimate#css-staggering-animations Click here to learn how CSS-based staggering works in ngAnimate.})
+ * * `staggerIndex` - The numeric index representing the stagger item (e.g. a value of 5 is equal to the sixth item in the stagger; therefore when a
+ * * `stagger` option value of `0.1` is used then there will be a stagger delay of `600ms`)
+ * * `applyClassesEarly` - Whether or not the classes being added or removed will be used when detecting the animation. This is set by `$animate` when enter/leave/move animations are fired to ensure that the CSS classes are resolved in time. (Note that this will prevent any transitions from occuring on the classes being added and removed.)
+ * * `cleanupStyles` - Whether or not the provided `from` and `to` styles will be removed once
+ * the animation is closed. This is useful for when the styles are used purely for the sake of
+ * the animation and do not have a lasting visual effect on the element (e.g. a colapse and open animation).
+ * By default this value is set to `false`.
+ *
+ * @return {object} an object with start and end methods and details about the animation.
+ *
+ * * `start` - The method to start the animation. This will return a `Promise` when called.
+ * * `end` - This method will cancel the animation and remove all applied CSS classes and styles.
+ */
+var ONE_SECOND = 1000;
+var BASE_TEN = 10;
+
+var ELAPSED_TIME_MAX_DECIMAL_PLACES = 3;
+var CLOSING_TIME_BUFFER = 1.5;
+
+var DETECT_CSS_PROPERTIES = {
+ transitionDuration: TRANSITION_DURATION_PROP,
+ transitionDelay: TRANSITION_DELAY_PROP,
+ transitionProperty: TRANSITION_PROP + PROPERTY_KEY,
+ animationDuration: ANIMATION_DURATION_PROP,
+ animationDelay: ANIMATION_DELAY_PROP,
+ animationIterationCount: ANIMATION_PROP + ANIMATION_ITERATION_COUNT_KEY
+};
+
+var DETECT_STAGGER_CSS_PROPERTIES = {
+ transitionDuration: TRANSITION_DURATION_PROP,
+ transitionDelay: TRANSITION_DELAY_PROP,
+ animationDuration: ANIMATION_DURATION_PROP,
+ animationDelay: ANIMATION_DELAY_PROP
+};
+
+function getCssKeyframeDurationStyle(duration) {
+ return [ANIMATION_DURATION_PROP, duration + 's'];
+}
+
+function getCssDelayStyle(delay, isKeyframeAnimation) {
+ var prop = isKeyframeAnimation ? ANIMATION_DELAY_PROP : TRANSITION_DELAY_PROP;
+ return [prop, delay + 's'];
+}
+
+function computeCssStyles($window, element, properties) {
+ var styles = Object.create(null);
+ var detectedStyles = $window.getComputedStyle(element) || {};
+ forEach(properties, function(formalStyleName, actualStyleName) {
+ var val = detectedStyles[formalStyleName];
+ if (val) {
+ var c = val.charAt(0);
+
+ // only numerical-based values have a negative sign or digit as the first value
+ if (c === '-' || c === '+' || c >= 0) {
+ val = parseMaxTime(val);
+ }
+
+ // by setting this to null in the event that the delay is not set or is set directly as 0
+ // then we can still allow for zegative values to be used later on and not mistake this
+ // value for being greater than any other negative value.
+ if (val === 0) {
+ val = null;
+ }
+ styles[actualStyleName] = val;
+ }
+ });
+
+ return styles;
+}
+
+function parseMaxTime(str) {
+ var maxValue = 0;
+ var values = str.split(/\s*,\s*/);
+ forEach(values, function(value) {
+ // it's always safe to consider only second values and omit `ms` values since
+ // getComputedStyle will always handle the conversion for us
+ if (value.charAt(value.length - 1) == 's') {
+ value = value.substring(0, value.length - 1);
+ }
+ value = parseFloat(value) || 0;
+ maxValue = maxValue ? Math.max(value, maxValue) : value;
+ });
+ return maxValue;
+}
+
+function truthyTimingValue(val) {
+ return val === 0 || val != null;
+}
+
+function getCssTransitionDurationStyle(duration, applyOnlyDuration) {
+ var style = TRANSITION_PROP;
+ var value = duration + 's';
+ if (applyOnlyDuration) {
+ style += DURATION_KEY;
+ } else {
+ value += ' linear all';
+ }
+ return [style, value];
+}
+
+function createLocalCacheLookup() {
+ var cache = Object.create(null);
+ return {
+ flush: function() {
+ cache = Object.create(null);
+ },
+
+ count: function(key) {
+ var entry = cache[key];
+ return entry ? entry.total : 0;
+ },
+
+ get: function(key) {
+ var entry = cache[key];
+ return entry && entry.value;
+ },
+
+ put: function(key, value) {
+ if (!cache[key]) {
+ cache[key] = { total: 1, value: value };
+ } else {
+ cache[key].total++;
+ }
+ }
+ };
+}
+
+// we do not reassign an already present style value since
+// if we detect the style property value again we may be
+// detecting styles that were added via the `from` styles.
+// We make use of `isDefined` here since an empty string
+// or null value (which is what getPropertyValue will return
+// for a non-existing style) will still be marked as a valid
+// value for the style (a falsy value implies that the style
+// is to be removed at the end of the animation). If we had a simple
+// "OR" statement then it would not be enough to catch that.
+function registerRestorableStyles(backup, node, properties) {
+ forEach(properties, function(prop) {
+ backup[prop] = isDefined(backup[prop])
+ ? backup[prop]
+ : node.style.getPropertyValue(prop);
+ });
+}
+
+var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
+ var gcsLookup = createLocalCacheLookup();
+ var gcsStaggerLookup = createLocalCacheLookup();
+
+ this.$get = ['$window', '$$jqLite', '$$AnimateRunner', '$timeout',
+ '$$forceReflow', '$sniffer', '$$rAFScheduler', '$$animateQueue',
+ function($window, $$jqLite, $$AnimateRunner, $timeout,
+ $$forceReflow, $sniffer, $$rAFScheduler, $$animateQueue) {
+
+ var applyAnimationClasses = applyAnimationClassesFactory($$jqLite);
+
+ var parentCounter = 0;
+ function gcsHashFn(node, extraClasses) {
+ var KEY = "$$ngAnimateParentKey";
+ var parentNode = node.parentNode;
+ var parentID = parentNode[KEY] || (parentNode[KEY] = ++parentCounter);
+ return parentID + '-' + node.getAttribute('class') + '-' + extraClasses;
+ }
+
+ function computeCachedCssStyles(node, className, cacheKey, properties) {
+ var timings = gcsLookup.get(cacheKey);
+
+ if (!timings) {
+ timings = computeCssStyles($window, node, properties);
+ if (timings.animationIterationCount === 'infinite') {
+ timings.animationIterationCount = 1;
+ }
+ }
+
+ // we keep putting this in multiple times even though the value and the cacheKey are the same
+ // because we're keeping an interal tally of how many duplicate animations are detected.
+ gcsLookup.put(cacheKey, timings);
+ return timings;
+ }
+
+ function computeCachedCssStaggerStyles(node, className, cacheKey, properties) {
+ var stagger;
+
+ // if we have one or more existing matches of matching elements
+ // containing the same parent + CSS styles (which is how cacheKey works)
+ // then staggering is possible
+ if (gcsLookup.count(cacheKey) > 0) {
+ stagger = gcsStaggerLookup.get(cacheKey);
+
+ if (!stagger) {
+ var staggerClassName = pendClasses(className, '-stagger');
+
+ $$jqLite.addClass(node, staggerClassName);
+
+ stagger = computeCssStyles($window, node, properties);
+
+ // force the conversion of a null value to zero incase not set
+ stagger.animationDuration = Math.max(stagger.animationDuration, 0);
+ stagger.transitionDuration = Math.max(stagger.transitionDuration, 0);
+
+ $$jqLite.removeClass(node, staggerClassName);
+
+ gcsStaggerLookup.put(cacheKey, stagger);
+ }
+ }
+
+ return stagger || {};
+ }
+
+ var cancelLastRAFRequest;
+ var rafWaitQueue = [];
+ function waitUntilQuiet(callback) {
+ rafWaitQueue.push(callback);
+ $$rAFScheduler.waitUntilQuiet(function() {
+ gcsLookup.flush();
+ gcsStaggerLookup.flush();
+
+ // DO NOT REMOVE THIS LINE OR REFACTOR OUT THE `pageWidth` variable.
+ // PLEASE EXAMINE THE `$$forceReflow` service to understand why.
+ var pageWidth = $$forceReflow();
+
+ // we use a for loop to ensure that if the queue is changed
+ // during this looping then it will consider new requests
+ for (var i = 0; i < rafWaitQueue.length; i++) {
+ rafWaitQueue[i](pageWidth);
+ }
+ rafWaitQueue.length = 0;
+ });
+ }
+
+ function computeTimings(node, className, cacheKey) {
+ var timings = computeCachedCssStyles(node, className, cacheKey, DETECT_CSS_PROPERTIES);
+ var aD = timings.animationDelay;
+ var tD = timings.transitionDelay;
+ timings.maxDelay = aD && tD
+ ? Math.max(aD, tD)
+ : (aD || tD);
+ timings.maxDuration = Math.max(
+ timings.animationDuration * timings.animationIterationCount,
+ timings.transitionDuration);
+
+ return timings;
+ }
+
+ return function init(element, initialOptions) {
+ // all of the animation functions should create
+ // a copy of the options data, however, if a
+ // parent service has already created a copy then
+ // we should stick to using that
+ var options = initialOptions || {};
+ if (!options.$$prepared) {
+ options = prepareAnimationOptions(copy(options));
+ }
+
+ var restoreStyles = {};
+ var node = getDomNode(element);
+ if (!node
+ || !node.parentNode
+ || !$$animateQueue.enabled()) {
+ return closeAndReturnNoopAnimator();
+ }
+
+ var temporaryStyles = [];
+ var classes = element.attr('class');
+ var styles = packageStyles(options);
+ var animationClosed;
+ var animationPaused;
+ var animationCompleted;
+ var runner;
+ var runnerHost;
+ var maxDelay;
+ var maxDelayTime;
+ var maxDuration;
+ var maxDurationTime;
+ var startTime;
+ var events = [];
+
+ if (options.duration === 0 || (!$sniffer.animations && !$sniffer.transitions)) {
+ return closeAndReturnNoopAnimator();
+ }
+
+ var method = options.event && isArray(options.event)
+ ? options.event.join(' ')
+ : options.event;
+
+ var isStructural = method && options.structural;
+ var structuralClassName = '';
+ var addRemoveClassName = '';
+
+ if (isStructural) {
+ structuralClassName = pendClasses(method, EVENT_CLASS_PREFIX, true);
+ } else if (method) {
+ structuralClassName = method;
+ }
+
+ if (options.addClass) {
+ addRemoveClassName += pendClasses(options.addClass, ADD_CLASS_SUFFIX);
+ }
+
+ if (options.removeClass) {
+ if (addRemoveClassName.length) {
+ addRemoveClassName += ' ';
+ }
+ addRemoveClassName += pendClasses(options.removeClass, REMOVE_CLASS_SUFFIX);
+ }
+
+ // there may be a situation where a structural animation is combined together
+ // with CSS classes that need to resolve before the animation is computed.
+ // However this means that there is no explicit CSS code to block the animation
+ // from happening (by setting 0s none in the class name). If this is the case
+ // we need to apply the classes before the first rAF so we know to continue if
+ // there actually is a detected transition or keyframe animation
+ if (options.applyClassesEarly && addRemoveClassName.length) {
+ applyAnimationClasses(element, options);
+ }
+
+ var preparationClasses = [structuralClassName, addRemoveClassName].join(' ').trim();
+ var fullClassName = classes + ' ' + preparationClasses;
+ var activeClasses = pendClasses(preparationClasses, ACTIVE_CLASS_SUFFIX);
+ var hasToStyles = styles.to && Object.keys(styles.to).length > 0;
+ var containsKeyframeAnimation = (options.keyframeStyle || '').length > 0;
+
+ // there is no way we can trigger an animation if no styles and
+ // no classes are being applied which would then trigger a transition,
+ // unless there a is raw keyframe value that is applied to the element.
+ if (!containsKeyframeAnimation
+ && !hasToStyles
+ && !preparationClasses) {
+ return closeAndReturnNoopAnimator();
+ }
+
+ var cacheKey, stagger;
+ if (options.stagger > 0) {
+ var staggerVal = parseFloat(options.stagger);
+ stagger = {
+ transitionDelay: staggerVal,
+ animationDelay: staggerVal,
+ transitionDuration: 0,
+ animationDuration: 0
+ };
+ } else {
+ cacheKey = gcsHashFn(node, fullClassName);
+ stagger = computeCachedCssStaggerStyles(node, preparationClasses, cacheKey, DETECT_STAGGER_CSS_PROPERTIES);
+ }
+
+ if (!options.$$skipPreparationClasses) {
+ $$jqLite.addClass(element, preparationClasses);
+ }
+
+ var applyOnlyDuration;
+
+ if (options.transitionStyle) {
+ var transitionStyle = [TRANSITION_PROP, options.transitionStyle];
+ applyInlineStyle(node, transitionStyle);
+ temporaryStyles.push(transitionStyle);
+ }
+
+ if (options.duration >= 0) {
+ applyOnlyDuration = node.style[TRANSITION_PROP].length > 0;
+ var durationStyle = getCssTransitionDurationStyle(options.duration, applyOnlyDuration);
+
+ // we set the duration so that it will be picked up by getComputedStyle later
+ applyInlineStyle(node, durationStyle);
+ temporaryStyles.push(durationStyle);
+ }
+
+ if (options.keyframeStyle) {
+ var keyframeStyle = [ANIMATION_PROP, options.keyframeStyle];
+ applyInlineStyle(node, keyframeStyle);
+ temporaryStyles.push(keyframeStyle);
+ }
+
+ var itemIndex = stagger
+ ? options.staggerIndex >= 0
+ ? options.staggerIndex
+ : gcsLookup.count(cacheKey)
+ : 0;
+
+ var isFirst = itemIndex === 0;
+
+ // this is a pre-emptive way of forcing the setup classes to be added and applied INSTANTLY
+ // without causing any combination of transitions to kick in. By adding a negative delay value
+ // it forces the setup class' transition to end immediately. We later then remove the negative
+ // transition delay to allow for the transition to naturally do it's thing. The beauty here is
+ // that if there is no transition defined then nothing will happen and this will also allow
+ // other transitions to be stacked on top of each other without any chopping them out.
+ if (isFirst && !options.skipBlocking) {
+ blockTransitions(node, SAFE_FAST_FORWARD_DURATION_VALUE);
+ }
+
+ var timings = computeTimings(node, fullClassName, cacheKey);
+ var relativeDelay = timings.maxDelay;
+ maxDelay = Math.max(relativeDelay, 0);
+ maxDuration = timings.maxDuration;
+
+ var flags = {};
+ flags.hasTransitions = timings.transitionDuration > 0;
+ flags.hasAnimations = timings.animationDuration > 0;
+ flags.hasTransitionAll = flags.hasTransitions && timings.transitionProperty == 'all';
+ flags.applyTransitionDuration = hasToStyles && (
+ (flags.hasTransitions && !flags.hasTransitionAll)
+ || (flags.hasAnimations && !flags.hasTransitions));
+ flags.applyAnimationDuration = options.duration && flags.hasAnimations;
+ flags.applyTransitionDelay = truthyTimingValue(options.delay) && (flags.applyTransitionDuration || flags.hasTransitions);
+ flags.applyAnimationDelay = truthyTimingValue(options.delay) && flags.hasAnimations;
+ flags.recalculateTimingStyles = addRemoveClassName.length > 0;
+
+ if (flags.applyTransitionDuration || flags.applyAnimationDuration) {
+ maxDuration = options.duration ? parseFloat(options.duration) : maxDuration;
+
+ if (flags.applyTransitionDuration) {
+ flags.hasTransitions = true;
+ timings.transitionDuration = maxDuration;
+ applyOnlyDuration = node.style[TRANSITION_PROP + PROPERTY_KEY].length > 0;
+ temporaryStyles.push(getCssTransitionDurationStyle(maxDuration, applyOnlyDuration));
+ }
+
+ if (flags.applyAnimationDuration) {
+ flags.hasAnimations = true;
+ timings.animationDuration = maxDuration;
+ temporaryStyles.push(getCssKeyframeDurationStyle(maxDuration));
+ }
+ }
+
+ if (maxDuration === 0 && !flags.recalculateTimingStyles) {
+ return closeAndReturnNoopAnimator();
+ }
+
+ if (options.delay != null) {
+ var delayStyle;
+ if (typeof options.delay !== "boolean") {
+ delayStyle = parseFloat(options.delay);
+ // number in options.delay means we have to recalculate the delay for the closing timeout
+ maxDelay = Math.max(delayStyle, 0);
+ }
+
+ if (flags.applyTransitionDelay) {
+ temporaryStyles.push(getCssDelayStyle(delayStyle));
+ }
+
+ if (flags.applyAnimationDelay) {
+ temporaryStyles.push(getCssDelayStyle(delayStyle, true));
+ }
+ }
+
+ // we need to recalculate the delay value since we used a pre-emptive negative
+ // delay value and the delay value is required for the final event checking. This
+ // property will ensure that this will happen after the RAF phase has passed.
+ if (options.duration == null && timings.transitionDuration > 0) {
+ flags.recalculateTimingStyles = flags.recalculateTimingStyles || isFirst;
+ }
+
+ maxDelayTime = maxDelay * ONE_SECOND;
+ maxDurationTime = maxDuration * ONE_SECOND;
+ if (!options.skipBlocking) {
+ flags.blockTransition = timings.transitionDuration > 0;
+ flags.blockKeyframeAnimation = timings.animationDuration > 0 &&
+ stagger.animationDelay > 0 &&
+ stagger.animationDuration === 0;
+ }
+
+ if (options.from) {
+ if (options.cleanupStyles) {
+ registerRestorableStyles(restoreStyles, node, Object.keys(options.from));
+ }
+ applyAnimationFromStyles(element, options);
+ }
+
+ if (flags.blockTransition || flags.blockKeyframeAnimation) {
+ applyBlocking(maxDuration);
+ } else if (!options.skipBlocking) {
+ blockTransitions(node, false);
+ }
+
+ // TODO(matsko): for 1.5 change this code to have an animator object for better debugging
+ return {
+ $$willAnimate: true,
+ end: endFn,
+ start: function() {
+ if (animationClosed) return;
+
+ runnerHost = {
+ end: endFn,
+ cancel: cancelFn,
+ resume: null, //this will be set during the start() phase
+ pause: null
+ };
+
+ runner = new $$AnimateRunner(runnerHost);
+
+ waitUntilQuiet(start);
+
+ // we don't have access to pause/resume the animation
+ // since it hasn't run yet. AnimateRunner will therefore
+ // set noop functions for resume and pause and they will
+ // later be overridden once the animation is triggered
+ return runner;
+ }
+ };
+
+ function endFn() {
+ close();
+ }
+
+ function cancelFn() {
+ close(true);
+ }
+
+ function close(rejected) { // jshint ignore:line
+ // if the promise has been called already then we shouldn't close
+ // the animation again
+ if (animationClosed || (animationCompleted && animationPaused)) return;
+ animationClosed = true;
+ animationPaused = false;
+
+ if (!options.$$skipPreparationClasses) {
+ $$jqLite.removeClass(element, preparationClasses);
+ }
+ $$jqLite.removeClass(element, activeClasses);
+
+ blockKeyframeAnimations(node, false);
+ blockTransitions(node, false);
+
+ forEach(temporaryStyles, function(entry) {
+ // There is only one way to remove inline style properties entirely from elements.
+ // By using `removeProperty` this works, but we need to convert camel-cased CSS
+ // styles down to hyphenated values.
+ node.style[entry[0]] = '';
+ });
+
+ applyAnimationClasses(element, options);
+ applyAnimationStyles(element, options);
+
+ if (Object.keys(restoreStyles).length) {
+ forEach(restoreStyles, function(value, prop) {
+ value ? node.style.setProperty(prop, value)
+ : node.style.removeProperty(prop);
+ });
+ }
+
+ // the reason why we have this option is to allow a synchronous closing callback
+ // that is fired as SOON as the animation ends (when the CSS is removed) or if
+ // the animation never takes off at all. A good example is a leave animation since
+ // the element must be removed just after the animation is over or else the element
+ // will appear on screen for one animation frame causing an overbearing flicker.
+ if (options.onDone) {
+ options.onDone();
+ }
+
+ if (events && events.length) {
+ // Remove the transitionend / animationend listener(s)
+ element.off(events.join(' '), onAnimationProgress);
+ }
+
+ //Cancel the fallback closing timeout and remove the timer data
+ var animationTimerData = element.data(ANIMATE_TIMER_KEY);
+ if (animationTimerData) {
+ $timeout.cancel(animationTimerData[0].timer);
+ element.removeData(ANIMATE_TIMER_KEY);
+ }
+
+ // if the preparation function fails then the promise is not setup
+ if (runner) {
+ runner.complete(!rejected);
+ }
+ }
+
+ function applyBlocking(duration) {
+ if (flags.blockTransition) {
+ blockTransitions(node, duration);
+ }
+
+ if (flags.blockKeyframeAnimation) {
+ blockKeyframeAnimations(node, !!duration);
+ }
+ }
+
+ function closeAndReturnNoopAnimator() {
+ runner = new $$AnimateRunner({
+ end: endFn,
+ cancel: cancelFn
+ });
+
+ // should flush the cache animation
+ waitUntilQuiet(noop);
+ close();
+
+ return {
+ $$willAnimate: false,
+ start: function() {
+ return runner;
+ },
+ end: endFn
+ };
+ }
+
+ function onAnimationProgress(event) {
+ event.stopPropagation();
+ var ev = event.originalEvent || event;
+
+ // we now always use `Date.now()` due to the recent changes with
+ // event.timeStamp in Firefox, Webkit and Chrome (see #13494 for more info)
+ var timeStamp = ev.$manualTimeStamp || 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 animationPauseds sometimes close off early */
+ if (Math.max(timeStamp - startTime, 0) >= maxDelayTime && elapsedTime >= maxDuration) {
+ // we set this flag to ensure that if the transition is paused then, when resumed,
+ // the animation will automatically close itself since transitions cannot be paused.
+ animationCompleted = true;
+ close();
+ }
+ }
+
+ function start() {
+ if (animationClosed) return;
+ if (!node.parentNode) {
+ close();
+ return;
+ }
+
+ // even though we only pause keyframe animations here the pause flag
+ // will still happen when transitions are used. Only the transition will
+ // not be paused since that is not possible. If the animation ends when
+ // paused then it will not complete until unpaused or cancelled.
+ var playPause = function(playAnimation) {
+ if (!animationCompleted) {
+ animationPaused = !playAnimation;
+ if (timings.animationDuration) {
+ var value = blockKeyframeAnimations(node, animationPaused);
+ animationPaused
+ ? temporaryStyles.push(value)
+ : removeFromArray(temporaryStyles, value);
+ }
+ } else if (animationPaused && playAnimation) {
+ animationPaused = false;
+ close();
+ }
+ };
+
+ // checking the stagger duration prevents an accidently cascade of the CSS delay style
+ // being inherited from the parent. If the transition duration is zero then we can safely
+ // rely that the delay value is an intential stagger delay style.
+ var maxStagger = itemIndex > 0
+ && ((timings.transitionDuration && stagger.transitionDuration === 0) ||
+ (timings.animationDuration && stagger.animationDuration === 0))
+ && Math.max(stagger.animationDelay, stagger.transitionDelay);
+ if (maxStagger) {
+ $timeout(triggerAnimationStart,
+ Math.floor(maxStagger * itemIndex * ONE_SECOND),
+ false);
+ } else {
+ triggerAnimationStart();
+ }
+
+ // this will decorate the existing promise runner with pause/resume methods
+ runnerHost.resume = function() {
+ playPause(true);
+ };
+
+ runnerHost.pause = function() {
+ playPause(false);
+ };
+
+ function triggerAnimationStart() {
+ // just incase a stagger animation kicks in when the animation
+ // itself was cancelled entirely
+ if (animationClosed) return;
+
+ applyBlocking(false);
+
+ forEach(temporaryStyles, function(entry) {
+ var key = entry[0];
+ var value = entry[1];
+ node.style[key] = value;
+ });
+
+ applyAnimationClasses(element, options);
+ $$jqLite.addClass(element, activeClasses);
+
+ if (flags.recalculateTimingStyles) {
+ fullClassName = node.className + ' ' + preparationClasses;
+ cacheKey = gcsHashFn(node, fullClassName);
+
+ timings = computeTimings(node, fullClassName, cacheKey);
+ relativeDelay = timings.maxDelay;
+ maxDelay = Math.max(relativeDelay, 0);
+ maxDuration = timings.maxDuration;
+
+ if (maxDuration === 0) {
+ close();
+ return;
+ }
+
+ flags.hasTransitions = timings.transitionDuration > 0;
+ flags.hasAnimations = timings.animationDuration > 0;
+ }
+
+ if (flags.applyAnimationDelay) {
+ relativeDelay = typeof options.delay !== "boolean" && truthyTimingValue(options.delay)
+ ? parseFloat(options.delay)
+ : relativeDelay;
+
+ maxDelay = Math.max(relativeDelay, 0);
+ timings.animationDelay = relativeDelay;
+ delayStyle = getCssDelayStyle(relativeDelay, true);
+ temporaryStyles.push(delayStyle);
+ node.style[delayStyle[0]] = delayStyle[1];
+ }
+
+ maxDelayTime = maxDelay * ONE_SECOND;
+ maxDurationTime = maxDuration * ONE_SECOND;
+
+ if (options.easing) {
+ var easeProp, easeVal = options.easing;
+ if (flags.hasTransitions) {
+ easeProp = TRANSITION_PROP + TIMING_KEY;
+ temporaryStyles.push([easeProp, easeVal]);
+ node.style[easeProp] = easeVal;
+ }
+ if (flags.hasAnimations) {
+ easeProp = ANIMATION_PROP + TIMING_KEY;
+ temporaryStyles.push([easeProp, easeVal]);
+ node.style[easeProp] = easeVal;
+ }
+ }
+
+ if (timings.transitionDuration) {
+ events.push(TRANSITIONEND_EVENT);
+ }
+
+ if (timings.animationDuration) {
+ events.push(ANIMATIONEND_EVENT);
+ }
+
+ startTime = Date.now();
+ var timerTime = maxDelayTime + CLOSING_TIME_BUFFER * maxDurationTime;
+ var endTime = startTime + timerTime;
+
+ var animationsData = element.data(ANIMATE_TIMER_KEY) || [];
+ var setupFallbackTimer = true;
+ if (animationsData.length) {
+ var currentTimerData = animationsData[0];
+ setupFallbackTimer = endTime > currentTimerData.expectedEndTime;
+ if (setupFallbackTimer) {
+ $timeout.cancel(currentTimerData.timer);
+ } else {
+ animationsData.push(close);
+ }
+ }
+
+ if (setupFallbackTimer) {
+ var timer = $timeout(onAnimationExpired, timerTime, false);
+ animationsData[0] = {
+ timer: timer,
+ expectedEndTime: endTime
+ };
+ animationsData.push(close);
+ element.data(ANIMATE_TIMER_KEY, animationsData);
+ }
+
+ if (events.length) {
+ element.on(events.join(' '), onAnimationProgress);
+ }
+
+ if (options.to) {
+ if (options.cleanupStyles) {
+ registerRestorableStyles(restoreStyles, node, Object.keys(options.to));
+ }
+ applyAnimationToStyles(element, options);
+ }
+ }
+
+ function onAnimationExpired() {
+ var animationsData = element.data(ANIMATE_TIMER_KEY);
+
+ // this will be false in the event that the element was
+ // removed from the DOM (via a leave animation or something
+ // similar)
+ if (animationsData) {
+ for (var i = 1; i < animationsData.length; i++) {
+ animationsData[i]();
+ }
+ element.removeData(ANIMATE_TIMER_KEY);
+ }
+ }
+ }
+ };
+ }];
+}];
+
+var $$AnimateCssDriverProvider = ['$$animationProvider', function($$animationProvider) {
+ $$animationProvider.drivers.push('$$animateCssDriver');
+
+ var NG_ANIMATE_SHIM_CLASS_NAME = 'ng-animate-shim';
+ var NG_ANIMATE_ANCHOR_CLASS_NAME = 'ng-anchor';
+
+ var NG_OUT_ANCHOR_CLASS_NAME = 'ng-anchor-out';
+ var NG_IN_ANCHOR_CLASS_NAME = 'ng-anchor-in';
+
+ function isDocumentFragment(node) {
+ return node.parentNode && node.parentNode.nodeType === 11;
+ }
+
+ this.$get = ['$animateCss', '$rootScope', '$$AnimateRunner', '$rootElement', '$sniffer', '$$jqLite', '$document',
+ function($animateCss, $rootScope, $$AnimateRunner, $rootElement, $sniffer, $$jqLite, $document) {
+
+ // only browsers that support these properties can render animations
+ if (!$sniffer.animations && !$sniffer.transitions) return noop;
+
+ var bodyNode = $document[0].body;
+ var rootNode = getDomNode($rootElement);
+
+ var rootBodyElement = jqLite(
+ // this is to avoid using something that exists outside of the body
+ // we also special case the doc fragement case because our unit test code
+ // appends the $rootElement to the body after the app has been bootstrapped
+ isDocumentFragment(rootNode) || bodyNode.contains(rootNode) ? rootNode : bodyNode
+ );
+
+ var applyAnimationClasses = applyAnimationClassesFactory($$jqLite);
+
+ return function initDriverFn(animationDetails) {
+ return animationDetails.from && animationDetails.to
+ ? prepareFromToAnchorAnimation(animationDetails.from,
+ animationDetails.to,
+ animationDetails.classes,
+ animationDetails.anchors)
+ : prepareRegularAnimation(animationDetails);
+ };
+
+ function filterCssClasses(classes) {
+ //remove all the `ng-` stuff
+ return classes.replace(/\bng-\S+\b/g, '');
+ }
+
+ function getUniqueValues(a, b) {
+ if (isString(a)) a = a.split(' ');
+ if (isString(b)) b = b.split(' ');
+ return a.filter(function(val) {
+ return b.indexOf(val) === -1;
+ }).join(' ');
+ }
+
+ function prepareAnchoredAnimation(classes, outAnchor, inAnchor) {
+ var clone = jqLite(getDomNode(outAnchor).cloneNode(true));
+ var startingClasses = filterCssClasses(getClassVal(clone));
+
+ outAnchor.addClass(NG_ANIMATE_SHIM_CLASS_NAME);
+ inAnchor.addClass(NG_ANIMATE_SHIM_CLASS_NAME);
+
+ clone.addClass(NG_ANIMATE_ANCHOR_CLASS_NAME);
+
+ rootBodyElement.append(clone);
+
+ var animatorIn, animatorOut = prepareOutAnimation();
+
+ // the user may not end up using the `out` animation and
+ // only making use of the `in` animation or vice-versa.
+ // In either case we should allow this and not assume the
+ // animation is over unless both animations are not used.
+ if (!animatorOut) {
+ animatorIn = prepareInAnimation();
+ if (!animatorIn) {
+ return end();
+ }
+ }
+
+ var startingAnimator = animatorOut || animatorIn;
+
+ return {
+ start: function() {
+ var runner;
+
+ var currentAnimation = startingAnimator.start();
+ currentAnimation.done(function() {
+ currentAnimation = null;
+ if (!animatorIn) {
+ animatorIn = prepareInAnimation();
+ if (animatorIn) {
+ currentAnimation = animatorIn.start();
+ currentAnimation.done(function() {
+ currentAnimation = null;
+ end();
+ runner.complete();
+ });
+ return currentAnimation;
+ }
+ }
+ // in the event that there is no `in` animation
+ end();
+ runner.complete();
+ });
+
+ runner = new $$AnimateRunner({
+ end: endFn,
+ cancel: endFn
+ });
+
+ return runner;
+
+ function endFn() {
+ if (currentAnimation) {
+ currentAnimation.end();
+ }
+ }
+ }
+ };
+
+ function calculateAnchorStyles(anchor) {
+ var styles = {};
+
+ var coords = getDomNode(anchor).getBoundingClientRect();
+
+ // we iterate directly since safari messes up and doesn't return
+ // all the keys for the coods object when iterated
+ forEach(['width','height','top','left'], function(key) {
+ var value = coords[key];
+ switch (key) {
+ case 'top':
+ value += bodyNode.scrollTop;
+ break;
+ case 'left':
+ value += bodyNode.scrollLeft;
+ break;
+ }
+ styles[key] = Math.floor(value) + 'px';
+ });
+ return styles;
+ }
+
+ function prepareOutAnimation() {
+ var animator = $animateCss(clone, {
+ addClass: NG_OUT_ANCHOR_CLASS_NAME,
+ delay: true,
+ from: calculateAnchorStyles(outAnchor)
+ });
+
+ // read the comment within `prepareRegularAnimation` to understand
+ // why this check is necessary
+ return animator.$$willAnimate ? animator : null;
+ }
+
+ function getClassVal(element) {
+ return element.attr('class') || '';
+ }
+
+ function prepareInAnimation() {
+ var endingClasses = filterCssClasses(getClassVal(inAnchor));
+ var toAdd = getUniqueValues(endingClasses, startingClasses);
+ var toRemove = getUniqueValues(startingClasses, endingClasses);
+
+ var animator = $animateCss(clone, {
+ to: calculateAnchorStyles(inAnchor),
+ addClass: NG_IN_ANCHOR_CLASS_NAME + ' ' + toAdd,
+ removeClass: NG_OUT_ANCHOR_CLASS_NAME + ' ' + toRemove,
+ delay: true
+ });
+
+ // read the comment within `prepareRegularAnimation` to understand
+ // why this check is necessary
+ return animator.$$willAnimate ? animator : null;
+ }
+
+ function end() {
+ clone.remove();
+ outAnchor.removeClass(NG_ANIMATE_SHIM_CLASS_NAME);
+ inAnchor.removeClass(NG_ANIMATE_SHIM_CLASS_NAME);
+ }
+ }
+
+ function prepareFromToAnchorAnimation(from, to, classes, anchors) {
+ var fromAnimation = prepareRegularAnimation(from, noop);
+ var toAnimation = prepareRegularAnimation(to, noop);
+
+ var anchorAnimations = [];
+ forEach(anchors, function(anchor) {
+ var outElement = anchor['out'];
+ var inElement = anchor['in'];
+ var animator = prepareAnchoredAnimation(classes, outElement, inElement);
+ if (animator) {
+ anchorAnimations.push(animator);
+ }
+ });
+
+ // no point in doing anything when there are no elements to animate
+ if (!fromAnimation && !toAnimation && anchorAnimations.length === 0) return;
+
+ return {
+ start: function() {
+ var animationRunners = [];
+
+ if (fromAnimation) {
+ animationRunners.push(fromAnimation.start());
+ }
+
+ if (toAnimation) {
+ animationRunners.push(toAnimation.start());
+ }
+
+ forEach(anchorAnimations, function(animation) {
+ animationRunners.push(animation.start());
+ });
+
+ var runner = new $$AnimateRunner({
+ end: endFn,
+ cancel: endFn // CSS-driven animations cannot be cancelled, only ended
+ });
+
+ $$AnimateRunner.all(animationRunners, function(status) {
+ runner.complete(status);
+ });
+
+ return runner;
+
+ function endFn() {
+ forEach(animationRunners, function(runner) {
+ runner.end();
+ });
+ }
+ }
+ };
+ }
+
+ function prepareRegularAnimation(animationDetails) {
+ var element = animationDetails.element;
+ var options = animationDetails.options || {};
+
+ if (animationDetails.structural) {
+ options.event = animationDetails.event;
+ options.structural = true;
+ options.applyClassesEarly = true;
+
+ // we special case the leave animation since we want to ensure that
+ // the element is removed as soon as the animation is over. Otherwise
+ // a flicker might appear or the element may not be removed at all
+ if (animationDetails.event === 'leave') {
+ options.onDone = options.domOperation;
+ }
+ }
+
+ // We assign the preparationClasses as the actual animation event since
+ // the internals of $animateCss will just suffix the event token values
+ // with `-active` to trigger the animation.
+ if (options.preparationClasses) {
+ options.event = concatWithSpace(options.event, options.preparationClasses);
+ }
+
+ var animator = $animateCss(element, options);
+
+ // the driver lookup code inside of $$animation attempts to spawn a
+ // driver one by one until a driver returns a.$$willAnimate animator object.
+ // $animateCss will always return an object, however, it will pass in
+ // a flag as a hint as to whether an animation was detected or not
+ return animator.$$willAnimate ? animator : null;
+ }
+ }];
+}];
+
+// TODO(matsko): use caching here to speed things up for detection
+// TODO(matsko): add documentation
+// by the time...
+
+var $$AnimateJsProvider = ['$animateProvider', function($animateProvider) {
+ this.$get = ['$injector', '$$AnimateRunner', '$$jqLite',
+ function($injector, $$AnimateRunner, $$jqLite) {
+
+ var applyAnimationClasses = applyAnimationClassesFactory($$jqLite);
+ // $animateJs(element, 'enter');
+ return function(element, event, classes, options) {
+ var animationClosed = false;
+
+ // the `classes` argument is optional and if it is not used
+ // then the classes will be resolved from the element's className
+ // property as well as options.addClass/options.removeClass.
+ if (arguments.length === 3 && isObject(classes)) {
+ options = classes;
+ classes = null;
+ }
+
+ options = prepareAnimationOptions(options);
+ if (!classes) {
+ classes = element.attr('class') || '';
+ if (options.addClass) {
+ classes += ' ' + options.addClass;
+ }
+ if (options.removeClass) {
+ classes += ' ' + options.removeClass;
+ }
+ }
+
+ var classesToAdd = options.addClass;
+ var classesToRemove = options.removeClass;
+
+ // the lookupAnimations function returns a series of animation objects that are
+ // matched up with one or more of the CSS classes. These animation objects are
+ // defined via the module.animation factory function. If nothing is detected then
+ // we don't return anything which then makes $animation query the next driver.
+ var animations = lookupAnimations(classes);
+ var before, after;
+ if (animations.length) {
+ var afterFn, beforeFn;
+ if (event == 'leave') {
+ beforeFn = 'leave';
+ afterFn = 'afterLeave'; // TODO(matsko): get rid of this
+ } else {
+ beforeFn = 'before' + event.charAt(0).toUpperCase() + event.substr(1);
+ afterFn = event;
+ }
+
+ if (event !== 'enter' && event !== 'move') {
+ before = packageAnimations(element, event, options, animations, beforeFn);
+ }
+ after = packageAnimations(element, event, options, animations, afterFn);
+ }
+
+ // no matching animations
+ if (!before && !after) return;
+
+ function applyOptions() {
+ options.domOperation();
+ applyAnimationClasses(element, options);
+ }
+
+ function close() {
+ animationClosed = true;
+ applyOptions();
+ applyAnimationStyles(element, options);
+ }
+
+ var runner;
+
+ return {
+ $$willAnimate: true,
+ end: function() {
+ if (runner) {
+ runner.end();
+ } else {
+ close();
+ runner = new $$AnimateRunner();
+ runner.complete(true);
+ }
+ return runner;
+ },
+ start: function() {
+ if (runner) {
+ return runner;
+ }
+
+ runner = new $$AnimateRunner();
+ var closeActiveAnimations;
+ var chain = [];
+
+ if (before) {
+ chain.push(function(fn) {
+ closeActiveAnimations = before(fn);
+ });
+ }
+
+ if (chain.length) {
+ chain.push(function(fn) {
+ applyOptions();
+ fn(true);
+ });
+ } else {
+ applyOptions();
+ }
+
+ if (after) {
+ chain.push(function(fn) {
+ closeActiveAnimations = after(fn);
+ });
+ }
+
+ runner.setHost({
+ end: function() {
+ endAnimations();
+ },
+ cancel: function() {
+ endAnimations(true);
+ }
+ });
+
+ $$AnimateRunner.chain(chain, onComplete);
+ return runner;
+
+ function onComplete(success) {
+ close(success);
+ runner.complete(success);
+ }
+
+ function endAnimations(cancelled) {
+ if (!animationClosed) {
+ (closeActiveAnimations || noop)(cancelled);
+ onComplete(cancelled);
+ }
+ }
+ }
+ };
+
+ function executeAnimationFn(fn, element, event, options, onDone) {
+ var args;
+ switch (event) {
+ case 'animate':
+ args = [element, options.from, options.to, onDone];
+ break;
+
+ case 'setClass':
+ args = [element, classesToAdd, classesToRemove, onDone];
+ break;
+
+ case 'addClass':
+ args = [element, classesToAdd, onDone];
+ break;
+
+ case 'removeClass':
+ args = [element, classesToRemove, onDone];
+ break;
+
+ default:
+ args = [element, onDone];
+ break;
+ }
+
+ args.push(options);
+
+ var value = fn.apply(fn, args);
+ if (value) {
+ if (isFunction(value.start)) {
+ value = value.start();
+ }
+
+ if (value instanceof $$AnimateRunner) {
+ value.done(onDone);
+ } else if (isFunction(value)) {
+ // optional onEnd / onCancel callback
+ return value;
+ }
+ }
+
+ return noop;
+ }
+
+ function groupEventedAnimations(element, event, options, animations, fnName) {
+ var operations = [];
+ forEach(animations, function(ani) {
+ var animation = ani[fnName];
+ if (!animation) return;
+
+ // note that all of these animations will run in parallel
+ operations.push(function() {
+ var runner;
+ var endProgressCb;
+
+ var resolved = false;
+ var onAnimationComplete = function(rejected) {
+ if (!resolved) {
+ resolved = true;
+ (endProgressCb || noop)(rejected);
+ runner.complete(!rejected);
+ }
+ };
+
+ runner = new $$AnimateRunner({
+ end: function() {
+ onAnimationComplete();
+ },
+ cancel: function() {
+ onAnimationComplete(true);
+ }
+ });
+
+ endProgressCb = executeAnimationFn(animation, element, event, options, function(result) {
+ var cancelled = result === false;
+ onAnimationComplete(cancelled);
+ });
+
+ return runner;
+ });
+ });
+
+ return operations;
+ }
+
+ function packageAnimations(element, event, options, animations, fnName) {
+ var operations = groupEventedAnimations(element, event, options, animations, fnName);
+ if (operations.length === 0) {
+ var a,b;
+ if (fnName === 'beforeSetClass') {
+ a = groupEventedAnimations(element, 'removeClass', options, animations, 'beforeRemoveClass');
+ b = groupEventedAnimations(element, 'addClass', options, animations, 'beforeAddClass');
+ } else if (fnName === 'setClass') {
+ a = groupEventedAnimations(element, 'removeClass', options, animations, 'removeClass');
+ b = groupEventedAnimations(element, 'addClass', options, animations, 'addClass');
+ }
+
+ if (a) {
+ operations = operations.concat(a);
+ }
+ if (b) {
+ operations = operations.concat(b);
+ }
+ }
+
+ if (operations.length === 0) return;
+
+ // TODO(matsko): add documentation
+ return function startAnimation(callback) {
+ var runners = [];
+ if (operations.length) {
+ forEach(operations, function(animateFn) {
+ runners.push(animateFn());
+ });
+ }
+
+ runners.length ? $$AnimateRunner.all(runners, callback) : callback();
+
+ return function endFn(reject) {
+ forEach(runners, function(runner) {
+ reject ? runner.cancel() : runner.end();
+ });
+ };
+ };
+ }
+ };
+
+ function lookupAnimations(classes) {
+ classes = isArray(classes) ? classes : classes.split(' ');
+ var matches = [], flagMap = {};
+ for (var i=0; i < classes.length; i++) {
+ var klass = classes[i],
+ animationFactory = $animateProvider.$$registeredAnimations[klass];
+ if (animationFactory && !flagMap[klass]) {
+ matches.push($injector.get(animationFactory));
+ flagMap[klass] = true;
+ }
+ }
+ return matches;
+ }
+ }];
+}];
+
+var $$AnimateJsDriverProvider = ['$$animationProvider', function($$animationProvider) {
+ $$animationProvider.drivers.push('$$animateJsDriver');
+ this.$get = ['$$animateJs', '$$AnimateRunner', function($$animateJs, $$AnimateRunner) {
+ return function initDriverFn(animationDetails) {
+ if (animationDetails.from && animationDetails.to) {
+ var fromAnimation = prepareAnimation(animationDetails.from);
+ var toAnimation = prepareAnimation(animationDetails.to);
+ if (!fromAnimation && !toAnimation) return;
+
+ return {
+ start: function() {
+ var animationRunners = [];
+
+ if (fromAnimation) {
+ animationRunners.push(fromAnimation.start());
+ }
+
+ if (toAnimation) {
+ animationRunners.push(toAnimation.start());
+ }
+
+ $$AnimateRunner.all(animationRunners, done);
+
+ var runner = new $$AnimateRunner({
+ end: endFnFactory(),
+ cancel: endFnFactory()
+ });
+
+ return runner;
+
+ function endFnFactory() {
+ return function() {
+ forEach(animationRunners, function(runner) {
+ // at this point we cannot cancel animations for groups just yet. 1.5+
+ runner.end();
+ });
+ };
+ }
+
+ function done(status) {
+ runner.complete(status);
+ }
+ }
+ };
+ } else {
+ return prepareAnimation(animationDetails);
+ }
+ };
+
+ function prepareAnimation(animationDetails) {
+ // TODO(matsko): make sure to check for grouped animations and delegate down to normal animations
+ var element = animationDetails.element;
+ var event = animationDetails.event;
+ var options = animationDetails.options;
+ var classes = animationDetails.classes;
+ return $$animateJs(element, event, classes, options);
+ }
+ }];
+}];
+
+var NG_ANIMATE_ATTR_NAME = 'data-ng-animate';
+var NG_ANIMATE_PIN_DATA = '$ngAnimatePin';
+var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
+ var PRE_DIGEST_STATE = 1;
+ var RUNNING_STATE = 2;
+ var ONE_SPACE = ' ';
+
+ var rules = this.rules = {
+ skip: [],
+ cancel: [],
+ join: []
+ };
+
+ function makeTruthyCssClassMap(classString) {
+ if (!classString) {
+ return null;
+ }
+
+ var keys = classString.split(ONE_SPACE);
+ var map = Object.create(null);
+
+ forEach(keys, function(key) {
+ map[key] = true;
+ });
+ return map;
+ }
+
+ function hasMatchingClasses(newClassString, currentClassString) {
+ if (newClassString && currentClassString) {
+ var currentClassMap = makeTruthyCssClassMap(currentClassString);
+ return newClassString.split(ONE_SPACE).some(function(className) {
+ return currentClassMap[className];
+ });
+ }
+ }
+
+ function isAllowed(ruleType, element, currentAnimation, previousAnimation) {
+ return rules[ruleType].some(function(fn) {
+ return fn(element, currentAnimation, previousAnimation);
+ });
+ }
+
+ function hasAnimationClasses(animation, and) {
+ var a = (animation.addClass || '').length > 0;
+ var b = (animation.removeClass || '').length > 0;
+ return and ? a && b : a || b;
+ }
+
+ rules.join.push(function(element, newAnimation, currentAnimation) {
+ // if the new animation is class-based then we can just tack that on
+ return !newAnimation.structural && hasAnimationClasses(newAnimation);
+ });
+
+ rules.skip.push(function(element, newAnimation, currentAnimation) {
+ // there is no need to animate anything if no classes are being added and
+ // there is no structural animation that will be triggered
+ return !newAnimation.structural && !hasAnimationClasses(newAnimation);
+ });
+
+ rules.skip.push(function(element, newAnimation, currentAnimation) {
+ // why should we trigger a new structural animation if the element will
+ // be removed from the DOM anyway?
+ return currentAnimation.event == 'leave' && newAnimation.structural;
+ });
+
+ rules.skip.push(function(element, newAnimation, currentAnimation) {
+ // if there is an ongoing current animation then don't even bother running the class-based animation
+ return currentAnimation.structural && currentAnimation.state === RUNNING_STATE && !newAnimation.structural;
+ });
+
+ rules.cancel.push(function(element, newAnimation, currentAnimation) {
+ // there can never be two structural animations running at the same time
+ return currentAnimation.structural && newAnimation.structural;
+ });
+
+ rules.cancel.push(function(element, newAnimation, currentAnimation) {
+ // if the previous animation is already running, but the new animation will
+ // be triggered, but the new animation is structural
+ return currentAnimation.state === RUNNING_STATE && newAnimation.structural;
+ });
+
+ rules.cancel.push(function(element, newAnimation, currentAnimation) {
+ var nA = newAnimation.addClass;
+ var nR = newAnimation.removeClass;
+ var cA = currentAnimation.addClass;
+ var cR = currentAnimation.removeClass;
+
+ // early detection to save the global CPU shortage :)
+ if ((isUndefined(nA) && isUndefined(nR)) || (isUndefined(cA) && isUndefined(cR))) {
+ return false;
+ }
+
+ return hasMatchingClasses(nA, cR) || hasMatchingClasses(nR, cA);
+ });
+
+ this.$get = ['$$rAF', '$rootScope', '$rootElement', '$document', '$$HashMap',
+ '$$animation', '$$AnimateRunner', '$templateRequest', '$$jqLite', '$$forceReflow',
+ function($$rAF, $rootScope, $rootElement, $document, $$HashMap,
+ $$animation, $$AnimateRunner, $templateRequest, $$jqLite, $$forceReflow) {
+
+ var activeAnimationsLookup = new $$HashMap();
+ var disabledElementsLookup = new $$HashMap();
+ var animationsEnabled = null;
+ // $document might be mocked out in tests and won't include a real document.
+ // Providing an empty object with hidden = true will prevent animations from running
+ var rawDocument = $document[0] || {hidden: true};
+
+ function postDigestTaskFactory() {
+ var postDigestCalled = false;
+ return function(fn) {
+ // we only issue a call to postDigest before
+ // it has first passed. This prevents any callbacks
+ // from not firing once the animation has completed
+ // since it will be out of the digest cycle.
+ if (postDigestCalled) {
+ fn();
+ } else {
+ $rootScope.$$postDigest(function() {
+ postDigestCalled = true;
+ fn();
+ });
+ }
+ };
+ }
+
+ // 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 === 0; },
+ function(isEmpty) {
+ if (!isEmpty) 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() {
+ // we check for null directly in the event that the application already called
+ // .enabled() with whatever arguments that it provided it with
+ if (animationsEnabled === null) {
+ animationsEnabled = true;
+ }
+ });
+ });
+ }
+ );
+
+ var callbackRegistry = {};
+
+ // remember that the classNameFilter is set during the provider/config
+ // stage therefore we can optimize here and setup a helper function
+ var classNameFilter = $animateProvider.classNameFilter();
+ var isAnimatableClassName = !classNameFilter
+ ? function() { return true; }
+ : function(className) {
+ return classNameFilter.test(className);
+ };
+
+ var applyAnimationClasses = applyAnimationClassesFactory($$jqLite);
+
+ function normalizeAnimationDetails(element, animation) {
+ return mergeAnimationDetails(element, animation, {});
+ }
+
+ // IE9-11 has no method "contains" in SVG element and in Node.prototype. Bug #10259.
+ var contains = Node.prototype.contains || function(arg) {
+ // jshint bitwise: false
+ return this === arg || !!(this.compareDocumentPosition(arg) & 16);
+ // jshint bitwise: true
+ };
+
+ function findCallbacks(parent, element, event) {
+ var targetNode = getDomNode(element);
+ var targetParentNode = getDomNode(parent);
+
+ var matches = [];
+ var entries = callbackRegistry[event];
+ if (entries) {
+ forEach(entries, function(entry) {
+ if (contains.call(entry.node, targetNode)) {
+ matches.push(entry.callback);
+ } else if (event === 'leave' && contains.call(entry.node, targetParentNode)) {
+ matches.push(entry.callback);
+ }
+ });
+ }
+
+ return matches;
+ }
+
+ return {
+ on: function(event, container, callback) {
+ var node = extractElementNode(container);
+ callbackRegistry[event] = callbackRegistry[event] || [];
+ callbackRegistry[event].push({
+ node: node,
+ callback: callback
+ });
+ },
+
+ off: function(event, container, callback) {
+ var entries = callbackRegistry[event];
+ if (!entries) return;
+
+ callbackRegistry[event] = arguments.length === 1
+ ? null
+ : filterFromRegistry(entries, container, callback);
+
+ function filterFromRegistry(list, matchContainer, matchCallback) {
+ var containerNode = extractElementNode(matchContainer);
+ return list.filter(function(entry) {
+ var isMatch = entry.node === containerNode &&
+ (!matchCallback || entry.callback === matchCallback);
+ return !isMatch;
+ });
+ }
+ },
+
+ pin: function(element, parentElement) {
+ assertArg(isElement(element), 'element', 'not an element');
+ assertArg(isElement(parentElement), 'parentElement', 'not an element');
+ element.data(NG_ANIMATE_PIN_DATA, parentElement);
+ },
+
+ push: function(element, event, options, domOperation) {
+ options = options || {};
+ options.domOperation = domOperation;
+ return queueAnimation(element, event, options);
+ },
+
+ // this method has four signatures:
+ // () - global getter
+ // (bool) - global setter
+ // (element) - element getter
+ // (element, bool) - element setter
+ enabled: function(element, bool) {
+ var argCount = arguments.length;
+
+ if (argCount === 0) {
+ // () - Global getter
+ bool = !!animationsEnabled;
+ } else {
+ var hasElement = isElement(element);
+
+ if (!hasElement) {
+ // (bool) - Global setter
+ bool = animationsEnabled = !!element;
+ } else {
+ var node = getDomNode(element);
+ var recordExists = disabledElementsLookup.get(node);
+
+ if (argCount === 1) {
+ // (element) - Element getter
+ bool = !recordExists;
+ } else {
+ // (element, bool) - Element setter
+ disabledElementsLookup.put(node, !bool);
+ }
+ }
+ }
+
+ return bool;
+ }
+ };
+
+ function queueAnimation(element, event, initialOptions) {
+ // we always make a copy of the options since
+ // there should never be any side effects on
+ // the input data when running `$animateCss`.
+ var options = copy(initialOptions);
+
+ var node, parent;
+ element = stripCommentsFromElement(element);
+ if (element) {
+ node = getDomNode(element);
+ parent = element.parent();
+ }
+
+ options = prepareAnimationOptions(options);
+
+ // we create a fake runner with a working promise.
+ // These methods will become available after the digest has passed
+ var runner = new $$AnimateRunner();
+
+ // this is used to trigger callbacks in postDigest mode
+ var runInNextPostDigestOrNow = postDigestTaskFactory();
+
+ if (isArray(options.addClass)) {
+ options.addClass = options.addClass.join(' ');
+ }
+
+ if (options.addClass && !isString(options.addClass)) {
+ options.addClass = null;
+ }
+
+ if (isArray(options.removeClass)) {
+ options.removeClass = options.removeClass.join(' ');
+ }
+
+ if (options.removeClass && !isString(options.removeClass)) {
+ options.removeClass = null;
+ }
+
+ if (options.from && !isObject(options.from)) {
+ options.from = null;
+ }
+
+ if (options.to && !isObject(options.to)) {
+ options.to = null;
+ }
+
+ // there are situations where a directive issues an animation for
+ // a jqLite wrapper that contains only comment nodes... If this
+ // happens then there is no way we can perform an animation
+ if (!node) {
+ close();
+ return runner;
+ }
+
+ var className = [node.className, options.addClass, options.removeClass].join(' ');
+ if (!isAnimatableClassName(className)) {
+ close();
+ return runner;
+ }
+
+ var isStructural = ['enter', 'move', 'leave'].indexOf(event) >= 0;
+
+ var documentHidden = rawDocument.hidden;
+
+ // this is a hard disable of all animations for the application or on
+ // the element itself, therefore there is no need to continue further
+ // past this point if not enabled
+ // Animations are also disabled if the document is currently hidden (page is not visible
+ // to the user), because browsers slow down or do not flush calls to requestAnimationFrame
+ var skipAnimations = !animationsEnabled || documentHidden || disabledElementsLookup.get(node);
+ var existingAnimation = (!skipAnimations && activeAnimationsLookup.get(node)) || {};
+ var hasExistingAnimation = !!existingAnimation.state;
+
+ // there is no point in traversing the same collection of parent ancestors if a followup
+ // animation will be run on the same element that already did all that checking work
+ if (!skipAnimations && (!hasExistingAnimation || existingAnimation.state != PRE_DIGEST_STATE)) {
+ skipAnimations = !areAnimationsAllowed(element, parent, event);
+ }
+
+ if (skipAnimations) {
+ // Callbacks should fire even if the document is hidden (regression fix for issue #14120)
+ if (documentHidden) notifyProgress(runner, event, 'start');
+ close();
+ if (documentHidden) notifyProgress(runner, event, 'close');
+ return runner;
+ }
+
+ if (isStructural) {
+ closeChildAnimations(element);
+ }
+
+ var newAnimation = {
+ structural: isStructural,
+ element: element,
+ event: event,
+ addClass: options.addClass,
+ removeClass: options.removeClass,
+ close: close,
+ options: options,
+ runner: runner
+ };
+
+ if (hasExistingAnimation) {
+ var skipAnimationFlag = isAllowed('skip', element, newAnimation, existingAnimation);
+ if (skipAnimationFlag) {
+ if (existingAnimation.state === RUNNING_STATE) {
+ close();
+ return runner;
+ } else {
+ mergeAnimationDetails(element, existingAnimation, newAnimation);
+ return existingAnimation.runner;
+ }
+ }
+ var cancelAnimationFlag = isAllowed('cancel', element, newAnimation, existingAnimation);
+ if (cancelAnimationFlag) {
+ if (existingAnimation.state === RUNNING_STATE) {
+ // this will end the animation right away and it is safe
+ // to do so since the animation is already running and the
+ // runner callback code will run in async
+ existingAnimation.runner.end();
+ } else if (existingAnimation.structural) {
+ // this means that the animation is queued into a digest, but
+ // hasn't started yet. Therefore it is safe to run the close
+ // method which will call the runner methods in async.
+ existingAnimation.close();
+ } else {
+ // this will merge the new animation options into existing animation options
+ mergeAnimationDetails(element, existingAnimation, newAnimation);
+
+ return existingAnimation.runner;
+ }
+ } else {
+ // a joined animation means that this animation will take over the existing one
+ // so an example would involve a leave animation taking over an enter. Then when
+ // the postDigest kicks in the enter will be ignored.
+ var joinAnimationFlag = isAllowed('join', element, newAnimation, existingAnimation);
+ if (joinAnimationFlag) {
+ if (existingAnimation.state === RUNNING_STATE) {
+ normalizeAnimationDetails(element, newAnimation);
+ } else {
+ applyGeneratedPreparationClasses(element, isStructural ? event : null, options);
+
+ event = newAnimation.event = existingAnimation.event;
+ options = mergeAnimationDetails(element, existingAnimation, newAnimation);
+
+ //we return the same runner since only the option values of this animation will
+ //be fed into the `existingAnimation`.
+ return existingAnimation.runner;
+ }
+ }
+ }
+ } else {
+ // normalization in this case means that it removes redundant CSS classes that
+ // already exist (addClass) or do not exist (removeClass) on the element
+ normalizeAnimationDetails(element, newAnimation);
+ }
+
+ // when the options are merged and cleaned up we may end up not having to do
+ // an animation at all, therefore we should check this before issuing a post
+ // digest callback. Structural animations will always run no matter what.
+ var isValidAnimation = newAnimation.structural;
+ if (!isValidAnimation) {
+ // animate (from/to) can be quickly checked first, otherwise we check if any classes are present
+ isValidAnimation = (newAnimation.event === 'animate' && Object.keys(newAnimation.options.to || {}).length > 0)
+ || hasAnimationClasses(newAnimation);
+ }
+
+ if (!isValidAnimation) {
+ close();
+ clearElementAnimationState(element);
+ return runner;
+ }
+
+ // the counter keeps track of cancelled animations
+ var counter = (existingAnimation.counter || 0) + 1;
+ newAnimation.counter = counter;
+
+ markElementAnimationState(element, PRE_DIGEST_STATE, newAnimation);
+
+ $rootScope.$$postDigest(function() {
+ var animationDetails = activeAnimationsLookup.get(node);
+ var animationCancelled = !animationDetails;
+ animationDetails = animationDetails || {};
+
+ // if addClass/removeClass is called before something like enter then the
+ // registered parent element may not be present. The code below will ensure
+ // that a final value for parent element is obtained
+ var parentElement = element.parent() || [];
+
+ // animate/structural/class-based animations all have requirements. Otherwise there
+ // is no point in performing an animation. The parent node must also be set.
+ var isValidAnimation = parentElement.length > 0
+ && (animationDetails.event === 'animate'
+ || animationDetails.structural
+ || hasAnimationClasses(animationDetails));
+
+ // this means that the previous animation was cancelled
+ // even if the follow-up animation is the same event
+ if (animationCancelled || animationDetails.counter !== counter || !isValidAnimation) {
+ // if another animation did not take over then we need
+ // to make sure that the domOperation and options are
+ // handled accordingly
+ if (animationCancelled) {
+ applyAnimationClasses(element, options);
+ applyAnimationStyles(element, options);
+ }
+
+ // if the event changed from something like enter to leave then we do
+ // it, otherwise if it's the same then the end result will be the same too
+ if (animationCancelled || (isStructural && animationDetails.event !== event)) {
+ options.domOperation();
+ runner.end();
+ }
+
+ // in the event that the element animation was not cancelled or a follow-up animation
+ // isn't allowed to animate from here then we need to clear the state of the element
+ // so that any future animations won't read the expired animation data.
+ if (!isValidAnimation) {
+ clearElementAnimationState(element);
+ }
+
+ return;
+ }
+
+ // this combined multiple class to addClass / removeClass into a setClass event
+ // so long as a structural event did not take over the animation
+ event = !animationDetails.structural && hasAnimationClasses(animationDetails, true)
+ ? 'setClass'
+ : animationDetails.event;
+
+ markElementAnimationState(element, RUNNING_STATE);
+ var realRunner = $$animation(element, event, animationDetails.options);
+
+ // this will update the runner's flow-control events based on
+ // the `realRunner` object.
+ runner.setHost(realRunner);
+ notifyProgress(runner, event, 'start', {});
+
+ realRunner.done(function(status) {
+ close(!status);
+ var animationDetails = activeAnimationsLookup.get(node);
+ if (animationDetails && animationDetails.counter === counter) {
+ clearElementAnimationState(getDomNode(element));
+ }
+ notifyProgress(runner, event, 'close', {});
+ });
+ });
+
+ return runner;
+
+ function notifyProgress(runner, event, phase, data) {
+ runInNextPostDigestOrNow(function() {
+ var callbacks = findCallbacks(parent, element, event);
+ if (callbacks.length) {
+ // do not optimize this call here to RAF because
+ // we don't know how heavy the callback code here will
+ // be and if this code is buffered then this can
+ // lead to a performance regression.
+ $$rAF(function() {
+ forEach(callbacks, function(callback) {
+ callback(element, phase, data);
+ });
+ });
+ }
+ });
+ runner.progress(event, phase, data);
+ }
+
+ function close(reject) { // jshint ignore:line
+ clearGeneratedClasses(element, options);
+ applyAnimationClasses(element, options);
+ applyAnimationStyles(element, options);
+ options.domOperation();
+ runner.complete(!reject);
+ }
+ }
+
+ function closeChildAnimations(element) {
+ var node = getDomNode(element);
+ var children = node.querySelectorAll('[' + NG_ANIMATE_ATTR_NAME + ']');
+ forEach(children, function(child) {
+ var state = parseInt(child.getAttribute(NG_ANIMATE_ATTR_NAME));
+ var animationDetails = activeAnimationsLookup.get(child);
+ if (animationDetails) {
+ switch (state) {
+ case RUNNING_STATE:
+ animationDetails.runner.end();
+ /* falls through */
+ case PRE_DIGEST_STATE:
+ activeAnimationsLookup.remove(child);
+ break;
+ }
+ }
+ });
+ }
+
+ function clearElementAnimationState(element) {
+ var node = getDomNode(element);
+ node.removeAttribute(NG_ANIMATE_ATTR_NAME);
+ activeAnimationsLookup.remove(node);
+ }
+
+ function isMatchingElement(nodeOrElmA, nodeOrElmB) {
+ return getDomNode(nodeOrElmA) === getDomNode(nodeOrElmB);
+ }
+
+ /**
+ * This fn returns false if any of the following is true:
+ * a) animations on any parent element are disabled, and animations on the element aren't explicitly allowed
+ * b) a parent element has an ongoing structural animation, and animateChildren is false
+ * c) the element is not a child of the body
+ * d) the element is not a child of the $rootElement
+ */
+ function areAnimationsAllowed(element, parentElement, event) {
+ var bodyElement = jqLite(rawDocument.body);
+ var bodyElementDetected = isMatchingElement(element, bodyElement) || element[0].nodeName === 'HTML';
+ var rootElementDetected = isMatchingElement(element, $rootElement);
+ var parentAnimationDetected = false;
+ var animateChildren;
+ var elementDisabled = disabledElementsLookup.get(getDomNode(element));
+
+ var parentHost = jqLite.data(element[0], NG_ANIMATE_PIN_DATA);
+ if (parentHost) {
+ parentElement = parentHost;
+ }
+
+ parentElement = getDomNode(parentElement);
+
+ while (parentElement) {
+ if (!rootElementDetected) {
+ // angular doesn't want to attempt to animate elements outside of the application
+ // therefore we need to ensure that the rootElement is an ancestor of the current element
+ rootElementDetected = isMatchingElement(parentElement, $rootElement);
+ }
+
+ if (parentElement.nodeType !== ELEMENT_NODE) {
+ // no point in inspecting the #document element
+ break;
+ }
+
+ var details = activeAnimationsLookup.get(parentElement) || {};
+ // either an enter, leave or move animation will commence
+ // therefore we can't allow any animations to take place
+ // but if a parent animation is class-based then that's ok
+ if (!parentAnimationDetected) {
+ var parentElementDisabled = disabledElementsLookup.get(parentElement);
+
+ if (parentElementDisabled === true && elementDisabled !== false) {
+ // disable animations if the user hasn't explicitly enabled animations on the
+ // current element
+ elementDisabled = true;
+ // element is disabled via parent element, no need to check anything else
+ break;
+ } else if (parentElementDisabled === false) {
+ elementDisabled = false;
+ }
+ parentAnimationDetected = details.structural;
+ }
+
+ if (isUndefined(animateChildren) || animateChildren === true) {
+ var value = jqLite.data(parentElement, NG_ANIMATE_CHILDREN_DATA);
+ if (isDefined(value)) {
+ animateChildren = value;
+ }
+ }
+
+ // there is no need to continue traversing at this point
+ if (parentAnimationDetected && animateChildren === false) break;
+
+ if (!bodyElementDetected) {
+ // we also need to ensure that the element is or will be a part of the body element
+ // otherwise it is pointless to even issue an animation to be rendered
+ bodyElementDetected = isMatchingElement(parentElement, bodyElement);
+ }
+
+ if (bodyElementDetected && rootElementDetected) {
+ // If both body and root have been found, any other checks are pointless,
+ // as no animation data should live outside the application
+ break;
+ }
+
+ if (!rootElementDetected) {
+ // If no rootElement is detected, check if the parentElement is pinned to another element
+ parentHost = jqLite.data(parentElement, NG_ANIMATE_PIN_DATA);
+ if (parentHost) {
+ // The pin target element becomes the next parent element
+ parentElement = getDomNode(parentHost);
+ continue;
+ }
+ }
+
+ parentElement = parentElement.parentNode;
+ }
+
+ var allowAnimation = (!parentAnimationDetected || animateChildren) && elementDisabled !== true;
+ return allowAnimation && rootElementDetected && bodyElementDetected;
+ }
+
+ function markElementAnimationState(element, state, details) {
+ details = details || {};
+ details.state = state;
+
+ var node = getDomNode(element);
+ node.setAttribute(NG_ANIMATE_ATTR_NAME, state);
+
+ var oldValue = activeAnimationsLookup.get(node);
+ var newValue = oldValue
+ ? extend(oldValue, details)
+ : details;
+ activeAnimationsLookup.put(node, newValue);
+ }
+ }];
+}];
+
+var $$AnimationProvider = ['$animateProvider', function($animateProvider) {
+ var NG_ANIMATE_REF_ATTR = 'ng-animate-ref';
+
+ var drivers = this.drivers = [];
+
+ var RUNNER_STORAGE_KEY = '$$animationRunner';
+
+ function setRunner(element, runner) {
+ element.data(RUNNER_STORAGE_KEY, runner);
+ }
+
+ function removeRunner(element) {
+ element.removeData(RUNNER_STORAGE_KEY);
+ }
+
+ function getRunner(element) {
+ return element.data(RUNNER_STORAGE_KEY);
+ }
+
+ this.$get = ['$$jqLite', '$rootScope', '$injector', '$$AnimateRunner', '$$HashMap', '$$rAFScheduler',
+ function($$jqLite, $rootScope, $injector, $$AnimateRunner, $$HashMap, $$rAFScheduler) {
+
+ var animationQueue = [];
+ var applyAnimationClasses = applyAnimationClassesFactory($$jqLite);
+
+ function sortAnimations(animations) {
+ var tree = { children: [] };
+ var i, lookup = new $$HashMap();
+
+ // this is done first beforehand so that the hashmap
+ // is filled with a list of the elements that will be animated
+ for (i = 0; i < animations.length; i++) {
+ var animation = animations[i];
+ lookup.put(animation.domNode, animations[i] = {
+ domNode: animation.domNode,
+ fn: animation.fn,
+ children: []
+ });
+ }
+
+ for (i = 0; i < animations.length; i++) {
+ processNode(animations[i]);
+ }
+
+ return flatten(tree);
+
+ function processNode(entry) {
+ if (entry.processed) return entry;
+ entry.processed = true;
+
+ var elementNode = entry.domNode;
+ var parentNode = elementNode.parentNode;
+ lookup.put(elementNode, entry);
+
+ var parentEntry;
+ while (parentNode) {
+ parentEntry = lookup.get(parentNode);
+ if (parentEntry) {
+ if (!parentEntry.processed) {
+ parentEntry = processNode(parentEntry);
+ }
+ break;
+ }
+ parentNode = parentNode.parentNode;
+ }
+
+ (parentEntry || tree).children.push(entry);
+ return entry;
+ }
+
+ function flatten(tree) {
+ var result = [];
+ var queue = [];
+ var i;
+
+ for (i = 0; i < tree.children.length; i++) {
+ queue.push(tree.children[i]);
+ }
+
+ var remainingLevelEntries = queue.length;
+ var nextLevelEntries = 0;
+ var row = [];
+
+ for (i = 0; i < queue.length; i++) {
+ var entry = queue[i];
+ if (remainingLevelEntries <= 0) {
+ remainingLevelEntries = nextLevelEntries;
+ nextLevelEntries = 0;
+ result.push(row);
+ row = [];
+ }
+ row.push(entry.fn);
+ entry.children.forEach(function(childEntry) {
+ nextLevelEntries++;
+ queue.push(childEntry);
+ });
+ remainingLevelEntries--;
+ }
+
+ if (row.length) {
+ result.push(row);
+ }
+
+ return result;
+ }
+ }
+
+ // TODO(matsko): document the signature in a better way
+ return function(element, event, options) {
+ options = prepareAnimationOptions(options);
+ var isStructural = ['enter', 'move', 'leave'].indexOf(event) >= 0;
+
+ // there is no animation at the current moment, however
+ // these runner methods will get later updated with the
+ // methods leading into the driver's end/cancel methods
+ // for now they just stop the animation from starting
+ var runner = new $$AnimateRunner({
+ end: function() { close(); },
+ cancel: function() { close(true); }
+ });
+
+ if (!drivers.length) {
+ close();
+ return runner;
+ }
+
+ setRunner(element, runner);
+
+ var classes = mergeClasses(element.attr('class'), mergeClasses(options.addClass, options.removeClass));
+ var tempClasses = options.tempClasses;
+ if (tempClasses) {
+ classes += ' ' + tempClasses;
+ options.tempClasses = null;
+ }
+
+ var prepareClassName;
+ if (isStructural) {
+ prepareClassName = 'ng-' + event + PREPARE_CLASS_SUFFIX;
+ $$jqLite.addClass(element, prepareClassName);
+ }
+
+ animationQueue.push({
+ // this data is used by the postDigest code and passed into
+ // the driver step function
+ element: element,
+ classes: classes,
+ event: event,
+ structural: isStructural,
+ options: options,
+ beforeStart: beforeStart,
+ close: close
+ });
+
+ element.on('$destroy', handleDestroyedElement);
+
+ // we only want there to be one function called within the post digest
+ // block. This way we can group animations for all the animations that
+ // were apart of the same postDigest flush call.
+ if (animationQueue.length > 1) return runner;
+
+ $rootScope.$$postDigest(function() {
+ var animations = [];
+ forEach(animationQueue, function(entry) {
+ // the element was destroyed early on which removed the runner
+ // form its storage. This means we can't animate this element
+ // at all and it already has been closed due to destruction.
+ if (getRunner(entry.element)) {
+ animations.push(entry);
+ } else {
+ entry.close();
+ }
+ });
+
+ // now any future animations will be in another postDigest
+ animationQueue.length = 0;
+
+ var groupedAnimations = groupAnimations(animations);
+ var toBeSortedAnimations = [];
+
+ forEach(groupedAnimations, function(animationEntry) {
+ toBeSortedAnimations.push({
+ domNode: getDomNode(animationEntry.from ? animationEntry.from.element : animationEntry.element),
+ fn: function triggerAnimationStart() {
+ // it's important that we apply the `ng-animate` CSS class and the
+ // temporary classes before we do any driver invoking since these
+ // CSS classes may be required for proper CSS detection.
+ animationEntry.beforeStart();
+
+ var startAnimationFn, closeFn = animationEntry.close;
+
+ // in the event that the element was removed before the digest runs or
+ // during the RAF sequencing then we should not trigger the animation.
+ var targetElement = animationEntry.anchors
+ ? (animationEntry.from.element || animationEntry.to.element)
+ : animationEntry.element;
+
+ if (getRunner(targetElement)) {
+ var operation = invokeFirstDriver(animationEntry);
+ if (operation) {
+ startAnimationFn = operation.start;
+ }
+ }
+
+ if (!startAnimationFn) {
+ closeFn();
+ } else {
+ var animationRunner = startAnimationFn();
+ animationRunner.done(function(status) {
+ closeFn(!status);
+ });
+ updateAnimationRunners(animationEntry, animationRunner);
+ }
+ }
+ });
+ });
+
+ // we need to sort each of the animations in order of parent to child
+ // relationships. This ensures that the child classes are applied at the
+ // right time.
+ $$rAFScheduler(sortAnimations(toBeSortedAnimations));
+ });
+
+ return runner;
+
+ // TODO(matsko): change to reference nodes
+ function getAnchorNodes(node) {
+ var SELECTOR = '[' + NG_ANIMATE_REF_ATTR + ']';
+ var items = node.hasAttribute(NG_ANIMATE_REF_ATTR)
+ ? [node]
+ : node.querySelectorAll(SELECTOR);
+ var anchors = [];
+ forEach(items, function(node) {
+ var attr = node.getAttribute(NG_ANIMATE_REF_ATTR);
+ if (attr && attr.length) {
+ anchors.push(node);
+ }
+ });
+ return anchors;
+ }
+
+ function groupAnimations(animations) {
+ var preparedAnimations = [];
+ var refLookup = {};
+ forEach(animations, function(animation, index) {
+ var element = animation.element;
+ var node = getDomNode(element);
+ var event = animation.event;
+ var enterOrMove = ['enter', 'move'].indexOf(event) >= 0;
+ var anchorNodes = animation.structural ? getAnchorNodes(node) : [];
+
+ if (anchorNodes.length) {
+ var direction = enterOrMove ? 'to' : 'from';
+
+ forEach(anchorNodes, function(anchor) {
+ var key = anchor.getAttribute(NG_ANIMATE_REF_ATTR);
+ refLookup[key] = refLookup[key] || {};
+ refLookup[key][direction] = {
+ animationID: index,
+ element: jqLite(anchor)
+ };
+ });
+ } else {
+ preparedAnimations.push(animation);
+ }
+ });
+
+ var usedIndicesLookup = {};
+ var anchorGroups = {};
+ forEach(refLookup, function(operations, key) {
+ var from = operations.from;
+ var to = operations.to;
+
+ if (!from || !to) {
+ // only one of these is set therefore we can't have an
+ // anchor animation since all three pieces are required
+ var index = from ? from.animationID : to.animationID;
+ var indexKey = index.toString();
+ if (!usedIndicesLookup[indexKey]) {
+ usedIndicesLookup[indexKey] = true;
+ preparedAnimations.push(animations[index]);
+ }
+ return;
+ }
+
+ var fromAnimation = animations[from.animationID];
+ var toAnimation = animations[to.animationID];
+ var lookupKey = from.animationID.toString();
+ if (!anchorGroups[lookupKey]) {
+ var group = anchorGroups[lookupKey] = {
+ structural: true,
+ beforeStart: function() {
+ fromAnimation.beforeStart();
+ toAnimation.beforeStart();
+ },
+ close: function() {
+ fromAnimation.close();
+ toAnimation.close();
+ },
+ classes: cssClassesIntersection(fromAnimation.classes, toAnimation.classes),
+ from: fromAnimation,
+ to: toAnimation,
+ anchors: [] // TODO(matsko): change to reference nodes
+ };
+
+ // the anchor animations require that the from and to elements both have at least
+ // one shared CSS class which effictively marries the two elements together to use
+ // the same animation driver and to properly sequence the anchor animation.
+ if (group.classes.length) {
+ preparedAnimations.push(group);
+ } else {
+ preparedAnimations.push(fromAnimation);
+ preparedAnimations.push(toAnimation);
+ }
+ }
+
+ anchorGroups[lookupKey].anchors.push({
+ 'out': from.element, 'in': to.element
+ });
+ });
+
+ return preparedAnimations;
+ }
+
+ function cssClassesIntersection(a,b) {
+ a = a.split(' ');
+ b = b.split(' ');
+ var matches = [];
+
+ for (var i = 0; i < a.length; i++) {
+ var aa = a[i];
+ if (aa.substring(0,3) === 'ng-') continue;
+
+ for (var j = 0; j < b.length; j++) {
+ if (aa === b[j]) {
+ matches.push(aa);
+ break;
+ }
+ }
+ }
+
+ return matches.join(' ');
+ }
+
+ function invokeFirstDriver(animationDetails) {
+ // we loop in reverse order since the more general drivers (like CSS and JS)
+ // may attempt more elements, but custom drivers are more particular
+ for (var i = drivers.length - 1; i >= 0; i--) {
+ var driverName = drivers[i];
+ if (!$injector.has(driverName)) continue; // TODO(matsko): remove this check
+
+ var factory = $injector.get(driverName);
+ var driver = factory(animationDetails);
+ if (driver) {
+ return driver;
+ }
+ }
+ }
+
+ function beforeStart() {
+ element.addClass(NG_ANIMATE_CLASSNAME);
+ if (tempClasses) {
+ $$jqLite.addClass(element, tempClasses);
+ }
+ if (prepareClassName) {
+ $$jqLite.removeClass(element, prepareClassName);
+ prepareClassName = null;
+ }
+ }
+
+ function updateAnimationRunners(animation, newRunner) {
+ if (animation.from && animation.to) {
+ update(animation.from.element);
+ update(animation.to.element);
+ } else {
+ update(animation.element);
+ }
+
+ function update(element) {
+ getRunner(element).setHost(newRunner);
+ }
+ }
+
+ function handleDestroyedElement() {
+ var runner = getRunner(element);
+ if (runner && (event !== 'leave' || !options.$$domOperationFired)) {
+ runner.end();
+ }
+ }
+
+ function close(rejected) { // jshint ignore:line
+ element.off('$destroy', handleDestroyedElement);
+ removeRunner(element);
+
+ applyAnimationClasses(element, options);
+ applyAnimationStyles(element, options);
+ options.domOperation();
+
+ if (tempClasses) {
+ $$jqLite.removeClass(element, tempClasses);
+ }
+
+ element.removeClass(NG_ANIMATE_CLASSNAME);
+ runner.complete(!rejected);
+ }
+ };
+ }];
+}];
+
+/* global angularAnimateModule: true,
+
+ $$AnimateAsyncRunFactory,
+ $$rAFSchedulerFactory,
+ $$AnimateChildrenDirective,
+ $$AnimateQueueProvider,
+ $$AnimationProvider,
+ $AnimateCssProvider,
+ $$AnimateCssDriverProvider,
+ $$AnimateJsProvider,
+ $$AnimateJsDriverProvider,
+*/
+
+/**
+ * @ngdoc module
+ * @name ngAnimate
+ * @description
+ *
+ * The `ngAnimate` module provides support for CSS-based animations (keyframes and transitions) as well as JavaScript-based animations via
+ * callback hooks. Animations are not enabled by default, however, by including `ngAnimate` the animation hooks are enabled for an Angular app.
+ *
+ *
+ *
+ * # Usage
+ * Simply put, there are two ways to make use of animations when ngAnimate is used: by using **CSS** and **JavaScript**. The former works purely based
+ * using CSS (by using matching CSS selectors/styles) and the latter triggers animations that are registered via `module.animation()`. For
+ * both CSS and JS animations the sole requirement is to have a matching `CSS class` that exists both in the registered animation and within
+ * the HTML element that the animation will be triggered on.
+ *
+ * ## Directive Support
+ * The following directives are "animation aware":
+ *
+ * | 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 |
+ *
+ * (More information can be found by visiting each the documentation associated with each directive.)
+ *
+ * ## CSS-based Animations
+ *
+ * CSS-based animations with ngAnimate are unique since they require no JavaScript code at all. By using a CSS class that we reference between our HTML
+ * and CSS code we can create an animation that will be picked up by Angular when an the underlying directive performs an operation.
+ *
+ * The example below shows how an `enter` animation can be made possible on an element using `ng-if`:
+ *
+ * ```html
+ *
+ * Fade me in out
+ *
+ *
+ *
+ * ```
+ *
+ * Notice the CSS class **fade**? We can now create the CSS transition code that references this class:
+ *
+ * ```css
+ * /* The starting CSS styles for the enter animation */
+ * .fade.ng-enter {
+ * transition:0.5s linear all;
+ * opacity:0;
+ * }
+ *
+ * /* The finishing CSS styles for the enter animation */
+ * .fade.ng-enter.ng-enter-active {
+ * opacity:1;
+ * }
+ * ```
+ *
+ * The key thing to remember here is that, depending on the animation event (which each of the directives above trigger depending on what's going on) two
+ * generated CSS classes will be applied to the element; in the example above we have `.ng-enter` and `.ng-enter-active`. For CSS transitions, the transition
+ * code **must** be defined within the starting CSS class (in this case `.ng-enter`). The destination class is what the transition will animate towards.
+ *
+ * If for example we wanted to create animations for `leave` and `move` (ngRepeat triggers move) then we can do so using the same CSS naming conventions:
+ *
+ * ```css
+ * /* now the element will fade out before it is removed from the DOM */
+ * .fade.ng-leave {
+ * transition:0.5s linear all;
+ * opacity:1;
+ * }
+ * .fade.ng-leave.ng-leave-active {
+ * opacity:0;
+ * }
+ * ```
+ *
+ * We can also make use of **CSS Keyframes** by referencing the keyframe animation within the starting CSS class:
+ *
+ * ```css
+ * /* there is no need to define anything inside of the destination
+ * CSS class since the keyframe will take charge of the animation */
+ * .fade.ng-leave {
+ * animation: my_fade_animation 0.5s linear;
+ * -webkit-animation: my_fade_animation 0.5s linear;
+ * }
+ *
+ * @keyframes my_fade_animation {
+ * from { opacity:1; }
+ * to { opacity:0; }
+ * }
+ *
+ * @-webkit-keyframes my_fade_animation {
+ * from { opacity:1; }
+ * to { opacity:0; }
+ * }
+ * ```
+ *
+ * Feel free also mix transitions and keyframes together as well as any other CSS classes on the same element.
+ *
+ * ### CSS Class-based Animations
+ *
+ * Class-based animations (animations that are triggered via `ngClass`, `ngShow`, `ngHide` and some other directives) have a slightly different
+ * naming convention. Class-based animations are basic enough that a standard transition or keyframe can be referenced on the class being added
+ * and removed.
+ *
+ * For example if we wanted to do a CSS animation for `ngHide` then we place an animation on the `.ng-hide` CSS class:
+ *
+ * ```html
+ *
+ * Show and hide me
+ *
+ *
+ *
+ *
+ * ```
+ *
+ * All that is going on here with ngShow/ngHide behind the scenes is the `.ng-hide` class is added/removed (when the hidden state is valid). Since
+ * ngShow and ngHide are animation aware then we can match up a transition and ngAnimate handles the rest.
+ *
+ * In addition the addition and removal of the CSS class, ngAnimate also provides two helper methods that we can use to further decorate the animation
+ * with CSS styles.
+ *
+ * ```html
+ *
+ * Highlight this box
+ *
+ *
+ *
+ *
+ * ```
+ *
+ * We can also make use of CSS keyframes by placing them within the CSS classes.
+ *
+ *
+ * ### 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 */
+ * transition: 1s linear all;
+ * opacity:0;
+ * }
+ * .my-animation.ng-enter-stagger {
+ * /* this will have a 100ms delay between each successive leave animation */
+ * transition-delay: 0.1s;
+ *
+ * /* As of 1.4.4, this must always be set: it signals ngAnimate
+ * to not accidentally inherit a delay property from another CSS class */
+ * 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 one or more animation frames have passed since the multiple calls to `$animate` were 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
+ *
+ * window.requestAnimationFrame(function() {
+ * //stagger has reset itself
+ * $animate.leave(kids[5]); //stagger index=0
+ * $animate.leave(kids[6]); //stagger index=1
+ *
+ * $scope.$digest();
+ * });
+ * ```
+ *
+ * Stagger animations are currently only supported within CSS-defined animations.
+ *
+ * ### The `ng-animate` CSS class
+ *
+ * When ngAnimate is animating an element it will apply the `ng-animate` CSS class to the element for the duration of the animation.
+ * This is a temporary CSS class and it will be removed once the animation is over (for both JavaScript and CSS-based animations).
+ *
+ * Therefore, animations can be applied to an element using this temporary class directly via CSS.
+ *
+ * ```css
+ * .zipper.ng-animate {
+ * transition:0.5s linear all;
+ * }
+ * .zipper.ng-enter {
+ * opacity:0;
+ * }
+ * .zipper.ng-enter.ng-enter-active {
+ * opacity:1;
+ * }
+ * .zipper.ng-leave {
+ * opacity:1;
+ * }
+ * .zipper.ng-leave.ng-leave-active {
+ * opacity:0;
+ * }
+ * ```
+ *
+ * (Note that the `ng-animate` CSS class is reserved and it cannot be applied on an element directly since ngAnimate will always remove
+ * the CSS class once an animation has completed.)
+ *
+ *
+ * ### The `ng-[event]-prepare` class
+ *
+ * This is a special class that can be used to prevent unwanted flickering / flash of content before
+ * the actual animation starts. The class is added as soon as an animation is initialized, but removed
+ * before the actual animation starts (after waiting for a $digest).
+ * It is also only added for *structural* animations (`enter`, `move`, and `leave`).
+ *
+ * In practice, flickering can appear when nesting elements with structural animations such as `ngIf`
+ * into elements that have class-based animations such as `ngClass`.
+ *
+ * ```html
+ *
+ *
+ *
+ *
+ *
+ * ```
+ *
+ * It is possible that during the `enter` animation, the `.message` div will be briefly visible before it starts animating.
+ * In that case, you can add styles to the CSS that make sure the element stays hidden before the animation starts:
+ *
+ * ```css
+ * .message.ng-enter-prepare {
+ * opacity: 0;
+ * }
+ *
+ * ```
+ *
+ * ## JavaScript-based Animations
+ *
+ * ngAnimate also allows for animations to be consumed by JavaScript code. The approach is similar to CSS-based animations (where there is a shared
+ * CSS class that is referenced in our HTML code) but in addition we need to register the JavaScript animation on the module. By making use of the
+ * `module.animation()` module function we can register the ainmation.
+ *
+ * Let's see an example of a enter/leave animation using `ngRepeat`:
+ *
+ * ```html
+ *
+ * {{ item }}
+ *
+ * ```
+ *
+ * See the **slide** CSS class? Let's use that class to define an animation that we'll structure in our module code by using `module.animation`:
+ *
+ * ```js
+ * myModule.animation('.slide', [function() {
+ * return {
+ * // make note that other events (like addClass/removeClass)
+ * // have different function input parameters
+ * enter: function(element, doneFn) {
+ * jQuery(element).fadeIn(1000, doneFn);
+ *
+ * // remember to call doneFn so that angular
+ * // knows that the animation has concluded
+ * },
+ *
+ * move: function(element, doneFn) {
+ * jQuery(element).fadeIn(1000, doneFn);
+ * },
+ *
+ * leave: function(element, doneFn) {
+ * jQuery(element).fadeOut(1000, doneFn);
+ * }
+ * }
+ * }]);
+ * ```
+ *
+ * The nice thing about JS-based animations is that we can inject other services and make use of advanced animation libraries such as
+ * greensock.js and velocity.js.
+ *
+ * If our animation code class-based (meaning that something like `ngClass`, `ngHide` and `ngShow` triggers it) then we can still define
+ * our animations inside of the same registered animation, however, the function input arguments are a bit different:
+ *
+ * ```html
+ *
+ * this box is moody
+ *
+ *
+ *
+ *
+ * ```
+ *
+ * ```js
+ * myModule.animation('.colorful', [function() {
+ * return {
+ * addClass: function(element, className, doneFn) {
+ * // do some cool animation and call the doneFn
+ * },
+ * removeClass: function(element, className, doneFn) {
+ * // do some cool animation and call the doneFn
+ * },
+ * setClass: function(element, addedClass, removedClass, doneFn) {
+ * // do some cool animation and call the doneFn
+ * }
+ * }
+ * }]);
+ * ```
+ *
+ * ## CSS + JS Animations Together
+ *
+ * AngularJS 1.4 and higher has taken steps to make the amalgamation of CSS and JS animations more flexible. However, unlike earlier versions of Angular,
+ * defining CSS and JS animations to work off of the same CSS class will not work anymore. Therefore the example below will only result in **JS animations taking
+ * charge of the animation**:
+ *
+ * ```html
+ *
+ * Slide in and out
+ *
+ * ```
+ *
+ * ```js
+ * myModule.animation('.slide', [function() {
+ * return {
+ * enter: function(element, doneFn) {
+ * jQuery(element).slideIn(1000, doneFn);
+ * }
+ * }
+ * }]);
+ * ```
+ *
+ * ```css
+ * .slide.ng-enter {
+ * transition:0.5s linear all;
+ * transform:translateY(-100px);
+ * }
+ * .slide.ng-enter.ng-enter-active {
+ * transform:translateY(0);
+ * }
+ * ```
+ *
+ * Does this mean that CSS and JS animations cannot be used together? Do JS-based animations always have higher priority? We can make up for the
+ * lack of CSS animations by using the `$animateCss` service to trigger our own tweaked-out, CSS-based animations directly from
+ * our own JS-based animation code:
+ *
+ * ```js
+ * myModule.animation('.slide', ['$animateCss', function($animateCss) {
+ * return {
+ * enter: function(element) {
+* // this will trigger `.slide.ng-enter` and `.slide.ng-enter-active`.
+ * return $animateCss(element, {
+ * event: 'enter',
+ * structural: true
+ * });
+ * }
+ * }
+ * }]);
+ * ```
+ *
+ * The nice thing here is that we can save bandwidth by sticking to our CSS-based animation code and we don't need to rely on a 3rd-party animation framework.
+ *
+ * The `$animateCss` service is very powerful since we can feed in all kinds of extra properties that will be evaluated and fed into a CSS transition or
+ * keyframe animation. For example if we wanted to animate the height of an element while adding and removing classes then we can do so by providing that
+ * data into `$animateCss` directly:
+ *
+ * ```js
+ * myModule.animation('.slide', ['$animateCss', function($animateCss) {
+ * return {
+ * enter: function(element) {
+ * return $animateCss(element, {
+ * event: 'enter',
+ * structural: true,
+ * addClass: 'maroon-setting',
+ * from: { height:0 },
+ * to: { height: 200 }
+ * });
+ * }
+ * }
+ * }]);
+ * ```
+ *
+ * Now we can fill in the rest via our transition CSS code:
+ *
+ * ```css
+ * /* the transition tells ngAnimate to make the animation happen */
+ * .slide.ng-enter { transition:0.5s linear all; }
+ *
+ * /* this extra CSS class will be absorbed into the transition
+ * since the $animateCss code is adding the class */
+ * .maroon-setting { background:red; }
+ * ```
+ *
+ * And `$animateCss` will figure out the rest. Just make sure to have the `done()` callback fire the `doneFn` function to signal when the animation is over.
+ *
+ * To learn more about what's possible be sure to visit the {@link ngAnimate.$animateCss $animateCss service}.
+ *
+ * ## Animation Anchoring (via `ng-animate-ref`)
+ *
+ * ngAnimate in AngularJS 1.4 comes packed with the ability to cross-animate elements between
+ * structural areas of an application (like views) by pairing up elements using an attribute
+ * called `ng-animate-ref`.
+ *
+ * Let's say for example we have two views that are managed by `ng-view` and we want to show
+ * that there is a relationship between two components situated in within these views. By using the
+ * `ng-animate-ref` attribute we can identify that the two components are paired together and we
+ * can then attach an animation, which is triggered when the view changes.
+ *
+ * Say for example we have the following template code:
+ *
+ * ```html
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * ```
+ *
+ * Now, when the view changes (once the link is clicked), ngAnimate will examine the
+ * HTML contents to see if there is a match reference between any components in the view
+ * that is leaving and the view that is entering. It will scan both the view which is being
+ * removed (leave) and inserted (enter) to see if there are any paired DOM elements that
+ * contain a matching ref value.
+ *
+ * The two images match since they share the same ref value. ngAnimate will now create a
+ * transport element (which is a clone of the first image element) and it will then attempt
+ * to animate to the position of the second image element in the next view. For the animation to
+ * work a special CSS class called `ng-anchor` will be added to the transported element.
+ *
+ * We can now attach a transition onto the `.banner.ng-anchor` CSS class and then
+ * ngAnimate will handle the entire transition for us as well as the addition and removal of
+ * any changes of CSS classes between the elements:
+ *
+ * ```css
+ * .banner.ng-anchor {
+ * /* this animation will last for 1 second since there are
+ * two phases to the animation (an `in` and an `out` phase) */
+ * transition:0.5s linear all;
+ * }
+ * ```
+ *
+ * We also **must** include animations for the views that are being entered and removed
+ * (otherwise anchoring wouldn't be possible since the new view would be inserted right away).
+ *
+ * ```css
+ * .view-animation.ng-enter, .view-animation.ng-leave {
+ * transition:0.5s linear all;
+ * position:fixed;
+ * left:0;
+ * top:0;
+ * width:100%;
+ * }
+ * .view-animation.ng-enter {
+ * transform:translateX(100%);
+ * }
+ * .view-animation.ng-leave,
+ * .view-animation.ng-enter.ng-enter-active {
+ * transform:translateX(0%);
+ * }
+ * .view-animation.ng-leave.ng-leave-active {
+ * transform:translateX(-100%);
+ * }
+ * ```
+ *
+ * Now we can jump back to the anchor animation. When the animation happens, there are two stages that occur:
+ * an `out` and an `in` stage. The `out` stage happens first and that is when the element is animated away
+ * from its origin. Once that animation is over then the `in` stage occurs which animates the
+ * element to its destination. The reason why there are two animations is to give enough time
+ * for the enter animation on the new element to be ready.
+ *
+ * The example above sets up a transition for both the in and out phases, but we can also target the out or
+ * in phases directly via `ng-anchor-out` and `ng-anchor-in`.
+ *
+ * ```css
+ * .banner.ng-anchor-out {
+ * transition: 0.5s linear all;
+ *
+ * /* the scale will be applied during the out animation,
+ * but will be animated away when the in animation runs */
+ * transform: scale(1.2);
+ * }
+ *
+ * .banner.ng-anchor-in {
+ * transition: 1s linear all;
+ * }
+ * ```
+ *
+ *
+ *
+ *
+ * ### Anchoring Demo
+ *
+
+
+ Home
+
+
+
+
+ .record {
+ display:block;
+ font-size:20px;
+ }
+ .profile {
+ background:black;
+ color:white;
+ font-size:100px;
+ }
+ .view-container {
+ position:relative;
+ }
+ .view-container > .view.ng-animate {
+ position:absolute;
+ top:0;
+ left:0;
+ width:100%;
+ min-height:500px;
+ }
+ .view.ng-enter, .view.ng-leave,
+ .record.ng-anchor {
+ transition:0.5s linear all;
+ }
+ .view.ng-enter {
+ transform:translateX(100%);
+ }
+ .view.ng-enter.ng-enter-active, .view.ng-leave {
+ transform:translateX(0%);
+ }
+ .view.ng-leave.ng-leave-active {
+ transform:translateX(-100%);
+ }
+ .record.ng-anchor-out {
+ background:red;
+ }
+
+
+ *
+ * ### How is the element transported?
+ *
+ * When an anchor animation occurs, ngAnimate will clone the starting element and position it exactly where the starting
+ * element is located on screen via absolute positioning. The cloned element will be placed inside of the root element
+ * of the application (where ng-app was defined) and all of the CSS classes of the starting element will be applied. The
+ * element will then animate into the `out` and `in` animations and will eventually reach the coordinates and match
+ * the dimensions of the destination element. During the entire animation a CSS class of `.ng-animate-shim` will be applied
+ * to both the starting and destination elements in order to hide them from being visible (the CSS styling for the class
+ * is: `visibility:hidden`). Once the anchor reaches its destination then it will be removed and the destination element
+ * will become visible since the shim class will be removed.
+ *
+ * ### How is the morphing handled?
+ *
+ * CSS Anchoring relies on transitions and keyframes and the internal code is intelligent enough to figure out
+ * what CSS classes differ between the starting element and the destination element. These different CSS classes
+ * will be added/removed on the anchor element and a transition will be applied (the transition that is provided
+ * in the anchor class). Long story short, ngAnimate will figure out what classes to add and remove which will
+ * make the transition of the element as smooth and automatic as possible. Be sure to use simple CSS classes that
+ * do not rely on DOM nesting structure so that the anchor element appears the same as the starting element (since
+ * the cloned element is placed inside of root element which is likely close to the body element).
+ *
+ * Note that if the root element is on the `` element then the cloned node will be placed inside of body.
+ *
+ *
+ * ## Using $animate in your directive code
+ *
+ * So far we've explored how to feed in animations into an Angular application, but how do we trigger animations within our own directives in our application?
+ * By injecting the `$animate` service into our directive code, we can trigger structural and class-based hooks which can then be consumed by animations. Let's
+ * imagine we have a greeting box that shows and hides itself when the data changes
+ *
+ * ```html
+ * Hi there
+ * ```
+ *
+ * ```js
+ * ngModule.directive('greetingBox', ['$animate', function($animate) {
+ * return function(scope, element, attrs) {
+ * attrs.$observe('active', function(value) {
+ * value ? $animate.addClass(element, 'on') : $animate.removeClass(element, 'on');
+ * });
+ * });
+ * }]);
+ * ```
+ *
+ * Now the `on` CSS class is added and removed on the greeting box component. Now if we add a CSS class on top of the greeting box element
+ * in our HTML code then we can trigger a CSS or JS animation to happen.
+ *
+ * ```css
+ * /* normally we would create a CSS class to reference on the element */
+ * greeting-box.on { transition:0.5s linear all; background:green; color:white; }
+ * ```
+ *
+ * The `$animate` service contains a variety of other methods like `enter`, `leave`, `animate` and `setClass`. To learn more about what's
+ * possible be sure to visit the {@link ng.$animate $animate service API page}.
+ *
+ *
+ * ### 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-/);
+ * ```
+ *
+ * This also may provide your application with a speed boost since only specific elements containing CSS class prefix
+ * will be evaluated for animation when any DOM changes occur in the application.
+ *
+ * ## Callbacks and Promises
+ *
+ * When `$animate` is called it returns a promise that can be used to capture when the animation has ended. Therefore if we were to trigger
+ * an animation (within our directive code) then we can continue performing directive and scope related activities after the animation has
+ * ended by chaining onto the returned promise that animation method returns.
+ *
+ * ```js
+ * // somewhere within the depths of the directive
+ * $animate.enter(element, parent).then(function() {
+ * //the animation has completed
+ * });
+ * ```
+ *
+ * (Note that earlier versions of Angular prior to v1.4 required the promise code to be wrapped using `$scope.$apply(...)`. This is not the case
+ * anymore.)
+ *
+ * In addition to the animation promise, we can also make use of animation-related callbacks within our directives and controller code by registering
+ * an event listener using the `$animate` service. Let's say for example that an animation was triggered on our view
+ * routing controller to hook into that:
+ *
+ * ```js
+ * ngModule.controller('HomePageController', ['$animate', function($animate) {
+ * $animate.on('enter', ngViewElement, function(element) {
+ * // the animation for this route has completed
+ * }]);
+ * }])
+ * ```
+ *
+ * (Note that you will need to trigger a digest within the callback to get angular to notice any scope-related changes.)
+ */
+
+/**
+ * @ngdoc service
+ * @name $animate
+ * @kind object
+ *
+ * @description
+ * The ngAnimate `$animate` service documentation is the same for the core `$animate` service.
+ *
+ * Click here {@link ng.$animate to learn more about animations with `$animate`}.
+ */
+angular.module('ngAnimate', [])
+ .directive('ngAnimateChildren', $$AnimateChildrenDirective)
+ .factory('$$rAFScheduler', $$rAFSchedulerFactory)
+
+ .provider('$$animateQueue', $$AnimateQueueProvider)
+ .provider('$$animation', $$AnimationProvider)
+
+ .provider('$animateCss', $AnimateCssProvider)
+ .provider('$$animateCssDriver', $$AnimateCssDriverProvider)
+
+ .provider('$$animateJs', $$AnimateJsProvider)
+ .provider('$$animateJsDriver', $$AnimateJsDriverProvider);
+
+
+})(window, window.angular);
diff --git a/third_party/js/angular-aria.js b/third_party/js/angular-aria.js
new file mode 100644
index 0000000..e220cbb
--- /dev/null
+++ b/third_party/js/angular-aria.js
@@ -0,0 +1,398 @@
+/**
+ * @license AngularJS v1.4.14
+ * (c) 2010-2015 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 `ngAria` as a dependency. The following
+ * directives are supported:
+ * `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);
+
+/**
+* Internal Utilities
+*/
+var nodeBlackList = ['BUTTON', 'A', 'INPUT', 'TEXTAREA', 'SELECT', 'DETAILS', 'SUMMARY'];
+
+var isNodeOneOf = function(elem, nodeTypeArray) {
+ if (nodeTypeArray.indexOf(elem[0].nodeName) !== -1) {
+ return true;
+ }
+};
+/**
+ * @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,
+ bindRoleForClick: 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
+ * - **bindRoleForClick** – `{boolean}` – Adds role=button to non-interactive elements like `div`
+ * using ng-click, making them more accessible to users of assistive technologies
+ *
+ * @description
+ * Enables/disables various ARIA attributes
+ */
+ this.config = function(newConfig) {
+ config = angular.extend(config, newConfig);
+ };
+
+ function watchExpr(attrName, ariaAttr, nodeBlackList, negate) {
+ return function(scope, elem, attr) {
+ var ariaCamelName = attr.$normalize(ariaAttr);
+ if (config[ariaCamelName] && !isNodeOneOf(elem, nodeBlackList) && !attr[ariaCamelName]) {
+ scope.$watch(attr[attrName], function(boolVal) {
+ // ensure boolean value
+ boolVal = 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
+ compile: function(elem, attr) {
+ var shape = getShape(attr, elem);
+
+ return {
+ pre: function(scope, elem, attr, ngModel) {
+ if (shape === 'checkbox' && attr.type !== 'checkbox') {
+ //Use the input[checkbox] $isEmpty implementation for elements with checkbox roles
+ ngModel.$isEmpty = function(value) {
+ return value === false;
+ };
+ }
+ },
+ post: function(scope, elem, attr, ngModel) {
+ var needsTabIndex = shouldAttachAttr('tabindex', 'tabindex', elem)
+ && !isNodeOneOf(elem, nodeBlackList);
+
+ 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() {
+ 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);
+ }
+ if (needsTabIndex) {
+ elem.attr('tabindex', 0);
+ }
+ break;
+ case 'range':
+ if (shouldAttachRole(shape, elem)) {
+ elem.attr('role', 'slider');
+ }
+ if ($aria.config('ariaValue')) {
+ var needsAriaValuemin = !elem.attr('aria-valuemin') &&
+ (attr.hasOwnProperty('min') || attr.hasOwnProperty('ngMin'));
+ var needsAriaValuemax = !elem.attr('aria-valuemax') &&
+ (attr.hasOwnProperty('max') || attr.hasOwnProperty('ngMax'));
+ var needsAriaValuenow = !elem.attr('aria-valuenow');
+
+ if (needsAriaValuemin) {
+ attr.$observe('min', function ngAriaValueMinReaction(newVal) {
+ elem.attr('aria-valuemin', newVal);
+ });
+ }
+ if (needsAriaValuemax) {
+ attr.$observe('max', function ngAriaValueMinReaction(newVal) {
+ elem.attr('aria-valuemax', newVal);
+ });
+ }
+ if (needsAriaValuenow) {
+ scope.$watch(ngAriaWatchModelValue, function ngAriaValueNowReaction(newVal) {
+ elem.attr('aria-valuenow', newVal);
+ });
+ }
+ }
+ if (needsTabIndex) {
+ elem.attr('tabindex', 0);
+ }
+ break;
+ case 'multiline':
+ if (shouldAttachAttr('aria-multiline', 'ariaMultiline', elem)) {
+ elem.attr('aria-multiline', true);
+ }
+ break;
+ }
+
+ 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) {
+
+ if (!isNodeOneOf(elem, nodeBlackList)) {
+
+ if ($aria.config('bindRoleForClick') && !elem.attr('role')) {
+ elem.attr('role', 'button');
+ }
+
+ if ($aria.config('tabindex') && !elem.attr('tabindex')) {
+ elem.attr('tabindex', 0);
+ }
+
+ if ($aria.config('bindKeypress') && !attr.ngKeypress) {
+ elem.on('keypress', function(event) {
+ var keyCode = event.which || event.keyCode;
+ if (keyCode === 32 || 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') && !isNodeOneOf(elem, nodeBlackList)) {
+ elem.attr('tabindex', 0);
+ }
+ };
+}]);
+
+
+})(window, window.angular);
diff --git a/third_party/js/angular-environment.min.js b/third_party/js/angular-environment.min.js
new file mode 100644
index 0000000..5097619
--- /dev/null
+++ b/third_party/js/angular-environment.min.js
@@ -0,0 +1,5 @@
+
+angular.module('environment',[]).provider('envService',function(){'use strict';var local={};local.pregQuote=function(string,delimiter){return(string+'').replace(new RegExp('[.\\\\+*?\\[\\^\\]$(){}=!<>|:\\'+(delimiter||'')+'-]','g'),'\\$&');};local.stringToRegex=function(string){return new RegExp(local.pregQuote(string).replace(/\\\*/g,'.*').replace(/\\\?/g,'.'),'g');};this.environment='development';this.data={};this.config=function(config){this.data=config;};this.set=function(environment){this.environment=environment;};this.get=function(){return this.environment;};this.read=function(variable){if(typeof variable==='undefined'||variable===''||variable==='all'){return this.data.vars[this.get()];}
+else if(typeof this.data.vars[this.get()][variable]==='undefined'){return this.data.vars.defaults[variable];}
+return this.data.vars[this.get()][variable];};this.is=function(environment){return(environment===this.environment);};this.check=function(){var self=this,location=window.location.host,matches=[],keepGoing=true;angular.forEach(this.data.domains,function(v,k){angular.forEach(v,function(v){if(location.match(local.stringToRegex(v))){matches.push({environment:k,domain:v});}});});angular.forEach(matches,function(v,k){if(keepGoing){if(location===v.domain){keepGoing=false;}
+console.log(v.domain);self.environment=v.environment;}});};this.$get=function(){return this;};});
\ No newline at end of file
diff --git a/third_party/js/angular-local-storage.min.js b/third_party/js/angular-local-storage.min.js
new file mode 100644
index 0000000..7424461
--- /dev/null
+++ b/third_party/js/angular-local-storage.min.js
@@ -0,0 +1,9 @@
+/**
+ * An Angular module that gives you access to the browsers local storage
+ * @version v0.7.1 - 2017-06-21
+ * @link https://github.com/grevory/angular-local-storage
+ * @author grevory
+ * @license MIT License, http://www.opensource.org/licenses/MIT
+ */
+!function(a,b){var c=b.isDefined,d=b.isUndefined,e=b.isNumber,f=b.isObject,g=b.isArray,h=b.isString,i=b.extend,j=b.toJson;b.module("LocalStorageModule",[]).provider("localStorageService",function(){this.prefix="ls",this.storageType="localStorage",this.cookie={expiry:30,path:"/",secure:!1},this.defaultToCookie=!0,this.notify={setItem:!0,removeItem:!1},this.setPrefix=function(a){return this.prefix=a,this},this.setStorageType=function(a){return this.storageType=a,this},this.setDefaultToCookie=function(a){return this.defaultToCookie=!!a,this},this.setStorageCookie=function(a,b,c){return this.cookie.expiry=a,this.cookie.path=b,this.cookie.secure=c,this},this.setStorageCookieDomain=function(a){return this.cookie.domain=a,this},this.setNotify=function(a,b){return this.notify={setItem:a,removeItem:b},this},this.$get=["$rootScope","$window","$document","$parse","$timeout",function(a,b,k,l,m){function n(c){if(c||(c=b.event),s.setItem&&h(c.key)&&w(c.key)){var d=v(c.key);m(function(){a.$broadcast("LocalStorageModule.notification.changed",{key:d,newvalue:c.newValue,storageType:p.storageType})})}}var o,p=this,q=p.prefix,r=p.cookie,s=p.notify,t=p.storageType;k?k[0]&&(k=k[0]):k=document,"."!==q.substr(-1)&&(q=q?q+".":"");var u=function(a){return q+a},v=function(a){return a.replace(new RegExp("^"+q,"g"),"")},w=function(a){return 0===a.indexOf(q)},x=function(){try{var c=t in b&&null!==b[t],d=u("__"+Math.round(1e7*Math.random()));return c&&(o=b[t],o.setItem(d,""),o.removeItem(d)),c}catch(b){return p.defaultToCookie&&(t="cookie"),a.$broadcast("LocalStorageModule.notification.error",b.message),!1}},y=x(),z=function(b,c,e){var f=J();try{if(K(e),c=d(c)?null:j(c),!y&&p.defaultToCookie||"cookie"===p.storageType)return y||a.$broadcast("LocalStorageModule.notification.warning","LOCAL_STORAGE_NOT_SUPPORTED"),s.setItem&&a.$broadcast("LocalStorageModule.notification.setitem",{key:b,newvalue:c,storageType:"cookie"}),F(b,c);try{o&&o.setItem(u(b),c),s.setItem&&a.$broadcast("LocalStorageModule.notification.setitem",{key:b,newvalue:c,storageType:p.storageType})}catch(d){return a.$broadcast("LocalStorageModule.notification.error",d.message),F(b,c)}return!0}finally{K(f)}},A=function(b,c){var d=J();try{if(K(c),!y&&p.defaultToCookie||"cookie"===p.storageType)return y||a.$broadcast("LocalStorageModule.notification.warning","LOCAL_STORAGE_NOT_SUPPORTED"),G(b);var e=o?o.getItem(u(b)):null;if(!e||"null"===e)return null;try{return JSON.parse(e)}catch(a){return e}}finally{K(d)}},B=function(){var b=J();try{var c=0;arguments.length>=1&&("localStorage"===arguments[arguments.length-1]||"sessionStorage"===arguments[arguments.length-1])&&(c=1,K(arguments[arguments.length-1]));var d,e;for(d=0;d0||(k.cookie="test").indexOf.call(k.cookie,"test")>-1)}catch(b){return a.$broadcast("LocalStorageModule.notification.error",b.message),!1}}(),F=function(b,c,h,i){if(d(c))return!1;if((g(c)||f(c))&&(c=j(c)),!E)return a.$broadcast("LocalStorageModule.notification.error","COOKIES_NOT_SUPPORTED"),!1;try{var l="",m=new Date,n="";if(null===c?(m.setTime(m.getTime()+-864e5),l="; expires="+m.toGMTString(),c=""):e(h)&&0!==h?(m.setTime(m.getTime()+24*h*60*60*1e3),l="; expires="+m.toGMTString()):0!==r.expiry&&(m.setTime(m.getTime()+24*r.expiry*60*60*1e3),l="; expires="+m.toGMTString()),b){var o="; path="+r.path;r.domain&&(n="; domain="+r.domain),"boolean"==typeof i?i===!0&&(n+="; secure"):r.secure===!0&&(n+="; secure"),k.cookie=u(b)+"="+encodeURIComponent(c)+l+o+n}}catch(b){return a.$broadcast("LocalStorageModule.notification.error",b.message),!1}return!0},G=function(b){if(!E)return a.$broadcast("LocalStorageModule.notification.error","COOKIES_NOT_SUPPORTED"),!1;for(var c=k.cookie&&k.cookie.split(";")||[],d=0;d -1 ) && (index < _items.length );
+ }
+
+ /**
+ * Can the iterator proceed to the next item in the list; relative to
+ * the specified item.
+ *
+ * @param item
+ * @returns {Array.length|*|number|boolean}
+ */
+ function hasNext(item) {
+ return item ? inRange(indexOf(item) + 1) : false;
+ }
+
+ /**
+ * Can the iterator proceed to the previous item in the list; relative to
+ * the specified item.
+ *
+ * @param item
+ * @returns {Array.length|*|number|boolean}
+ */
+ function hasPrevious(item) {
+ return item ? inRange(indexOf(item) - 1) : false;
+ }
+
+ /**
+ * Get item at specified index/position
+ * @param index
+ * @returns {*}
+ */
+ function itemAt(index) {
+ return inRange(index) ? _items[index] : null;
+ }
+
+ /**
+ * Find all elements matching the key/value pair
+ * otherwise return null
+ *
+ * @param val
+ * @param key
+ *
+ * @return array
+ */
+ function findBy(key, val) {
+ return _items.filter(function(item) {
+ return item[key] === val;
+ });
+ }
+
+ /**
+ * Add item to list
+ * @param item
+ * @param index
+ * @returns {*}
+ */
+ function add(item, index) {
+ if ( !item ) return -1;
+
+ if (!angular.isNumber(index)) {
+ index = _items.length;
+ }
+
+ _items.splice(index, 0, item);
+
+ return indexOf(item);
+ }
+
+ /**
+ * Remove item from list...
+ * @param item
+ */
+ function remove(item) {
+ if ( contains(item) ){
+ _items.splice(indexOf(item), 1);
+ }
+ }
+
+ /**
+ * Get the zero-based index of the target item
+ * @param item
+ * @returns {*}
+ */
+ function indexOf(item) {
+ return _items.indexOf(item);
+ }
+
+ /**
+ * Boolean existence check
+ * @param item
+ * @returns {boolean}
+ */
+ function contains(item) {
+ return item && (indexOf(item) > -1);
+ }
+
+ /**
+ * Return first item in the list
+ * @returns {*}
+ */
+ function first() {
+ return _items.length ? _items[0] : null;
+ }
+
+ /**
+ * Return last item in the list...
+ * @returns {*}
+ */
+ function last() {
+ return _items.length ? _items[_items.length - 1] : null;
+ }
+
+ /**
+ * Find the next item. If reloop is true and at the end of the list, it will go back to the
+ * first item. If given, the `validate` callback will be used to determine whether the next item
+ * is valid. If not valid, it will try to find the next item again.
+ *
+ * @param {boolean} backwards Specifies the direction of searching (forwards/backwards)
+ * @param {*} item The item whose subsequent item we are looking for
+ * @param {Function=} validate The `validate` function
+ * @param {integer=} limit The recursion limit
+ *
+ * @returns {*} The subsequent item or null
+ */
+ function findSubsequentItem(backwards, item, validate, limit) {
+ validate = validate || trueFn;
+
+ var curIndex = indexOf(item);
+ while (true) {
+ if (!inRange(curIndex)) return null;
+
+ var nextIndex = curIndex + (backwards ? -1 : 1);
+ var foundItem = null;
+ if (inRange(nextIndex)) {
+ foundItem = _items[nextIndex];
+ } else if (reloop) {
+ foundItem = backwards ? last() : first();
+ nextIndex = indexOf(foundItem);
+ }
+
+ if ((foundItem === null) || (nextIndex === limit)) return null;
+ if (validate(foundItem)) return foundItem;
+
+ if (angular.isUndefined(limit)) limit = nextIndex;
+
+ curIndex = nextIndex;
+ }
+ }
+ }
+
+
+})();
+(function(){
+"use strict";
+
+angular.module('material.core')
+.factory('$mdMedia', mdMediaFactory);
+
+/**
+ * @ngdoc service
+ * @name $mdMedia
+ * @module material.core
+ *
+ * @description
+ * `$mdMedia` is used to evaluate whether a given media query is true or false given the
+ * current device's screen / window size. The media query will be re-evaluated on resize, allowing
+ * you to register a watch.
+ *
+ * `$mdMedia` also has pre-programmed support for media queries that match the layout breakpoints.
+ * (`sm`, `gt-sm`, `md`, `gt-md`, `lg`, `gt-lg`).
+ *
+ * @returns {boolean} a boolean representing whether or not the given media query is true or false.
+ *
+ * @usage
+ *
+ * app.controller('MyController', function($mdMedia, $scope) {
+ * $scope.$watch(function() { return $mdMedia('lg'); }, function(big) {
+ * $scope.bigScreen = big;
+ * });
+ *
+ * $scope.screenIsSmall = $mdMedia('sm');
+ * $scope.customQuery = $mdMedia('(min-width: 1234px)');
+ * $scope.anotherCustom = $mdMedia('max-width: 300px');
+ * });
+ *
+ */
+
+function mdMediaFactory($mdConstant, $rootScope, $window) {
+ var queries = {};
+ var mqls = {};
+ var results = {};
+ var normalizeCache = {};
+
+ $mdMedia.getResponsiveAttribute = getResponsiveAttribute;
+ $mdMedia.getQuery = getQuery;
+ $mdMedia.watchResponsiveAttributes = watchResponsiveAttributes;
+
+ return $mdMedia;
+
+ function $mdMedia(query) {
+ var validated = queries[query];
+ if (angular.isUndefined(validated)) {
+ validated = queries[query] = validate(query);
+ }
+
+ var result = results[validated];
+ if (angular.isUndefined(result)) {
+ result = add(validated);
+ }
+
+ return result;
+ }
+
+ function validate(query) {
+ return $mdConstant.MEDIA[query] ||
+ ((query.charAt(0) !== '(') ? ('(' + query + ')') : query);
+ }
+
+ function add(query) {
+ var result = mqls[query] = $window.matchMedia(query);
+ result.addListener(onQueryChange);
+ return (results[result.media] = !!result.matches);
+ }
+
+ function onQueryChange(query) {
+ $rootScope.$evalAsync(function() {
+ results[query.media] = !!query.matches;
+ });
+ }
+
+ function getQuery(name) {
+ return mqls[name];
+ }
+
+ function getResponsiveAttribute(attrs, attrName) {
+ for (var i = 0; i < $mdConstant.MEDIA_PRIORITY.length; i++) {
+ var mediaName = $mdConstant.MEDIA_PRIORITY[i];
+ if (!mqls[queries[mediaName]].matches) {
+ continue;
+ }
+
+ var normalizedName = getNormalizedName(attrs, attrName + '-' + mediaName);
+ if (attrs[normalizedName]) {
+ return attrs[normalizedName];
+ }
+ }
+
+ // fallback on unprefixed
+ return attrs[getNormalizedName(attrs, attrName)];
+ }
+
+ function watchResponsiveAttributes(attrNames, attrs, watchFn) {
+ var unwatchFns = [];
+ attrNames.forEach(function(attrName) {
+ var normalizedName = getNormalizedName(attrs, attrName);
+ if (angular.isDefined(attrs[normalizedName])) {
+ unwatchFns.push(
+ attrs.$observe(normalizedName, angular.bind(void 0, watchFn, null)));
+ }
+
+ for (var mediaName in $mdConstant.MEDIA) {
+ normalizedName = getNormalizedName(attrs, attrName + '-' + mediaName);
+ if (angular.isDefined(attrs[normalizedName])) {
+ unwatchFns.push(
+ attrs.$observe(normalizedName, angular.bind(void 0, watchFn, mediaName)));
+ }
+ }
+ });
+
+ return function unwatch() {
+ unwatchFns.forEach(function(fn) { fn(); })
+ };
+ }
+
+ // Improves performance dramatically
+ function getNormalizedName(attrs, attrName) {
+ return normalizeCache[attrName] ||
+ (normalizeCache[attrName] = attrs.$normalize(attrName));
+ }
+}
+mdMediaFactory.$inject = ["$mdConstant", "$rootScope", "$window"];
+
+})();
+(function(){
+"use strict";
+
+/*
+ * This var has to be outside the angular factory, otherwise when
+ * there are multiple material apps on the same page, each app
+ * will create its own instance of this array and the app's IDs
+ * will not be unique.
+ */
+var nextUniqueId = 0;
+
+angular
+ .module('material.core')
+ .factory('$mdUtil', UtilFactory);
+
+function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $interpolate, $log) {
+ // Setup some core variables for the processTemplate method
+ var startSymbol = $interpolate.startSymbol(),
+ endSymbol = $interpolate.endSymbol(),
+ usesStandardSymbols = ((startSymbol === '{{') && (endSymbol === '}}'));
+
+ var $mdUtil = {
+ dom: {},
+ now: window.performance ?
+ angular.bind(window.performance, window.performance.now) : Date.now || function() {
+ return new Date().getTime();
+ },
+
+ clientRect: function(element, offsetParent, isOffsetRect) {
+ var node = getNode(element);
+ offsetParent = getNode(offsetParent || node.offsetParent || document.body);
+ var nodeRect = node.getBoundingClientRect();
+
+ // The user can ask for an offsetRect: a rect relative to the offsetParent,
+ // or a clientRect: a rect relative to the page
+ var offsetRect = isOffsetRect ?
+ offsetParent.getBoundingClientRect() :
+ {left: 0, top: 0, width: 0, height: 0};
+ return {
+ left: nodeRect.left - offsetRect.left,
+ top: nodeRect.top - offsetRect.top,
+ width: nodeRect.width,
+ height: nodeRect.height
+ };
+ },
+ offsetRect: function(element, offsetParent) {
+ return $mdUtil.clientRect(element, offsetParent, true);
+ },
+
+ // Annoying method to copy nodes to an array, thanks to IE
+ nodesToArray: function(nodes) {
+ nodes = nodes || [];
+
+ var results = [];
+ for (var i = 0; i < nodes.length; ++i) {
+ results.push(nodes.item(i));
+ }
+ return results;
+ },
+
+ /**
+ * Calculate the positive scroll offset
+ * TODO: Check with pinch-zoom in IE/Chrome;
+ * https://code.google.com/p/chromium/issues/detail?id=496285
+ */
+ scrollTop: function(element) {
+ element = angular.element(element || $document[0].body);
+
+ var body = (element[0] == $document[0].body) ? $document[0].body : undefined;
+ var scrollTop = body ? body.scrollTop + body.parentElement.scrollTop : 0;
+
+ // Calculate the positive scroll offset
+ return scrollTop || Math.abs(element[0].getBoundingClientRect().top);
+ },
+
+ /**
+ * `findFocusTarget()` provides an optional way to identify the focused element when a dialog, bottomsheet, sideNav
+ * or other element opens. This is optional attribute finds a nested element with the mdAutoFocus attribute and optional
+ * expression. An expression may be specified as the directive value; to enable conditional activation of the autoFocus.
+ *
+ * NOTE: It is up to the component logic to use the '$mdUtil.findFocusTarget()'
+ *
+ * @usage
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * Comment Actions
+ *
+ *
+ *
+ *
+ *
+ * {{ item.name }}
+ *
+ *
+ *
+ *
+ *
+ *
+ **/
+ findFocusTarget: function(containerEl, attributeVal) {
+ var AUTO_FOCUS = '[md-autofocus]';
+ var elToFocus;
+
+ elToFocus = scanForFocusable(containerEl, attributeVal || AUTO_FOCUS);
+
+ if ( !elToFocus && attributeVal != AUTO_FOCUS) {
+ // Scan for deprecated attribute
+ elToFocus = scanForFocusable(containerEl, '[md-auto-focus]');
+
+ if ( !elToFocus ) {
+ // Scan for fallback to 'universal' API
+ elToFocus = scanForFocusable(containerEl, AUTO_FOCUS);
+ }
+ }
+
+ return elToFocus;
+
+ /**
+ * Can target and nested children for specified Selector (attribute)
+ * whose value may be an expression that evaluates to True/False.
+ */
+ function scanForFocusable(target, selector) {
+ var elFound, items = target[0].querySelectorAll(selector);
+
+ // Find the last child element with the focus attribute
+ if ( items && items.length ){
+ var EXP_ATTR = /\s*\[?([\-a-z]*)\]?\s*/i;
+ var matches = EXP_ATTR.exec(selector);
+ var attribute = matches ? matches[1] : null;
+
+ items.length && angular.forEach(items, function(it) {
+ it = angular.element(it);
+
+ // If the expression evaluates to FALSE, then it is not focusable target
+ var focusExpression = it[0].getAttribute(attribute);
+ var isFocusable = !focusExpression || !$mdUtil.validateScope(it) ? true :
+ (it.scope().$eval(focusExpression) !== false );
+
+ if (isFocusable) elFound = it;
+ });
+ }
+ return elFound;
+ }
+ },
+
+ // Disables scroll around the passed element.
+ disableScrollAround: function(element, parent) {
+ $mdUtil.disableScrollAround._count = $mdUtil.disableScrollAround._count || 0;
+ ++$mdUtil.disableScrollAround._count;
+ if ($mdUtil.disableScrollAround._enableScrolling) return $mdUtil.disableScrollAround._enableScrolling;
+ element = angular.element(element);
+ var body = $document[0].body,
+ restoreBody = disableBodyScroll(),
+ restoreElement = disableElementScroll(parent);
+
+ return $mdUtil.disableScrollAround._enableScrolling = function() {
+ if (!--$mdUtil.disableScrollAround._count) {
+ restoreBody();
+ restoreElement();
+ delete $mdUtil.disableScrollAround._enableScrolling;
+ }
+ };
+
+ // Creates a virtual scrolling mask to absorb touchmove, keyboard, scrollbar clicking, and wheel events
+ function disableElementScroll(element) {
+ element = angular.element(element || body)[0];
+ var zIndex = 50;
+ var scrollMask = angular.element(
+ '
' +
+ ' ' +
+ '
');
+ element.appendChild(scrollMask[0]);
+
+ scrollMask.on('wheel', preventDefault);
+ scrollMask.on('touchmove', preventDefault);
+ $document.on('keydown', disableKeyNav);
+
+ return function restoreScroll() {
+ scrollMask.off('wheel');
+ scrollMask.off('touchmove');
+ scrollMask[0].parentNode.removeChild(scrollMask[0]);
+ $document.off('keydown', disableKeyNav);
+ delete $mdUtil.disableScrollAround._enableScrolling;
+ };
+
+ // Prevent keypresses from elements inside the body
+ // used to stop the keypresses that could cause the page to scroll
+ // (arrow keys, spacebar, tab, etc).
+ function disableKeyNav(e) {
+ //-- temporarily removed this logic, will possibly re-add at a later date
+ //if (!element[0].contains(e.target)) {
+ // e.preventDefault();
+ // e.stopImmediatePropagation();
+ //}
+ }
+
+ function preventDefault(e) {
+ e.preventDefault();
+ }
+ }
+
+ // Converts the body to a position fixed block and translate it to the proper scroll
+ // position
+ function disableBodyScroll() {
+ var htmlNode = body.parentNode;
+ var restoreHtmlStyle = htmlNode.getAttribute('style') || '';
+ var restoreBodyStyle = body.getAttribute('style') || '';
+ var scrollOffset = $mdUtil.scrollTop(body);
+ var clientWidth = body.clientWidth;
+
+ if (body.scrollHeight > body.clientHeight) {
+ applyStyles(body, {
+ position: 'fixed',
+ width: '100%',
+ top: -scrollOffset + 'px'
+ });
+
+ applyStyles(htmlNode, {
+ overflowY: 'scroll'
+ });
+ }
+
+ if (body.clientWidth < clientWidth) applyStyles(body, {overflow: 'hidden'});
+
+ return function restoreScroll() {
+ body.setAttribute('style', restoreBodyStyle);
+ htmlNode.setAttribute('style', restoreHtmlStyle);
+ body.scrollTop = scrollOffset;
+ };
+ }
+
+ function applyStyles(el, styles) {
+ for (var key in styles) {
+ el.style[key] = styles[key];
+ }
+ }
+ },
+ enableScrolling: function() {
+ var method = this.disableScrollAround._enableScrolling;
+ method && method();
+ },
+ floatingScrollbars: function() {
+ if (this.floatingScrollbars.cached === undefined) {
+ var tempNode = angular.element('
');
+ $document[0].body.appendChild(tempNode[0]);
+ this.floatingScrollbars.cached = (tempNode[0].offsetWidth == tempNode[0].childNodes[0].offsetWidth);
+ tempNode.remove();
+ }
+ return this.floatingScrollbars.cached;
+ },
+
+ // Mobile safari only allows you to set focus in click event listeners...
+ forceFocus: function(element) {
+ var node = element[0] || element;
+
+ document.addEventListener('click', function focusOnClick(ev) {
+ if (ev.target === node && ev.$focus) {
+ node.focus();
+ ev.stopImmediatePropagation();
+ ev.preventDefault();
+ node.removeEventListener('click', focusOnClick);
+ }
+ }, true);
+
+ var newEvent = document.createEvent('MouseEvents');
+ newEvent.initMouseEvent('click', false, true, window, {}, 0, 0, 0, 0,
+ false, false, false, false, 0, null);
+ newEvent.$material = true;
+ newEvent.$focus = true;
+ node.dispatchEvent(newEvent);
+ },
+
+ /**
+ * facade to build md-backdrop element with desired styles
+ * NOTE: Use $compile to trigger backdrop postLink function
+ */
+ createBackdrop: function(scope, addClass) {
+ return $compile($mdUtil.supplant('', [addClass]))(scope);
+ },
+
+ /**
+ * supplant() method from Crockford's `Remedial Javascript`
+ * Equivalent to use of $interpolate; without dependency on
+ * interpolation symbols and scope. Note: the '{}' can
+ * be property names, property chains, or array indices.
+ */
+ supplant: function(template, values, pattern) {
+ pattern = pattern || /\{([^\{\}]*)\}/g;
+ return template.replace(pattern, function(a, b) {
+ var p = b.split('.'),
+ r = values;
+ try {
+ for (var s in p) {
+ if (p.hasOwnProperty(s) ) {
+ r = r[p[s]];
+ }
+ }
+ } catch (e) {
+ r = a;
+ }
+ return (typeof r === 'string' || typeof r === 'number') ? r : a;
+ });
+ },
+
+ fakeNgModel: function() {
+ return {
+ $fake: true,
+ $setTouched: angular.noop,
+ $setViewValue: function(value) {
+ this.$viewValue = value;
+ this.$render(value);
+ this.$viewChangeListeners.forEach(function(cb) {
+ cb();
+ });
+ },
+ $isEmpty: function(value) {
+ return ('' + value).length === 0;
+ },
+ $parsers: [],
+ $formatters: [],
+ $viewChangeListeners: [],
+ $render: angular.noop
+ };
+ },
+
+ // Returns a function, that, as long as it continues to be invoked, will not
+ // be triggered. The function will be called after it stops being called for
+ // N milliseconds.
+ // @param wait Integer value of msecs to delay (since last debounce reset); default value 10 msecs
+ // @param invokeApply should the $timeout trigger $digest() dirty checking
+ debounce: function(func, wait, scope, invokeApply) {
+ var timer;
+
+ return function debounced() {
+ var context = scope,
+ args = Array.prototype.slice.call(arguments);
+
+ $timeout.cancel(timer);
+ timer = $timeout(function() {
+
+ timer = undefined;
+ func.apply(context, args);
+
+ }, wait || 10, invokeApply);
+ };
+ },
+
+ // Returns a function that can only be triggered every `delay` milliseconds.
+ // In other words, the function will not be called unless it has been more
+ // than `delay` milliseconds since the last call.
+ throttle: function throttle(func, delay) {
+ var recent;
+ return function throttled() {
+ var context = this;
+ var args = arguments;
+ var now = $mdUtil.now();
+
+ if (!recent || (now - recent > delay)) {
+ func.apply(context, args);
+ recent = now;
+ }
+ };
+ },
+
+ /**
+ * Measures the number of milliseconds taken to run the provided callback
+ * function. Uses a high-precision timer if available.
+ */
+ time: function time(cb) {
+ var start = $mdUtil.now();
+ cb();
+ return $mdUtil.now() - start;
+ },
+
+ /**
+ * Create an implicit getter that caches its `getter()`
+ * lookup value
+ */
+ valueOnUse : function (scope, key, getter) {
+ var value = null, args = Array.prototype.slice.call(arguments);
+ var params = (args.length > 3) ? args.slice(3) : [ ];
+
+ Object.defineProperty(scope, key, {
+ get: function () {
+ if (value === null) value = getter.apply(scope, params);
+ return value;
+ }
+ });
+ },
+
+ /**
+ * Get a unique ID.
+ *
+ * @returns {string} an unique numeric string
+ */
+ nextUid: function() {
+ return '' + nextUniqueId++;
+ },
+
+ /**
+ * By default AngularJS attaches information about binding and scopes to DOM nodes,
+ * and adds CSS classes to data-bound elements. But this information is NOT available
+ * when `$compileProvider.debugInfoEnabled(false);`
+ *
+ * @see https://docs.angularjs.org/guide/production
+ */
+ validateScope : function(element) {
+ var hasScope = element && angular.isDefined(element.scope());
+ if ( !hasScope ) {
+ $log.warn("element.scope() is not available when 'debug mode' == false. @see https://docs.angularjs.org/guide/production!");
+ }
+
+ return hasScope;
+ },
+
+ // 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, onlyParent) {
+ if (el instanceof angular.element) el = el[0];
+ tagName = tagName.toUpperCase();
+ if (onlyParent) el = el.parentNode;
+ if (!el) return null;
+ do {
+ if (el.nodeName === tagName) {
+ return el;
+ }
+ } while (el = el.parentNode);
+ return null;
+ },
+
+ /**
+ * Build polyfill for the Node.contains feature (if needed)
+ */
+ elementContains: function(node, child) {
+ var hasContains = (window.Node && window.Node.prototype && Node.prototype.contains);
+ var findFn = hasContains ? angular.bind(node, node.contains) : angular.bind(node, function(arg) {
+ // compares the positions of two nodes and returns a bitmask
+ return (node === child) || !!(this.compareDocumentPosition(arg) & 16)
+ });
+
+ return findFn(child);
+ },
+
+ /**
+ * Functional equivalent for $element.filter(‘md-bottom-sheet’)
+ * useful with interimElements where the element and its container are important...
+ *
+ * @param {[]} elements to scan
+ * @param {string} name of node to find (e.g. 'md-dialog')
+ * @param {boolean=} optional flag to allow deep scans; defaults to 'false'.
+ * @param {boolean=} optional flag to enable log warnings; defaults to false
+ */
+ extractElementByName: function(element, nodeName, scanDeep, warnNotFound) {
+ var found = scanTree(element);
+ if (!found && !!warnNotFound) {
+ $log.warn( $mdUtil.supplant("Unable to find node '{0}' in element '{1}'.",[nodeName, element[0].outerHTML]) );
+ }
+
+ return angular.element(found || element);
+
+ /**
+ * Breadth-First tree scan for element with matching `nodeName`
+ */
+ function scanTree(element) {
+ return scanLevel(element) || (!!scanDeep ? scanChildren(element) : null);
+ }
+
+ /**
+ * Case-insensitive scan of current elements only (do not descend).
+ */
+ function scanLevel(element) {
+ if ( element ) {
+ for (var i = 0, len = element.length; i < len; i++) {
+ if (element[i].nodeName.toLowerCase() === nodeName) {
+ return element[i];
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Scan children of specified node
+ */
+ function scanChildren(element) {
+ var found;
+ if ( element ) {
+ for (var i = 0, len = element.length; i < len; i++) {
+ var target = element[i];
+ if ( !found ) {
+ for (var j = 0, numChild = target.childNodes.length; j < numChild; j++) {
+ found = found || scanTree([target.childNodes[j]]);
+ }
+ }
+ }
+ }
+ return found;
+ }
+
+ },
+
+ /**
+ * Give optional properties with no value a boolean true if attr provided or false otherwise
+ */
+ initOptionalProperties: function(scope, attr, defaults) {
+ defaults = defaults || {};
+ angular.forEach(scope.$$isolateBindings, function(binding, key) {
+ if (binding.optional && angular.isUndefined(scope[key])) {
+ var attrIsDefined = angular.isDefined(attr[binding.attrName]);
+ scope[key] = angular.isDefined(defaults[key]) ? defaults[key] : attrIsDefined;
+ }
+ });
+ },
+
+ /**
+ * Alternative to $timeout calls with 0 delay.
+ * nextTick() coalesces all calls within a single frame
+ * to minimize $digest thrashing
+ *
+ * @param callback
+ * @param digest
+ * @returns {*}
+ */
+ nextTick: function(callback, digest, scope) {
+ //-- grab function reference for storing state details
+ var nextTick = $mdUtil.nextTick;
+ var timeout = nextTick.timeout;
+ var queue = nextTick.queue || [];
+
+ //-- add callback to the queue
+ queue.push(callback);
+
+ //-- set default value for digest
+ if (digest == null) digest = true;
+
+ //-- store updated digest/queue values
+ nextTick.digest = nextTick.digest || digest;
+ nextTick.queue = queue;
+
+ //-- either return existing timeout or create a new one
+ return timeout || (nextTick.timeout = $timeout(processQueue, 0, false));
+
+ /**
+ * Grab a copy of the current queue
+ * Clear the queue for future use
+ * Process the existing queue
+ * Trigger digest if necessary
+ */
+ function processQueue() {
+ var skip = scope && scope.$$destroyed;
+ var queue = !skip ? nextTick.queue : [];
+ var digest = !skip ? nextTick.digest : null;
+
+ nextTick.queue = [];
+ nextTick.timeout = null;
+ nextTick.digest = false;
+
+ queue.forEach(function(callback) {
+ callback();
+ });
+
+ if (digest) $rootScope.$digest();
+ }
+ },
+
+ /**
+ * Processes a template and replaces the start/end symbols if the application has
+ * overriden them.
+ *
+ * @param template The template to process whose start/end tags may be replaced.
+ * @returns {*}
+ */
+ processTemplate: function(template) {
+ if (usesStandardSymbols) {
+ return template;
+ } else {
+ if (!template || !angular.isString(template)) return template;
+ return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol);
+ }
+ }
+ };
+
+// Instantiate other namespace utility methods
+
+ $mdUtil.dom.animator = $$mdAnimate($mdUtil);
+
+ return $mdUtil;
+
+ function getNode(el) {
+ return el[0] || el;
+ }
+
+}
+UtilFactory.$inject = ["$document", "$timeout", "$compile", "$rootScope", "$$mdAnimate", "$interpolate", "$log"];
+
+/*
+ * Since removing jQuery from the demos, some code that uses `element.focus()` is broken.
+ * We need to add `element.focus()`, because it's testable unlike `element[0].focus`.
+ */
+
+angular.element.prototype.focus = angular.element.prototype.focus || function() {
+ if (this.length) {
+ this[0].focus();
+ }
+ return this;
+ };
+angular.element.prototype.blur = angular.element.prototype.blur || function() {
+ if (this.length) {
+ this[0].blur();
+ }
+ return this;
+ };
+
+
+})();
+(function(){
+"use strict";
+
+
+angular.module('material.core')
+ .service('$mdAria', AriaService);
+
+/*
+ * @ngInject
+ */
+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 = angular.element(element)[0] || element;
+
+ // if node exists and neither it nor its children have the attribute
+ if (node &&
+ ((!node.hasAttribute(attrName) ||
+ node.getAttribute(attrName).length === 0) &&
+ !childHasAttribute(node, attrName))) {
+
+ defaultValue = angular.isString(defaultValue) ? defaultValue.trim() : '';
+ if (defaultValue.length) {
+ element.attr(attrName, defaultValue);
+ } else {
+ $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
+ * $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.
+ */
+ this.compile = function(options) {
+ var templateUrl = options.templateUrl;
+ var template = options.template || '';
+ var controller = options.controller;
+ var controllerAs = options.controllerAs;
+ var resolve = angular.extend({}, options.resolve || {});
+ var locals = angular.extend({}, 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 compiledData;
+ 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 compiledData = {
+ locals: locals,
+ element: element,
+ link: function link(scope) {
+ locals.$scope = scope;
+
+ //Instantiate controller if it exists, because we have scope
+ if (controller) {
+ var invokeCtrl = $controller(controller, locals, true);
+ if (bindToController) {
+ angular.extend(invokeCtrl.instance, locals);
+ }
+ var ctrl = invokeCtrl();
+ //See angular-route source for this logic
+ element.data('$ngControllerController', ctrl);
+ element.children().data('$ngControllerController', ctrl);
+
+ if (controllerAs) {
+ scope[controllerAs] = ctrl;
+ }
+
+ // Publish reference to this controller
+ compiledData.controller = ctrl;
+ }
+ return linkFn(scope);
+ }
+ };
+ });
+
+ };
+}
+mdCompilerService.$inject = ["$q", "$http", "$injector", "$compile", "$controller", "$templateCache"];
+
+})();
+(function(){
+"use strict";
+
+ var HANDLERS = {};
+ /* The state of the current 'pointer'
+ * The pointer represents the state of the current touch.
+ * It contains normalized x and y coordinates from DOM events,
+ * as well as other information abstracted from the DOM.
+ */
+ var pointer, lastPointer, forceSkipClickHijack = false;
+
+ /**
+ * The position of the most recent click if that click was on a label element.
+ * @type {{x: number, y: number}?}
+ */
+ var lastLabelClickPos = null;
+
+ // Used to attach event listeners once when multiple ng-apps are running.
+ var isInitialized = false;
+
+ angular
+ .module('material.core.gestures', [ ])
+ .provider('$mdGesture', MdGestureProvider)
+ .factory('$$MdGestureHandler', MdGestureHandler)
+ .run( attachToDocument );
+
+ /**
+ * @ngdoc service
+ * @name $mdGestureProvider
+ * @module material.core.gestures
+ *
+ * @description
+ * In some scenarios on Mobile devices (without jQuery), the click events should NOT be hijacked.
+ * `$mdGestureProvider` is used to configure the Gesture module to ignore or skip click hijacking on mobile
+ * devices.
+ *
+ *
+ * app.config(function($mdGestureProvider) {
+ *
+ * // For mobile devices without jQuery loaded, do not
+ * // intercept click events during the capture phase.
+ * $mdGestureProvider.skipClickHijack();
+ *
+ * });
+ *
+ *
+ */
+ function MdGestureProvider() { }
+
+ MdGestureProvider.prototype = {
+
+ // Publish access to setter to configure a variable BEFORE the
+ // $mdGesture service is instantiated...
+ skipClickHijack: function() {
+ return forceSkipClickHijack = true;
+ },
+
+ /**
+ * $get is used to build an instance of $mdGesture
+ * @ngInject
+ */
+ $get : ["$$MdGestureHandler", "$$rAF", "$timeout", function($$MdGestureHandler, $$rAF, $timeout) {
+ return new MdGesture($$MdGestureHandler, $$rAF, $timeout);
+ }]
+ };
+
+
+
+ /**
+ * MdGesture factory construction function
+ * @ngInject
+ */
+ function MdGesture($$MdGestureHandler, $$rAF, $timeout) {
+ var userAgent = navigator.userAgent || navigator.vendor || window.opera;
+ var isIos = userAgent.match(/ipad|iphone|ipod/i);
+ var isAndroid = userAgent.match(/android/i);
+ var hasJQuery = (typeof window.jQuery !== 'undefined') && (angular.element === window.jQuery);
+
+ var self = {
+ handler: addHandler,
+ register: register,
+ // On mobile w/out jQuery, we normally intercept clicks. Should we skip that?
+ isHijackingClicks: (isIos || isAndroid) && !hasJQuery && !forceSkipClickHijack
+ };
+
+ if (self.isHijackingClicks) {
+ self.handler('click', {
+ options: {
+ maxDistance: 6
+ },
+ onEnd: function (ev, pointer) {
+ if (pointer.distance < this.state.options.maxDistance) {
+ this.dispatchEvent(ev, 'click');
+ }
+ }
+ });
+ }
+
+ /*
+ * Register an element to listen for a handler.
+ * This allows an element to override the default options for a handler.
+ * Additionally, some handlers like drag and hold only dispatch events if
+ * the domEvent happens inside an element that's registered to listen for these events.
+ *
+ * @see GestureHandler for how overriding of default options works.
+ * @example $mdGesture.register(myElement, 'drag', { minDistance: 20, horziontal: false })
+ */
+ function register(element, handlerName, options) {
+ var handler = HANDLERS[handlerName.replace(/^\$md./, '')];
+ if (!handler) {
+ throw new Error('Failed to register element with handler ' + handlerName + '. ' +
+ 'Available handlers: ' + Object.keys(HANDLERS).join(', '));
+ }
+ return handler.registerElement(element, options);
+ }
+
+ /*
+ * add a handler to $mdGesture. see below.
+ */
+ function addHandler(name, definition) {
+ var handler = new $$MdGestureHandler(name);
+ angular.extend(handler, definition);
+ HANDLERS[name] = handler;
+
+ return self;
+ }
+
+ /*
+ * Register handlers. These listen to touch/start/move events, interpret them,
+ * and dispatch gesture events depending on options & conditions. These are all
+ * instances of GestureHandler.
+ * @see GestureHandler
+ */
+ return self
+ /*
+ * The press handler dispatches an event on touchdown/touchend.
+ * It's a simple abstraction of touch/mouse/pointer start and end.
+ */
+ .handler('press', {
+ onStart: function (ev, pointer) {
+ this.dispatchEvent(ev, '$md.pressdown');
+ },
+ onEnd: function (ev, pointer) {
+ this.dispatchEvent(ev, '$md.pressup');
+ }
+ })
+
+ /*
+ * The hold handler dispatches an event if the user keeps their finger within
+ * the same area for ms.
+ * The hold handler will only run if a parent of the touch target is registered
+ * to listen for hold events through $mdGesture.register()
+ */
+ .handler('hold', {
+ options: {
+ maxDistance: 6,
+ delay: 500
+ },
+ onCancel: function () {
+ $timeout.cancel(this.state.timeout);
+ },
+ onStart: function (ev, pointer) {
+ // For hold, require a parent to be registered with $mdGesture.register()
+ // Because we prevent scroll events, this is necessary.
+ if (!this.state.registeredParent) return this.cancel();
+
+ this.state.pos = {x: pointer.x, y: pointer.y};
+ this.state.timeout = $timeout(angular.bind(this, function holdDelayFn() {
+ this.dispatchEvent(ev, '$md.hold');
+ this.cancel(); //we're done!
+ }), this.state.options.delay, false);
+ },
+ onMove: function (ev, pointer) {
+ // Don't scroll while waiting for hold.
+ // If we don't preventDefault touchmove events here, Android will assume we don't
+ // want to listen to anymore touch events. It will start scrolling and stop sending
+ // touchmove events.
+ ev.preventDefault();
+
+ // If the user moves greater than pixels, stop the hold timer
+ // set in onStart
+ var dx = this.state.pos.x - pointer.x;
+ var dy = this.state.pos.y - pointer.y;
+ if (Math.sqrt(dx * dx + dy * dy) > this.options.maxDistance) {
+ this.cancel();
+ }
+ },
+ onEnd: function () {
+ this.onCancel();
+ }
+ })
+
+ /*
+ * The drag handler dispatches a drag event if the user holds and moves his finger greater than
+ * px in the x or y direction, depending on options.horizontal.
+ * The drag will be cancelled if the user moves his finger greater than * in
+ * the perpindicular direction. Eg if the drag is horizontal and the user moves his finger *
+ * pixels vertically, this handler won't consider the move part of a drag.
+ */
+ .handler('drag', {
+ options: {
+ minDistance: 6,
+ horizontal: true,
+ cancelMultiplier: 1.5
+ },
+ onStart: function (ev) {
+ // For drag, require a parent to be registered with $mdGesture.register()
+ if (!this.state.registeredParent) this.cancel();
+ },
+ onMove: function (ev, pointer) {
+ var shouldStartDrag, shouldCancel;
+ // Don't scroll while deciding if this touchmove qualifies as a drag event.
+ // If we don't preventDefault touchmove events here, Android will assume we don't
+ // want to listen to anymore touch events. It will start scrolling and stop sending
+ // touchmove events.
+ ev.preventDefault();
+
+ if (!this.state.dragPointer) {
+ if (this.state.options.horizontal) {
+ shouldStartDrag = Math.abs(pointer.distanceX) > this.state.options.minDistance;
+ shouldCancel = Math.abs(pointer.distanceY) > this.state.options.minDistance * this.state.options.cancelMultiplier;
+ } else {
+ shouldStartDrag = Math.abs(pointer.distanceY) > this.state.options.minDistance;
+ shouldCancel = Math.abs(pointer.distanceX) > this.state.options.minDistance * this.state.options.cancelMultiplier;
+ }
+
+ if (shouldStartDrag) {
+ // Create a new pointer representing this drag, starting at this point where the drag started.
+ this.state.dragPointer = makeStartPointer(ev);
+ updatePointerState(ev, this.state.dragPointer);
+ this.dispatchEvent(ev, '$md.dragstart', this.state.dragPointer);
+
+ } else if (shouldCancel) {
+ this.cancel();
+ }
+ } else {
+ this.dispatchDragMove(ev);
+ }
+ },
+ // Only dispatch dragmove events every frame; any more is 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);
+ }
+ }
+ })
+
+ /*
+ * The swipe handler will dispatch a swipe event if, on the end of a touch,
+ * the velocity and distance were high enough.
+ * TODO: add vertical swiping with a `horizontal` option similar to the drag handler.
+ */
+ .handler('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);
+ }
+ }
+ });
+
+ }
+ MdGesture.$inject = ["$$MdGestureHandler", "$$rAF", "$timeout"];
+
+ /**
+ * MdGestureHandler
+ * A GestureHandler is an object which is able to dispatch custom dom events
+ * based on native dom {touch,pointer,mouse}{start,move,end} events.
+ *
+ * A gesture will manage its lifecycle through the start,move,end, and cancel
+ * functions, which are called by native dom events.
+ *
+ * A gesture has the concept of 'options' (eg a swipe's required velocity), which can be
+ * overridden by elements registering through $mdGesture.register()
+ */
+ function GestureHandler (name) {
+ this.name = name;
+ this.state = {};
+ }
+
+ function MdGestureHandler() {
+ var hasJQuery = (typeof window.jQuery !== 'undefined') && (angular.element === window.jQuery);
+
+ GestureHandler.prototype = {
+ options: {},
+ // jQuery listeners don't work with custom DOMEvents, so we have to dispatch events
+ // differently when jQuery is loaded
+ dispatchEvent: hasJQuery ? jQueryDispatchEvent : nativeDispatchEvent,
+
+ // These are overridden by the registered handler
+ onStart: angular.noop,
+ onMove: angular.noop,
+ onEnd: angular.noop,
+ onCancel: angular.noop,
+
+ // onStart sets up a new state for the handler, which includes options from the
+ // nearest registered parent element of ev.target.
+ start: function (ev, pointer) {
+ if (this.state.isRunning) return;
+ var parentTarget = this.getNearestParent(ev.target);
+ // Get the options from the nearest registered parent
+ var parentTargetOptions = parentTarget && parentTarget.$mdGesture[this.name] || {};
+
+ this.state = {
+ isRunning: true,
+ // Override the default options with the nearest registered parent's options
+ options: angular.extend({}, this.options, parentTargetOptions),
+ // Pass in the registered parent node to the state so the onStart listener can use
+ registeredParent: parentTarget
+ };
+ this.onStart(ev, pointer);
+ },
+ move: function (ev, pointer) {
+ if (!this.state.isRunning) return;
+ this.onMove(ev, pointer);
+ },
+ end: function (ev, pointer) {
+ if (!this.state.isRunning) return;
+ this.onEnd(ev, pointer);
+ this.state.isRunning = false;
+ },
+ cancel: function (ev, pointer) {
+ this.onCancel(ev, pointer);
+ this.state = {};
+ },
+
+ // Find and return the nearest parent element that has been registered to
+ // listen for this handler via $mdGesture.register(element, 'handlerName').
+ getNearestParent: function (node) {
+ var current = node;
+ while (current) {
+ if ((current.$mdGesture || {})[this.name]) {
+ return current;
+ }
+ current = current.parentNode;
+ }
+ return null;
+ },
+
+ // Called from $mdGesture.register when an element reigsters itself with a handler.
+ // Store the options the user gave on the DOMElement itself. These options will
+ // be retrieved with getNearestParent when the handler starts.
+ registerElement: function (element, options) {
+ var self = this;
+ element[0].$mdGesture = element[0].$mdGesture || {};
+ element[0].$mdGesture[this.name] = options || {};
+ element.on('$destroy', onDestroy);
+
+ return onDestroy;
+
+ function onDestroy() {
+ delete element[0].$mdGesture[self.name];
+ element.off('$destroy', onDestroy);
+ }
+ }
+ };
+
+ return GestureHandler;
+
+ /*
+ * Dispatch an event with jQuery
+ * TODO: Make sure this sends bubbling events
+ *
+ * @param srcEvent the original DOM touch event that started this.
+ * @param eventType the name of the custom event to send (eg 'click' or '$md.drag')
+ * @param eventPointer the pointer object that matches this event.
+ */
+ function jQueryDispatchEvent(srcEvent, eventType, eventPointer) {
+ eventPointer = eventPointer || pointer;
+ var eventObj = new angular.element.Event(eventType);
+
+ eventObj.$material = true;
+ eventObj.pointer = eventPointer;
+ eventObj.srcEvent = srcEvent;
+
+ angular.extend(eventObj, {
+ clientX: eventPointer.x,
+ clientY: eventPointer.y,
+ screenX: eventPointer.x,
+ screenY: eventPointer.y,
+ pageX: eventPointer.x,
+ pageY: eventPointer.y,
+ ctrlKey: srcEvent.ctrlKey,
+ altKey: srcEvent.altKey,
+ shiftKey: srcEvent.shiftKey,
+ metaKey: srcEvent.metaKey
+ });
+ angular.element(eventPointer.target).trigger(eventObj);
+ }
+
+ /*
+ * NOTE: nativeDispatchEvent is very performance sensitive.
+ * @param srcEvent the original DOM touch event that started this.
+ * @param eventType the name of the custom event to send (eg 'click' or '$md.drag')
+ * @param eventPointer the pointer object that matches this event.
+ */
+ function nativeDispatchEvent(srcEvent, eventType, eventPointer) {
+ eventPointer = eventPointer || pointer;
+ var eventObj;
+
+ if (eventType === 'click') {
+ 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);
+ }
+
+ }
+
+ /**
+ * Attach Gestures: hook document and check shouldHijack clicks
+ * @ngInject
+ */
+ function attachToDocument( $mdGesture, $$MdGestureHandler ) {
+
+ // Polyfill document.contains for IE11.
+ // TODO: move to util
+ document.contains || (document.contains = function (node) {
+ return document.body.contains(node);
+ });
+
+ if (!isInitialized && $mdGesture.isHijackingClicks ) {
+ /*
+ * If hijack clicks is true, we preventDefault any click that wasn't
+ * sent by ngMaterial. This is because on older Android & iOS, a false, or 'ghost',
+ * click event will be sent ~400ms after a touchend event happens.
+ * The only way to know if this click is real is to prevent any normal
+ * click events, and add a flag to events sent by material so we know not to prevent those.
+ *
+ * Two exceptions to click events that should be prevented are:
+ * - click events sent by the keyboard (eg form submit)
+ * - events that originate from an Ionic app
+ */
+ document.addEventListener('click', function clickHijacker(ev) {
+ var isKeyClick = ev.clientX === 0 && ev.clientY === 0;
+ if (!isKeyClick && !ev.$material && !ev.isIonicTap
+ && !isInputEventFromLabelClick(ev)) {
+ ev.preventDefault();
+ ev.stopPropagation();
+ lastLabelClickPos = null;
+ } else {
+ lastLabelClickPos = null;
+ if (ev.target.tagName.toLowerCase() == 'label') {
+ lastLabelClickPos = {x: ev.x, y: ev.y};
+ }
+ }
+ }, true);
+
+ isInitialized = true;
+ }
+
+ // Listen to all events to cover all platforms.
+ var START_EVENTS = 'mousedown touchstart pointerdown';
+ var MOVE_EVENTS = 'mousemove touchmove pointermove';
+ var END_EVENTS = 'mouseup mouseleave touchend touchcancel pointerup pointercancel';
+
+ angular.element(document)
+ .on(START_EVENTS, gestureStart)
+ .on(MOVE_EVENTS, gestureMove)
+ .on(END_EVENTS, gestureEnd)
+ // For testing
+ .on('$$mdGestureReset', function gestureClearCache () {
+ lastPointer = pointer = null;
+ });
+
+ /*
+ * When a DOM event happens, run all registered gesture handlers' lifecycle
+ * methods which match the DOM event.
+ * Eg when a 'touchstart' event happens, runHandlers('start') will call and
+ * run `handler.cancel()` and `handler.start()` on all registered handlers.
+ */
+ function runHandlers(handlerEvent, event) {
+ var handler;
+ for (var name in HANDLERS) {
+ handler = HANDLERS[name];
+ if( handler instanceof $$MdGestureHandler ) {
+
+ if (handlerEvent === 'start') {
+ // Run cancel to reset any handlers' state
+ handler.cancel();
+ }
+ handler[handlerEvent](event, pointer);
+
+ }
+ }
+ }
+
+ /*
+ * gestureStart vets if a start event is legitimate (and not part of a 'ghost click' from iOS/Android)
+ * If it is legitimate, we initiate the pointer state and mark the current pointer's type
+ * For example, for a touchstart event, mark the current pointer as a 'touch' pointer, so mouse events
+ * won't effect it.
+ */
+ function gestureStart(ev) {
+ // If we're already touched down, abort
+ if (pointer) return;
+
+ var now = +Date.now();
+
+ // iOS & old android bug: after a touch event, a click event is sent 350 ms later.
+ // If <400ms have passed, don't allow an event of a different type than the previous event
+ if (lastPointer && !typesMatch(ev, lastPointer) && (now - lastPointer.endTime < 1500)) {
+ return;
+ }
+
+ pointer = makeStartPointer(ev);
+
+ runHandlers('start', ev);
+ }
+ /*
+ * If a move event happens of the right type, update the pointer and run all the move handlers.
+ * "of the right type": if a mousemove happens but our pointer started with a touch event, do nothing.
+ */
+ function gestureMove(ev) {
+ if (!pointer || !typesMatch(ev, pointer)) return;
+
+ updatePointerState(ev, pointer);
+ runHandlers('move', ev);
+ }
+ /*
+ * If an end event happens of the right type, update the pointer, run endHandlers, and save the pointer as 'lastPointer'
+ */
+ function gestureEnd(ev) {
+ if (!pointer || !typesMatch(ev, pointer)) return;
+
+ updatePointerState(ev, pointer);
+ pointer.endTime = +Date.now();
+
+ runHandlers('end', ev);
+
+ lastPointer = pointer;
+ pointer = null;
+ }
+
+ }
+ attachToDocument.$inject = ["$mdGesture", "$$MdGestureHandler"];
+
+ // ********************
+ // Module Functions
+ // ********************
+
+ /*
+ * Initiate the pointer. x, y, and the pointer's type.
+ */
+ function makeStartPointer(ev) {
+ var point = getEventPoint(ev);
+ var startPointer = {
+ startTime: +Date.now(),
+ target: ev.target,
+ // 'p' for pointer events, 'm' for mouse, 't' for touch
+ type: ev.type.charAt(0)
+ };
+ startPointer.startX = startPointer.x = point.pageX;
+ startPointer.startY = startPointer.y = point.pageY;
+ return startPointer;
+ }
+
+ /*
+ * return whether the pointer's type matches the event's type.
+ * Eg if a touch event happens but the pointer has a mouse type, return false.
+ */
+ function typesMatch(ev, pointer) {
+ return ev && pointer && ev.type.charAt(0) === pointer.type;
+ }
+
+ /**
+ * Gets whether the given event is an input event that was caused by clicking on an
+ * associated label element.
+ *
+ * This is necessary because the browser will, upon clicking on a label element, fire an
+ * *extra* click event on its associated input (if any). mdGesture is able to flag the label
+ * click as with `$material` correctly, but not the second input click.
+ *
+ * In order to determine whether an input event is from a label click, we compare the (x, y) for
+ * the event to the (x, y) for the most recent label click (which is cleared whenever a non-label
+ * click occurs). Unfortunately, there are no event properties that tie the input and the label
+ * together (such as relatedTarget).
+ *
+ * @param {MouseEvent} event
+ * @returns {boolean}
+ */
+ function isInputEventFromLabelClick(event) {
+ return lastLabelClickPos
+ && lastLabelClickPos.x == event.x
+ && lastLabelClickPos.y == event.y;
+ }
+
+ /*
+ * Update the given pointer based upon the given DOMEvent.
+ * Distance, velocity, direction, duration, etc
+ */
+ function updatePointerState(ev, pointer) {
+ var point = getEventPoint(ev);
+ var x = pointer.x = point.pageX;
+ var y = pointer.y = point.pageY;
+
+ pointer.distanceX = x - pointer.startX;
+ pointer.distanceY = y - pointer.startY;
+ pointer.distance = Math.sqrt(
+ pointer.distanceX * pointer.distanceX + pointer.distanceY * pointer.distanceY
+ );
+
+ pointer.directionX = pointer.distanceX > 0 ? 'right' : pointer.distanceX < 0 ? 'left' : '';
+ pointer.directionY = pointer.distanceY > 0 ? 'up' : pointer.distanceY < 0 ? 'down' : '';
+
+ pointer.duration = +Date.now() - pointer.startTime;
+ pointer.velocityX = pointer.distanceX / pointer.duration;
+ pointer.velocityY = pointer.distanceY / pointer.duration;
+ }
+
+ /*
+ * Normalize the point where the DOM event happened whether it's touch or mouse.
+ * @returns point event obj with pageX and pageY on it.
+ */
+ function getEventPoint(ev) {
+ ev = ev.originalEvent || ev; // support jQuery events
+ return (ev.touches && ev.touches[0]) ||
+ (ev.changedTouches && ev.changedTouches[0]) ||
+ ev;
+ }
+
+})();
+(function(){
+"use strict";
+
+angular.module('material.core')
+ .provider('$$interimElement', InterimElementProvider);
+
+/*
+ * @ngdoc service
+ * @name $$interimElement
+ * @module material.core
+ *
+ * @description
+ *
+ * Factory that contructs `$$interimElement.$service` services.
+ * Used internally in material design for elements that appear on screen temporarily.
+ * The service provides a promise-like API for interacting with the temporary
+ * elements.
+ *
+ * ```js
+ * app.service('$mdToast', function($$interimElement) {
+ * var $mdToast = $$interimElement(toastDefaultOptions);
+ * return $mdToast;
+ * });
+ * ```
+ * @param {object=} defaultOptions Options used by default for the `show` method on the service.
+ *
+ * @returns {$$interimElement.$service}
+ *
+ */
+
+function InterimElementProvider() {
+ createInterimElementProvider.$get = InterimElementFactory;
+ InterimElementFactory.$inject = ["$document", "$q", "$$q", "$rootScope", "$timeout", "$rootElement", "$animate", "$mdUtil", "$mdCompiler", "$mdTheming", "$log"];
+ 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", "$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, $injector) {
+ var defaultMethods;
+ var defaultOptions;
+ var interimElementService = $$interimElement();
+
+ /*
+ * publicService is what the developer will be using.
+ * It has methods hide(), cancel(), show(), build(), and any other
+ * presets which were set during the config phase.
+ */
+ var publicService = {
+ hide: interimElementService.hide,
+ cancel: interimElementService.cancel,
+ show: showInterimElement,
+
+ // Special internal method to destroy an interim element without animations
+ // used when navigation changes causes a $scope.$destroy() action
+ destroy : destroyInterimElement
+ };
+
+
+ defaultMethods = providerConfig.methods || [];
+ // This must be invoked after the publicService is initialized
+ defaultOptions = invokeFactory(providerConfig.optionsFactory, {});
+
+ // Copy over the simple custom methods
+ angular.forEach(customMethods, function(fn, name) {
+ publicService[name] = fn;
+ });
+
+ angular.forEach(providerConfig.presets, function(definition, name) {
+ var presetDefaults = invokeFactory(definition.optionsFactory, {});
+ var presetMethods = (definition.methods || []).concat(defaultMethods);
+
+ // Every interimElement built with a preset has a field called `$type`,
+ // which matches the name of the preset.
+ // Eg in preset 'confirm', options.$type === 'confirm'
+ angular.extend(presetDefaults, { $type: name });
+
+ // This creates a preset class which has setter methods for every
+ // method given in the `.addPreset()` function, as well as every
+ // method given in the `.setDefaults()` function.
+ //
+ // @example
+ // .setDefaults({
+ // methods: ['hasBackdrop', 'clickOutsideToClose', 'escapeToClose', 'targetEvent'],
+ // options: dialogDefaultOptions
+ // })
+ // .addPreset('alert', {
+ // methods: ['title', 'ok'],
+ // options: alertDialogOptions
+ // })
+ //
+ // Set values will be passed to the options when interimElement.show() is called.
+ function Preset(opts) {
+ this._options = angular.extend({}, presetDefaults, opts);
+ }
+ angular.forEach(presetMethods, function(name) {
+ Preset.prototype[name] = function(value) {
+ this._options[name] = value;
+ return this;
+ };
+ });
+
+ // Create shortcut method for one-linear methods
+ if (definition.argOption) {
+ var methodName = 'show' + name.charAt(0).toUpperCase() + name.slice(1);
+ publicService[methodName] = function(arg) {
+ var config = publicService[name](arg);
+ return publicService.show(config);
+ };
+ }
+
+ // eg $mdDialog.alert() will return a new alert preset
+ publicService[name] = function(arg) {
+ // If argOption is supplied, eg `argOption: 'content'`, then we assume
+ // if the argument is not an options object then it is the `argOption` option.
+ //
+ // @example `$mdToast.simple('hello')` // sets options.content to hello
+ // // because argOption === 'content'
+ if (arguments.length && definition.argOption &&
+ !angular.isObject(arg) && !angular.isArray(arg)) {
+
+ return (new Preset())[definition.argOption](arg);
+
+ } else {
+ return new Preset(arg);
+ }
+
+ };
+ });
+
+ return publicService;
+
+ /**
+ *
+ */
+ function showInterimElement(opts) {
+ // opts is either a preset which stores its options on an _options field,
+ // or just an object made up of options
+ opts = opts || { };
+ if (opts._options) opts = opts._options;
+
+ return interimElementService.show(
+ angular.extend({}, defaultOptions, opts)
+ );
+ }
+
+ /**
+ * Special method to hide and destroy an interimElement WITHOUT
+ * any 'leave` or hide animations ( an immediate force hide/remove )
+ *
+ * NOTE: This calls the onRemove() subclass method for each component...
+ * which must have code to respond to `options.$destroy == true`
+ */
+ function destroyInterimElement(opts) {
+ return interimElementService.destroy(opts);
+ }
+
+ /**
+ * Helper to call $injector.invoke with a local of the factory name for
+ * this provider.
+ * If an $mdDialog is providing options for a dialog and tries to inject
+ * $mdDialog, a circular dependency error will happen.
+ * We get around that by manually injecting $mdDialog as a local.
+ */
+ function invokeFactory(factory, defaultVal) {
+ var locals = {};
+ locals[interimFactoryName] = publicService;
+ return $injector.invoke(factory || function() { return defaultVal; }, {}, locals);
+ }
+
+ }
+
+ }
+
+ /* @ngInject */
+ function InterimElementFactory($document, $q, $$q, $rootScope, $timeout, $rootElement, $animate,
+ $mdUtil, $mdCompiler, $mdTheming, $log ) {
+ return function createInterimElementService() {
+ var SHOW_CANCELLED = false;
+
+ /*
+ * @ngdoc service
+ * @name $$interimElement.$service
+ *
+ * @description
+ * A service used to control inserting and removing an element into the DOM.
+ *
+ */
+ var service, stack = [];
+
+ // Publish instance $$interimElement service;
+ // ... used as $mdDialog, $mdToast, $mdMenu, and $mdSelect
+
+ return service = {
+ show: show,
+ hide: hide,
+ cancel: cancel,
+ destroy : destroy
+ };
+
+ /*
+ * @ngdoc method
+ * @name $$interimElement.$service#show
+ * @kind function
+ *
+ * @description
+ * Adds the `$interimElement` to the DOM and returns a special promise that will be resolved or rejected
+ * with hide or cancel, respectively. To external cancel/hide, developers should use the
+ *
+ * @param {*} options is hashMap of settings
+ * @returns a Promise
+ *
+ */
+ function show(options) {
+ options = options || {};
+ var interimElement = new InterimElement(options || {});
+ var hideExisting = !options.skipHide && stack.length ? service.hide() : $q.when(true);
+
+ // This hide()s only the current interim element before showing the next, new one
+ // NOTE: this is not reversible (e.g. interim elements are not stackable)
+
+ hideExisting.finally(function() {
+
+ stack.push(interimElement);
+ interimElement
+ .show()
+ .catch(function( reason ) {
+ //$log.error("InterimElement.show() error: " + reason );
+ return reason;
+ });
+
+ });
+
+ // Return a promise that will be resolved when the interim
+ // element is hidden or cancelled...
+
+ return interimElement.deferred.promise;
+ }
+
+ /*
+ * @ngdoc method
+ * @name $$interimElement.$service#hide
+ * @kind function
+ *
+ * @description
+ * Removes the `$interimElement` from the DOM and resolves the promise returned from `show`
+ *
+ * @param {*} resolveParam Data to resolve the promise with
+ * @returns a Promise that will be resolved after the element has been removed.
+ *
+ */
+ function hide(reason, options) {
+ if ( !stack.length ) return $q.when(reason);
+ options = options || {};
+
+ if (options.closeAll) {
+ var promise = $q.all(stack.reverse().map(closeElement));
+ stack = [];
+ return promise;
+ } else if (options.closeTo !== undefined) {
+ return $q.all(stack.splice(options.closeTo).map(closeElement));
+ } else {
+ var interim = stack.pop();
+ return closeElement(interim);
+ }
+
+ function closeElement(interim) {
+ interim
+ .remove(reason, false, options || { })
+ .catch(function( reason ) {
+ //$log.error("InterimElement.hide() error: " + reason );
+ return reason;
+ });
+ return interim.deferred.promise;
+ }
+ }
+
+ /*
+ * @ngdoc method
+ * @name $$interimElement.$service#cancel
+ * @kind function
+ *
+ * @description
+ * Removes the `$interimElement` from the DOM and rejects the promise returned from `show`
+ *
+ * @param {*} reason Data to reject the promise with
+ * @returns Promise that will be resolved after the element has been removed.
+ *
+ */
+ function cancel(reason, options) {
+ var interim = stack.shift();
+ if ( !interim ) return $q.when(reason);
+
+ interim
+ .remove(reason, true, options || { })
+ .catch(function( reason ) {
+ //$log.error("InterimElement.cancel() error: " + reason );
+ return reason;
+ });
+
+ return interim.deferred.promise;
+ }
+
+ /*
+ * Special method to quick-remove the interim element without animations
+ */
+ function destroy() {
+ var interim = stack.shift();
+
+ return interim ? interim.remove(SHOW_CANCELLED, false, {'$destroy':true}) :
+ $q.when(SHOW_CANCELLED);
+ }
+
+
+ /*
+ * Internal Interim Element Object
+ * Used internally to manage the DOM element and related data
+ */
+ function InterimElement(options) {
+ var self, element, showAction = $q.when(true);
+
+ options = configureScopeAndTransitions(options);
+
+ return self = {
+ options : options,
+ deferred: $q.defer(),
+ show : createAndTransitionIn,
+ remove : transitionOutAndRemove
+ };
+
+ /**
+ * Compile, link, and show this interim element
+ * Use optional autoHided and transition-in effects
+ */
+ function createAndTransitionIn() {
+ return $q(function(resolve, reject){
+
+ compileElement(options)
+ .then(function( compiledData ) {
+ element = linkElement( compiledData, options );
+
+ showAction = showElement(element, options, compiledData.controller)
+ .then(resolve, rejectAll );
+
+ }, rejectAll);
+
+ function rejectAll(fault) {
+ // Force the '$md.show()' promise to reject
+ self.deferred.reject(fault);
+
+ // Continue rejection propagation
+ reject(fault);
+ }
+ });
+ }
+
+ /**
+ * After the show process has finished/rejected:
+ * - announce 'removing',
+ * - perform the transition-out, and
+ * - perform optional clean up scope.
+ */
+ function transitionOutAndRemove(response, isCancelled, opts) {
+
+ // abort if the show() and compile failed
+ if ( !element ) return $q.when(false);
+
+ options = angular.extend(options || {}, opts || {});
+ options.cancelAutoHide && options.cancelAutoHide();
+ options.element.triggerHandler('$mdInterimElementRemove');
+
+ if ( options.$destroy === true ) {
+
+ return hideElement(options.element, options);
+
+ } else {
+
+ $q.when(showAction)
+ .finally(function() {
+ hideElement(options.element, options).then(function() {
+
+ (isCancelled && rejectAll(response)) || resolveAll(response);
+
+ }, rejectAll);
+ });
+
+ return self.deferred.promise;
+ }
+
+
+ /**
+ * The `show()` returns a promise that will be resolved when the interim
+ * element is hidden or cancelled...
+ */
+ function resolveAll(response) {
+ self.deferred.resolve(response);
+ }
+
+ /**
+ * Force the '$md.show()' promise to reject
+ */
+ function rejectAll(fault) {
+ self.deferred.reject(fault);
+ }
+ }
+
+ /**
+ * Prepare optional isolated scope and prepare $animate with default enter and leave
+ * transitions for the new element instance.
+ */
+ function configureScopeAndTransitions(options) {
+ options = options || { };
+ if ( options.template ) {
+ options.template = $mdUtil.processTemplate(options.template);
+ }
+
+ return angular.extend({
+ preserveScope: false,
+ cancelAutoHide : angular.noop,
+ scope: options.scope || $rootScope.$new(options.isolateScope),
+
+ /**
+ * Default usage to enable $animate to transition-in; can be easily overridden via 'options'
+ */
+ onShow: function transitionIn(scope, element, options) {
+ return $animate.enter(element, options.parent);
+ },
+
+ /**
+ * Default usage to enable $animate to transition-out; can be easily overridden via 'options'
+ */
+ onRemove: function transitionOut(scope, element) {
+ // Element could be undefined if a new element is shown before
+ // the old one finishes compiling.
+ return element && $animate.leave(element) || $q.when();
+ }
+ }, options );
+
+ }
+
+ /**
+ * Compile an element with a templateUrl, controller, and locals
+ */
+ function compileElement(options) {
+
+ var compiled = !options.skipCompile ? $mdCompiler.compile(options) : null;
+
+ return compiled || $q(function (resolve) {
+ resolve({
+ locals: {},
+ link: function () {
+ return options.element;
+ }
+ });
+ });
+ }
+
+ /**
+ * Link an element with compiled configuration
+ */
+ function linkElement(compileData, options){
+ angular.extend(compileData.locals, options);
+
+ var element = compileData.link(options.scope);
+
+ // Search for parent at insertion time, if not specified
+ options.element = element;
+ options.parent = findParent(element, options);
+ if (options.themable) $mdTheming(element);
+
+ return element;
+ }
+
+ /**
+ * Search for parent at insertion time, if not specified
+ */
+ function findParent(element, options) {
+ var parent = options.parent;
+
+ // Search for parent at insertion time, if not specified
+ if (angular.isFunction(parent)) {
+ parent = parent(options.scope, element, options);
+ } else if (angular.isString(parent)) {
+ parent = angular.element($document[0].querySelector(parent));
+ } else {
+ parent = angular.element(parent);
+ }
+
+ // If parent querySelector/getter function fails, or it's just null,
+ // find a default.
+ if (!(parent || {}).length) {
+ var el;
+ if ($rootElement[0] && $rootElement[0].querySelector) {
+ el = $rootElement[0].querySelector(':not(svg) > body');
+ }
+ if (!el) el = $rootElement[0];
+ if (el.nodeName == '#comment') {
+ el = $document[0].body;
+ }
+ return angular.element(el);
+ }
+
+ return parent;
+ }
+
+ /**
+ * If auto-hide is enabled, start timer and prepare cancel function
+ */
+ function startAutoHide() {
+ var autoHideTimer, cancelAutoHide = angular.noop;
+
+ if (options.hideDelay) {
+ autoHideTimer = $timeout(service.hide, options.hideDelay) ;
+ cancelAutoHide = function() {
+ $timeout.cancel(autoHideTimer);
+ }
+ }
+
+ // Cache for subsequent use
+ options.cancelAutoHide = function() {
+ cancelAutoHide();
+ options.cancelAutoHide = undefined;
+ }
+ }
+
+ /**
+ * Show the element ( with transitions), notify complete and start
+ * optional auto-Hide
+ */
+ function showElement(element, options, controller) {
+ // Trigger onShowing callback before the `show()` starts
+ var notifyShowing = options.onShowing || angular.noop;
+ // Trigger onComplete callback when the `show()` finishes
+ var notifyComplete = options.onComplete || angular.noop;
+
+ return $q(function (resolve, reject) {
+ try {
+ notifyShowing(options.scope, element, options);
+
+ // Start transitionIn
+ $q.when(options.onShow(options.scope, element, options, controller))
+ .then(function () {
+ notifyComplete(options.scope, element, options);
+ startAutoHide();
+
+ resolve(element);
+
+ }, reject );
+
+ } catch(e) {
+ reject(e.message);
+ }
+ });
+ }
+
+ function hideElement(element, options) {
+ var announceRemoving = options.onRemoving || angular.noop;
+
+ return $$q(function (resolve, reject) {
+ try {
+ // Start transitionIn
+ var action = $$q.when( options.onRemove(options.scope, element, options) || true );
+
+ // Trigger callback *before* the remove operation starts
+ announceRemoving(element, action);
+
+ if ( options.$destroy == true ) {
+
+ // For $destroy, onRemove should be synchronous
+ resolve(element);
+
+ } else {
+
+ // Wait until transition-out is done
+ action.then(function () {
+
+ if (!options.preserveScope && options.scope ) {
+ options.scope.$destroy();
+ }
+
+ resolve(element);
+
+ }, reject );
+ }
+
+ } catch(e) {
+ reject(e.message);
+ }
+ });
+ }
+
+ }
+ };
+
+ }
+
+}
+
+})();
+(function(){
+"use strict";
+
+ /**
+ * @ngdoc module
+ * @name material.core.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"];
+
+})();
+(function(){
+"use strict";
+
+(function() {
+ 'use strict';
+
+ var $mdUtil, $interpolate;
+
+ var SUFFIXES = /(-gt)?-(sm|md|lg)/g;
+ var WHITESPACE = /\s+/g;
+
+ var FLEX_OPTIONS = ['grow', 'initial', 'auto', 'none'];
+ var LAYOUT_OPTIONS = ['row', 'column'];
+ var ALIGNMENT_OPTIONS = [
+ "start start", "start center", "start end",
+ "center", "center center", "center start", "center end",
+ "end", "end center", "end start", "end end",
+ "space-around", "space-around center", "space-around start", "space-around end",
+ "space-between", "space-between center", "space-between start", "space-between end"
+ ];
+
+
+ var config = {
+ /**
+ * Enable directive attribute-to-class conversions
+ */
+ enabled: true,
+
+ /**
+ * After translation to classname equivalents, remove the
+ * original Layout attribute
+ */
+ removeAttributes : true,
+
+ /**
+ * List of mediaQuery breakpoints and associated suffixes
+ *
+ * [
+ * { suffix: "sm", mediaQuery: "screen and (max-width: 599px)" },
+ * { suffix: "md", mediaQuery: "screen and (min-width: 600px) and (max-width: 959px)" }
+ * ]
+ */
+ breakpoints: []
+ };
+
+ /**
+ * The original ngMaterial Layout solution used attribute selectors and CSS.
+ *
+ * ```html
+ *
My Content
+ * ```
+ *
+ * ```css
+ * [layout] {
+ * box-sizing: border-box;
+ * display:flex;
+ * }
+ * [layout=column] {
+ * flex-direction : column
+ * }
+ * ```
+ *
+ * Use of attribute selectors creates significant performance impacts in some
+ * browsers... mainly IE.
+ *
+ * This module registers directives that allow the same layout attributes to be
+ * interpreted and converted to class selectors. The directive will add equivalent classes to each element that
+ * contains a Layout directive.
+ *
+ * ```html
+ *
My Content
+ *```
+ *
+ * ```css
+ * .layout {
+ * box-sizing: border-box;
+ * display:flex;
+ * }
+ * .layout-column {
+ * flex-direction : column
+ * }
+ * ```
+ */
+ angular.module('material.core.layout', ['ng'])
+
+ .directive('mdLayoutCss', disableLayoutDirective )
+
+ .directive('layout', attributeWithObserve('layout'))
+ .directive('layoutSm', attributeWithObserve('layout-sm'))
+ .directive('layoutGtSm', attributeWithObserve('layout-gt-sm'))
+ .directive('layoutMd', attributeWithObserve('layout-md'))
+ .directive('layoutGtMd', attributeWithObserve('layout-gt-md'))
+ .directive('layoutLg', attributeWithObserve('layout-lg'))
+ .directive('layoutGtLg', attributeWithObserve('layout-gt-lg'))
+
+ .directive('flex', attributeWithObserve('flex'))
+ .directive('flexSm', attributeWithObserve('flex-sm'))
+ .directive('flexGtSm', attributeWithObserve('flex-gt-sm'))
+ .directive('flexMd', attributeWithObserve('flex-md'))
+ .directive('flexGtMd', attributeWithObserve('flex-gt-md'))
+ .directive('flexLg', attributeWithObserve('flex-lg'))
+ .directive('flexGtLg', attributeWithObserve('flex-gt-lg'))
+
+ .directive('flexOrder', attributeWithObserve('flex-order'))
+ .directive('flexOrderSm', attributeWithObserve('flex-order-sm'))
+ .directive('flexOrderGtSm', attributeWithObserve('flex-order-gt-sm'))
+ .directive('flexOrderMd', attributeWithObserve('flex-order-md'))
+ .directive('flexOrderGtMd', attributeWithObserve('flex-order-gt-md'))
+ .directive('flexOrderLg', attributeWithObserve('flex-order-lg'))
+ .directive('flexOrderGtLg', attributeWithObserve('flex-order-gt-lg'))
+
+ .directive('flexOffset', attributeWithObserve('flex-offset'))
+ .directive('flexOffsetSm', attributeWithObserve('flex-offset-sm'))
+ .directive('flexOffsetGtSm', attributeWithObserve('flex-offset-gt-sm'))
+ .directive('flexOffsetMd', attributeWithObserve('flex-offset-md'))
+ .directive('flexOffsetGtMd', attributeWithObserve('flex-offset-gt-md'))
+ .directive('flexOffsetLg', attributeWithObserve('flex-offset-lg'))
+ .directive('flexOffsetGtLg', attributeWithObserve('flex-offset-gt-lg'))
+
+ .directive('layoutAlign', attributeWithObserve('layout-align'))
+ .directive('layoutAlignSm', attributeWithObserve('layout-align-sm'))
+ .directive('layoutAlignGtSm', attributeWithObserve('layout-align-gt-sm'))
+ .directive('layoutAlignMd', attributeWithObserve('layout-align-md'))
+ .directive('layoutAlignGtMd', attributeWithObserve('layout-align-gt-md'))
+ .directive('layoutAlignLg', attributeWithObserve('layout-align-lg'))
+ .directive('layoutAlignGtLg', attributeWithObserve('layout-align-gt-lg'))
+
+ // Attribute directives with no value(s)
+
+ .directive('hide', attributeWithoutValue('hide'))
+ .directive('hideSm', attributeWithoutValue('hide-sm'))
+ .directive('hideGtSm', attributeWithoutValue('hide-gt-sm'))
+ .directive('hideMd', attributeWithoutValue('hide-md'))
+ .directive('hideGtMd', attributeWithoutValue('hide-gt-md'))
+ .directive('hideLg', attributeWithoutValue('hide-lg'))
+ .directive('hideGtLg', attributeWithoutValue('hide-gt-lg'))
+ .directive('show', attributeWithoutValue('show'))
+ .directive('showSm', attributeWithoutValue('show-sm'))
+ .directive('showGtSm', attributeWithoutValue('show-gt-sm'))
+ .directive('showMd', attributeWithoutValue('show-md'))
+ .directive('showGtMd', attributeWithoutValue('show-gt-md'))
+ .directive('showLg', attributeWithoutValue('show-lg'))
+ .directive('showGtLg', attributeWithoutValue('show-gt-lg'))
+
+ // Attribute directives with no value(s) and NO breakpoints
+
+ .directive('layoutMargin', attributeWithoutValue('layout-margin'))
+ .directive('layoutPadding', attributeWithoutValue('layout-padding'))
+ .directive('layoutWrap', attributeWithoutValue('layout-wrap'))
+ .directive('layoutNoWrap', attributeWithoutValue('layout-no-wrap'))
+ .directive('layoutFill', attributeWithoutValue('layout-fill'))
+
+ // !! Deprecated attributes: use the `-lt` (aka less-than) notations
+
+ .directive('layoutLtMd', warnAttrNotSupported('layout-lt-md', true))
+ .directive('layoutLtLg', warnAttrNotSupported('layout-lt-lg', true))
+ .directive('flexLtMd', warnAttrNotSupported('flex-lt-md', true))
+ .directive('flexLtLg', warnAttrNotSupported('flex-lt-lg', true))
+
+ .directive('layoutAlignLtMd', warnAttrNotSupported('layout-align-lt-md'))
+ .directive('layoutAlignLtLg', warnAttrNotSupported('layout-align-lt-lg'))
+ .directive('flexOrderLtMd', warnAttrNotSupported('flex-order-lt-md'))
+ .directive('flexOrderLtLg', warnAttrNotSupported('flex-order-lt-lg'))
+ .directive('offsetLtMd', warnAttrNotSupported('flex-offset-lt-md'))
+ .directive('offsetLtLg', warnAttrNotSupported('flex-offset-lt-lg'))
+
+ .directive('hideLtMd', warnAttrNotSupported('hide-lt-md'))
+ .directive('hideLtLg', warnAttrNotSupported('hide-lt-lg'))
+ .directive('showLtMd', warnAttrNotSupported('show-lt-md'))
+ .directive('showLtLg', warnAttrNotSupported('show-lt-lg'));
+
+ /**
+ * Special directive that will disable ALL Layout conversions of layout
+ * attribute(s) to classname(s).
+ *
+ *
+ *
+ *
+ *
+ * ...
+ *
+ *
+ * Note: Using md-layout-css directive requires the developer to load the Material
+ * Layout Attribute stylesheet (which only uses attribute selectors):
+ *
+ * `angular-material.layout.css`
+ *
+ * Another option is to use the LayoutProvider to configure and disable the attribute
+ * conversions; this would obviate the use of the `md-layout-css` directive
+ *
+ */
+ function disableLayoutDirective() {
+ return {
+ restrict : 'A',
+ priority : '900',
+ compile : function(element, attr) {
+ config.enabled = false;
+ return angular.noop;
+ }
+ };
+ }
+
+ // *********************************************************************************
+ //
+ // These functions create registration functions for ngMaterial Layout attribute directives
+ // This provides easy translation to switch ngMaterial attribute selectors to
+ // CLASS selectors and directives; which has huge performance implications
+ // for IE Browsers
+ //
+ // *********************************************************************************
+
+
+ /**
+ * Creates a directive registration function where a possible dynamic attribute
+ * value will be observed/watched.
+ * @param {string} className attribute name; eg `layout-gt-md` with value ="row"
+ */
+ function attributeWithObserve(className) {
+
+ return ['$mdUtil', '$interpolate', function(_$mdUtil_, _$interpolate_) {
+ $mdUtil = _$mdUtil_;
+ $interpolate = _$interpolate_;
+
+ return {
+ restrict: 'A',
+ compile: function(element, attr) {
+ var linkFn;
+ if (config.enabled) {
+ // immediately replace static (non-interpolated) invalid values...
+
+ validateAttributeValue( className,
+ getNormalizedAttrValue(className, attr, ""),
+ buildUpdateFn(element, className, attr)
+ );
+
+ linkFn = translateWithValueToCssClass;
+ }
+
+ // Use for postLink to account for transforms after ng-transclude.
+ return linkFn || angular.noop;
+ }
+ };
+ }];
+
+ /**
+ * Add as transformed class selector(s), then
+ * remove the deprecated attribute selector
+ */
+ function translateWithValueToCssClass(scope, element, attrs) {
+ var updateFn = updateClassWithValue(element, className, attrs);
+ var unwatch = attrs.$observe(attrs.$normalize(className), updateFn);
+
+ updateFn(getNormalizedAttrValue(className, attrs, ""));
+ scope.$on("$destroy", function() { unwatch() });
+
+ if (config.removeAttributes) element.removeAttr(className);
+ }
+ }
+
+ /**
+ * Creates a registration function for ngMaterial Layout attribute directive.
+ * This is a `simple` transpose of attribute usage to class usage; where we ignore
+ * any attribute value
+ */
+ function attributeWithoutValue(className) {
+ return ['$interpolate', function(_$interpolate_) {
+ $interpolate = _$interpolate_;
+
+ return {
+ restrict: 'A',
+ compile: function(element, attr) {
+ var linkFn;
+ if (config.enabled) {
+ // immediately replace static (non-interpolated) invalid values...
+
+ validateAttributeValue( className,
+ getNormalizedAttrValue(className, attr, ""),
+ buildUpdateFn(element, className, attr)
+ );
+
+ translateToCssClass(null, element);
+
+ // Use for postLink to account for transforms after ng-transclude.
+ linkFn = translateToCssClass;
+ }
+
+ return linkFn || angular.noop;
+ }
+ };
+ }];
+
+ /**
+ * Add as transformed class selector, then
+ * remove the deprecated attribute selector
+ */
+ function translateToCssClass(scope, element) {
+ element.addClass(className);
+
+ if (config.removeAttributes) {
+ // After link-phase, remove deprecated layout attribute selector
+ element.removeAttr(className);
+ }
+ }
+ }
+
+
+
+ /**
+ * After link-phase, do NOT remove deprecated layout attribute selector.
+ * Instead watch the attribute so interpolated data-bindings to layout
+ * selectors will continue to be supported.
+ *
+ * $observe() the className and update with new class (after removing the last one)
+ *
+ * e.g. `layout="{{layoutDemo.direction}}"` will update...
+ *
+ * NOTE: The value must match one of the specified styles in the CSS.
+ * For example `flex-gt-md="{{size}}` where `scope.size == 47` will NOT work since
+ * only breakpoints for 0, 5, 10, 15... 100, 33, 34, 66, 67 are defined.
+ *
+ */
+ function updateClassWithValue(element, className) {
+ var lastClass;
+
+ return function updateClassFn(newValue) {
+ var value = validateAttributeValue(className, newValue || "");
+ if ( angular.isDefined(value) ) {
+ element.removeClass(lastClass);
+ lastClass = !value ? className : className + "-" + value.replace(WHITESPACE, "-")
+ element.addClass(lastClass);
+ }
+ };
+ }
+
+ /**
+ * Provide console warning that this layout attribute has been deprecated
+ *
+ */
+ function warnAttrNotSupported(className) {
+ var parts = className.split("-");
+ return ["$log", function($log) {
+ $log.warn(className + "has been deprecated. Please use a `" + parts[0] + "-gt-` variant.");
+ return angular.noop;
+ }];
+ }
+
+ /**
+ * For the Layout attribute value, validate or replace with default
+ * fallback value
+ */
+ function validateAttributeValue(className, value, updateFn) {
+ var origValue = value;
+
+ if (!needsInterpolation(value)) {
+ switch (className.replace(SUFFIXES,"")) {
+ case 'layout' :
+ if ( !findIn(value, LAYOUT_OPTIONS) ) {
+ value = LAYOUT_OPTIONS[0]; // 'row';
+ }
+ break;
+
+ case 'flex' :
+ if (!findIn(value, FLEX_OPTIONS)) {
+ if (isNaN(value)) {
+ value = '';
+ }
+ }
+ break;
+
+ case 'flex-offset' :
+ case 'flex-order' :
+ if (!value || isNaN(+value)) {
+ value = '0';
+ }
+ break;
+
+ case 'layout-align' :
+ if (!findIn(value, ALIGNMENT_OPTIONS, "-")) {
+ value = ALIGNMENT_OPTIONS[0]; // 'start-start';
+ }
+ break;
+
+ case 'layout-padding' :
+ case 'layout-margin' :
+ case 'layout-fill' :
+ case 'layout-wrap' :
+ case 'layout-no-wrap' :
+ value = '';
+ break;
+ }
+
+ if (value != origValue) {
+ (updateFn || angular.noop)(value);
+ }
+ }
+
+ return value;
+ }
+
+ /**
+ * Replace current attribute value with fallback value
+ */
+ function buildUpdateFn(element, className, attrs) {
+ return function updateAttrValue(fallback) {
+ if (!needsInterpolation(fallback)) {
+ element.attr(className, fallback);
+ attrs[attrs.$normalize(className)] = fallback;
+ }
+ };
+ }
+
+ /**
+ * See if the original value has interpolation symbols:
+ * e.g. flex-gt-md="{{triggerPoint}}"
+ */
+ function needsInterpolation(value) {
+ return (value || "").indexOf($interpolate.startSymbol()) > -1;
+ }
+
+ function getNormalizedAttrValue(className, attrs, defaultVal) {
+ var normalizedAttr = attrs.$normalize(className);
+ return attrs[normalizedAttr] ? attrs[normalizedAttr].replace(WHITESPACE, "-") : defaultVal || null;
+ }
+
+ function findIn(item, list, replaceWith) {
+ item = replaceWith && item ? item.replace(WHITESPACE, replaceWith) : item;
+
+ var found = false;
+ if (item) {
+ list.forEach(function(it) {
+ it = replaceWith ? it.replace(WHITESPACE, replaceWith) : it;
+ found = found || (it === item);
+ });
+ }
+ return found;
+ }
+
+})();
+
+})();
+(function(){
+"use strict";
+
+(function() {
+ 'use strict';
+
+ /**
+ * @ngdoc service
+ * @name $mdButtonInkRipple
+ * @module material.core
+ *
+ * @description
+ * Provides ripple effects for md-button. See $mdInkRipple service for all possible configuration options.
+ *
+ * @param {object=} scope Scope within the current context
+ * @param {object=} element The element the ripple effect should be applied to
+ * @param {object=} options (Optional) Configuration options to override the default ripple configuration
+ */
+
+ angular.module('material.core')
+ .factory('$mdButtonInkRipple', MdButtonInkRipple);
+
+ function MdButtonInkRipple($mdInkRipple) {
+ return {
+ attach: function attachRipple(scope, element, options) {
+ options = angular.extend(optionsForElement(element), options);
+
+ return $mdInkRipple.attach(scope, element, options);
+ }
+ };
+
+ function optionsForElement(element) {
+ if (element.hasClass('md-icon-button')) {
+ return {
+ isMenuItem: element.hasClass('md-menu-item'),
+ fitRipple: true,
+ center: true
+ };
+ } else {
+ return {
+ isMenuItem: element.hasClass('md-menu-item'),
+ dimBackground: true
+ }
+ }
+ };
+ }
+ MdButtonInkRipple.$inject = ["$mdInkRipple"];;
+})();
+
+})();
+(function(){
+"use strict";
+
+(function() {
+ 'use strict';
+
+ /**
+ * @ngdoc service
+ * @name $mdCheckboxInkRipple
+ * @module material.core
+ *
+ * @description
+ * Provides ripple effects for md-checkbox. See $mdInkRipple service for all possible configuration options.
+ *
+ * @param {object=} scope Scope within the current context
+ * @param {object=} element The element the ripple effect should be applied to
+ * @param {object=} options (Optional) Configuration options to override the defaultripple configuration
+ */
+
+ angular.module('material.core')
+ .factory('$mdCheckboxInkRipple', MdCheckboxInkRipple);
+
+ function MdCheckboxInkRipple($mdInkRipple) {
+ return {
+ attach: attach
+ };
+
+ function attach(scope, element, options) {
+ return $mdInkRipple.attach(scope, element, angular.extend({
+ center: true,
+ dimBackground: false,
+ fitRipple: true
+ }, options));
+ };
+ }
+ MdCheckboxInkRipple.$inject = ["$mdInkRipple"];;
+})();
+
+})();
+(function(){
+"use strict";
+
+(function() {
+ 'use strict';
+
+ /**
+ * @ngdoc service
+ * @name $mdListInkRipple
+ * @module material.core
+ *
+ * @description
+ * Provides ripple effects for md-list. See $mdInkRipple service for all possible configuration options.
+ *
+ * @param {object=} scope Scope within the current context
+ * @param {object=} element The element the ripple effect should be applied to
+ * @param {object=} options (Optional) Configuration options to override the defaultripple configuration
+ */
+
+ angular.module('material.core')
+ .factory('$mdListInkRipple', MdListInkRipple);
+
+ function MdListInkRipple($mdInkRipple) {
+ return {
+ attach: attach
+ };
+
+ function attach(scope, element, options) {
+ return $mdInkRipple.attach(scope, element, angular.extend({
+ center: false,
+ dimBackground: true,
+ outline: false,
+ rippleSize: 'full'
+ }, options));
+ };
+ }
+ MdListInkRipple.$inject = ["$mdInkRipple"];;
+})();
+
+})();
+(function(){
+"use strict";
+
+angular.module('material.core')
+ .factory('$mdInkRipple', InkRippleService)
+ .directive('mdInkRipple', InkRippleDirective)
+ .directive('mdNoInk', attrNoDirective)
+ .directive('mdNoBar', attrNoDirective)
+ .directive('mdNoStretch', attrNoDirective);
+
+var DURATION = 450;
+
+/**
+ * Directive used to add ripples to any element
+ * @ngInject
+ */
+function InkRippleDirective ($mdButtonInkRipple, $mdCheckboxInkRipple) {
+ return {
+ controller: angular.noop,
+ link: function (scope, element, attr) {
+ attr.hasOwnProperty('mdInkRippleCheckbox')
+ ? $mdCheckboxInkRipple.attach(scope, element)
+ : $mdButtonInkRipple.attach(scope, element);
+ }
+ };
+}
+InkRippleDirective.$inject = ["$mdButtonInkRipple", "$mdCheckboxInkRipple"];
+
+/**
+ * Service for adding ripples to any element
+ * @ngInject
+ */
+function InkRippleService ($injector) {
+ return { attach: attach };
+ function attach (scope, element, options) {
+ if (element.controller('mdNoInk')) return angular.noop;
+ return $injector.instantiate(InkRippleCtrl, {
+ $scope: scope,
+ $element: element,
+ rippleOptions: options
+ });
+ }
+}
+InkRippleService.$inject = ["$injector"];
+
+/**
+ * Controller used by the ripple service in order to apply ripples
+ * @ngInject
+ */
+function InkRippleCtrl ($scope, $element, rippleOptions, $window, $timeout, $mdUtil) {
+ this.$window = $window;
+ this.$timeout = $timeout;
+ this.$mdUtil = $mdUtil;
+ this.$scope = $scope;
+ this.$element = $element;
+ this.options = rippleOptions;
+ this.mousedown = false;
+ this.ripples = [];
+ this.timeout = null; // Stores a reference to the most-recent ripple timeout
+ this.lastRipple = null;
+
+ $mdUtil.valueOnUse(this, 'container', this.createContainer);
+ $mdUtil.valueOnUse(this, 'background', this.getColor, 0.5);
+
+ this.color = this.getColor(1);
+ this.$element.addClass('md-ink-ripple');
+
+ // attach method for unit tests
+ ($element.controller('mdInkRipple') || {}).createRipple = angular.bind(this, this.createRipple);
+ ($element.controller('mdInkRipple') || {}).setColor = angular.bind(this, this.setColor);
+
+ this.bindEvents();
+}
+InkRippleCtrl.$inject = ["$scope", "$element", "rippleOptions", "$window", "$timeout", "$mdUtil"];
+
+/**
+ * Returns the color that the ripple should be (either based on CSS or hard-coded)
+ * @returns {string}
+ */
+InkRippleCtrl.prototype.getColor = function () {
+ return this._parseColor(this.$element.attr('md-ink-ripple'))
+ || this._parseColor(getElementColor.call(this));
+
+ /**
+ * Finds the color element and returns its text color for use as default ripple color
+ * @returns {string}
+ */
+ function getElementColor () {
+ var colorElement = this.options.colorElement && this.options.colorElement[ 0 ];
+ colorElement = colorElement || this.$element[ 0 ];
+ return colorElement ? this.$window.getComputedStyle(colorElement).color : 'rgb(0,0,0)';
+ }
+};
+/**
+ * Takes a string color and converts it to RGBA format
+ * @param color {string}
+ * @param [multiplier] {int}
+ * @returns {string}
+ */
+
+InkRippleCtrl.prototype._parseColor = function parseColor (color, multiplier) {
+ multiplier = multiplier || 1;
+
+ if (!color) return;
+ if (color.indexOf('rgba') === 0) return color.replace(/\d?\.?\d*\s*\)\s*$/, (0.1 * multiplier).toString() + ')');
+ if (color.indexOf('rgb') === 0) return rgbToRGBA(color);
+ if (color.indexOf('#') === 0) return hexToRGBA(color);
+
+ /**
+ * Converts hex value to RGBA string
+ * @param color {string}
+ * @returns {string}
+ */
+ function hexToRGBA (color) {
+ var hex = color[ 0 ] === '#' ? color.substr(1) : color,
+ dig = hex.length / 3,
+ red = hex.substr(0, dig),
+ green = hex.substr(dig, dig),
+ blue = hex.substr(dig * 2);
+ if (dig === 1) {
+ red += red;
+ green += green;
+ blue += blue;
+ }
+ return 'rgba(' + parseInt(red, 16) + ',' + parseInt(green, 16) + ',' + parseInt(blue, 16) + ',0.1)';
+ }
+
+ /**
+ * Converts an RGB color to RGBA
+ * @param color {string}
+ * @returns {string}
+ */
+ function rgbToRGBA (color) {
+ return color.replace(')', ', 0.1)').replace('(', 'a(');
+ }
+
+};
+
+/**
+ * Binds events to the root element for
+ */
+InkRippleCtrl.prototype.bindEvents = function () {
+ this.$element.on('mousedown', angular.bind(this, this.handleMousedown));
+ this.$element.on('mouseup', angular.bind(this, this.handleMouseup));
+ this.$element.on('mouseleave', angular.bind(this, this.handleMouseup));
+};
+
+/**
+ * Create a new ripple on every mousedown event from the root element
+ * @param event {MouseEvent}
+ */
+InkRippleCtrl.prototype.handleMousedown = function (event) {
+ if ( this.mousedown ) return;
+
+ this.setColor(window.getComputedStyle(this.$element[0])['color']);
+
+ // When jQuery is loaded, we have to get the original event
+ if (event.hasOwnProperty('originalEvent')) event = event.originalEvent;
+ this.mousedown = true;
+ if (this.options.center) {
+ this.createRipple(this.container.prop('clientWidth') / 2, this.container.prop('clientWidth') / 2);
+ } else {
+ this.createRipple(event.layerX, event.layerY);
+ }
+};
+
+/**
+ * Either remove or unlock any remaining ripples when the user mouses off of the element (either by
+ * mouseup or mouseleave event)
+ */
+InkRippleCtrl.prototype.handleMouseup = function () {
+ if ( this.mousedown || this.lastRipple ) {
+ var ctrl = this;
+ this.mousedown = false;
+ this.$mdUtil.nextTick(function () {
+ ctrl.clearRipples();
+ }, false);
+ }
+};
+
+/**
+ * Cycles through all ripples and attempts to remove them.
+ * Depending on logic within `fadeInComplete`, some removals will be postponed.
+ */
+InkRippleCtrl.prototype.clearRipples = function () {
+ for (var i = 0; i < this.ripples.length; i++) {
+ this.fadeInComplete(this.ripples[ i ]);
+ }
+};
+
+/**
+ * Creates the ripple container element
+ * @returns {*}
+ */
+InkRippleCtrl.prototype.createContainer = function () {
+ var container = angular.element('');
+ this.$element.append(container);
+ return container;
+};
+
+InkRippleCtrl.prototype.clearTimeout = function () {
+ if (this.timeout) {
+ this.$timeout.cancel(this.timeout);
+ this.timeout = null;
+ }
+};
+
+InkRippleCtrl.prototype.isRippleAllowed = function () {
+ var element = this.$element[0];
+ do {
+ if (!element.tagName || element.tagName === 'BODY') break;
+ if (element && element.hasAttribute && element.hasAttribute('disabled')) return false;
+ } while (element = element.parentNode);
+ return true;
+};
+
+/**
+ * Creates a new ripple and adds it to the container. Also tracks ripple in `this.ripples`.
+ * @param left
+ * @param top
+ */
+InkRippleCtrl.prototype.createRipple = function (left, top) {
+ if (!this.isRippleAllowed()) return;
+
+ var ctrl = this;
+ var ripple = angular.element('');
+ var width = this.$element.prop('clientWidth');
+ var height = this.$element.prop('clientHeight');
+ var x = Math.max(Math.abs(width - left), left) * 2;
+ var y = Math.max(Math.abs(height - top), top) * 2;
+ var size = getSize(this.options.fitRipple, x, y);
+
+ ripple.css({
+ left: left + 'px',
+ top: top + 'px',
+ background: 'black',
+ width: size + 'px',
+ height: size + 'px',
+ backgroundColor: rgbaToRGB(this.color),
+ borderColor: rgbaToRGB(this.color)
+ });
+ this.lastRipple = ripple;
+
+ // we only want one timeout to be running at a time
+ this.clearTimeout();
+ this.timeout = this.$timeout(function () {
+ ctrl.clearTimeout();
+ if (!ctrl.mousedown) ctrl.fadeInComplete(ripple);
+ }, DURATION * 0.35, false);
+
+ if (this.options.dimBackground) this.container.css({ backgroundColor: this.background });
+ this.container.append(ripple);
+ this.ripples.push(ripple);
+ ripple.addClass('md-ripple-placed');
+
+ this.$mdUtil.nextTick(function () {
+
+ ripple.addClass('md-ripple-scaled md-ripple-active');
+ ctrl.$timeout(function () {
+ ctrl.clearRipples();
+ }, DURATION, false);
+
+ }, false);
+
+ function rgbaToRGB (color) {
+ return color
+ ? color.replace('rgba', 'rgb').replace(/,[^\),]+\)/, ')')
+ : 'rgb(0,0,0)';
+ }
+
+ function getSize (fit, x, y) {
+ return fit
+ ? Math.max(x, y)
+ : Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
+ }
+};
+
+InkRippleCtrl.prototype.setColor = function (color) {
+ this.color = this._parseColor(color);
+};
+
+/**
+ * Either kicks off the fade-out animation or queues the element for removal on mouseup
+ * @param ripple
+ */
+InkRippleCtrl.prototype.fadeInComplete = function (ripple) {
+ if (this.lastRipple === ripple) {
+ if (!this.timeout && !this.mousedown) {
+ this.removeRipple(ripple);
+ }
+ } else {
+ this.removeRipple(ripple);
+ }
+};
+
+/**
+ * Kicks off the animation for removing a ripple
+ * @param ripple {Element}
+ */
+InkRippleCtrl.prototype.removeRipple = function (ripple) {
+ var ctrl = this;
+ var index = this.ripples.indexOf(ripple);
+ if (index < 0) return;
+ this.ripples.splice(this.ripples.indexOf(ripple), 1);
+ ripple.removeClass('md-ripple-active');
+ if (this.ripples.length === 0) this.container.css({ backgroundColor: '' });
+ // use a 2-second timeout in order to allow for the animation to finish
+ // we don't actually care how long the animation takes
+ this.$timeout(function () {
+ ctrl.fadeOutComplete(ripple);
+ }, DURATION, false);
+};
+
+/**
+ * Removes the provided ripple from the DOM
+ * @param ripple
+ */
+InkRippleCtrl.prototype.fadeOutComplete = function (ripple) {
+ ripple.remove();
+ this.lastRipple = null;
+};
+
+/**
+ * Used to create an empty directive. This is used to track flag-directives whose children may have
+ * functionality based on them.
+ *
+ * Example: `md-no-ink` will potentially be used by all child directives.
+ */
+function attrNoDirective () {
+ return { controller: angular.noop };
+}
+
+})();
+(function(){
+"use strict";
+
+(function() {
+ 'use strict';
+
+ /**
+ * @ngdoc service
+ * @name $mdTabInkRipple
+ * @module material.core
+ *
+ * @description
+ * Provides ripple effects for md-tabs. See $mdInkRipple service for all possible configuration options.
+ *
+ * @param {object=} scope Scope within the current context
+ * @param {object=} element The element the ripple effect should be applied to
+ * @param {object=} options (Optional) Configuration options to override the defaultripple configuration
+ */
+
+ angular.module('material.core')
+ .factory('$mdTabInkRipple', MdTabInkRipple);
+
+ function MdTabInkRipple($mdInkRipple) {
+ return {
+ attach: attach
+ };
+
+ function attach(scope, element, options) {
+ return $mdInkRipple.attach(scope, element, angular.extend({
+ center: false,
+ dimBackground: true,
+ outline: false,
+ rippleSize: 'full'
+ }, options));
+ };
+ }
+ MdTabInkRipple.$inject = ["$mdInkRipple"];;
+})();
+
+})();
+(function(){
+"use strict";
+
+angular.module('material.core.theming.palette', [])
+.constant('$mdColorPalette', {
+ 'red': {
+ '50': '#ffebee',
+ '100': '#ffcdd2',
+ '200': '#ef9a9a',
+ '300': '#e57373',
+ '400': '#ef5350',
+ '500': '#f44336',
+ '600': '#e53935',
+ '700': '#d32f2f',
+ '800': '#c62828',
+ '900': '#b71c1c',
+ 'A100': '#ff8a80',
+ 'A200': '#ff5252',
+ 'A400': '#ff1744',
+ 'A700': '#d50000',
+ 'contrastDefaultColor': 'light',
+ 'contrastDarkColors': '50 100 200 300 A100',
+ 'contrastStrongLightColors': '400 500 600 700 A200 A400 A700'
+ },
+ 'pink': {
+ '50': '#fce4ec',
+ '100': '#f8bbd0',
+ '200': '#f48fb1',
+ '300': '#f06292',
+ '400': '#ec407a',
+ '500': '#e91e63',
+ '600': '#d81b60',
+ '700': '#c2185b',
+ '800': '#ad1457',
+ '900': '#880e4f',
+ 'A100': '#ff80ab',
+ 'A200': '#ff4081',
+ 'A400': '#f50057',
+ 'A700': '#c51162',
+ 'contrastDefaultColor': 'light',
+ 'contrastDarkColors': '50 100 200 A100',
+ 'contrastStrongLightColors': '500 600 A200 A400 A700'
+ },
+ 'purple': {
+ '50': '#f3e5f5',
+ '100': '#e1bee7',
+ '200': '#ce93d8',
+ '300': '#ba68c8',
+ '400': '#ab47bc',
+ '500': '#9c27b0',
+ '600': '#8e24aa',
+ '700': '#7b1fa2',
+ '800': '#6a1b9a',
+ '900': '#4a148c',
+ 'A100': '#ea80fc',
+ 'A200': '#e040fb',
+ 'A400': '#d500f9',
+ 'A700': '#aa00ff',
+ 'contrastDefaultColor': 'light',
+ 'contrastDarkColors': '50 100 200 A100',
+ 'contrastStrongLightColors': '300 400 A200 A400 A700'
+ },
+ 'deep-purple': {
+ '50': '#ede7f6',
+ '100': '#d1c4e9',
+ '200': '#b39ddb',
+ '300': '#9575cd',
+ '400': '#7e57c2',
+ '500': '#673ab7',
+ '600': '#5e35b1',
+ '700': '#512da8',
+ '800': '#4527a0',
+ '900': '#311b92',
+ 'A100': '#b388ff',
+ 'A200': '#7c4dff',
+ 'A400': '#651fff',
+ 'A700': '#6200ea',
+ 'contrastDefaultColor': 'light',
+ 'contrastDarkColors': '50 100 200 A100',
+ 'contrastStrongLightColors': '300 400 A200'
+ },
+ 'indigo': {
+ '50': '#e8eaf6',
+ '100': '#c5cae9',
+ '200': '#9fa8da',
+ '300': '#7986cb',
+ '400': '#5c6bc0',
+ '500': '#3f51b5',
+ '600': '#3949ab',
+ '700': '#303f9f',
+ '800': '#283593',
+ '900': '#1a237e',
+ 'A100': '#8c9eff',
+ 'A200': '#536dfe',
+ 'A400': '#3d5afe',
+ 'A700': '#304ffe',
+ 'contrastDefaultColor': 'light',
+ 'contrastDarkColors': '50 100 200 A100',
+ 'contrastStrongLightColors': '300 400 A200 A400'
+ },
+ 'blue': {
+ '50': '#e3f2fd',
+ '100': '#bbdefb',
+ '200': '#90caf9',
+ '300': '#64b5f6',
+ '400': '#42a5f5',
+ '500': '#2196f3',
+ '600': '#1e88e5',
+ '700': '#1976d2',
+ '800': '#1565c0',
+ '900': '#0d47a1',
+ 'A100': '#82b1ff',
+ 'A200': '#448aff',
+ 'A400': '#2979ff',
+ 'A700': '#2962ff',
+ 'contrastDefaultColor': 'light',
+ 'contrastDarkColors': '50 100 200 300 400 A100',
+ 'contrastStrongLightColors': '500 600 700 A200 A400 A700'
+ },
+ 'light-blue': {
+ '50': '#e1f5fe',
+ '100': '#b3e5fc',
+ '200': '#81d4fa',
+ '300': '#4fc3f7',
+ '400': '#29b6f6',
+ '500': '#03a9f4',
+ '600': '#039be5',
+ '700': '#0288d1',
+ '800': '#0277bd',
+ '900': '#01579b',
+ 'A100': '#80d8ff',
+ 'A200': '#40c4ff',
+ 'A400': '#00b0ff',
+ 'A700': '#0091ea',
+ 'contrastDefaultColor': 'dark',
+ 'contrastLightColors': '600 700 800 900 A700',
+ 'contrastStrongLightColors': '600 700 800 A700'
+ },
+ 'cyan': {
+ '50': '#e0f7fa',
+ '100': '#b2ebf2',
+ '200': '#80deea',
+ '300': '#4dd0e1',
+ '400': '#26c6da',
+ '500': '#00bcd4',
+ '600': '#00acc1',
+ '700': '#0097a7',
+ '800': '#00838f',
+ '900': '#006064',
+ 'A100': '#84ffff',
+ 'A200': '#18ffff',
+ 'A400': '#00e5ff',
+ 'A700': '#00b8d4',
+ 'contrastDefaultColor': 'dark',
+ 'contrastLightColors': '700 800 900',
+ 'contrastStrongLightColors': '700 800 900'
+ },
+ 'teal': {
+ '50': '#e0f2f1',
+ '100': '#b2dfdb',
+ '200': '#80cbc4',
+ '300': '#4db6ac',
+ '400': '#26a69a',
+ '500': '#009688',
+ '600': '#00897b',
+ '700': '#00796b',
+ '800': '#00695c',
+ '900': '#004d40',
+ 'A100': '#a7ffeb',
+ 'A200': '#64ffda',
+ 'A400': '#1de9b6',
+ 'A700': '#00bfa5',
+ 'contrastDefaultColor': 'dark',
+ 'contrastLightColors': '500 600 700 800 900',
+ 'contrastStrongLightColors': '500 600 700'
+ },
+ 'green': {
+ '50': '#e8f5e9',
+ '100': '#c8e6c9',
+ '200': '#a5d6a7',
+ '300': '#81c784',
+ '400': '#66bb6a',
+ '500': '#4caf50',
+ '600': '#43a047',
+ '700': '#388e3c',
+ '800': '#2e7d32',
+ '900': '#1b5e20',
+ 'A100': '#b9f6ca',
+ 'A200': '#69f0ae',
+ 'A400': '#00e676',
+ 'A700': '#00c853',
+ 'contrastDefaultColor': 'dark',
+ 'contrastLightColors': '600 700 800 900',
+ 'contrastStrongLightColors': '600 700'
+ },
+ 'light-green': {
+ '50': '#f1f8e9',
+ '100': '#dcedc8',
+ '200': '#c5e1a5',
+ '300': '#aed581',
+ '400': '#9ccc65',
+ '500': '#8bc34a',
+ '600': '#7cb342',
+ '700': '#689f38',
+ '800': '#558b2f',
+ '900': '#33691e',
+ 'A100': '#ccff90',
+ 'A200': '#b2ff59',
+ 'A400': '#76ff03',
+ 'A700': '#64dd17',
+ 'contrastDefaultColor': 'dark',
+ 'contrastLightColors': '700 800 900',
+ 'contrastStrongLightColors': '700 800 900'
+ },
+ 'lime': {
+ '50': '#f9fbe7',
+ '100': '#f0f4c3',
+ '200': '#e6ee9c',
+ '300': '#dce775',
+ '400': '#d4e157',
+ '500': '#cddc39',
+ '600': '#c0ca33',
+ '700': '#afb42b',
+ '800': '#9e9d24',
+ '900': '#827717',
+ 'A100': '#f4ff81',
+ 'A200': '#eeff41',
+ 'A400': '#c6ff00',
+ 'A700': '#aeea00',
+ 'contrastDefaultColor': 'dark',
+ 'contrastLightColors': '900',
+ 'contrastStrongLightColors': '900'
+ },
+ 'yellow': {
+ '50': '#fffde7',
+ '100': '#fff9c4',
+ '200': '#fff59d',
+ '300': '#fff176',
+ '400': '#ffee58',
+ '500': '#ffeb3b',
+ '600': '#fdd835',
+ '700': '#fbc02d',
+ '800': '#f9a825',
+ '900': '#f57f17',
+ 'A100': '#ffff8d',
+ 'A200': '#ffff00',
+ 'A400': '#ffea00',
+ 'A700': '#ffd600',
+ 'contrastDefaultColor': 'dark'
+ },
+ 'amber': {
+ '50': '#fff8e1',
+ '100': '#ffecb3',
+ '200': '#ffe082',
+ '300': '#ffd54f',
+ '400': '#ffca28',
+ '500': '#ffc107',
+ '600': '#ffb300',
+ '700': '#ffa000',
+ '800': '#ff8f00',
+ '900': '#ff6f00',
+ 'A100': '#ffe57f',
+ 'A200': '#ffd740',
+ 'A400': '#ffc400',
+ 'A700': '#ffab00',
+ 'contrastDefaultColor': 'dark'
+ },
+ 'orange': {
+ '50': '#fff3e0',
+ '100': '#ffe0b2',
+ '200': '#ffcc80',
+ '300': '#ffb74d',
+ '400': '#ffa726',
+ '500': '#ff9800',
+ '600': '#fb8c00',
+ '700': '#f57c00',
+ '800': '#ef6c00',
+ '900': '#e65100',
+ 'A100': '#ffd180',
+ 'A200': '#ffab40',
+ 'A400': '#ff9100',
+ 'A700': '#ff6d00',
+ 'contrastDefaultColor': 'dark',
+ 'contrastLightColors': '800 900',
+ 'contrastStrongLightColors': '800 900'
+ },
+ 'deep-orange': {
+ '50': '#fbe9e7',
+ '100': '#ffccbc',
+ '200': '#ffab91',
+ '300': '#ff8a65',
+ '400': '#ff7043',
+ '500': '#ff5722',
+ '600': '#f4511e',
+ '700': '#e64a19',
+ '800': '#d84315',
+ '900': '#bf360c',
+ 'A100': '#ff9e80',
+ 'A200': '#ff6e40',
+ 'A400': '#ff3d00',
+ 'A700': '#dd2c00',
+ 'contrastDefaultColor': 'light',
+ 'contrastDarkColors': '50 100 200 300 400 A100 A200',
+ 'contrastStrongLightColors': '500 600 700 800 900 A400 A700'
+ },
+ 'brown': {
+ '50': '#efebe9',
+ '100': '#d7ccc8',
+ '200': '#bcaaa4',
+ '300': '#a1887f',
+ '400': '#8d6e63',
+ '500': '#795548',
+ '600': '#6d4c41',
+ '700': '#5d4037',
+ '800': '#4e342e',
+ '900': '#3e2723',
+ 'A100': '#d7ccc8',
+ 'A200': '#bcaaa4',
+ 'A400': '#8d6e63',
+ 'A700': '#5d4037',
+ 'contrastDefaultColor': 'light',
+ 'contrastDarkColors': '50 100 200',
+ 'contrastStrongLightColors': '300 400'
+ },
+ 'grey': {
+ '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'
+ }
+});
+
+})();
+(function(){
+"use strict";
+
+angular.module('material.core.theming', ['material.core.theming.palette'])
+ .directive('mdTheme', ThemingDirective)
+ .directive('mdThemable', ThemableDirective)
+ .provider('$mdTheming', ThemingProvider)
+ .run(generateThemes);
+
+/**
+ * @ngdoc service
+ * @name $mdThemingProvider
+ * @module material.core.theming
+ *
+ * @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.
+ */
+
+/* Some Example Valid Theming Expressions
+ * =======================================
+ *
+ * Intention group expansion: (valid for primary, accent, warn, background)
+ *
+ * {{primary-100}} - grab shade 100 from the primary palette
+ * {{primary-100-0.7}} - grab shade 100, apply opacity of 0.7
+ * {{primary-100-contrast}} - grab shade 100's contrast color
+ * {{primary-hue-1}} - grab the shade assigned to hue-1 from the primary palette
+ * {{primary-hue-1-0.7}} - apply 0.7 opacity to primary-hue-1
+ * {{primary-color}} - Generates .md-hue-1, .md-hue-2, .md-hue-3 with configured shades set for each hue
+ * {{primary-color-0.7}} - Apply 0.7 opacity to each of the above rules
+ * {{primary-contrast}} - Generates .md-hue-1, .md-hue-2, .md-hue-3 with configured contrast (ie. text) color shades set for each hue
+ * {{primary-contrast-0.7}} - Apply 0.7 opacity to each of the above rules
+ *
+ * Foreground expansion: Applies rgba to black/white foreground text
+ *
+ * {{foreground-1}} - used for primary text
+ * {{foreground-2}} - used for secondary text/divider
+ * {{foreground-3}} - used for disabled text
+ * {{foreground-4}} - used for dividers
+ *
+ */
+
+// In memory generated CSS rules; registered by theme.name
+var GENERATED = { };
+
+// In memory storage of defined themes and color palettes (both loaded by CSS, and user specified)
+var PALETTES;
+var THEMES;
+
+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'
+ },
+ 'background': {
+ 'default': 'A100',
+ 'hue-1': '300',
+ 'hue-2': '800',
+ 'hue-3': '900'
+ }
+};
+
+var DARK_DEFAULT_HUES = {
+ 'background': {
+ 'default': '800',
+ 'hue-1': '600',
+ 'hue-2': '300',
+ 'hue-3': '900'
+ }
+};
+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 themingProvider;
+ 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) {
+ if (THEMES[name]) return THEMES[name];
+
+ inheritFrom = inheritFrom || 'default';
+
+ var parentTheme = typeof inheritFrom === 'string' ? THEMES[inheritFrom] : inheritFrom;
+ var theme = new Theme(name);
+
+ if (parentTheme) {
+ angular.forEach(parentTheme.colors, function(color, colorType) {
+ theme.colors[colorType] = {
+ name: color.name,
+ // Make sure a COPY of the hues is given to the child color,
+ // not the same reference.
+ hues: angular.extend({}, color.hues)
+ };
+ });
+ }
+ THEMES[name] = theme;
+
+ return theme;
+ }
+
+ function Theme(name) {
+ var self = this;
+ self.name = name;
+ self.colors = {};
+
+ self.dark = setDark;
+ setDark(false);
+
+ function setDark(isDark) {
+ isDark = arguments.length === 0 ? true : !!isDark;
+
+ // If no change, abort
+ if (isDark === self.isDark) return;
+
+ self.isDark = isDark;
+
+ self.foregroundPalette = self.isDark ? LIGHT_FOREGROUND : DARK_FOREGROUND;
+ self.foregroundShadow = self.isDark ? DARK_SHADOW : LIGHT_SHADOW;
+
+ // Light and dark themes have different default hues.
+ // Go through each existing color type for this theme, and for every
+ // hue value that is still the default hue value from the previous light/dark setting,
+ // set it to the default hue value from the new light/dark setting.
+ var newDefaultHues = self.isDark ? DARK_DEFAULT_HUES : LIGHT_DEFAULT_HUES;
+ var oldDefaultHues = self.isDark ? LIGHT_DEFAULT_HUES : DARK_DEFAULT_HUES;
+ angular.forEach(newDefaultHues, function(newDefaults, colorType) {
+ var color = self.colors[colorType];
+ var oldDefaults = oldDefaultHues[colorType];
+ if (color) {
+ for (var hueName in color.hues) {
+ if (color.hues[hueName] === oldDefaults[hueName]) {
+ color.hues[hueName] = newDefaults[hueName];
+ }
+ }
+ }
+ });
+
+ return self;
+ }
+
+ THEME_COLOR_TYPES.forEach(function(colorType) {
+ var defaultHues = (self.isDark ? DARK_DEFAULT_HUES : LIGHT_DEFAULT_HUES)[colorType];
+ self[colorType + 'Palette'] = function setPaletteType(paletteName, hues) {
+ var color = self.colors[colorType] = {
+ name: paletteName,
+ hues: angular.extend({}, defaultHues, hues)
+ };
+
+ Object.keys(color.hues).forEach(function(name) {
+ if (!defaultHues[name]) {
+ throw new Error("Invalid hue name '%1' in theme %2's %3 color %4. Available hue names: %4"
+ .replace('%1', name)
+ .replace('%2', self.name)
+ .replace('%3', paletteName)
+ .replace('%4', Object.keys(defaultHues).join(', '))
+ );
+ }
+ });
+ Object.keys(color.hues).map(function(key) {
+ return color.hues[key];
+ }).forEach(function(hueValue) {
+ if (VALID_HUE_VALUES.indexOf(hueValue) == -1) {
+ throw new Error("Invalid hue value '%1' in theme %2's %3 color %4. Available hue values: %5"
+ .replace('%1', hueValue)
+ .replace('%2', self.name)
+ .replace('%3', colorType)
+ .replace('%4', paletteName)
+ .replace('%5', VALID_HUE_VALUES.join(', '))
+ );
+ }
+ });
+ return self;
+ };
+
+ self[colorType + 'Color'] = function() {
+ var args = Array.prototype.slice.call(arguments);
+ console.warn('$mdThemingProviderTheme.' + colorType + 'Color() has been deprecated. ' +
+ 'Use $mdThemingProviderTheme.' + colorType + 'Palette() instead.');
+ return self[colorType + 'Palette'].apply(self, args);
+ };
+ });
+ }
+
+ /**
+ * @ngdoc service
+ * @name $mdTheming
+ *
+ * @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 == 'default' ? '' : defaultTheme);
+ }, changeTheme);
+ el.on('$destroy', deregisterWatch);
+ } else {
+ var theme = ctrl && ctrl.$mdTheme || (defaultTheme == 'default' ? '' : defaultTheme);
+ changeTheme(theme);
+ }
+
+ function changeTheme(theme) {
+ if (!theme) return;
+ if (!registered(theme)) {
+ $log.warn('Attempted to use unregistered theme \'' + theme + '\'. ' +
+ 'Register it with $mdThemingProvider.theme().');
+ }
+ var oldTheme = el.data('$mdThemeName');
+ if (oldTheme) el.removeClass('md-' + oldTheme +'-theme');
+ el.addClass('md-' + theme + '-theme');
+ el.data('$mdThemeName', theme);
+ if (ctrl) {
+ el.data('$mdThemeController', ctrl);
+ }
+ }
+ };
+
+ applyTheme.THEMES = angular.extend({}, THEMES);
+ applyTheme.defaultTheme = function() { return defaultTheme; };
+ applyTheme.registered = registered;
+
+ return applyTheme;
+
+ function registered(themeName) {
+ if (themeName === undefined || themeName === '') return true;
+ return applyTheme.THEMES[themeName] !== 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*)?(contrast)?\s*\}\}'?"?/g;
+ var palette = PALETTES[color.name];
+
+ // find and replace simple variables where we use a specific hue, not an entire palette
+ // eg. "{{primary-100}}"
+ //\(' + THEME_COLOR_TYPES.join('\|') + '\)'
+ rules = rules.replace(simpleVariableRegex, function(match, colorType, hue, opacity, contrast) {
+ if (colorType === 'foreground') {
+ if (hue == 'shadow') {
+ return theme.foregroundShadow;
+ } else {
+ return theme.foregroundPalette[hue] || theme.foregroundPalette['1'];
+ }
+ }
+ if (hue.indexOf('hue') === 0) {
+ hue = theme.colors[colorType].hues[hue];
+ }
+ return rgba( (PALETTES[ theme.colors[colorType].name ][hue] || '')[contrast ? 'contrast' : 'value'], opacity );
+ });
+
+ // For each type, generate rules for each hue (ie. default, md-hue-1, md-hue-2, md-hue-3)
+ angular.forEach(color.hues, function(hueValue, hueName) {
+ var newRule = rules
+ .replace(hueRegex, function(match, _, colorType, hueType, opacity) {
+ return rgba(palette[hueValue][hueType === 'color' ? 'value' : 'contrast'], opacity);
+ });
+ if (hueName !== 'default') {
+ newRule = newRule.replace(themeNameRegex, '.md-' + theme.name + '-theme.md-' + hueName);
+ }
+
+ // Don't apply a selector rule to the default theme, making it easier to override
+ // styles of the base-component
+ if (theme.name == 'default') {
+ newRule = newRule.replace(/((\w|\.|-)+)\.md-default-theme((\.|\w|-|:|\(|\)|\[|\]|"|'|=)*)/g, '$&, $1$3');
+ }
+ generatedRules.push(newRule);
+ });
+
+ return generatedRules;
+}
+
+// Generate our themes at run time given the state of THEMES and PALETTES
+function generateThemes($injector) {
+
+ var head = document.getElementsByTagName('head')[0];
+ var firstChild = head ? head.firstElementChild : null;
+ var themeCss = $injector.has('$MD_THEME_CSS') ? $injector.get('$MD_THEME_CSS') : '';
+
+ if ( !firstChild ) return;
+ if (themeCss.length === 0) return; // no rules, so no point in running this expensive task
+
+ // Expose contrast colors for palettes to ensure that text is always readable
+ angular.forEach(PALETTES, sanitizePalette);
+
+ // MD_THEME_CSS is a string generated by the build process that includes all the themable
+ // components as templates
+
+ // Break the CSS into individual rules
+ var rulesByType = {};
+ var rules = themeCss
+ .split(/\}(?!(\}|'|"|;))/)
+ .filter(function(rule) { return rule && rule.length; })
+ .map(function(rule) { return rule.trim() + '}'; });
+
+
+ var ruleMatchRegex = new RegExp('md-(' + THEME_COLOR_TYPES.join('|') + ')', 'g');
+
+ THEME_COLOR_TYPES.forEach(function(type) {
+ rulesByType[type] = '';
+ });
+
+
+ // Sort the rules based on type, allowing us to do color substitution on a per-type basis
+ rules.forEach(function(rule) {
+ var match = rule.match(ruleMatchRegex);
+ // First: test that if the rule has '.md-accent', it goes into the accent set of rules
+ for (var i = 0, type; type = THEME_COLOR_TYPES[i]; i++) {
+ if (rule.indexOf('.md-' + type) > -1) {
+ return rulesByType[type] += rule;
+ }
+ }
+
+ // If no eg 'md-accent' class is found, try to just find 'accent' in the rule and guess from
+ // there
+ for (i = 0; type = THEME_COLOR_TYPES[i]; i++) {
+ if (rule.indexOf(type) > -1) {
+ return rulesByType[type] += rule;
+ }
+ }
+
+ // Default to the primary array
+ return rulesByType[DEFAULT_COLOR_TYPE] += rule;
+ });
+
+ // For each theme, use the color palettes specified for
+ // `primary`, `warn` and `accent` to generate CSS rules.
+
+ angular.forEach(THEMES, function(theme) {
+ if ( !GENERATED[theme.name] ) {
+
+
+ THEME_COLOR_TYPES.forEach(function(colorType) {
+ var styleStrings = parseRules(theme, colorType, rulesByType[colorType]);
+ while (styleStrings.length) {
+ var style = document.createElement('style');
+ style.setAttribute('type', 'text/css');
+ style.appendChild(document.createTextNode(styleStrings.shift()));
+ head.insertBefore(style, firstChild);
+ }
+ });
+
+
+ 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.");
+ }
+
+ GENERATED[theme.name] = true;
+ }
+ });
+
+
+ // *************************
+ // Internal functions
+ // *************************
+
+ // The user specifies a 'default' contrast color as either light or dark,
+ // then explicitly lists which hues are the opposite contrast (eg. A100 has dark, A200 has light)
+ function sanitizePalette(palette) {
+ 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 ) return "rgb('0,0,0')";
+
+ if (rgbArray.length == 4) {
+ rgbArray = angular.copy(rgbArray);
+ opacity ? rgbArray.pop() : opacity = rgbArray.pop();
+ }
+ return opacity && (typeof opacity == 'number' || (typeof opacity == 'string' && opacity.length)) ?
+ 'rgba(' + rgbArray.join(',') + ',' + opacity + ')' :
+ 'rgb(' + rgbArray.join(',') + ')';
+}
+
+
+})();
+(function(){
+"use strict";
+
+// Polyfill angular < 1.4 (provide $animateCss)
+angular
+ .module('material.core')
+ .factory('$$mdAnimate', ["$q", "$timeout", "$mdConstant", "$animateCss", function($q, $timeout, $mdConstant, $animateCss){
+
+ // Since $$mdAnimate is injected into $mdUtil... use a wrapper function
+ // to subsequently inject $mdUtil as an argument to the AnimateDomUtils
+
+ return function($mdUtil) {
+ return AnimateDomUtils( $mdUtil, $q, $timeout, $mdConstant, $animateCss);
+ };
+ }]);
+
+/**
+ * Factory function that requires special injections
+ */
+function AnimateDomUtils($mdUtil, $q, $timeout, $mdConstant, $animateCss) {
+ var self;
+ return self = {
+ /**
+ *
+ */
+ translate3d : function( target, from, to, options ) {
+ return $animateCss(target,{
+ from:from,
+ to:to,
+ addClass:options.transitionInClass
+ })
+ .start()
+ .then(function(){
+ // Resolve with reverser function...
+ return reverseTranslate;
+ });
+
+ /**
+ * Specific reversal of the request translate animation above...
+ */
+ function reverseTranslate (newFrom) {
+ return $animateCss(target, {
+ to: newFrom || from,
+ addClass: options.transitionOutClass,
+ removeClass: options.transitionInClass
+ }).start();
+
+ }
+ },
+
+ /**
+ * Listen for transitionEnd event (with optional timeout)
+ * Announce completion or failure via promise handlers
+ */
+ waitTransitionEnd: function (element, opts) {
+ var TIMEOUT = 3000; // fallback is 3 secs
+
+ return $q(function(resolve, reject){
+ opts = opts || { };
+
+ var timer = $timeout(finished, opts.timeout || TIMEOUT);
+ element.on($mdConstant.CSS.TRANSITIONEND, finished);
+
+ /**
+ * Upon timeout or transitionEnd, reject or resolve (respectively) this promise.
+ * NOTE: Make sure this transitionEnd didn't bubble up from a child
+ */
+ function finished(ev) {
+ if ( ev && ev.target !== element[0]) return;
+
+ if ( ev ) $timeout.cancel(timer);
+ element.off($mdConstant.CSS.TRANSITIONEND, finished);
+
+ // Never reject since ngAnimate may cause timeouts due missed transitionEnd events
+ resolve();
+
+ }
+
+ });
+ },
+
+ /**
+ * Calculate the zoom transform from dialog to origin.
+ *
+ * We use this to set the dialog position immediately;
+ * then the md-transition-in actually translates back to
+ * `translate3d(0,0,0) scale(1.0)`...
+ *
+ * NOTE: all values are rounded to the nearest integer
+ */
+ calculateZoomToOrigin: function (element, originator) {
+ var origin = originator.element;
+ var zoomTemplate = "translate3d( {centerX}px, {centerY}px, 0 ) scale( {scaleX}, {scaleY} )";
+ var buildZoom = angular.bind(null, $mdUtil.supplant, zoomTemplate);
+ var zoomStyle = buildZoom({centerX: 0, centerY: 0, scaleX: 0.5, scaleY: 0.5});
+
+ if (origin) {
+ var originBnds = self.clientRect(origin) || self.copyRect(originator.bounds);
+ var dialogRect = self.copyRect(element[0].getBoundingClientRect());
+ var dialogCenterPt = self.centerPointFor(dialogRect);
+ var originCenterPt = self.centerPointFor(originBnds);
+
+ // Build the transform to zoom from the dialog center to the origin center
+
+ zoomStyle = buildZoom({
+ centerX: originCenterPt.x - dialogCenterPt.x,
+ centerY: originCenterPt.y - dialogCenterPt.y,
+ scaleX: Math.round(100 * Math.min(0.5, originBnds.width / dialogRect.width))/100,
+ scaleY: Math.round(100 * Math.min(0.5, originBnds.height / dialogRect.height))/100
+ });
+ }
+
+ return zoomStyle;
+ },
+
+ /**
+ * Enhance raw values to represent valid css stylings...
+ */
+ toCss : function( raw ) {
+ var css = { };
+ var lookups = 'left top right bottom width height x y min-width min-height max-width max-height';
+
+ angular.forEach(raw, function(value,key) {
+ if ( angular.isUndefined(value) ) return;
+
+ if ( lookups.indexOf(key) >= 0 ) {
+ css[key] = value + 'px';
+ } else {
+ switch (key) {
+ case 'transition':
+ convertToVendor(key, $mdConstant.CSS.TRANSITION, value);
+ break;
+ case 'transform':
+ convertToVendor(key, $mdConstant.CSS.TRANSFORM, value);
+ break;
+ case 'transformOrigin':
+ convertToVendor(key, $mdConstant.CSS.TRANSFORM_ORIGIN, value);
+ break;
+ }
+ }
+ });
+
+ return css;
+
+ function convertToVendor(key, vendor, value) {
+ angular.forEach(vendor.split(' '), function (key) {
+ css[key] = value;
+ });
+ }
+ },
+
+ /**
+ * Convert the translate CSS value to key/value pair(s).
+ */
+ toTransformCss: function (transform, addTransition, transition) {
+ var css = {};
+ angular.forEach($mdConstant.CSS.TRANSFORM.split(' '), function (key) {
+ css[key] = transform;
+ });
+
+ if (addTransition) {
+ transition = transition || "all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) !important";
+ css['transition'] = transition;
+ }
+
+ return css;
+ },
+
+ /**
+ * Clone the Rect and calculate the height/width if needed
+ */
+ copyRect: function (source, destination) {
+ if (!source) return null;
+
+ destination = destination || {};
+
+ angular.forEach('left top right bottom width height'.split(' '), function (key) {
+ destination[key] = Math.round(source[key])
+ });
+
+ destination.width = destination.width || (destination.right - destination.left);
+ destination.height = destination.height || (destination.bottom - destination.top);
+
+ return destination;
+ },
+
+ /**
+ * Calculate ClientRect of element; return null if hidden or zero size
+ */
+ clientRect: function (element) {
+ var bounds = angular.element(element)[0].getBoundingClientRect();
+ var isPositiveSizeClientRect = function (rect) {
+ return rect && (rect.width > 0) && (rect.height > 0);
+ };
+
+ // If the event origin element has zero size, it has probably been hidden.
+ return isPositiveSizeClientRect(bounds) ? self.copyRect(bounds) : null;
+ },
+
+ /**
+ * Calculate 'rounded' center point of Rect
+ */
+ centerPointFor: function (targetRect) {
+ return {
+ x: Math.round(targetRect.left + (targetRect.width / 2)),
+ y: Math.round(targetRect.top + (targetRect.height / 2))
+ }
+ }
+
+ };
+};
+
+
+})();
+(function(){
+"use strict";
+
+"use strict";
+
+if (angular.version.minor >= 4) {
+ angular.module('material.core.animate', []);
+} else {
+(function() {
+
+ var forEach = angular.forEach;
+
+ var WEBKIT = angular.isDefined(document.documentElement.style.WebkitAppearance);
+ var TRANSITION_PROP = WEBKIT ? 'WebkitTransition' : 'transition';
+ var ANIMATION_PROP = WEBKIT ? 'WebkitAnimation' : 'animation';
+ var PREFIX = WEBKIT ? '-webkit-' : '';
+
+ var TRANSITION_EVENTS = (WEBKIT ? 'webkitTransitionEnd ' : '') + 'transitionend';
+ var ANIMATION_EVENTS = (WEBKIT ? 'webkitAnimationEnd ' : '') + 'animationend';
+
+ var $$ForceReflowFactory = ['$document', function($document) {
+ return function() {
+ return $document[0].body.clientWidth + 1;
+ }
+ }];
+
+ var $$rAFMutexFactory = ['$$rAF', function($$rAF) {
+ return function() {
+ var passed = false;
+ $$rAF(function() {
+ passed = true;
+ });
+ return function(fn) {
+ passed ? fn() : $$rAF(fn);
+ };
+ };
+ }];
+
+ var $$AnimateRunnerFactory = ['$q', '$$rAFMutex', function($q, $$rAFMutex) {
+ var INITIAL_STATE = 0;
+ var DONE_PENDING_STATE = 1;
+ var DONE_COMPLETE_STATE = 2;
+
+ function AnimateRunner(host) {
+ this.setHost(host);
+
+ this._doneCallbacks = [];
+ this._runInAnimationFrame = $$rAFMutex();
+ this._state = 0;
+ }
+
+ AnimateRunner.prototype = {
+ setHost: function(host) {
+ this.host = host || {};
+ },
+
+ done: function(fn) {
+ if (this._state === DONE_COMPLETE_STATE) {
+ fn();
+ } else {
+ this._doneCallbacks.push(fn);
+ }
+ },
+
+ progress: angular.noop,
+
+ getPromise: function() {
+ if (!this.promise) {
+ var self = this;
+ this.promise = $q(function(resolve, reject) {
+ self.done(function(status) {
+ status === false ? reject() : resolve();
+ });
+ });
+ }
+ return this.promise;
+ },
+
+ then: function(resolveHandler, rejectHandler) {
+ return this.getPromise().then(resolveHandler, rejectHandler);
+ },
+
+ 'catch': function(handler) {
+ return this.getPromise()['catch'](handler);
+ },
+
+ 'finally': function(handler) {
+ return this.getPromise()['finally'](handler);
+ },
+
+ pause: function() {
+ if (this.host.pause) {
+ this.host.pause();
+ }
+ },
+
+ resume: function() {
+ if (this.host.resume) {
+ this.host.resume();
+ }
+ },
+
+ end: function() {
+ if (this.host.end) {
+ this.host.end();
+ }
+ this._resolve(true);
+ },
+
+ cancel: function() {
+ if (this.host.cancel) {
+ this.host.cancel();
+ }
+ this._resolve(false);
+ },
+
+ complete: function(response) {
+ var self = this;
+ if (self._state === INITIAL_STATE) {
+ self._state = DONE_PENDING_STATE;
+ self._runInAnimationFrame(function() {
+ self._resolve(response);
+ });
+ }
+ },
+
+ _resolve: function(response) {
+ if (this._state !== DONE_COMPLETE_STATE) {
+ forEach(this._doneCallbacks, function(fn) {
+ fn(response);
+ });
+ this._doneCallbacks.length = 0;
+ this._state = DONE_COMPLETE_STATE;
+ }
+ }
+ };
+
+ return AnimateRunner;
+ }];
+
+ angular
+ .module('material.core.animate', [])
+ .factory('$$forceReflow', $$ForceReflowFactory)
+ .factory('$$AnimateRunner', $$AnimateRunnerFactory)
+ .factory('$$rAFMutex', $$rAFMutexFactory)
+ .factory('$animateCss', ['$window', '$$rAF', '$$AnimateRunner', '$$forceReflow', '$$jqLite', '$timeout',
+ function($window, $$rAF, $$AnimateRunner, $$forceReflow, $$jqLite, $timeout) {
+
+ function init(element, options) {
+
+ var temporaryStyles = [];
+ var node = getDomNode(element);
+
+ if (options.transitionStyle) {
+ temporaryStyles.push([PREFIX + 'transition', options.transitionStyle]);
+ }
+
+ if (options.keyframeStyle) {
+ temporaryStyles.push([PREFIX + 'animation', options.keyframeStyle]);
+ }
+
+ if (options.delay) {
+ temporaryStyles.push([PREFIX + 'transition-delay', options.delay + 's']);
+ }
+
+ if (options.duration) {
+ temporaryStyles.push([PREFIX + 'transition-duration', options.duration + 's']);
+ }
+
+ var hasCompleteStyles = options.keyframeStyle ||
+ (options.to && (options.duration > 0 || options.transitionStyle));
+ var hasCompleteClasses = !!options.addClass || !!options.removeClass;
+ var hasCompleteAnimation = hasCompleteStyles || hasCompleteClasses;
+
+ blockTransition(element, true);
+ applyAnimationFromStyles(element, options);
+
+ var animationClosed = false;
+ var events, eventFn;
+
+ return {
+ close: $window.close,
+ start: function() {
+ var runner = new $$AnimateRunner();
+ waitUntilQuiet(function() {
+ blockTransition(element, false);
+ if (!hasCompleteAnimation) {
+ return close();
+ }
+
+ forEach(temporaryStyles, function(entry) {
+ var key = entry[0];
+ var value = entry[1];
+ node.style[camelCase(key)] = value;
+ });
+
+ applyClasses(element, options);
+
+ var timings = computeTimings(element);
+ if (timings.duration === 0) {
+ return close();
+ }
+
+ var moreStyles = [];
+
+ if (options.easing) {
+ if (timings.transitionDuration) {
+ moreStyles.push([PREFIX + 'transition-timing-function', options.easing]);
+ }
+ if (timings.animationDuration) {
+ moreStyles.push([PREFIX + 'animation-timing-function', options.easing]);
+ }
+ }
+
+ if (options.delay && timings.animationDelay) {
+ moreStyles.push([PREFIX + 'animation-delay', options.delay + 's']);
+ }
+
+ if (options.duration && timings.animationDuration) {
+ moreStyles.push([PREFIX + 'animation-duration', options.duration + 's']);
+ }
+
+ forEach(moreStyles, function(entry) {
+ var key = entry[0];
+ var value = entry[1];
+ node.style[camelCase(key)] = value;
+ temporaryStyles.push(entry);
+ });
+
+ var maxDelay = timings.delay;
+ var maxDelayTime = maxDelay * 1000;
+ var maxDuration = timings.duration;
+ var maxDurationTime = maxDuration * 1000;
+ var startTime = Date.now();
+
+ events = [];
+ if (timings.transitionDuration) {
+ events.push(TRANSITION_EVENTS);
+ }
+ if (timings.animationDuration) {
+ events.push(ANIMATION_EVENTS);
+ }
+ events = events.join(' ');
+ eventFn = function(event) {
+ event.stopPropagation();
+ var ev = event.originalEvent || event;
+ var timeStamp = ev.timeStamp || Date.now();
+ var elapsedTime = parseFloat(ev.elapsedTime.toFixed(3));
+ if (Math.max(timeStamp - startTime, 0) >= maxDelayTime && elapsedTime >= maxDuration) {
+ close();
+ }
+ };
+ element.on(events, eventFn);
+
+ applyAnimationToStyles(element, options);
+
+ $timeout(close, maxDelayTime + maxDurationTime * 1.5, false);
+ });
+
+ return runner;
+
+ function close() {
+ if (animationClosed) return;
+ animationClosed = true;
+
+ if (events && eventFn) {
+ element.off(events, eventFn);
+ }
+ applyClasses(element, options);
+ applyAnimationStyles(element, options);
+ forEach(temporaryStyles, function(entry) {
+ node.style[camelCase(entry[0])] = '';
+ });
+ runner.complete(true);
+ return runner;
+ }
+ }
+ }
+ }
+
+ function applyClasses(element, options) {
+ if (options.addClass) {
+ $$jqLite.addClass(element, options.addClass);
+ options.addClass = null;
+ }
+ if (options.removeClass) {
+ $$jqLite.removeClass(element, options.removeClass);
+ options.removeClass = null;
+ }
+ }
+
+ function computeTimings(element) {
+ var node = getDomNode(element);
+ var cs = $window.getComputedStyle(node)
+ var tdr = parseMaxTime(cs[prop('transitionDuration')]);
+ var adr = parseMaxTime(cs[prop('animationDuration')]);
+ var tdy = parseMaxTime(cs[prop('transitionDelay')]);
+ var ady = parseMaxTime(cs[prop('animationDelay')]);
+
+ adr *= (parseInt(cs[prop('animationIterationCount')], 10) || 1);
+ var duration = Math.max(adr, tdr);
+ var delay = Math.max(ady, tdy);
+
+ return {
+ duration: duration,
+ delay: delay,
+ animationDuration: adr,
+ transitionDuration: tdr,
+ animationDelay: ady,
+ transitionDelay: tdy
+ };
+
+ function prop(key) {
+ return WEBKIT ? 'Webkit' + key.charAt(0).toUpperCase() + key.substr(1)
+ : key;
+ }
+ }
+
+ function parseMaxTime(str) {
+ var maxValue = 0;
+ var values = (str || "").split(/\s*,\s*/);
+ forEach(values, function(value) {
+ // it's always safe to consider only second values and omit `ms` values since
+ // getComputedStyle will always handle the conversion for us
+ if (value.charAt(value.length - 1) == 's') {
+ value = value.substring(0, value.length - 1);
+ }
+ value = parseFloat(value) || 0;
+ maxValue = maxValue ? Math.max(value, maxValue) : value;
+ });
+ return maxValue;
+ }
+
+ var cancelLastRAFRequest;
+ var rafWaitQueue = [];
+ function waitUntilQuiet(callback) {
+ if (cancelLastRAFRequest) {
+ cancelLastRAFRequest(); //cancels the request
+ }
+ rafWaitQueue.push(callback);
+ cancelLastRAFRequest = $$rAF(function() {
+ cancelLastRAFRequest = null;
+
+ // DO NOT REMOVE THIS LINE OR REFACTOR OUT THE `pageWidth` variable.
+ // PLEASE EXAMINE THE `$$forceReflow` service to understand why.
+ var pageWidth = $$forceReflow();
+
+ // we use a for loop to ensure that if the queue is changed
+ // during this looping then it will consider new requests
+ for (var i = 0; i < rafWaitQueue.length; i++) {
+ rafWaitQueue[i](pageWidth);
+ }
+ rafWaitQueue.length = 0;
+ });
+ }
+
+ function applyAnimationStyles(element, options) {
+ applyAnimationFromStyles(element, options);
+ applyAnimationToStyles(element, options);
+ }
+
+ function applyAnimationFromStyles(element, options) {
+ if (options.from) {
+ element.css(options.from);
+ options.from = null;
+ }
+ }
+
+ function applyAnimationToStyles(element, options) {
+ if (options.to) {
+ element.css(options.to);
+ options.to = null;
+ }
+ }
+
+ function getDomNode(element) {
+ for (var i = 0; i < element.length; i++) {
+ if (element[i].nodeType === 1) return element[i];
+ }
+ }
+
+ function blockTransition(element, bool) {
+ var node = getDomNode(element);
+ var key = camelCase(PREFIX + 'transition-delay');
+ node.style[key] = bool ? '-9999s' : '';
+ }
+
+ return init;
+ }]);
+
+ /**
+ * Older browsers [FF31] expect camelCase
+ * property keys.
+ * e.g.
+ * animation-duration --> animationDuration
+ */
+ function camelCase(str) {
+ return str.replace(/-[a-z]/g, function(str) {
+ return str.charAt(1).toUpperCase();
+ });
+ }
+
+})();
+
+}
+
+})();
+(function(){
+"use strict";
+
+/**
+ * @ngdoc module
+ * @name material.components.autocomplete
+ */
+/*
+ * @see js folder for autocomplete implementation
+ */
+angular.module('material.components.autocomplete', [
+ 'material.core',
+ 'material.components.icon',
+ 'material.components.virtualRepeat'
+]);
+
+})();
+(function(){
+"use strict";
+
+/*
+ * @ngdoc module
+ * @name material.components.backdrop
+ * @description Backdrop
+ */
+
+/**
+ * @ngdoc directive
+ * @name mdBackdrop
+ * @module material.components.backdrop
+ *
+ * @restrict E
+ *
+ * @description
+ * `` is a backdrop element used by other components, such as dialog and bottom sheet.
+ * Apply class `opaque` to make the backdrop use the theme backdrop color.
+ *
+ */
+
+angular
+ .module('material.components.backdrop', ['material.core'])
+ .directive('mdBackdrop', ["$mdTheming", "$animate", "$rootElement", "$window", "$log", "$$rAF", "$document", function BackdropDirective($mdTheming, $animate, $rootElement, $window, $log, $$rAF, $document) {
+ var ERROR_CSS_POSITION = " may not work properly in a scrolled, static-positioned parent container.";
+
+ return {
+ restrict: 'E',
+ link: postLink
+ };
+
+ function postLink(scope, element, attrs) {
+
+ // If body scrolling has been disabled using mdUtil.disableBodyScroll(),
+ // adjust the 'backdrop' height to account for the fixed 'body' top offset
+ var body = $window.getComputedStyle($document[0].body);
+ if (body.position == 'fixed') {
+ var hViewport = parseInt(body.height, 10) + Math.abs(parseInt(body.top, 10));
+ element.css({
+ height: hViewport + 'px'
+ });
+ }
+
+ // backdrop may be outside the $rootElement, tell ngAnimate to animate regardless
+ if ($animate.pin) $animate.pin(element, $rootElement);
+
+ $$rAF(function () {
+
+ // Often $animate.enter() is used to append the backDrop element
+ // so let's wait until $animate is done...
+ var parent = element.parent()[0];
+ if (parent) {
+ var styles = $window.getComputedStyle(parent);
+ if (styles.position == 'static') {
+ // backdrop uses position:absolute and will not work properly with parent position:static (default)
+ $log.warn(ERROR_CSS_POSITION);
+ }
+ }
+
+ $mdTheming.inherit(element, element.parent());
+ });
+
+ }
+
+ }]);
+
+})();
+(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);
+
+/* @ngInject */
+function MdBottomSheetDirective($mdBottomSheet) {
+ return {
+ restrict: 'E',
+ link : function postLink(scope, element, attr) {
+ // When navigation force destroys an interimElement, then
+ // listen and $destroy() that interim instance...
+ scope.$on('$destroy', function() {
+ $mdBottomSheet.destroy();
+ });
+ }
+ };
+}
+MdBottomSheetDirective.$inject = ["$mdBottomSheet"];
+
+
+/**
+ * @ngdoc service
+ * @name $mdBottomSheet
+ * @module material.components.bottomSheet
+ *
+ * @description
+ * `$mdBottomSheet` opens a bottom sheet over the app and provides a simple promise API.
+ *
+ * ## Restrictions
+ *
+ * - The bottom sheet's template must have an outer `` element.
+ * - Add the `md-grid` class to the bottom sheet for a grid layout.
+ * - Add the `md-list` class to the bottom sheet for a list layout.
+ *
+ * @usage
+ *
+ *
+ *
+ * Open a Bottom Sheet!
+ *
+ *
+ *
+ *
+ * var app = angular.module('app', ['ngMaterial']);
+ * app.controller('MyController', function($scope, $mdBottomSheet) {
+ * $scope.openBottomSheet = function() {
+ * $mdBottomSheet.show({
+ * template: 'Hello!'
+ * });
+ * };
+ * });
+ *
+ */
+
+ /**
+ * @ngdoc method
+ * @name $mdBottomSheet#show
+ *
+ * @description
+ * Show a bottom sheet with the specified options.
+ *
+ * @param {object} options An options object, with the following properties:
+ *
+ * - `templateUrl` - `{string=}`: The url of an html template file that will
+ * be used as the content of the bottom sheet. Restrictions: the template must
+ * have an outer `md-bottom-sheet` element.
+ * - `template` - `{string=}`: Same as templateUrl, except this is an actual
+ * template string.
+ * - `scope` - `{object=}`: the scope to link the template / controller to. If none is specified, it will create a new child scope.
+ * This scope will be destroyed when the bottom sheet is removed unless `preserveScope` is set to true.
+ * - `preserveScope` - `{boolean=}`: whether to preserve the scope when the element is removed. Default is false
+ * - `controller` - `{string=}`: The controller to associate with this bottom sheet.
+ * - `locals` - `{string=}`: An object containing key/value pairs. The keys will
+ * be used as names of values to inject into the controller. For example,
+ * `locals: {three: 3}` would inject `three` into the controller with the value
+ * of 3.
+ * - `clickOutsideToClose` - `{boolean=}`: Whether the user can click outside the bottom sheet to
+ * close it. Default true.
+ * - `escapeToClose` - `{boolean=}`: Whether the user can press escape to close the bottom sheet.
+ * Default true.
+ * - `resolve` - `{object=}`: Similar to locals, except it takes promises as values
+ * and the bottom sheet will not open until the promises resolve.
+ * - `controllerAs` - `{string=}`: An alias to assign the controller to on the scope.
+ * - `parent` - `{element=}`: The element to append the bottom sheet to. The `parent` may be a `function`, `string`,
+ * `object`, or null. Defaults to appending to the body of the root element (or the root element) of the application.
+ * e.g. angular.element(document.getElementById('content')) or "#content"
+ * - `disableParentScroll` - `{boolean=}`: Whether to disable scrolling while the bottom sheet is open.
+ * Default true.
+ *
+ * @returns {promise} A promise that can be resolved with `$mdBottomSheet.hide()` or
+ * rejected with `$mdBottomSheet.cancel()`.
+ */
+
+/**
+ * @ngdoc method
+ * @name $mdBottomSheet#hide
+ *
+ * @description
+ * Hide the existing bottom sheet and resolve the promise returned from
+ * `$mdBottomSheet.show()`. This call will close the most recently opened/current bottomsheet (if any).
+ *
+ * @param {*=} response An argument for the resolved promise.
+ *
+ */
+
+/**
+ * @ngdoc method
+ * @name $mdBottomSheet#cancel
+ *
+ * @description
+ * Hide the existing bottom sheet and reject the promise returned from
+ * `$mdBottomSheet.show()`.
+ *
+ * @param {*=} response An argument for the rejected promise.
+ *
+ */
+
+function MdBottomSheetProvider($$interimElementProvider) {
+ // how fast we need to flick down to close the sheet, pixels/ms
+ var CLOSING_VELOCITY = 0.5;
+ var PADDING = 80; // same as css
+
+ bottomSheetDefaults.$inject = ["$animate", "$mdConstant", "$mdUtil", "$mdTheming", "$mdBottomSheet", "$rootElement", "$mdGesture"];
+ return $$interimElementProvider('$mdBottomSheet')
+ .setDefaults({
+ methods: ['disableParentScroll', 'escapeToClose', 'clickOutsideToClose'],
+ options: bottomSheetDefaults
+ });
+
+ /* @ngInject */
+ function bottomSheetDefaults($animate, $mdConstant, $mdUtil, $mdTheming, $mdBottomSheet, $rootElement, $mdGesture) {
+ var backdrop;
+
+ return {
+ themable: true,
+ onShow: onShow,
+ onRemove: onRemove,
+ escapeToClose: true,
+ clickOutsideToClose: true,
+ disableParentScroll: true
+ };
+
+
+ function onShow(scope, element, options, controller) {
+
+ element = $mdUtil.extractElementByName(element, 'md-bottom-sheet');
+
+ // Add a backdrop that will close on click
+ backdrop = $mdUtil.createBackdrop(scope, "md-bottom-sheet-backdrop md-opaque");
+
+ if (options.clickOutsideToClose) {
+ backdrop.on('click', function() {
+ $mdUtil.nextTick($mdBottomSheet.cancel,true);
+ });
+ }
+
+ $mdTheming.inherit(backdrop, options.parent);
+
+ $animate.enter(backdrop, options.parent, null);
+
+ var bottomSheet = new BottomSheet(element, options.parent);
+ options.bottomSheet = bottomSheet;
+
+ $mdTheming.inherit(bottomSheet.element, options.parent);
+
+ if (options.disableParentScroll) {
+ options.restoreScroll = $mdUtil.disableScrollAround(bottomSheet.element, options.parent);
+ }
+
+ return $animate.enter(bottomSheet.element, options.parent)
+ .then(function() {
+ var focusable = $mdUtil.findFocusTarget(element) || 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) {
+ $mdUtil.nextTick($mdBottomSheet.cancel,true);
+ }
+ };
+ $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.restoreScroll();
+ delete options.restoreScroll;
+ }
+
+ bottomSheet.cleanup();
+ });
+ }
+
+ /**
+ * BottomSheet class to apply bottom-sheet behavior to an element
+ */
+ function BottomSheet(element, parent) {
+ var deregister = $mdGesture.register(parent, 'drag', { horizontal: false });
+ parent.on('$md.dragstart', onDragStart)
+ .on('$md.drag', onDrag)
+ .on('$md.dragend', onDragEnd);
+
+ return {
+ element: element,
+ cleanup: function cleanup() {
+ deregister();
+ parent.off('$md.dragstart', onDragStart);
+ parent.off('$md.drag', onDrag);
+ parent.off('$md.dragend', onDragEnd);
+ }
+ };
+
+ function onDragStart(ev) {
+ // Disable transitions on transform so that it feels fast
+ element.css($mdConstant.CSS.TRANSITION_DURATION, '0ms');
+ }
+
+ function onDrag(ev) {
+ var transform = ev.pointer.distanceY;
+ if (transform < 5) {
+ // Slow down drag when trying to drag up, and stop after PADDING
+ transform = Math.max(-PADDING, transform / 2);
+ }
+ element.css($mdConstant.CSS.TRANSFORM, 'translate3d(0,' + (PADDING + transform) + 'px,0)');
+ }
+
+ function onDragEnd(ev) {
+ if (ev.pointer.distanceY > 0 &&
+ (ev.pointer.distanceY > 20 || Math.abs(ev.pointer.velocityY) > CLOSING_VELOCITY)) {
+ var distanceRemaining = element.prop('offsetHeight') - ev.pointer.distanceY;
+ var transitionDuration = Math.min(distanceRemaining / ev.pointer.velocityY * 0.75, 500);
+ element.css($mdConstant.CSS.TRANSITION_DURATION, transitionDuration + 'ms');
+ $mdUtil.nextTick($mdBottomSheet.cancel,true);
+ } else {
+ element.css($mdConstant.CSS.TRANSITION_DURATION, '');
+ element.css($mdConstant.CSS.TRANSFORM, '');
+ }
+ }
+ }
+
+ }
+
+}
+MdBottomSheetProvider.$inject = ["$$interimElementProvider"];
+
+})();
+(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 `