summaryrefslogtreecommitdiff
path: root/etc/js
diff options
context:
space:
mode:
Diffstat (limited to 'etc/js')
-rw-r--r--etc/js/angular-animate.js3721
-rw-r--r--etc/js/angular-awesome-slider.js1267
-rw-r--r--etc/js/angular-carousel.js2286
-rw-r--r--etc/js/angular-circular-navigation.js70
-rw-r--r--etc/js/angular-cookies.js330
-rw-r--r--etc/js/angular-ios9-uiwebview.patch.js73
-rw-r--r--etc/js/angular-sanitize.js717
-rw-r--r--etc/js/angular-touch.js735
-rw-r--r--etc/js/angular-translate-loader-static-files.js107
-rw-r--r--etc/js/angular-translate-storage-cookie.js97
-rw-r--r--etc/js/angular-translate-storage-local.js123
-rw-r--r--etc/js/angular-translate.js3472
-rw-r--r--etc/js/angular-wizard.js446
-rw-r--r--etc/js/crypto-js.js5988
-rw-r--r--etc/js/draggabilly.pkgd.js1530
-rw-r--r--etc/js/filelogger.js363
-rw-r--r--etc/js/holder.js3070
-rw-r--r--etc/js/imagesloaded.pkgd.js487
-rw-r--r--etc/js/ion-pullup.js363
-rw-r--r--etc/js/ionRadio.js55
-rw-r--r--etc/js/ionic.content.banner.js190
-rw-r--r--etc/js/localforage-cordovasqlitedriver.js156
-rw-r--r--etc/js/localforage.js2797
-rw-r--r--etc/js/mfb-directive.js175
-rw-r--r--etc/js/ng-cordova.js7361
25 files changed, 35979 insertions, 0 deletions
diff --git a/etc/js/angular-animate.js b/etc/js/angular-animate.js
new file mode 100644
index 00000000..fc0e217f
--- /dev/null
+++ b/etc/js/angular-animate.js
@@ -0,0 +1,3721 @@
+/**
+ * @license AngularJS v1.4.3
+ * (c) 2010-2015 Google, Inc. http://angularjs.org
+ * License: MIT
+ */
+(function(window, angular, undefined) {'use strict';
+
+/* jshint ignore:start */
+var noop = angular.noop;
+var extend = angular.extend;
+var jqLite = angular.element;
+var forEach = angular.forEach;
+var isArray = angular.isArray;
+var isString = angular.isString;
+var isObject = angular.isObject;
+var isUndefined = angular.isUndefined;
+var isDefined = angular.isDefined;
+var isFunction = angular.isFunction;
+var isElement = angular.isElement;
+
+var ELEMENT_NODE = 1;
+var COMMENT_NODE = 8;
+
+var NG_ANIMATE_CLASSNAME = 'ng-animate';
+var NG_ANIMATE_CHILDREN_DATA = '$$ngAnimateChildren';
+
+var isPromiseLike = function(p) {
+ return p && p.then ? true : false;
+}
+
+function assertArg(arg, name, reason) {
+ if (!arg) {
+ throw ngMinErr('areq', "Argument '{0}' is {1}", (name || '?'), (reason || "required"));
+ }
+ return arg;
+}
+
+function mergeClasses(a,b) {
+ if (!a && !b) return '';
+ if (!a) return b;
+ if (!b) return a;
+ if (isArray(a)) a = a.join(' ');
+ if (isArray(b)) b = b.join(' ');
+ return a + ' ' + b;
+}
+
+function packageStyles(options) {
+ var styles = {};
+ if (options && (options.to || options.from)) {
+ styles.to = options.to;
+ styles.from = options.from;
+ }
+ return styles;
+}
+
+function pendClasses(classes, fix, isPrefix) {
+ var className = '';
+ classes = isArray(classes)
+ ? classes
+ : classes && isString(classes) && classes.length
+ ? classes.split(/\s+/)
+ : [];
+ forEach(classes, function(klass, i) {
+ if (klass && klass.length > 0) {
+ className += (i > 0) ? ' ' : '';
+ className += isPrefix ? fix + klass
+ : klass + fix;
+ }
+ });
+ return className;
+}
+
+function removeFromArray(arr, val) {
+ var index = arr.indexOf(val);
+ if (val >= 0) {
+ arr.splice(index, 1);
+ }
+}
+
+function stripCommentsFromElement(element) {
+ if (element instanceof jqLite) {
+ switch (element.length) {
+ case 0:
+ return [];
+ break;
+
+ case 1:
+ // there is no point of stripping anything if the element
+ // is the only element within the jqLite wrapper.
+ // (it's important that we retain the element instance.)
+ if (element[0].nodeType === ELEMENT_NODE) {
+ return element;
+ }
+ break;
+
+ default:
+ return jqLite(extractElementNode(element));
+ break;
+ }
+ }
+
+ if (element.nodeType === ELEMENT_NODE) {
+ return jqLite(element);
+ }
+}
+
+function extractElementNode(element) {
+ if (!element[0]) return element;
+ for (var i = 0; i < element.length; i++) {
+ var elm = element[i];
+ if (elm.nodeType == ELEMENT_NODE) {
+ return elm;
+ }
+ }
+}
+
+function $$addClass($$jqLite, element, className) {
+ forEach(element, function(elm) {
+ $$jqLite.addClass(elm, className);
+ });
+}
+
+function $$removeClass($$jqLite, element, className) {
+ forEach(element, function(elm) {
+ $$jqLite.removeClass(elm, className);
+ });
+}
+
+function applyAnimationClassesFactory($$jqLite) {
+ return function(element, options) {
+ if (options.addClass) {
+ $$addClass($$jqLite, element, options.addClass);
+ options.addClass = null;
+ }
+ if (options.removeClass) {
+ $$removeClass($$jqLite, element, options.removeClass);
+ options.removeClass = null;
+ }
+ }
+}
+
+function prepareAnimationOptions(options) {
+ options = options || {};
+ if (!options.$$prepared) {
+ var domOperation = options.domOperation || noop;
+ options.domOperation = function() {
+ options.$$domOperationFired = true;
+ domOperation();
+ domOperation = noop;
+ };
+ options.$$prepared = true;
+ }
+ return options;
+}
+
+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 mergeAnimationOptions(element, target, newOptions) {
+ var toAdd = (target.addClass || '') + ' ' + (newOptions.addClass || '');
+ var toRemove = (target.removeClass || '') + ' ' + (newOptions.removeClass || '');
+ var classes = resolveElementClasses(element.attr('class'), toAdd, toRemove);
+
+ extend(target, newOptions);
+
+ if (classes.addClass) {
+ target.addClass = classes.addClass;
+ } else {
+ target.addClass = null;
+ }
+
+ if (classes.removeClass) {
+ target.removeClass = classes.removeClass;
+ } else {
+ target.removeClass = null;
+ }
+
+ return target;
+}
+
+function resolveElementClasses(existing, toAdd, toRemove) {
+ var ADD_CLASS = 1;
+ var REMOVE_CLASS = -1;
+
+ var flags = {};
+ existing = splitClassesToLookup(existing);
+
+ toAdd = splitClassesToLookup(toAdd);
+ forEach(toAdd, function(value, key) {
+ flags[key] = ADD_CLASS;
+ });
+
+ toRemove = splitClassesToLookup(toRemove);
+ forEach(toRemove, function(value, key) {
+ flags[key] = flags[key] === ADD_CLASS ? null : REMOVE_CLASS;
+ });
+
+ var classes = {
+ addClass: '',
+ removeClass: ''
+ };
+
+ forEach(flags, function(val, klass) {
+ var prop, allow;
+ if (val === ADD_CLASS) {
+ prop = 'addClass';
+ allow = !existing[klass];
+ } else if (val === REMOVE_CLASS) {
+ prop = 'removeClass';
+ allow = existing[klass];
+ }
+ if (allow) {
+ if (classes[prop].length) {
+ classes[prop] += ' ';
+ }
+ classes[prop] += klass;
+ }
+ });
+
+ function splitClassesToLookup(classes) {
+ if (isString(classes)) {
+ classes = classes.split(' ');
+ }
+
+ var obj = {};
+ forEach(classes, function(klass) {
+ // sometimes the split leaves empty string values
+ // incase extra spaces were applied to the options
+ if (klass.length) {
+ obj[klass] = true;
+ }
+ });
+ return obj;
+ }
+
+ return classes;
+}
+
+function getDomNode(element) {
+ return (element instanceof angular.element) ? element[0] : element;
+}
+
+var $$rAFSchedulerFactory = ['$$rAF', function($$rAF) {
+ var tickQueue = [];
+ var cancelFn;
+
+ function scheduler(tasks) {
+ // we make a copy since RAFScheduler mutates the state
+ // of the passed in array variable and this would be difficult
+ // to track down on the outside code
+ tickQueue.push([].concat(tasks));
+ nextTick();
+ }
+
+ /* waitUntilQuiet does two things:
+ * 1. It will run the FINAL `fn` value only when an uncancelled RAF has passed through
+ * 2. It will delay the next wave of tasks from running until the quiet `fn` has run.
+ *
+ * The motivation here is that animation code can request more time from the scheduler
+ * before the next wave runs. This allows for certain DOM properties such as classes to
+ * be resolved in time for the next animation to run.
+ */
+ scheduler.waitUntilQuiet = function(fn) {
+ if (cancelFn) cancelFn();
+
+ cancelFn = $$rAF(function() {
+ cancelFn = null;
+ fn();
+ nextTick();
+ });
+ };
+
+ return scheduler;
+
+ function nextTick() {
+ if (!tickQueue.length) return;
+
+ var updatedQueue = [];
+ for (var i = 0; i < tickQueue.length; i++) {
+ var innerQueue = tickQueue[i];
+ runNextTask(innerQueue);
+ if (innerQueue.length) {
+ updatedQueue.push(innerQueue);
+ }
+ }
+ tickQueue = updatedQueue;
+
+ if (!cancelFn) {
+ $$rAF(function() {
+ if (!cancelFn) nextTick();
+ });
+ }
+ }
+
+ function runNextTask(tasks) {
+ var nextTask = tasks.shift();
+ nextTask();
+ }
+}];
+
+var $$AnimateChildrenDirective = [function() {
+ return 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 {
+ attrs.$observe('ngAnimateChildren', function(value) {
+ value = value === 'on' || value === 'true';
+ element.data(NG_ANIMATE_CHILDREN_DATA, value);
+ });
+ }
+ };
+}];
+
+/**
+ * @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
+ * <!-- notice the `fold-animation` CSS class -->
+ * <div ng-if="onOff" class="fold-animation">
+ * This element will go BOOM
+ * </div>
+ * <button ng-click="onOff=true">Fold In</button>
+ * ```
+ *
+ * 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
+ * /&#42; 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 &#42;/
+ * .red { background:red; }
+ * .large-text { font-size:20px; }
+ *
+ * /&#42; we can also use a keyframe animation and $animateCss will make it work alongside the transition &#42;/
+ * .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.)
+ * * `easing` - The CSS easing value that will be applied to the transition or keyframe animation (or both).
+ * * `transition` - 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.)
+ *
+ * @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.
+ */
+
+// Detect proper transitionend/animationend event names.
+var CSS_PREFIX = '', TRANSITION_PROP, TRANSITIONEND_EVENT, ANIMATION_PROP, ANIMATIONEND_EVENT;
+
+// If unprefixed events are not supported but webkit-prefixed are, use the latter.
+// Otherwise, just use W3C names, browsers not supporting them at all will just ignore them.
+// Note: Chrome implements `window.onwebkitanimationend` and doesn't implement `window.onanimationend`
+// but at the same time dispatches the `animationend` event and not `webkitAnimationEnd`.
+// Register both events in case `window.onanimationend` is not supported because of that,
+// do the same for `transitionend` as Safari is likely to exhibit similar behavior.
+// Also, the only modern browser that uses vendor prefixes for transitions/keyframes is webkit
+// therefore there is no reason to test anymore for other vendor prefixes:
+// http://caniuse.com/#search=transition
+if (window.ontransitionend === undefined && window.onwebkittransitionend !== undefined) {
+ CSS_PREFIX = '-webkit-';
+ TRANSITION_PROP = 'WebkitTransition';
+ TRANSITIONEND_EVENT = 'webkitTransitionEnd transitionend';
+} else {
+ TRANSITION_PROP = 'transition';
+ TRANSITIONEND_EVENT = 'transitionend';
+}
+
+if (window.onanimationend === undefined && window.onwebkitanimationend !== undefined) {
+ CSS_PREFIX = '-webkit-';
+ ANIMATION_PROP = 'WebkitAnimation';
+ ANIMATIONEND_EVENT = 'webkitAnimationEnd animationend';
+} else {
+ ANIMATION_PROP = 'animation';
+ ANIMATIONEND_EVENT = 'animationend';
+}
+
+var DURATION_KEY = 'Duration';
+var PROPERTY_KEY = 'Property';
+var DELAY_KEY = 'Delay';
+var TIMING_KEY = 'TimingFunction';
+var ANIMATION_ITERATION_COUNT_KEY = 'IterationCount';
+var ANIMATION_PLAYSTATE_KEY = 'PlayState';
+var ELAPSED_TIME_MAX_DECIMAL_PLACES = 3;
+var CLOSING_TIME_BUFFER = 1.5;
+var ONE_SECOND = 1000;
+var BASE_TEN = 10;
+
+var SAFE_FAST_FORWARD_DURATION_VALUE = 9999;
+
+var ANIMATION_DELAY_PROP = ANIMATION_PROP + DELAY_KEY;
+var ANIMATION_DURATION_PROP = ANIMATION_PROP + DURATION_KEY;
+
+var TRANSITION_DELAY_PROP = TRANSITION_PROP + DELAY_KEY;
+var TRANSITION_DURATION_PROP = TRANSITION_PROP + DURATION_KEY;
+
+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 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 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 blockTransitions(node, duration) {
+ // we use a negative delay value since it performs blocking
+ // yet it doesn't kill any existing transitions running on the
+ // same element which makes this safe for class-based animations
+ var value = duration ? '-' + duration + 's' : '';
+ applyInlineStyle(node, [TRANSITION_DELAY_PROP, value]);
+ return [TRANSITION_DELAY_PROP, value];
+}
+
+function blockKeyframeAnimations(node, applyBlock) {
+ var value = applyBlock ? 'paused' : '';
+ var key = ANIMATION_PROP + ANIMATION_PLAYSTATE_KEY;
+ applyInlineStyle(node, [key, value]);
+ return [key, value];
+}
+
+function applyInlineStyle(node, styleTuple) {
+ var prop = styleTuple[0];
+ var value = styleTuple[1];
+ node.style[prop] = 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++;
+ }
+ }
+ };
+}
+
+var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
+ var gcsLookup = createLocalCacheLookup();
+ var gcsStaggerLookup = createLocalCacheLookup();
+
+ this.$get = ['$window', '$$jqLite', '$$AnimateRunner', '$timeout',
+ '$document', '$sniffer', '$$rAFScheduler',
+ function($window, $$jqLite, $$AnimateRunner, $timeout,
+ $document, $sniffer, $$rAFScheduler) {
+
+ 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 bod = getDomNode($document).body;
+ var rafWaitQueue = [];
+ function waitUntilQuiet(callback) {
+ rafWaitQueue.push(callback);
+ $$rAFScheduler.waitUntilQuiet(function() {
+ gcsLookup.flush();
+ gcsStaggerLookup.flush();
+
+ //the line below will force the browser to perform a repaint so
+ //that all the animated elements within the animation frame will
+ //be properly updated and drawn on screen. This is required to
+ //ensure that the preparation animation is properly flushed so that
+ //the active state picks up from there. DO NOT REMOVE THIS LINE.
+ //DO NOT OPTIMIZE THIS LINE. THE MINIFIER WILL REMOVE IT OTHERWISE WHICH
+ //WILL RESULT IN AN UNPREDICTABLE BUG THAT IS VERY HARD TO TRACK DOWN AND
+ //WILL TAKE YEARS AWAY FROM YOUR LIFE.
+ var width = bod.offsetWidth + 1;
+
+ // 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](width);
+ }
+ rafWaitQueue.length = 0;
+ });
+ }
+
+ return init;
+
+ 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;
+ }
+
+ function init(element, options) {
+ var node = getDomNode(element);
+ if (!node || !node.parentNode) {
+ return closeAndReturnNoopAnimator();
+ }
+
+ options = prepareAnimationOptions(options);
+
+ 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;
+
+ 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, 'ng-', true);
+ } else if (method) {
+ structuralClassName = method;
+ }
+
+ if (options.addClass) {
+ addRemoveClassName += pendClasses(options.addClass, '-add');
+ }
+
+ if (options.removeClass) {
+ if (addRemoveClassName.length) {
+ addRemoveClassName += ' ';
+ }
+ addRemoveClassName += pendClasses(options.removeClass, '-remove');
+ }
+
+ // 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);
+ addRemoveClassName = '';
+ }
+
+ var setupClasses = [structuralClassName, addRemoveClassName].join(' ').trim();
+ var fullClassName = classes + ' ' + setupClasses;
+ var activeClasses = pendClasses(setupClasses, '-active');
+ 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
+ && !setupClasses) {
+ 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, setupClasses, cacheKey, DETECT_STAGGER_CSS_PROPERTIES);
+ }
+
+ $$jqLite.addClass(element, setupClasses);
+
+ 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) {
+ 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();
+ }
+
+ // 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;
+ }
+
+ applyAnimationFromStyles(element, options);
+ if (!flags.blockTransition) {
+ blockTransitions(node, false);
+ }
+
+ applyBlocking(maxDuration);
+
+ // 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;
+
+ $$jqLite.removeClass(element, setupClasses);
+ $$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);
+
+ // 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 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
+ });
+
+ close();
+
+ return {
+ $$willAnimate: false,
+ start: function() {
+ return runner;
+ },
+ end: endFn
+ };
+ }
+
+ function start() {
+ if (animationClosed) return;
+ if (!node.parentNode) {
+ close();
+ return;
+ }
+
+ var startTime, events = [];
+
+ // 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 + ' ' + setupClasses;
+ 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.applyTransitionDelay || flags.applyAnimationDelay) {
+ relativeDelay = typeof options.delay !== "boolean" && truthyTimingValue(options.delay)
+ ? parseFloat(options.delay)
+ : relativeDelay;
+
+ maxDelay = Math.max(relativeDelay, 0);
+
+ var delayStyle;
+ if (flags.applyTransitionDelay) {
+ timings.transitionDelay = relativeDelay;
+ delayStyle = getCssDelayStyle(relativeDelay);
+ temporaryStyles.push(delayStyle);
+ node.style[delayStyle[0]] = delayStyle[1];
+ }
+
+ if (flags.applyAnimationDelay) {
+ 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();
+ element.on(events.join(' '), onAnimationProgress);
+ $timeout(onAnimationExpired, maxDelayTime + CLOSING_TIME_BUFFER * maxDurationTime);
+
+ applyAnimationToStyles(element, options);
+ }
+
+ function onAnimationExpired() {
+ // although an expired animation is a failed animation, getting to
+ // this outcome is very easy if the CSS code screws up. Therefore we
+ // should still continue normally as if the animation completed correctly.
+ close();
+ }
+
+ function onAnimationProgress(event) {
+ event.stopPropagation();
+ var ev = event.originalEvent || event;
+ var timeStamp = ev.$manualTimeStamp || ev.timeStamp || Date.now();
+
+ /* Firefox (or possibly just Gecko) likes to not round values up
+ * when a ms measurement is used for the animation */
+ var elapsedTime = parseFloat(ev.elapsedTime.toFixed(ELAPSED_TIME_MAX_DECIMAL_PLACES));
+
+ /* $manualTimeStamp is a mocked timeStamp value which is set
+ * within browserTrigger(). This is only here so that tests can
+ * mock animations properly. Real events fallback to event.timeStamp,
+ * or, if they don't, then a timeStamp is automatically created for them.
+ * We're checking to see if the timeStamp surpasses the expected delay,
+ * but we're using elapsedTime instead of the timeStamp on the 2nd
+ * pre-condition since animations sometimes close off early */
+ if (Math.max(timeStamp - startTime, 0) >= maxDelayTime && elapsedTime >= maxDuration) {
+ // 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();
+ }
+ }
+ }
+ }
+ }];
+}];
+
+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';
+
+ this.$get = ['$animateCss', '$rootScope', '$$AnimateRunner', '$rootElement', '$document', '$sniffer',
+ function($animateCss, $rootScope, $$AnimateRunner, $rootElement, $document, $sniffer) {
+
+ // only browsers that support these properties can render animations
+ if (!$sniffer.animations && !$sniffer.transitions) return noop;
+
+ var bodyNode = getDomNode($document).body;
+ var rootNode = getDomNode($rootElement);
+
+ var rootBodyElement = jqLite(bodyNode.parentNode === rootNode ? bodyNode : rootNode);
+
+ 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);
+ var toAnimation = prepareRegularAnimation(to);
+
+ 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) {
+ // structural animations ensure that the CSS classes are always applied
+ // before the detection starts.
+ options.structural = 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
+ options.event = animationDetails.event;
+ if (options.event === 'leave') {
+ options.onDone = options.domOperation;
+ }
+ } else {
+ options.event = null;
+ }
+
+ 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', '$$rAFMutex', '$$jqLite',
+ function($injector, $$AnimateRunner, $$rAFMutex, $$jqLite) {
+
+ var applyAnimationClasses = applyAnimationClassesFactory($$jqLite);
+ // $animateJs(element, 'enter');
+ return function(element, event, classes, options) {
+ // 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);
+ }
+
+ return {
+ start: function() {
+ 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);
+ });
+ }
+
+ var animationClosed = false;
+ var runner = new $$AnimateRunner({
+ end: function() {
+ endAnimations();
+ },
+ cancel: function() {
+ endAnimations(true);
+ }
+ });
+
+ $$AnimateRunner.chain(chain, onComplete);
+ return runner;
+
+ function onComplete(success) {
+ animationClosed = true;
+ applyOptions();
+ applyAnimationStyles(element, options);
+ 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 rules = this.rules = {
+ skip: [],
+ cancel: [],
+ join: []
+ };
+
+ function isAllowed(ruleType, element, currentAnimation, previousAnimation) {
+ return rules[ruleType].some(function(fn) {
+ return fn(element, currentAnimation, previousAnimation);
+ });
+ }
+
+ function hasAnimationClasses(options, and) {
+ options = options || {};
+ var a = (options.addClass || '').length > 0;
+ var b = (options.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.options);
+ });
+
+ 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.options);
+ });
+
+ 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 a current animation then skip the class-based animation
+ return currentAnimation.structural && !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 nO = newAnimation.options;
+ var cO = currentAnimation.options;
+
+ // if the exact same CSS class is added/removed then it's safe to cancel it
+ return (nO.addClass && nO.addClass === cO.removeClass) || (nO.removeClass && nO.removeClass === cO.addClass);
+ });
+
+ this.$get = ['$$rAF', '$rootScope', '$rootElement', '$document', '$$HashMap',
+ '$$animation', '$$AnimateRunner', '$templateRequest', '$$jqLite',
+ function($$rAF, $rootScope, $rootElement, $document, $$HashMap,
+ $$animation, $$AnimateRunner, $templateRequest, $$jqLite) {
+
+ var activeAnimationsLookup = new $$HashMap();
+ var disabledElementsLookup = new $$HashMap();
+
+ var animationsEnabled = null;
+
+ // 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 bodyElement = jqLite($document[0].body);
+
+ 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 normalizeAnimationOptions(element, options) {
+ return mergeAnimationOptions(element, options, {});
+ }
+
+ function findCallbacks(element, event) {
+ var targetNode = getDomNode(element);
+
+ var matches = [];
+ var entries = callbackRegistry[event];
+ if (entries) {
+ forEach(entries, function(entry) {
+ if (entry.node.contains(targetNode)) {
+ matches.push(entry.callback);
+ }
+ });
+ }
+
+ return matches;
+ }
+
+ function triggerCallback(event, element, phase, data) {
+ $$rAF(function() {
+ forEach(findCallbacks(element, event), function(callback) {
+ callback(element, phase, data);
+ });
+ });
+ }
+
+ 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<F37>
+ 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
+ bool = !!bool;
+ if (!bool) {
+ disabledElementsLookup.put(node, true);
+ } else if (recordExists) {
+ disabledElementsLookup.remove(node);
+ }
+ }
+ }
+ }
+
+ return bool;
+ }
+ };
+
+ function queueAnimation(element, event, options) {
+ 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();
+
+ // 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;
+ }
+
+ if (isArray(options.addClass)) {
+ options.addClass = options.addClass.join(' ');
+ }
+
+ if (isArray(options.removeClass)) {
+ options.removeClass = options.removeClass.join(' ');
+ }
+
+ if (options.from && !isObject(options.from)) {
+ options.from = null;
+ }
+
+ if (options.to && !isObject(options.to)) {
+ options.to = null;
+ }
+
+ var className = [node.className, options.addClass, options.removeClass].join(' ');
+ if (!isAnimatableClassName(className)) {
+ close();
+ return runner;
+ }
+
+ var isStructural = ['enter', 'move', 'leave'].indexOf(event) >= 0;
+
+ // 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
+ var skipAnimations = !animationsEnabled || 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) {
+ close();
+ return runner;
+ }
+
+ if (isStructural) {
+ closeChildAnimations(element);
+ }
+
+ var newAnimation = {
+ structural: isStructural,
+ element: element,
+ event: event,
+ 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 {
+ mergeAnimationOptions(element, existingAnimation.options, options);
+ 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 existing animation options into this new follow-up animation
+ mergeAnimationOptions(element, newAnimation.options, existingAnimation.options);
+ }
+ } 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) {
+ normalizeAnimationOptions(element, options);
+ } else {
+ event = newAnimation.event = existingAnimation.event;
+ options = mergeAnimationOptions(element, existingAnimation.options, newAnimation.options);
+ return 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
+ normalizeAnimationOptions(element, options);
+ }
+
+ // 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.options);
+ }
+
+ if (!isValidAnimation) {
+ close();
+ clearElementAnimationState(element);
+ return runner;
+ }
+
+ if (isStructural) {
+ closeParentClassBasedAnimations(parent);
+ }
+
+ // 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.options));
+
+ // 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.options, true)
+ ? 'setClass'
+ : animationDetails.event;
+
+ if (animationDetails.structural) {
+ closeParentClassBasedAnimations(parentElement);
+ }
+
+ markElementAnimationState(element, RUNNING_STATE);
+ var realRunner = $$animation(element, event, animationDetails.options);
+ realRunner.done(function(status) {
+ close(!status);
+ var animationDetails = activeAnimationsLookup.get(node);
+ if (animationDetails && animationDetails.counter === counter) {
+ clearElementAnimationState(getDomNode(element));
+ }
+ notifyProgress(runner, event, 'close', {});
+ });
+
+ // this will update the runner's flow-control events based on
+ // the `realRunner` object.
+ runner.setHost(realRunner);
+ notifyProgress(runner, event, 'start', {});
+ });
+
+ return runner;
+
+ function notifyProgress(runner, event, phase, data) {
+ triggerCallback(event, element, phase, data);
+ runner.progress(event, phase, data);
+ }
+
+ function close(reject) { // jshint ignore:line
+ 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);
+ switch (state) {
+ case RUNNING_STATE:
+ animationDetails.runner.end();
+ /* falls through */
+ case PRE_DIGEST_STATE:
+ if (animationDetails) {
+ 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);
+ }
+
+ function closeParentClassBasedAnimations(startingElement) {
+ var parentNode = getDomNode(startingElement);
+ do {
+ if (!parentNode || parentNode.nodeType !== ELEMENT_NODE) break;
+
+ var animationDetails = activeAnimationsLookup.get(parentNode);
+ if (animationDetails) {
+ examineParentAnimation(parentNode, animationDetails);
+ }
+
+ parentNode = parentNode.parentNode;
+ } while (true);
+
+ // since animations are detected from CSS classes, we need to flush all parent
+ // class-based animations so that the parent classes are all present for child
+ // animations to properly function (otherwise any CSS selectors may not work)
+ function examineParentAnimation(node, animationDetails) {
+ // enter/leave/move always have priority
+ if (animationDetails.structural || !hasAnimationClasses(animationDetails.options)) return;
+
+ if (animationDetails.state === RUNNING_STATE) {
+ animationDetails.runner.end();
+ }
+ clearElementAnimationState(node);
+ }
+ }
+
+ function areAnimationsAllowed(element, parentElement, event) {
+ var bodyElementDetected = false;
+ var rootElementDetected = false;
+ var parentAnimationDetected = false;
+ var animateChildren;
+
+ var parentHost = element.data(NG_ANIMATE_PIN_DATA);
+ if (parentHost) {
+ parentElement = parentHost;
+ }
+
+ while (parentElement && parentElement.length) {
+ 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);
+ }
+
+ var parentNode = parentElement[0];
+ if (parentNode.nodeType !== ELEMENT_NODE) {
+ // no point in inspecting the #document element
+ break;
+ }
+
+ var details = activeAnimationsLookup.get(parentNode) || {};
+ // 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) {
+ parentAnimationDetected = details.structural || disabledElementsLookup.get(parentNode);
+ }
+
+ if (isUndefined(animateChildren) || animateChildren === true) {
+ var value = parentElement.data(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 (!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 (!rootElementDetected) {
+ parentHost = parentElement.data(NG_ANIMATE_PIN_DATA);
+ if (parentHost) {
+ parentElement = parentHost;
+ }
+ }
+ }
+
+ if (!bodyElementDetected) {
+ // we also need to ensure that the element is or will be apart of the body element
+ // otherwise it is pointless to even issue an animation to be rendered
+ bodyElementDetected = isMatchingElement(parentElement, bodyElement);
+ }
+
+ parentElement = parentElement.parent();
+ }
+
+ var allowAnimation = !parentAnimationDetected || animateChildren;
+ 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 $$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;
+
+ AnimateRunner.chain = function(chain, callback) {
+ var index = 0;
+
+ next();
+ function next() {
+ if (index === chain.length) {
+ callback(true);
+ return;
+ }
+
+ chain[index](function(response) {
+ if (response === false) {
+ callback(false);
+ return;
+ }
+ index++;
+ next();
+ });
+ }
+ };
+
+ AnimateRunner.all = function(runners, callback) {
+ var count = 0;
+ var status = true;
+ forEach(runners, function(runner) {
+ runner.done(onProgress);
+ });
+
+ function onProgress(response) {
+ status = status && response;
+ if (++count === runners.length) {
+ callback(status);
+ }
+ }
+ };
+
+ 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: 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;
+}];
+
+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', '$$rAFScheduler',
+ function($$jqLite, $rootScope, $injector, $$AnimateRunner, $$rAFScheduler) {
+
+ var animationQueue = [];
+ var applyAnimationClasses = applyAnimationClassesFactory($$jqLite);
+
+ var totalPendingClassBasedAnimations = 0;
+ var totalActiveClassBasedAnimations = 0;
+ var classBasedAnimationsQueue = [];
+
+ // 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 classBasedIndex;
+ if (!isStructural) {
+ classBasedIndex = totalPendingClassBasedAnimations;
+ totalPendingClassBasedAnimations += 1;
+ }
+
+ animationQueue.push({
+ // this data is used by the postDigest code and passed into
+ // the driver step function
+ element: element,
+ classes: classes,
+ event: event,
+ classBasedIndex: classBasedIndex,
+ 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() {
+ totalActiveClassBasedAnimations = totalPendingClassBasedAnimations;
+ totalPendingClassBasedAnimations = 0;
+ classBasedAnimationsQueue.length = 0;
+
+ 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);
+ }
+ });
+
+ // now any future animations will be in another postDigest
+ animationQueue.length = 0;
+
+ forEach(groupAnimations(animations), function(animationEntry) {
+ if (animationEntry.structural) {
+ triggerAnimationStart();
+ } else {
+ classBasedAnimationsQueue.push({
+ node: getDomNode(animationEntry.element),
+ fn: triggerAnimationStart
+ });
+
+ if (animationEntry.classBasedIndex === totalActiveClassBasedAnimations - 1) {
+ // 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.
+ classBasedAnimationsQueue = classBasedAnimationsQueue.sort(function(a,b) {
+ return b.node.contains(a.node);
+ }).map(function(entry) {
+ return entry.fn;
+ });
+
+ $$rAFScheduler(classBasedAnimationsQueue);
+ }
+ }
+
+ 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) && getDomNode(targetElement).parentNode) {
+ 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);
+ }
+ }
+ });
+ });
+
+ 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);
+ }
+ }
+
+ 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,
+
+ $$rAFMutexFactory,
+ $$rAFSchedulerFactory,
+ $$AnimateChildrenDirective,
+ $$AnimateRunnerFactory,
+ $$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` then the animation hooks are enabled for an Angular app.
+ *
+ * <div doc-module-components="ngAnimate"></div>
+ *
+ * # 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 a element using `ng-if`:
+ *
+ * ```html
+ * <div ng-if="bool" class="fade">
+ * Fade me in out
+ * </div>
+ * <button ng-click="bool=true">Fade In!</button>
+ * <button ng-click="bool=false">Fade Out!</button>
+ * ```
+ *
+ * Notice the CSS class **fade**? We can now create the CSS transition code that references this class:
+ *
+ * ```css
+ * /&#42; The starting CSS styles for the enter animation &#42;/
+ * .fade.ng-enter {
+ * transition:0.5s linear all;
+ * opacity:0;
+ * }
+ *
+ * /&#42; The finishing CSS styles for the enter animation &#42;/
+ * .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
+ * /&#42; now the element will fade out before it is removed from the DOM &#42;/
+ * .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
+ * /&#42; there is no need to define anything inside of the destination
+ * CSS class since the keyframe will take charge of the animation &#42;/
+ * .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
+ * <div ng-show="bool" class="fade">
+ * Show and hide me
+ * </div>
+ * <button ng-click="bool=true">Toggle</button>
+ *
+ * <style>
+ * .fade.ng-hide {
+ * transition:0.5s linear all;
+ * opacity:0;
+ * }
+ * </style>
+ * ```
+ *
+ * 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
+ * <div ng-class="{on:onOff}" class="highlight">
+ * Highlight this box
+ * </div>
+ * <button ng-click="onOff=!onOff">Toggle</button>
+ *
+ * <style>
+ * .highlight {
+ * transition:0.5s linear all;
+ * }
+ * .highlight.on-add {
+ * background:white;
+ * }
+ * .highlight.on {
+ * background:yellow;
+ * }
+ * .highlight.on-remove {
+ * background:black;
+ * }
+ * </style>
+ * ```
+ *
+ * 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 {
+ * /&#42; standard transition code &#42;/
+ * transition: 1s linear all;
+ * opacity:0;
+ * }
+ * .my-animation.ng-enter-stagger {
+ * /&#42; this will have a 100ms delay between each successive leave animation &#42;/
+ * transition-delay: 0.1s;
+ *
+ * /&#42; in case the stagger doesn't work then the duration value
+ * must be set to 0 to avoid an accidental CSS inheritance &#42;/
+ * transition-duration: 0s;
+ * }
+ * .my-animation.ng-enter.ng-enter-active {
+ * /&#42; standard transition styles &#42;/
+ * 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.)
+ *
+ *
+ * ## 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
+ * <div ng-repeat="item in items" class="slide">
+ * {{ item }}
+ * </div>
+ * ```
+ *
+ * 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
+ * <div ng-class="color" class="colorful">
+ * this box is moody
+ * </div>
+ * <button ng-click="color='red'">Change to red</button>
+ * <button ng-click="color='blue'">Change to blue</button>
+ * <button ng-click="color='green'">Change to green</button>
+ * ```
+ *
+ * ```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
+ * <div ng-if="bool" class="slide">
+ * Slide in and out
+ * </div>
+ * ```
+ *
+ * ```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, doneFn) {
+* // this will trigger `.slide.ng-enter` and `.slide.ng-enter-active`.
+ * var runner = $animateCss(element, {
+ * event: 'enter',
+ * structural: true
+ * }).start();
+* runner.done(doneFn);
+ * }
+ * }
+ * }]
+ * ```
+ *
+ * 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, doneFn) {
+ * var runner = $animateCss(element, {
+ * event: 'enter',
+ * addClass: 'maroon-setting',
+ * from: { height:0 },
+ * to: { height: 200 }
+ * }).start();
+ *
+ * runner.done(doneFn);
+ * }
+ * }
+ * }]
+ * ```
+ *
+ * Now we can fill in the rest via our transition CSS code:
+ *
+ * ```css
+ * /&#42; the transition tells ngAnimate to make the animation happen &#42;/
+ * .slide.ng-enter { transition:0.5s linear all; }
+ *
+ * /&#42; this extra CSS class will be absorbed into the transition
+ * since the $animateCss code is adding the class &#42;/
+ * .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
+ * <!-- index.html -->
+ * <div ng-view class="view-animation">
+ * </div>
+ *
+ * <!-- home.html -->
+ * <a href="#/banner-page">
+ * <img src="./banner.jpg" class="banner" ng-animate-ref="banner">
+ * </a>
+ *
+ * <!-- banner-page.html -->
+ * <img src="./banner.jpg" class="banner" ng-animate-ref="banner">
+ * ```
+ *
+ * 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 {
+ * /&#42; this animation will last for 1 second since there are
+ * two phases to the animation (an `in` and an `out` phase) &#42;/
+ * 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;
+ *
+ * /&#42; the scale will be applied during the out animation,
+ * but will be animated away when the in animation runs &#42;/
+ * transform: scale(1.2);
+ * }
+ *
+ * .banner.ng-anchor-in {
+ * transition: 1s linear all;
+ * }
+ * ```
+ *
+ *
+ *
+ *
+ * ### Anchoring Demo
+ *
+ <example module="anchoringExample"
+ name="anchoringExample"
+ id="anchoringExample"
+ deps="angular-animate.js;angular-route.js"
+ animations="true">
+ <file name="index.html">
+ <a href="#/">Home</a>
+ <hr />
+ <div class="view-container">
+ <div ng-view class="view"></div>
+ </div>
+ </file>
+ <file name="script.js">
+ angular.module('anchoringExample', ['ngAnimate', 'ngRoute'])
+ .config(['$routeProvider', function($routeProvider) {
+ $routeProvider.when('/', {
+ templateUrl: 'home.html',
+ controller: 'HomeController as home'
+ });
+ $routeProvider.when('/profile/:id', {
+ templateUrl: 'profile.html',
+ controller: 'ProfileController as profile'
+ });
+ }])
+ .run(['$rootScope', function($rootScope) {
+ $rootScope.records = [
+ { id:1, title: "Miss Beulah Roob" },
+ { id:2, title: "Trent Morissette" },
+ { id:3, title: "Miss Ava Pouros" },
+ { id:4, title: "Rod Pouros" },
+ { id:5, title: "Abdul Rice" },
+ { id:6, title: "Laurie Rutherford Sr." },
+ { id:7, title: "Nakia McLaughlin" },
+ { id:8, title: "Jordon Blanda DVM" },
+ { id:9, title: "Rhoda Hand" },
+ { id:10, title: "Alexandrea Sauer" }
+ ];
+ }])
+ .controller('HomeController', [function() {
+ //empty
+ }])
+ .controller('ProfileController', ['$rootScope', '$routeParams', function($rootScope, $routeParams) {
+ var index = parseInt($routeParams.id, 10);
+ var record = $rootScope.records[index - 1];
+
+ this.title = record.title;
+ this.id = record.id;
+ }]);
+ </file>
+ <file name="home.html">
+ <h2>Welcome to the home page</h1>
+ <p>Please click on an element</p>
+ <a class="record"
+ ng-href="#/profile/{{ record.id }}"
+ ng-animate-ref="{{ record.id }}"
+ ng-repeat="record in records">
+ {{ record.title }}
+ </a>
+ </file>
+ <file name="profile.html">
+ <div class="profile record" ng-animate-ref="{{ profile.id }}">
+ {{ profile.title }}
+ </div>
+ </file>
+ <file name="animations.css">
+ .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;
+ }
+ </file>
+ </example>
+ *
+ * ### 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 `<html>` 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
+ * <greeting-box active="onOrOff">Hi there</greeting-box>
+ * ```
+ *
+ * ```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
+ * /&#42; normally we would create a CSS class to reference on the element &#42;/
+ * 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
+ * /&#42; prefixed with animate- &#42;/
+ * .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 $animate to learn more about animations with `$animate`}.
+ */
+angular.module('ngAnimate', [])
+ .directive('ngAnimateChildren', $$AnimateChildrenDirective)
+
+ .factory('$$rAFMutex', $$rAFMutexFactory)
+ .factory('$$rAFScheduler', $$rAFSchedulerFactory)
+
+ .factory('$$AnimateRunner', $$AnimateRunnerFactory)
+
+ .provider('$$animateQueue', $$AnimateQueueProvider)
+ .provider('$$animation', $$AnimationProvider)
+
+ .provider('$animateCss', $AnimateCssProvider)
+ .provider('$$animateCssDriver', $$AnimateCssDriverProvider)
+
+ .provider('$$animateJs', $$AnimateJsProvider)
+ .provider('$$animateJsDriver', $$AnimateJsDriverProvider);
+
+
+})(window, window.angular);
diff --git a/etc/js/angular-awesome-slider.js b/etc/js/angular-awesome-slider.js
new file mode 100644
index 00000000..49749246
--- /dev/null
+++ b/etc/js/angular-awesome-slider.js
@@ -0,0 +1,1267 @@
+(function (angular) {
+ 'use strict';
+
+ angular.module('angularAwesomeSlider', [])
+ // DIRECTIVE
+ .directive('slider', [
+ '$compile', '$templateCache','$timeout', '$window', 'slider',
+ function(compile, templateCache, timeout, win, Slider) {
+ return {
+ restrict : 'AE',
+ require: '?ngModel',
+ scope: { options:'=', ngDisabled: '='},
+ priority: 1,
+ link : function(scope, element, attrs, ngModel) {
+
+ if(!ngModel) return;
+
+ if (!scope.options)
+ throw new Error('You must provide a value for "options" attribute.');
+
+ var injector = angular.injector();
+
+ // options as inline variable
+ if (angular.isString(scope.options)) {
+ scope.options = angular.toJson(scope.options);
+ }
+
+ scope.mainSliderClass = 'jslider';
+ scope.mainSliderClass += scope.options.skin ? ' jslider_' + scope.options.skin : ' ';
+ scope.mainSliderClass += scope.options.vertical ? ' vertical ' : '';
+ scope.mainSliderClass += scope.options.css ? ' sliderCSS' : '';
+ scope.mainSliderClass += scope.options.className ? ' ' + scope.options.className : '';
+
+ // handle limit labels visibility
+ scope.options.limits = angular.isDefined(scope.options.limits) ? scope.options.limits : true;
+
+ // compile template
+ element.after(compile(templateCache.get('ng-slider/slider-bar.tmpl.html'))(scope, function(clonedElement, scope) {
+ scope.tmplElt = clonedElement;
+ }));
+
+ // init
+
+ var initialized = false;
+
+ var init = function(value) {
+ scope.from = ''+scope.options.from;
+ scope.to = ''+scope.options.to;
+ if (scope.options.calculate && angular.isFunction(scope.options.calculate)) {
+ scope.from = scope.options.calculate(scope.from);
+ scope.to = scope.options.calculate(scope.to);
+ }
+
+ var OPTIONS = {
+ from: !scope.options.round ? parseInt(scope.options.from, 10) : parseFloat(scope.options.from),
+ to: !scope.options.round ? parseInt(scope.options.to, 10) : parseFloat(scope.options.to),
+ step: scope.options.step,
+ smooth: scope.options.smooth,
+ limits: scope.options.limits,
+ round: scope.options.round || false,
+ value: value || ngModel.$viewValue,
+ dimension: '',
+ scale: scope.options.scale,
+ modelLabels: scope.options.modelLabels,
+ vertical: scope.options.vertical,
+ css: scope.options.css,
+ className: scope.options.className,
+ realtime: scope.options.realtime,
+ cb: forceApply,
+ threshold: scope.options.threshold,
+ heterogeneity: scope.options.heterogeneity
+ };
+
+ OPTIONS.calculate = scope.options.calculate || undefined;
+ OPTIONS.onstatechange = scope.options.onstatechange || undefined;
+
+ // slider
+ scope.slider = !scope.slider ? slidering(element, scope.tmplElt, OPTIONS) : scope.slider.init(element, scope.tmplElt, OPTIONS);
+
+ if (!initialized) {
+ initListener();
+ }
+
+ // scale
+ var scaleDiv = scope.tmplElt.find('div')[7];
+ angular.element(scaleDiv).html(scope.slider.generateScale());
+ scope.slider.drawScale(scaleDiv);
+
+ if (scope.ngDisabled) {
+ disabler(scope.ngDisabled);
+ }
+
+ initialized = true;
+ };
+
+ function initListener() {
+ // window resize listener
+ angular.element(win).bind('resize', function(event) {
+ scope.slider.onresize();
+ });
+ }
+
+ // model -> view
+ ngModel.$render = function () {
+ //elm.html(ctrl.$viewValue);
+ var singleValue = false;
+
+ if (!ngModel.$viewValue && ngModel.$viewValue !== 0) {
+ return;
+ }
+
+ if (typeof(ngModel.$viewValue) === 'number') {
+ ngModel.$viewValue = ''+ngModel.$viewValue;
+ }
+
+ if( !ngModel.$viewValue.split(';')[1]) {
+ scope.mainSliderClass += ' jslider-single';
+ }
+ else {
+ scope.mainSliderClass = scope.mainSliderClass.replace(' jslider-single', '');
+ }
+
+ if (scope.slider) {
+ var vals = ngModel.$viewValue.split(";");
+ scope.slider.getPointers()[0].set(vals[0], true);
+ if (vals[1]) {
+ scope.slider.getPointers()[1].set(vals[1], true);
+ //if moving left to right with two pointers
+ //we need to "finish" moving the first
+ if(parseInt(vals[1]) > parseInt(vals[0])){
+ scope.slider.getPointers()[0].set(vals[0], true);
+ }
+ }
+ }
+
+ };
+
+ scope.$on('slider-value-update', function(e, msg){
+ init(msg.value);
+ timeout(function(){
+ scope.slider.redrawPointers();
+ });
+ });
+
+ // view -> model
+ var forceApply = function(value, released) {
+ if (scope.disabled)
+ return;
+ scope.$apply(function() {
+ ngModel.$setViewValue(value);
+ });
+ if (scope.options.callback){
+ scope.options.callback(value, released);
+ }
+ };
+
+ // watch options
+ scope.$watch('options', function(value) {
+ timeout(function(){
+ init();
+ });
+ }, scope.watchOptions || true);
+
+ // disabling
+ var disabler = function(value) {
+ scope.disabled = value;
+ if (scope.slider) {
+ scope.tmplElt.toggleClass('disabled');
+ scope.slider.disable(value);
+ }
+ };
+
+ scope.$watch('ngDisabled', function(value) {
+ disabler(value);
+ });
+
+ scope.limitValue = function(value) {
+ if (scope.options.modelLabels) {
+ if (angular.isFunction(scope.options.modelLabels)) {
+ return scope.options.modelLabels(value);
+ }
+ return scope.options.modelLabels[value] !== undefined ? scope.options.modelLabels[value] : value;
+ }
+ return value;
+ };
+
+ var slidering = function( inputElement, element, settings) {
+ return new Slider( inputElement, element, settings );
+ };
+ }
+ };
+ }])
+.config(function() {})
+.run(function() {});
+})(angular);
+;(function(angular){
+ 'use strict';
+ angular.module('angularAwesomeSlider').constant('sliderConstants', {
+ SLIDER: {
+ settings: {
+ from: 1,
+ to: 40,
+ step: 1,
+ smooth: true,
+ limits: false,
+ round: false,
+ value: "3",
+ dimension: "",
+ vertical: false,
+ calculate: false,
+ onstatechange: false,
+ callback: false,
+ realtime: false
+ },
+ className: "jslider",
+ selector: ".jslider-",
+ css: {
+ visible : { visibility: "visible" },
+ hidden : { visibility: "hidden" }
+ }
+ },
+ EVENTS: {
+
+ }
+ });
+
+}(angular));;(function(angular){
+ 'use strict';
+
+ angular.module('angularAwesomeSlider').factory('sliderUtils', ['$window', function(win) {
+ return {
+ offset: function(elm) {
+ // try {return elm.offset();} catch(e) {}
+ var rawDom = elm[0];
+ var _x = 0;
+ var _y = 0;
+ var body = document.documentElement || document.body;
+ var scrollX = window.pageXOffset || body.scrollLeft;
+ var scrollY = window.pageYOffset || body.scrollTop;
+ _x = rawDom.getBoundingClientRect().left + scrollX;
+ _y = rawDom.getBoundingClientRect().top + scrollY;
+ return { left: _x, top:_y };
+ },
+ browser: function() {
+ // TODO finish browser detection and this case
+ var userAgent = win.navigator.userAgent;
+ var browsers = {mozilla: /mozilla/i, chrome: /chrome/i, safari: /safari/i, firefox: /firefox/i, ie: /internet explorer/i};
+ for(var key in browsers) {
+ if (browsers[key].test(userAgent)) {
+ return key;
+ }
+ }
+ return 'unknown';
+ }
+ };
+ }]);
+})(angular);
+
+
+
+;(function(angular){
+ 'use strict';
+
+ angular.module('angularAwesomeSlider').factory('sliderDraggable', ['sliderUtils', function(utils) {
+
+ function Draggable(){
+ this._init.apply( this, arguments );
+ }
+
+ Draggable.prototype.oninit = function(){
+ };
+
+ Draggable.prototype.events = function(){
+ };
+
+ Draggable.prototype.onmousedown = function(){
+ this.ptr.css({ position: 'absolute' });
+ };
+
+ Draggable.prototype.onmousemove = function( evt, x, y ){
+ this.ptr.css({ left: x, top: y });
+ };
+
+ Draggable.prototype.onmouseup = function(){};
+
+ Draggable.prototype.isDefault = {
+ drag: false,
+ clicked: false,
+ toclick: true,
+ mouseup: false
+ };
+
+ Draggable.prototype._init = function() {
+ if( arguments.length > 0 ){
+ this.ptr = arguments[0];
+ this.label = arguments[3];
+ this.parent = arguments[2];
+
+ if (!this.ptr)
+ return;
+ //this.outer = $(".draggable-outer");
+
+ this.is = {};
+ angular.extend( this.is, this.isDefault );
+ var offset = utils.offset(this.ptr);
+
+ this.d = {
+ left: offset.left,
+ top: offset.top,
+ width: this.ptr[0].clientWidth,
+ height: this.ptr[0].clientHeight
+ };
+
+ this.oninit.apply( this, arguments );
+
+ this._events();
+ }
+ };
+
+ Draggable.prototype._getPageCoords = function( event ){
+ if( event.targetTouches && event.targetTouches[0] ){
+ return { x: event.targetTouches[0].pageX, y: event.targetTouches[0].pageY };
+ } else
+ return { x: event.pageX, y: event.pageY };
+ };
+
+ Draggable.prototype._bindEvent = function( ptr, eventType, handler ){
+ var self = this;
+
+ // PS need to bind to touch and non-touch events for devices which support both
+ if( this.supportTouches_ ){
+ ptr[0].addEventListener( this.events_[ eventType ].touch, handler, false );
+ }
+
+ ptr.bind( this.events_[ eventType ].nonTouch, handler );
+ };
+
+ Draggable.prototype._events = function(){
+ var self = this;
+
+ this.supportTouches_ = 'ontouchend' in document;
+ this.events_ = {
+ 'click': { touch : 'touchstart', nonTouch : 'click' },
+ 'down': { touch : 'touchstart', nonTouch : 'mousedown' },
+ 'move': { touch : 'touchmove', nonTouch : 'mousemove' },
+ 'up' : { touch : 'touchend', nonTouch: 'mouseup'},
+ 'mousedown' : { touch : 'mousedown', nonTouch : 'mousedown' }
+ };
+
+ var documentElt = angular.element(window.document);
+
+ this._bindEvent(documentElt, 'move', function(event) {
+ if(self.is.drag) {
+ event.stopPropagation();
+ event.preventDefault();
+ if (!self.parent.disabled) {
+ self._mousemove(event);
+ }
+ }
+ });
+ this._bindEvent(documentElt, 'down', function(event) {
+ if(self.is.drag) {
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ });
+ this._bindEvent(documentElt, 'up', function(event) {
+ self._mouseup(event);
+ });
+
+ this._bindEvent( this.label, 'down', function(event) {
+ self._mousedown( event );
+ return false;
+ });
+ this._bindEvent( this.label, 'up', function(event) {
+ self._mouseup( event );
+ });
+
+ this._bindEvent( this.ptr, 'down', function(event) {
+ self._mousedown( event );
+ return false;
+ });
+ this._bindEvent( this.ptr, 'up', function(event) {
+ self._mouseup( event );
+ });
+
+ // TODO see if needed
+ this.events();
+ };
+
+ Draggable.prototype._mousedown = function( evt ){
+ this.is.drag = true;
+ this.is.clicked = false;
+ this.is.mouseup = false;
+
+ var coords = this._getPageCoords( evt );
+ this.cx = coords.x - this.ptr[0].offsetLeft;
+ this.cy = coords.y - this.ptr[0].offsetTop;
+
+ angular.extend(this.d, {
+ left: coords.x,
+ top: coords.y,
+ width: this.ptr[0].clientWidth,
+ height: this.ptr[0].clientHeight
+ });
+
+ if( this.outer && this.outer.get(0) ){
+ this.outer.css({ height: Math.max(this.outer.height(), $(document.body).height()), overflow: 'hidden' });
+ }
+
+ this.onmousedown( evt );
+ };
+
+ Draggable.prototype._mousemove = function( evt ){
+ this.is.toclick = false;
+ var coords = this._getPageCoords( evt );
+ this.onmousemove( evt, coords.x - this.cx, coords.y - this.cy );
+ };
+
+ Draggable.prototype._mouseup = function( evt ){
+ var oThis = this;
+
+ if( this.is.drag ){
+ this.is.drag = false;
+
+ var browser = utils.browser();
+
+ if( this.outer && this.outer.get(0) ) {
+
+ if( browser === 'mozilla' ){
+ this.outer.css({ overflow: "hidden" });
+ } else {
+ this.outer.css({ overflow: "visible" });
+ }
+
+ // TODO finish browser detection and this case, remove following line
+ this.outer.css({ height: "auto" });
+ // if( browser === 'ie' && $.browser.version == '6.0' ){
+ // this.outer.css({ height: "100%" });
+ // } else {
+ // this.outer.css({ height: "auto" });
+ // }
+
+ }
+
+ this.onmouseup( evt );
+ }
+ };
+
+ return Draggable;
+ }]);
+}(angular));
+;(function(angular){
+ 'use strict';
+
+ angular.module('angularAwesomeSlider').factory('sliderPointer', ['sliderDraggable', 'sliderUtils', function(Draggable, utils) {
+
+ function SliderPointer() {
+ Draggable.apply(this, arguments);
+ }
+
+ SliderPointer.prototype = new Draggable();
+
+ SliderPointer.prototype.oninit = function( ptr, id, vertical, label, _constructor ) {
+ this.uid = id;
+ this.parent = _constructor;
+ this.value = {};
+ this.vertical = vertical;
+ this.settings = angular.copy(_constructor.settings);
+ this.threshold = this.settings.threshold;
+ };
+
+ SliderPointer.prototype.onmousedown = function( evt ) {
+ var off = utils.offset(this.parent.domNode);
+
+ var offset = {
+ left: off.left,
+ top: off.top,
+ width: this.parent.domNode[0].clientWidth,
+ height: this.parent.domNode[0].clientHeight
+ };
+
+ this._parent = {
+ offset: offset,
+ width: offset.width,
+ height: offset.height
+ };
+
+ this.ptr.addClass('jslider-pointer-hover');
+ };
+
+ SliderPointer.prototype.onmousemove = function( evt, x, y ){
+ var coords = this._getPageCoords( evt );
+ this._set(!this.vertical ? this.calc( coords.x ) : this.calc( coords.y ));
+ if( this.settings.realtime && this.settings.cb && angular.isFunction(this.settings.cb) )
+ this.settings.cb.call( this.parent, this.parent.getValue(), !this.is.drag );
+ };
+
+ SliderPointer.prototype.onmouseup = function(evt){
+ if( this.settings.cb && angular.isFunction(this.settings.cb))
+ this.settings.cb.call( this.parent, this.parent.getValue(), !this.is.drag );
+
+ if (!this.is.drag)
+ this.ptr.removeClass('jslider-pointer-hover');
+ };
+
+ SliderPointer.prototype.limits = function( x ){
+ return this.parent.limits(x, this);
+ };
+
+ SliderPointer.prototype.calc = function( coords ){
+ return !this.vertical ?
+ this.limits(((coords-this._parent.offset.left)*100)/this._parent.width)
+ :
+ this.limits(((coords-this._parent.offset.top)*100)/this._parent.height);
+ };
+
+ SliderPointer.prototype.set = function( value, opt_origin ){
+ this.value.origin = this.parent.round(value);
+ this._set(this.parent.valueToPrc(value,this), opt_origin);
+ };
+
+ SliderPointer.prototype._set = function( prc, opt_origin ){
+ this.allowed = true;
+
+ var oldOrigin = this.value.origin;
+ var oldPerc = this.value.prc;
+
+ this.value.origin = this.parent.prcToValue(prc);
+ this.value.prc = prc;
+
+ // check threshold
+ if (this.threshold && this.parent.o.pointers[1]) {
+ var v1 = this.value.origin,
+ v2 = this.parent.o.pointers[this.uid === 0 ? 1:0].value.origin;
+ this.allowed = Math.abs(v2 - v1) >= this.threshold;
+ if (!this.allowed && oldOrigin !== undefined && oldPerc !== undefined){
+ this.value.origin = oldOrigin;
+ this.value.prc = oldPerc;
+ }
+ }
+
+ if (!this.vertical)
+ this.ptr.css({left:this.value.prc+'%'});
+ else
+ this.ptr.css({top:this.value.prc+'%', marginTop: -5});
+ this.parent.redraw(this);
+ };
+
+ return SliderPointer;
+ }]);
+}(angular));
+;(function(angular){
+ 'use strict';
+
+ angular.module('angularAwesomeSlider').factory('slider', ['sliderPointer', 'sliderConstants', 'sliderUtils', function(SliderPointer, sliderConstants, utils) {
+
+ function Slider(){
+ return this.init.apply(this, arguments);
+ }
+
+ Slider.prototype.init = function( inputNode, templateNode, settings ){
+ this.settings = settings;
+ this.inputNode = inputNode;
+ this.inputNode.addClass('ng-hide');
+
+ this.settings.interval = this.settings.to-this.settings.from;
+
+ if(this.settings.calculate && angular.isFunction(this.settings.calculate)) {
+ this.nice = this.settings.calculate;
+ }
+
+ if(this.settings.onstatechange && angular.isFunction(this.settings.onstatechange)) {
+ this.onstatechange = this.settings.onstatechange;
+ }
+
+ this.css = sliderConstants.SLIDER.css;
+ this.is = {init: false};
+ this.o = {};
+ this.initValue = {};
+ this.isAsc = settings.from < settings.to;
+
+ this.create(templateNode);
+
+ return this;
+ };
+
+ Slider.prototype.create = function( templateNode ){
+ // set skin class
+ // if( this.settings.skin && this.settings.skin.length > 0 )
+ // this.setSkin( this.settings.skin );
+
+ var self = this;
+
+ this.domNode = templateNode;
+
+ var divs = this.domNode.find('div'),
+ is = this.domNode.find('i'),
+ angElt = angular.element,
+ angExt = angular.extend,
+ angForEach = angular.forEach,
+ pointer1 = angElt(divs[1]),
+ pointer2 = angElt(divs[2]),
+ pointerLabel1 = angElt(divs[5]),
+ pointerLabel2 = angElt(divs[6]),
+ indicatorLeft = angElt(is[0]),
+ indicatorRight = angElt(is[1]),
+ range = angElt(is[2]),
+ indicator1 = angElt(is[3]),
+ indicator2 = angElt(is[4]),
+ indicator3 = angElt(is[5]),
+ indicator4 = angElt(is[6]),
+ labels = [pointerLabel1, pointerLabel2],
+ pointers = [pointer1, pointer2],
+ off = utils.offset(this.domNode),
+ offset = {
+ left: off.left,
+ top: off.top,
+ width: this.domNode[0].clientWidth,
+ height: this.domNode[0].clientHeight
+ },
+ values = self.settings.value.split(';');
+
+ this.sizes = {
+ domWidth: this.domNode[0].clientWidth,
+ domHeight: this.domNode[0].clientHeight,
+ domOffset: offset
+ };
+
+ // find some objects
+ angExt(this.o, {
+ pointers: {},
+ labels: { 0: { o : pointerLabel1 }, 1: { o : pointerLabel2 } },
+ limits: { 0: angular.element(divs[3]), 1: angular.element(divs[4]) },
+ indicators: { 0: indicator1, 1: indicator2, 2: indicator3, 3: indicator4 }
+ });
+
+ angExt(this.o.labels[0], {
+ value: this.o.labels[0].o.find("span")
+ });
+
+ angExt(this.o.labels[1], {
+ value: this.o.labels[1].o.find("span")
+ });
+
+ // single pointer
+ this.settings.single = !self.settings.value.split(";")[1];
+
+ if (this.settings.single) {
+ range.addClass('ng-hide');
+ } else {
+ range.removeClass('ng-hide');
+ }
+
+ angForEach(pointers, function(pointer, key) {
+ self.settings = angular.copy(self.settings);
+
+ var value = values[key],
+ prev,
+ prevValue,
+ test,
+ value1,
+ offset;
+
+ if(value) {
+ self.o.pointers[key] = new SliderPointer(pointer, key, self.settings.vertical, labels[key], self);
+
+ prev = values[key-1];
+ prevValue = prev ? parseInt(prev, 10 ) : undefined;
+
+ value = self.settings.round ? parseFloat(value) : parseInt(value, 10);
+
+ if( prev && self.isAsc ? value < prevValue : value > prevValue ) {
+ value = prev;
+ }
+
+ // limit threshold adjust
+/* test = self.isAsc ? value < self.settings.from : value > self.settings.from,
+ value1 = test ? self.settings.from : value; */
+
+ test = self.isAsc ? value > self.settings.to : value < self.settings.to;
+ value1 = test ? self.settings.to : value;
+
+ self.o.pointers[key].set( value1, true );
+
+ // reinit position d
+ offset = utils.offset(self.o.pointers[key].ptr);
+
+ self.o.pointers[key].d = {
+ left: offset.left,
+ top: offset.top
+ };
+ }
+ });
+
+ self.domNode.bind('mousedown', self.clickHandler.apply(self));
+
+ this.o.value = angElt(this.domNode.find("i")[2]);
+ this.is.init = true;
+
+ // CSS SKIN
+ if (this.settings.css) {
+ indicatorLeft.css(this.settings.css.background ? this.settings.css.background : {});
+ indicatorRight.css(this.settings.css.background ? this.settings.css.background : {});
+ if (!this.o.pointers[1]) {
+ indicator1.css(this.settings.css.before ? this.settings.css.before : {});
+ indicator4.css(this.settings.css.after ? this.settings.css.after : {});
+ }
+
+ indicator2.css(this.settings.css.default ? this.settings.css.default : {});
+ indicator3.css(this.settings.css.default ? this.settings.css.default : {});
+
+ range.css(this.settings.css.range ? this.settings.css.range : {});
+ pointer1.css(this.settings.css.pointer ? this.settings.css.pointer : {});
+ pointer2.css(this.settings.css.pointer ? this.settings.css.pointer : {});
+ }
+
+ this.redrawPointers();
+ };
+
+ Slider.prototype.clickHandler = function() {
+ var self = this;
+
+ // in case of showing/hiding
+ var resetPtrSize = function( ptr ) {
+ var ptr1 = self.o.pointers[0].ptr,
+ ptr2 = self.o.pointers[1].ptr,
+ offset1 = utils.offset(ptr1),
+ offset2 = utils.offset(ptr2);
+
+ self.o.pointers[0].d = {
+ left: offset1.left,
+ top: offset1.top,
+ width: ptr1[0].clientWidth,
+ height: ptr1[0].clientHeight
+ };
+
+ self.o.pointers[1].d = {
+ left: offset2.left,
+ top: offset2.top,
+ width: ptr2[0].clientWidth,
+ height: ptr2[0].clientHeight
+ };
+ };
+
+ return function(evt) {
+ if (self.disabled)
+ return;
+ var vertical = self.settings.vertical,
+ targetIdx = 0,
+ _off = utils.offset(self.domNode),
+ firstPtr = self.o.pointers[0],
+ secondPtr = self.o.pointers[1] ? self.o.pointers[1] : null,
+ tmpPtr,
+ evtPosition = evt.originalEvent ? evt.originalEvent: evt,
+ mouse = vertical ? evtPosition.pageY : evtPosition.pageX,
+ css = vertical ? 'top' : 'left',
+ offset = { left: _off.left, top: _off.top, width: self.domNode[0].clientWidth, height: self.domNode[0].clientHeight },
+ targetPtr = self.o.pointers[targetIdx];
+
+ if (secondPtr) {
+ if (!secondPtr.d.width) {
+ resetPtrSize();
+ }
+ var firstPtrPosition = utils.offset(firstPtr.ptr)[css];
+ var secondPtrPosition = utils.offset(secondPtr.ptr)[css];
+ var middleGap = Math.abs((secondPtrPosition - firstPtrPosition) / 2);
+ var testSecondPtrToMove = mouse >= secondPtrPosition || mouse >= (secondPtrPosition - middleGap);
+ if (testSecondPtrToMove) {
+ targetPtr = secondPtr;
+ }
+ }
+ targetPtr._parent = {offset: offset, width: offset.width, height: offset.height};
+ var coords = firstPtr._getPageCoords( evt );
+ targetPtr.cx = coords.x - targetPtr.d.left;
+ targetPtr.cy = coords.y - targetPtr.d.top;
+ targetPtr.onmousemove( evt, coords.x, coords.y);
+ targetPtr.onmouseup();
+ angular.extend(targetPtr.d, {
+ left: coords.x,
+ top: coords.y
+ });
+
+ self.redraw(targetPtr);
+ return false;
+ };
+ };
+
+
+ Slider.prototype.disable = function( bool ) {
+ this.disabled = bool;
+ };
+
+ Slider.prototype.nice = function( value ){
+ return value;
+ };
+
+ Slider.prototype.onstatechange = function(){};
+
+ Slider.prototype.limits = function( x, pointer ){
+ // smooth
+ if(!this.settings.smooth){
+ var step = this.settings.step*100 / ( this.settings.interval );
+ x = Math.round( x/step ) * step;
+ }
+
+ if (pointer) {
+ var another = this.o.pointers[1-pointer.uid];
+ if(another && pointer.uid && x < another.value.prc) x = another.value.prc;
+ if(another && !pointer.uid && x > another.value.prc) x = another.value.prc;
+ }
+ // base limit
+ if(x < 0) x = 0;
+ if(x > 100) x = 100;
+
+ return Math.round(x*10) / 10;
+ };
+
+ Slider.prototype.getPointers = function(){
+ return this.o.pointers;
+ };
+
+ Slider.prototype.generateScale = function(){
+ if (this.settings.scale && this.settings.scale.length > 0){
+ var str = '',
+ s = this.settings.scale,
+ // FIX Big Scale Failure #34
+ // var prc = Math.round((100/(s.length-1))*10)/10;
+ prc,
+ label,
+ duplicate = {},
+ position = this.settings.vertical ? 'top' : 'left',
+ i=0;
+ for(; i < s.length; i++) {
+ if (!angular.isDefined(s[i].val)) {
+ prc = (100/(s.length-1)).toFixed(2);
+ str += '<span style="'+ position + ': ' + i*prc + '%">' + ( s[i] != '|' ? '<ins>' + s[i] + '</ins>' : '' ) + '</span>';
+ }
+
+ if (s[i].val <= this.settings.to && s[i].val >= this.settings.from && ! duplicate[s[i].val]) {
+ duplicate[s[i].val] = true;
+ prc = this.valueToPrc(s[i].val);
+ label = s[i].label ? s[i].label : s[i].val;
+ str += '<span style="'+ position + ': ' + prc + '%">' + '<ins>' + label + '</ins>' + '</span>';
+ }
+ }
+ return str;
+ }
+
+ return '';
+ };
+
+ Slider.prototype.onresize = function(){
+ var self = this;
+
+ this.sizes = {
+ domWidth: this.domNode[0].clientWidth,
+ domHeight: this.domNode[0].clientHeight,
+ domOffset: {
+ left: this.domNode[0].offsetLeft,
+ top: this.domNode[0].offsetTop,
+ width: this.domNode[0].clientWidth,
+ height: this.domNode[0].clientHeight
+ }
+ };
+
+ this.redrawPointers();
+ };
+
+ Slider.prototype.update = function(){
+ this.onresize();
+ this.drawScale();
+ };
+
+ Slider.prototype.drawScale = function( scaleDiv ){
+ angular.forEach(angular.element(scaleDiv).find('ins'), function(scaleLabel, key) {
+ scaleLabel.style.marginLeft = - scaleLabel.clientWidth / 2;
+ });
+ };
+
+ Slider.prototype.redrawPointers = function() {
+
+ angular.forEach(this.o.pointers, function(pointer){
+ this.redraw(pointer);
+ }, this);
+ };
+
+ Slider.prototype.redraw = function( pointer ){
+ if(!this.is.init) {
+ // this.settings.single
+ if(this.o.pointers[0] && !this.o.pointers[1]) {
+ this.originValue = this.o.pointers[0].value.prc;
+ this.o.indicators[0].css(!this.settings.vertical ? {left:0, width:this.o.pointers[0].value.prc + "%"} : {top:0, height:this.o.pointers[0].value.prc + "%"});
+ this.o.indicators[1].css(!this.settings.vertical ? {left:this.o.pointers[0].value.prc + "%"} : {top:this.o.pointers[0].value.prc + "%"});
+ this.o.indicators[3].css(!this.settings.vertical ? {left:this.o.pointers[0].value.prc + "%"} : {top:this.o.pointers[0].value.prc + "%"});
+ } else {
+ this.o.indicators[2].css(!this.settings.vertical ? {left:this.o.pointers[1].value.prc + "%"} : {top:this.o.pointers[1].value.prc + "%"});
+ this.o.indicators[0].css(!this.settings.vertical ? {left:0, width:"0"} : {top:0, height:"0"});
+ this.o.indicators[3].css(!this.settings.vertical ? {left:"0", width:"0"} : {top:"0", height:"0"});
+ }
+
+ return false;
+ }
+
+ this.setValue();
+
+ var newPos,
+ newWidth;
+
+ // redraw range line
+ if(this.o.pointers[0] && this.o.pointers[1]) {
+ newPos = !this.settings.vertical ?
+ { left: this.o.pointers[0].value.prc + "%", width: ( this.o.pointers[1].value.prc - this.o.pointers[0].value.prc ) + "%" }
+ :
+ { top: this.o.pointers[0].value.prc + "%", height: ( this.o.pointers[1].value.prc - this.o.pointers[0].value.prc ) + "%" };
+
+ this.o.value.css(newPos);
+
+ // both pointer overlap on limits
+ if (this.o.pointers[0].value.prc === this.o.pointers[1].value.prc) {
+ this.o.pointers[1].ptr.css("z-index", this.o.pointers[0].value.prc === 0 ? '3' : '1');
+ }
+
+ }
+
+ if(this.o.pointers[0] && !this.o.pointers[1]) {
+ newWidth = this.o.pointers[0].value.prc - this.originValue;
+ if (newWidth >= 0) {
+ this.o.indicators[3].css(!this.settings.vertical ? {width: newWidth + "%"} : {height: newWidth + "%"});
+ }
+ else {
+ this.o.indicators[3].css(!this.settings.vertical ? {width: 0} : {height: 0});
+ }
+
+ if (this.o.pointers[0].value.prc < this.originValue) {
+ this.o.indicators[0].css(!this.settings.vertical ? {width: this.o.pointers[0].value.prc + "%"} : {height: this.o.pointers[0].value.prc + "%"});
+ }
+ else {
+ this.o.indicators[0].css(!this.settings.vertical ? {width: this.originValue + "%"} : {height: this.originValue + "%"});
+ }
+
+ }
+
+ var value = this.nice(pointer.value.origin);
+ if (this.settings.modelLabels) {
+ if (angular.isFunction(this.settings.modelLabels)) {
+ value = this.settings.modelLabels(value);
+ }
+ else {
+ value = this.settings.modelLabels[value] !== undefined ? this.settings.modelLabels[value] : value;
+ }
+ }
+
+ this.o.labels[pointer.uid].value.html(value);
+
+ // redraw position of labels
+ this.redrawLabels( pointer );
+ };
+
+ Slider.prototype.redrawLabels = function( pointer ) {
+
+ function setPosition( label, sizes, prc ) {
+ sizes.margin = -sizes.label/2;
+ var domSize = !self.settings.vertical ? self.sizes.domWidth : self.sizes.domHeight;
+
+ if (self.sizes.domWidth) {
+ // left limit
+ var label_left = sizes.border + sizes.margin;
+ if(label_left < 0)
+ sizes.margin -= label_left;
+
+ // right limit
+ if(self.sizes.domWidth > 0 && sizes.border+sizes.label / 2 > domSize){
+ sizes.margin = 0;
+ sizes.right = true;
+ } else
+ sizes.right = false;
+ }
+
+ if (!self.settings.vertical)
+ label.o.css({ left: prc + "%", marginLeft: sizes.margin+"px", right: "auto" });
+ else
+ label.o.css({ top: prc + "%", marginLeft: "20px", marginTop: sizes.margin, bottom: "auto" });
+ if(sizes.right && self.sizes.domWidth > 0) {
+ if (!self.settings.vertical)
+ label.o.css({ left: "auto", right: 0 });
+ else
+ label.o.css({ top: "auto", bottom: 0 });
+ }
+ return sizes;
+ }
+
+ var self = this,
+ label = this.o.labels[pointer.uid],
+ prc = pointer.value.prc,
+ // case hidden
+ labelWidthSize = label.o[0].offsetWidth === 0 ? (label.o[0].textContent.length)*7 : label.o[0].offsetWidth;
+
+ this.sizes.domWidth = this.domNode[0].clientWidth;
+ this.sizes.domHeight = this.domNode[0].clientHeight;
+
+ var sizes = {
+ label: !self.settings.vertical ? labelWidthSize : label.o[0].offsetHeight,
+ right: false,
+ border: (prc * (!self.settings.vertical ? this.sizes.domWidth: this.sizes.domHeight)) / 100
+ };
+
+ var anotherIdx = pointer.uid === 0 ? 1:0,
+ anotherLabel,
+ anotherPtr;
+
+ if (!this.settings.single && !this.settings.vertical){
+ // glue if near;
+ anotherLabel = this.o.labels[anotherIdx];
+ anotherPtr = this.o.pointers[anotherIdx];
+ var label1 = this.o.labels[0],
+ label2 = this.o.labels[1],
+ ptr1 = this.o.pointers[0],
+ ptr2 = this.o.pointers[1],
+ gapBetweenLabel = ptr2.ptr[0].offsetLeft - ptr1.ptr[0].offsetLeft,
+ value = this.nice(anotherPtr.value.origin);
+
+ label1.o.css(this.css.visible);
+ label2.o.css(this.css.visible);
+
+ value = this.getLabelValue(value);
+
+ if (gapBetweenLabel + 10 < label1.o[0].offsetWidth+label2.o[0].offsetWidth) {
+ anotherLabel.o.css(this.css.hidden);
+
+ anotherLabel.value.html(value);
+ prc = (anotherPtr.value.prc - prc) / 2 + prc;
+ if(anotherPtr.value.prc != pointer.value.prc){
+ value = this.nice(this.o.pointers[0].value.origin);
+ var value1 = this.nice(this.o.pointers[1].value.origin);
+ value = this.getLabelValue(value);
+ value1 = this.getLabelValue(value1);
+
+ label.value.html(value + "&nbsp;&ndash;&nbsp;" + value1);
+ sizes.label = label.o[0].offsetWidth;
+ sizes.border = (prc * domSize) / 100;
+ }
+ }
+ else {
+ anotherLabel.value.html(value);
+ anotherLabel.o.css(this.css.visible);
+ }
+ }
+
+ sizes = setPosition(label, sizes, prc);
+
+ var domSize = !self.settings.vertical ? self.sizes.domWidth : self.sizes.domHeight;
+
+ /* draw second label */
+ if(anotherLabel){
+ // case hidden
+ var labelWidthSize2 = label.o[0].offsetWidth === 0 ? (label.o[0].textContent.length/2)*7 : label.o[0].offsetWidth,
+ sizes2 = {
+ label: !self.settings.vertical ? labelWidthSize2: anotherLabel.o[0].offsetHeight,
+ right: false,
+ border: (anotherPtr.value.prc * this.sizes.domWidth) / 100
+ };
+ sizes = setPosition(anotherLabel, sizes2, anotherPtr.value.prc);
+ }
+
+ this.redrawLimits();
+ };
+
+ Slider.prototype.redrawLimits = function() {
+ if (this.settings.limits) {
+
+ var limits = [true, true],
+ i = 0;
+
+ for(var key in this.o.pointers){
+
+ if(!this.settings.single || key === 0){
+
+ var pointer = this.o.pointers[key],
+ label = this.o.labels[pointer.uid],
+ label_left = label.o[0].offsetLeft - this.sizes.domOffset.left,
+ limit = this.o.limits[0];
+
+ if(label_left < limit[0].clientWidth) {
+ limits[0] = false;
+ }
+
+ limit = this.o.limits[1];
+ if(label_left + label.o[0].clientWidth > this.sizes.domWidth - limit[0].clientWidth){
+ limits[1] = false;
+ }
+
+ }
+ }
+
+ for(; i < limits.length; i++){
+ if(limits[i]){ // TODO animate
+ angular.element(this.o.limits[i]).addClass("animate-show");
+ }
+ else{
+ angular.element(this.o.limits[i]).addClass("animate-hidde");
+ }
+ }
+ }
+ };
+
+ Slider.prototype.setValue = function(){
+ var value = this.getValue();
+ this.inputNode.attr("value", value);
+ this.onstatechange.call(this, value, this.inputNode);
+ };
+
+ Slider.prototype.getValue = function(){
+ if(!this.is.init) return false;
+ var $this = this;
+
+ var value = "";
+ angular.forEach(this.o.pointers, function(pointer, key){
+ if(pointer.value.prc !== undefined && !isNaN(pointer.value.prc))
+ value += (key > 0 ? ";" : "") + $this.prcToValue(pointer.value.prc);
+ });
+ return value;
+ };
+
+ Slider.prototype.getLabelValue = function(value){
+ if (this.settings.modelLabels) {
+ if (angular.isFunction(this.settings.modelLabels)) {
+ return this.settings.modelLabels(value);
+ }
+ else {
+ return this.settings.modelLabels[value] !== undefined ? this.settings.modelLabels[value] : value;
+ }
+ }
+
+ return value;
+ };
+
+ Slider.prototype.getPrcValue = function(){
+ if(!this.is.init) return false;
+ var $this = this;
+
+ var value = "";
+ // TODO remove jquery and see if % value is nice feature
+ /*$.each(this.o.pointers, function(i){
+ if(this.value.prc !== undefined && !isNaN(this.value.prc)) value += (i > 0 ? ";" : "") + this.value.prc;
+ });*/
+ return value;
+ };
+
+ Slider.prototype.prcToValue = function( prc ){
+ var value;
+ if (this.settings.heterogeneity && this.settings.heterogeneity.length > 0){
+ var h = this.settings.heterogeneity,
+ _start = 0,
+ _from = this.settings.round ? parseFloat(this.settings.from) : parseInt(this.settings.from, 10),
+ _to = this.settings.round ? parseFloat(this.settings.to) : parseInt(this.settings.to, 10),
+ i = 0;
+
+ for (; i <= h.length; i++){
+ var v;
+ if(h[i])
+ v = h[i].split('/');
+ else
+ v = [100, _to];
+
+ var v1 = this.settings.round ? parseFloat(v[0]) : parseInt(v[0], 10);
+ var v2 = this.settings.round ? parseFloat(v[1]) : parseInt(v[1], 10);
+
+ if (prc >= _start && prc <= v1) {
+ value = _from + ((prc-_start) * (v2-_from)) / (v1-_start);
+ }
+
+ _start = v1;
+ _from = v2;
+ }
+ }
+ else {
+ value = this.settings.from + (prc * this.settings.interval) / 100;
+ }
+
+ return this.round(value);
+ };
+
+ Slider.prototype.valueToPrc = function( value, pointer ){
+ var prc,
+ _from = this.settings.round ? parseFloat(this.settings.from) : parseInt(this.settings.from, 10);
+
+ if (this.settings.heterogeneity && this.settings.heterogeneity.length > 0){
+ var h = this.settings.heterogeneity,
+ _start = 0,
+ i = 0;
+
+ for (; i <= h.length; i++) {
+ var v;
+ if(h[i])
+ v = h[i].split('/');
+ else
+ v = [100, this.settings.to];
+
+ var v1 = this.settings.round ? parseFloat(v[0]) : parseInt(v[0], 10);
+ var v2 = this.settings.round ? parseFloat(v[1]) : parseInt(v[1], 10);
+
+ if(value >= _from && value <= v2){
+ if (pointer) {
+ prc = pointer.limits(_start + (value-_from)*(v1-_start)/(v2-_from));
+ } else {
+ prc = this.limits(_start + (value-_from)*(v1-_start)/(v2-_from));
+ }
+ }
+
+ _start = v1; _from = v2;
+ }
+
+ } else {
+ if (pointer) {
+ prc = pointer.limits((value-_from)*100/this.settings.interval);
+ } else {
+ prc = this.limits((value-_from)*100/this.settings.interval);
+ }
+ }
+
+ return prc;
+ };
+
+ Slider.prototype.round = function( value ){
+ value = Math.round(value / this.settings.step) * this.settings.step;
+
+ if(this.settings.round)
+ value = Math.round(value * Math.pow(10, this.settings.round)) / Math.pow(10, this.settings.round);
+ else
+ value = Math.round(value);
+ return value;
+ };
+
+ return Slider;
+
+ }]);
+}(angular));
+;(function(angular, undefined) {
+'use strict';
+
+ angular.module('angularAwesomeSlider')
+ .run(['$templateCache', function ($templateCache) {
+ $templateCache.put('ng-slider/slider-bar.tmpl.html',
+ '<span ng-class="mainSliderClass" id="{{sliderTmplId}}">' +
+ '<table><tr><td>' +
+ '<div class="jslider-bg">' +
+ '<i class="left"></i>'+
+ '<i class="right"></i>'+
+ '<i class="range"></i>'+
+ '<i class="before"></i>'+
+ '<i class="default"></i>'+
+ '<i class="default"></i>'+
+ '<i class="after"></i>'+
+ '</div>' +
+ '<div class="jslider-pointer"></div>' +
+ '<div class="jslider-pointer jslider-pointer-to"></div>' +
+ '<div class="jslider-label" ng-show="options.limits"><span ng-bind="limitValue(options.from)"></span>{{options.dimension}}</div>' +
+ '<div class="jslider-label jslider-label-to" ng-show="options.limits"><span ng-bind="limitValue(options.to)"></span>{{options.dimension}}</div>' +
+ '<div class="jslider-value"><span></span>{{options.dimension}}</div>' +
+ '<div class="jslider-value jslider-value-to"><span></span>{{options.dimension}}</div>' +
+ '<div class="jslider-scale" id="{{sliderScaleDivTmplId}}"></div>' +
+ '</td></tr></table>' +
+ '</span>');
+ }]);
+
+})(window.angular);
diff --git a/etc/js/angular-carousel.js b/etc/js/angular-carousel.js
new file mode 100644
index 00000000..48c94258
--- /dev/null
+++ b/etc/js/angular-carousel.js
@@ -0,0 +1,2286 @@
+
+//Angular Carousel - Mobile friendly touch carousel for AngularJS
+//@version v0.3.12 - 2015-06-11
+//@link http://revolunet.github.com/angular-carousel
+//@author Julien Bouquillon <julien@revolunet.com>
+//@license MIT License, http://www.opensource.org/licenses/MIT
+
+
+
+//Angular touch carousel with CSS GPU accel and slide buffering
+//http://github.com/revolunet/angular-carousel
+
+
+// Modified by PP for mobile friendly touch, start without auto slide but enabled it if tapped
+// and logic to wait for an image to load before it slides to the next one
+
+/* jshint ignore:start */
+/*global angular */
+angular.module('angular-carousel', [
+ 'ngTouch',
+ 'angular-carousel.shifty'
+]);
+
+angular.module('angular-carousel')
+
+// PP modified carousel to work in conjunction with lazy loading
+// so slide does not progress if image is not loaded or gets an error
+// imageLoadingDataShare is the factory that has a value
+// of 0 if no image is being loaded, -1 if there was an error and 1 if an image is currently being loaded
+.directive('rnCarouselAutoSlide', ['$interval','$timeout', 'imageLoadingDataShare', 'carouselUtils', function($interval,$timeout, imageLoadingDataShare, carouselUtils ) {
+ return {
+ restrict: 'A',
+ link: function (scope, element, attrs) {
+
+
+ var stopAutoPlay = function() {
+ if (scope.autoSlider) {
+ //console.log ("Cancelling timer");
+ //$interval.cancel(scope.autoSlider);
+ scope.autoSlider = null;
+ }
+ };
+ var restartTimer = function() {
+ //console.log ("restart timer");
+ if (carouselUtils.isStopped() == false)
+ {
+ //console.log ("Timer restart rnForceStop "+ carouselUtils.get());
+ //console.log ("Calling autoslide because isStopped is false");
+ scope.autoSlide();
+ }
+ else {
+ //console.log ("Not sliding as stop=true");
+ }
+ };
+ //PP - don't auto play if user taps
+ var toggleAutoPlay = function() {
+ //scope.rnForceStop = !scope.rnForceStop;
+ carouselUtils.setStop(!carouselUtils.getStop());
+ if (carouselUtils.isStopped())
+ carouselUtils.setIndex (scope.carouselIndex);
+ //console.log ("Autoplay is " + carouselUtils.get());
+ if (scope.autoSlider )
+ {
+ // PP - If autoslide was on and we tapped, stop auto slide
+ //scope.rnForceStop = true; //PP
+ carouselUtils.setStop (true);
+ carouselUtils.setIndex (scope.carouselIndex);
+ //console.log ("***RN: Stopping Play rnForceStop is "+carouselUtils.getStop());
+ stopAutoPlay();
+ }
+ else
+ {
+ //scope.rnForceStop = false; //PP
+ carouselUtils.setStop (false);
+ //console.log ("***RN: Starting Play rnForceStop is "+carouselUtils.getStop());
+ //scope.carouselIndex = carouselUtils.getIndex();
+ restartTimer();
+ }
+ };
+
+// start with autoplay and require tap to stop
+ //console.log ("*** Setting rnForceStop to false and watching");
+ //scope.rnForceStop = false; // PP
+ carouselUtils.setStop (false);
+ scope.$watch('carouselIndex', restartTimer);
+
+ if (attrs.hasOwnProperty('rnCarouselPauseOnHover') && attrs.rnCarouselPauseOnHover !== 'false'){
+ // PP - added touchend to make it react to touch devices
+ if (attrs.hasOwnProperty('rnPlatform') && attrs.rnPlatform == 'desktop')
+ {
+ //console.log ("ranPlatform is " + attrs.rnPlatform);
+ //console.log ("Desktop, de-registering any old click");
+ element.off('click', toggleAutoPlay); // PP - remove mouse click for desktop
+ element.on('click', toggleAutoPlay); // PP for desktop
+ //console.log ("Desktop, registering click");
+ }
+ else
+ {
+ //console.log ("ranPlatform is " + attrs.rnPlatform);
+ //console.log ("Device, de-registering any old touch");
+ element.off('touchend', toggleAutoPlay); // PP - remove touchend too
+ element.on('touchend', toggleAutoPlay);
+ //console.log ("Device, registering touch");
+ }
+ //element.on('mouseenter', stopAutoPlay);
+ //element.on('mouseleave', restartTimer);
+ }
+
+ scope.$on('$destroy', function(){
+ stopAutoPlay();
+ //element.off('mouseenter', stopAutoPlay);
+ //element.off('mouseleave', restartTimer);
+ element.off('touchend', toggleAutoPlay); // PP - remove touchend too
+ element.off('click', toggleAutoPlay); // PP - remove mouse click for desktop
+ });
+ }
+ };
+}]);
+
+angular.module('angular-carousel')
+
+.directive('rnCarouselIndicators', ['$parse', function($parse) {
+ return {
+ restrict: 'A',
+ scope: {
+ slides: '=',
+ index: '=rnCarouselIndex'
+ },
+ templateUrl: 'carousel-indicators.html',
+ link: function(scope, iElement, iAttributes) {
+ var indexModel = $parse(iAttributes.rnCarouselIndex);
+ scope.goToSlide = function(index) {
+ indexModel.assign(scope.$parent.$parent, index);
+ };
+ }
+ };
+}]);
+
+angular.module('angular-carousel').run(['$templateCache', function($templateCache) {
+ $templateCache.put('carousel-indicators.html',
+ '<div class="rn-carousel-indicator">\n' +
+ '<span ng-repeat="slide in slides" ng-class="{active: $index==index}" ng-click="goToSlide($index)">●</span>' +
+ '</div>'
+ );
+}]);
+
+(function() {
+ "use strict";
+
+ angular.module('angular-carousel')
+
+ .service('DeviceCapabilities', function() {
+
+ // TODO: merge in a single function
+
+ // detect supported CSS property
+ function detectTransformProperty() {
+ var transformProperty = 'transform',
+ safariPropertyHack = 'webkitTransform';
+ if (typeof document.body.style[transformProperty] !== 'undefined') {
+
+ ['webkit', 'moz', 'o', 'ms'].every(function (prefix) {
+ var e = '-' + prefix + '-transform';
+ if (typeof document.body.style[e] !== 'undefined') {
+ transformProperty = e;
+ return false;
+ }
+ return true;
+ });
+ } else if (typeof document.body.style[safariPropertyHack] !== 'undefined') {
+ transformProperty = '-webkit-transform';
+ } else {
+ transformProperty = undefined;
+ }
+ return transformProperty;
+ }
+
+ //Detect support of translate3d
+ function detect3dSupport() {
+ var el = document.createElement('p'),
+ has3d,
+ transforms = {
+ 'webkitTransform': '-webkit-transform',
+ 'msTransform': '-ms-transform',
+ 'transform': 'transform'
+ };
+ // Add it to the body to get the computed style
+ document.body.insertBefore(el, null);
+ for (var t in transforms) {
+ if (el.style[t] !== undefined) {
+ el.style[t] = 'translate3d(1px,1px,1px)';
+ has3d = window.getComputedStyle(el).getPropertyValue(transforms[t]);
+ }
+ }
+ document.body.removeChild(el);
+ return (has3d !== undefined && has3d.length > 0 && has3d !== "none");
+ }
+
+ return {
+ has3d: detect3dSupport(),
+ transformProperty: detectTransformProperty()
+ };
+
+ })
+
+ .service('computeCarouselSlideStyle', ["DeviceCapabilities", function(DeviceCapabilities) {
+ // compute transition transform properties for a given slide and global offset
+ return function(slideIndex, offset, transitionType) {
+ var style = {
+ display: 'inline-block'
+ },
+ opacity,
+ absoluteLeft = (slideIndex * 100) + offset,
+ slideTransformValue = DeviceCapabilities.has3d ? 'translate3d(' + absoluteLeft + '%, 0, 0)' : 'translate3d(' + absoluteLeft + '%, 0)',
+ distance = ((100 - Math.abs(absoluteLeft)) / 100);
+
+ if (!DeviceCapabilities.transformProperty) {
+ // fallback to default slide if transformProperty is not available
+ style['margin-left'] = absoluteLeft + '%';
+ } else {
+ if (transitionType == 'fadeAndSlide') {
+ style[DeviceCapabilities.transformProperty] = slideTransformValue;
+ opacity = 0;
+ if (Math.abs(absoluteLeft) < 100) {
+ opacity = 0.3 + distance * 0.7;
+ }
+ style.opacity = opacity;
+ } else if (transitionType == 'hexagon') {
+ var transformFrom = 100,
+ degrees = 0,
+ maxDegrees = 60 * (distance - 1);
+
+ transformFrom = offset < (slideIndex * -100) ? 100 : 0;
+ degrees = offset < (slideIndex * -100) ? maxDegrees : -maxDegrees;
+ style[DeviceCapabilities.transformProperty] = slideTransformValue + ' ' + 'rotateY(' + degrees + 'deg)';
+ style[DeviceCapabilities.transformProperty + '-origin'] = transformFrom + '% 50%';
+ } else if (transitionType == 'zoom') {
+ style[DeviceCapabilities.transformProperty] = slideTransformValue;
+ var scale = 1;
+ if (Math.abs(absoluteLeft) < 100) {
+ scale = 1 + ((1 - distance) * 2);
+ }
+ style[DeviceCapabilities.transformProperty] += ' scale(' + scale + ')';
+ style[DeviceCapabilities.transformProperty + '-origin'] = '50% 50%';
+ opacity = 0;
+ if (Math.abs(absoluteLeft) < 100) {
+ opacity = 0.3 + distance * 0.7;
+ }
+ style.opacity = opacity;
+ } else {
+ style[DeviceCapabilities.transformProperty] = slideTransformValue;
+ }
+ }
+ return style;
+ };
+ }])
+
+ //PP
+ .factory ('carouselUtils', function() {
+ var isstopped = false;
+ var duration = 0;
+ var stoppedIndex = 0;
+ return {
+
+ setDuration: function (val)
+ {
+ duration = val;
+ //console.log (">>>>>>>>>>>>>>>> DURATION SET TO (secs) " + duration);
+ },
+
+ getDuration: function ()
+ {
+ return duration;
+ },
+
+ setStop: function(val)
+ {
+ isstopped = val;
+ if (isstopped)
+ {
+ console.log ("Paused at " + stoppedIndex);
+ }
+ },
+
+ setIndex: function(val)
+ {
+ stoppedIndex = val;
+ //console.log ("setting saved index to " + stoppedIndex);
+ },
+ getIndex: function()
+ {
+ return stoppedIndex;
+ },
+ getStop: function()
+ {
+ return isstopped;
+ },
+ isStopped: function()
+ {
+ return isstopped;
+ },
+ hello: function()
+ {
+ //console.log ("Hello from carouselUtils");
+ }
+
+ }
+ })
+
+
+ .service('createStyleString', function() {
+ return function(object) {
+ var styles = [];
+ angular.forEach(object, function(value, key) {
+ styles.push(key + ':' + value);
+ });
+ return styles.join(';');
+ };
+ })
+
+ .directive('rnCarousel', ['$swipe', '$window', '$document', '$parse', '$compile', '$timeout', '$interval', 'computeCarouselSlideStyle', 'createStyleString', 'Tweenable', 'imageLoadingDataShare', 'carouselUtils',
+ function($swipe, $window, $document, $parse, $compile, $timeout, $interval, computeCarouselSlideStyle, createStyleString, Tweenable, imageLoadingDataShare, carouselUtils) {
+ // internal ids to allow multiple instances
+ var carouselId = 0,
+ // in absolute pixels, at which distance the slide stick to the edge on release
+ rubberTreshold = 3;
+
+ var requestAnimationFrame = $window.requestAnimationFrame || $window.webkitRequestAnimationFrame || $window.mozRequestAnimationFrame;
+
+ function getItemIndex(collection, target, defaultIndex) {
+ var result = defaultIndex;
+ collection.every(function(item, index) {
+ if (angular.equals(item, target)) {
+ result = index;
+ return false;
+ }
+ return true;
+ });
+ return result;
+ }
+
+ return {
+ restrict: 'A',
+ scope: true,
+
+
+ compile: function(tElement, tAttributes) {
+ // use the compile phase to customize the DOM
+ var firstChild = tElement[0].querySelector('li'),
+ firstChildAttributes = (firstChild) ? firstChild.attributes : [],
+ isRepeatBased = false,
+ isBuffered = false,
+ repeatItem,
+ repeatCollection;
+
+ // try to find an ngRepeat expression
+ // at this point, the attributes are not yet normalized so we need to try various syntax
+ ['ng-repeat', 'data-ng-repeat', 'ng:repeat', 'x-ng-repeat'].every(function(attr) {
+ var repeatAttribute = firstChildAttributes[attr];
+ if (angular.isDefined(repeatAttribute)) {
+ // ngRepeat regexp extracted from angular 1.2.7 src
+ var exprMatch = repeatAttribute.value.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/),
+ trackProperty = exprMatch[3];
+
+ repeatItem = exprMatch[1];
+ repeatCollection = exprMatch[2];
+
+ if (repeatItem) {
+ if (angular.isDefined(tAttributes['rnCarouselBuffered'])) {
+ // update the current ngRepeat expression and add a slice operator if buffered
+ isBuffered = true;
+ repeatAttribute.value = repeatItem + ' in ' + repeatCollection + '|carouselSlice:carouselBufferIndex:carouselBufferSize';
+ if (trackProperty) {
+ repeatAttribute.value += ' track by ' + trackProperty;
+ }
+ }
+ isRepeatBased = true;
+ return false;
+ }
+ }
+ return true;
+ });
+
+ return function(scope, iElement, iAttributes, containerCtrl) {
+
+ carouselId++;
+
+ var defaultOptions = {
+ transitionType: iAttributes.rnCarouselTransition || 'slide',
+ transitionEasing: iAttributes.rnCarouselEasing || 'easeTo',
+ transitionDuration: parseFloat(iAttributes.rnCarouselDuration, 10) || 300,
+ isSequential: true,
+ autoSlideDuration: 3,
+ bufferSize: 9,
+ /* in container % how much we need to drag to trigger the slide change */
+ moveTreshold: 0.1,
+ defaultIndex: 0
+ };
+
+ // TODO
+ var options = angular.extend({}, defaultOptions);
+
+ var pressed,
+ startX,
+ isIndexBound = false,
+ offset = 0,
+ destination,
+ swipeMoved = false,
+ //animOnIndexChange = true,
+ currentSlides = [],
+ elWidth = null,
+ elX = null,
+ animateTransitions = true,
+ intialState = true,
+ animating = false,
+ mouseUpBound = false,
+ locked = false;
+
+ //rn-swipe-disabled =true will only disable swipe events
+ if(iAttributes.rnSwipeDisabled !== "true") {
+ $swipe.bind(iElement, {
+ start: swipeStart,
+ move: swipeMove,
+ end: swipeEnd,
+ cancel: function(event) {
+ swipeEnd({}, event);
+ }
+ });
+ }
+
+ function getSlidesDOM() {
+ return iElement[0].querySelectorAll('ul[rn-carousel] > li');
+ }
+
+ function documentMouseUpEvent(event) {
+ // in case we click outside the carousel, trigger a fake swipeEnd
+ swipeMoved = true;
+ swipeEnd({
+ x: event.clientX,
+ y: event.clientY
+ }, event);
+ }
+
+ function updateSlidesPosition(offset) {
+ // manually apply transformation to carousel childrens
+ // todo : optim : apply only to visible items
+ var x = scope.carouselBufferIndex * 100 + offset;
+ angular.forEach(getSlidesDOM(), function(child, index) {
+ child.style.cssText = createStyleString(computeCarouselSlideStyle(index, x, options.transitionType));
+ });
+ }
+
+ scope.nextSlide = function(slideOptions) {
+ if (carouselUtils.isStopped()==true)
+ {
+ //console.log ("Just updated index, but we are stopped");
+ return;
+ }
+ var index = scope.carouselIndex + 1;
+ if (index > currentSlides.length - 1) {
+//PP
+ index--;
+ }
+
+ //console.log ("inside next slide with index = " + index);
+ // PP - we keep moving the index but don't load so it looks nicer
+ // and we don't mess up the rate
+ if (imageLoadingDataShare.get() != 1) // PP- If the image is still being loaded, hold on, don't change
+ {
+ goToSlide(index, slideOptions);
+ // console.log ("loaded carousel is " + scope.carouselIndex);
+
+ }
+ else
+ {
+ scope.autoSlide();
+ // lets move the index along - PP
+ // so playback total time is not affected
+
+ // scope.carouselIndex = index;
+ //console.log ("Image is still loading, not skipping slides, index at "+index);
+ // updateBufferIndex();
+ //console.log ("NOT LOADED but advancing carousel to " + scope.carouselIndex);
+
+}
+
+ };
+
+ scope.prevSlide = function(slideOptions) {
+ var index = scope.carouselIndex - 1;
+ if (index < 0) {
+ index = currentSlides.length - 1;
+ }
+ goToSlide(index, slideOptions);
+ };
+
+ function goToSlide(index, slideOptions) {
+ //console.log('goToSlide', arguments);
+ // move a to the given slide index
+ if (index === undefined) {
+ index = scope.carouselIndex;
+ }
+
+ slideOptions = slideOptions || {};
+ if (slideOptions.animate === false || options.transitionType === 'none') {
+ locked = false;
+ offset = index * -100;
+ scope.carouselIndex = index;
+ //console.log ("inside goToSlide: updating carousel index to " + scope.carouselIndex);
+ updateBufferIndex();
+ return;
+ }
+
+ locked = true;
+ var tweenable = new Tweenable();
+ tweenable.tween({
+ from: {
+ 'x': offset
+ },
+ to: {
+ 'x': index * -100
+ },
+ duration: options.transitionDuration,
+ easing: options.transitionEasing,
+ step: function(state) {
+ updateSlidesPosition(state.x);
+ },
+ finish: function() {
+ scope.$apply(function() {
+ scope.carouselIndex = index;
+ offset = index * -100;
+ updateBufferIndex();
+ $timeout(function () {
+ locked = false;
+ }, 0, false);
+ });
+ }
+ });
+ }
+
+ function getContainerWidth() {
+ var rect = iElement[0].getBoundingClientRect();
+ return rect.width ? rect.width : rect.right - rect.left;
+ }
+
+ function updateContainerWidth() {
+ elWidth = getContainerWidth();
+ }
+
+ function bindMouseUpEvent() {
+ if (!mouseUpBound) {
+ mouseUpBound = true;
+ $document.bind('mouseup', documentMouseUpEvent);
+ }
+ }
+
+ function unbindMouseUpEvent() {
+ if (mouseUpBound) {
+ mouseUpBound = false;
+ $document.unbind('mouseup', documentMouseUpEvent);
+ }
+ }
+
+ function swipeStart(coords, event) {
+ // console.log('swipeStart', coords, event);
+ if (locked || currentSlides.length <= 1) {
+ return;
+ }
+ updateContainerWidth();
+ elX = iElement[0].querySelector('li').getBoundingClientRect().left;
+ pressed = true;
+ startX = coords.x;
+ return false;
+ }
+
+ function swipeMove(coords, event) {
+ //console.log('swipeMove', coords, event);
+ var x, delta;
+ bindMouseUpEvent();
+ if (pressed) {
+ x = coords.x;
+ delta = startX - x;
+ if (delta > 2 || delta < -2) {
+ swipeMoved = true;
+ var moveOffset = offset + (-delta * 100 / elWidth);
+ updateSlidesPosition(moveOffset);
+ }
+ }
+ return false;
+ }
+
+ var init = true;
+ scope.carouselIndex = 0;
+
+ if (!isRepeatBased) {
+ // fake array when no ng-repeat
+ currentSlides = [];
+ angular.forEach(getSlidesDOM(), function(node, index) {
+ currentSlides.push({id: index});
+ });
+ }
+
+ if (iAttributes.rnCarouselControls!==undefined) {
+ // dont use a directive for this
+ var nextSlideIndexCompareValue = isRepeatBased ? repeatCollection.replace('::', '') + '.length - 1' : currentSlides.length - 1;
+ var tpl = '<div class="rn-carousel-controls">\n' +
+ ' <span class="rn-carousel-control rn-carousel-control-prev" ng-click="prevSlide()" ng-if="carouselIndex > 0"></span>\n' +
+ ' <span class="rn-carousel-control rn-carousel-control-next" ng-click="nextSlide()" ng-if="carouselIndex < ' + nextSlideIndexCompareValue + '"></span>\n' +
+ '</div>';
+ iElement.parent().append($compile(angular.element(tpl))(scope));
+ }
+
+ if (iAttributes.rnCarouselAutoSlide!==undefined) {
+ // PP was parseInt, changed to parseFloat so I can specify fractions of seconds
+ var duration = parseFloat(iAttributes.rnCarouselAutoSlide, 10) || options.autoSlideDuration;
+ carouselUtils.setDuration(duration);
+ console.log ("Setting duration - should only happen once");
+ scope.autoSlide = function() {
+ //console.log ("Inside autoslide");
+ if (scope.autoSlider) {
+ //$interval.cancel(scope.autoSlider);
+ scope.autoSlider = null;
+ }
+ if (carouselUtils.isStopped() == false) //PP - don't move slide if this variable is set
+ {
+ var mydur = carouselUtils.getDuration();
+
+ // what happens if mydur needs to be less than a millisecond?
+ var mydurms = mydur * 1000.0;
+
+ if (mydurms < 5)
+ {
+ console.log ("duration is too small at "+mydurms+" making it to 5");
+ mydurms = 5;
+
+ }
+ //console.log ("Setting next slide duration at " + mydur *1000);
+ scope.autoSlider = $timeout(function() {
+ //console.log ("setting time to " + mydurms);
+ //if (!locked && !pressed ) {
+ if (1 ) {
+ scope.nextSlide();
+ }
+ }, mydurms);
+ }
+ else { console.log ("We are stopped, doing nothing"); }
+ };
+ }
+
+ if (iAttributes.rnCarouselDefaultIndex) {
+ var defaultIndexModel = $parse(iAttributes.rnCarouselDefaultIndex);
+ options.defaultIndex = defaultIndexModel(scope.$parent) || 0;
+ }
+
+
+ var shouldInitialySlideTo = null;
+ if (iAttributes.rnCarouselIndex) {
+ var updateParentIndex = function(value) {
+ if (value < 0) {
+ return;
+ }
+ //console.log ("Still indexing");
+ indexModel.assign(scope.$parent, value);
+ };
+ var indexModel = $parse(iAttributes.rnCarouselIndex);
+ if (angular.isFunction(indexModel.assign)) {
+ /* check if this property is assignable then watch it */
+ scope.$watch('carouselIndex', function(newValue) {
+ //if (carouselUtils.isStopped() == false)
+ //console.log ("Carouselc chanhed 1");
+ updateParentIndex(newValue);
+ });
+ scope.$parent.$watch(function () {
+ return indexModel(scope.$parent);
+ }, function(newValue, oldValue) {
+ shouldInitialySlideTo = newValue;
+
+ if (newValue !== undefined && newValue !== null) {
+ if (currentSlides && currentSlides.length > 0 && newValue >= currentSlides.length) {
+ newValue = currentSlides.length - 1;
+ updateParentIndex(newValue);
+ } else if (currentSlides && newValue < 0) {
+ newValue = 0; // PP
+ updateParentIndex(newValue);
+ }
+ if (!locked) {
+ goToSlide(newValue, {
+ animate: !init
+ });
+ }
+ init = false;
+ }
+ });
+ isIndexBound = true;
+
+ if (options.defaultIndex) {
+ goToSlide(options.defaultIndex, {
+ animate: !init
+ });
+ }
+ } else if (!isNaN(iAttributes.rnCarouselIndex)) {
+ /* if user just set an initial number, set it */
+ goToSlide(parseInt(iAttributes.rnCarouselIndex, 10), {
+ animate: false
+ });
+ }
+ } else {
+ goToSlide(options.defaultIndex, {
+ animate: !init
+ });
+ init = false;
+ }
+
+ if (iAttributes.rnCarouselLocked) {
+ scope.$watch(iAttributes.rnCarouselLocked, function(newValue, oldValue) {
+ // only bind swipe when it's not switched off
+ if(newValue === true) {
+ locked = true;
+ } else {
+ locked = false;
+ }
+ });
+ }
+
+ if (isRepeatBased) {
+ // use rn-carousel-deep-watch to fight the Angular $watchCollection weakness : https://github.com/angular/angular.js/issues/2621
+ // optional because it have some performance impacts (deep watch)
+ var deepWatch = (iAttributes.rnCarouselDeepWatch!==undefined);
+
+ scope[deepWatch?'$watch':'$watchCollection'](repeatCollection, function(newValue, oldValue) {
+ //console.log('repeatCollection', currentSlides);
+ currentSlides = newValue;
+// This will force the required initial carouselIndex
+ // specified with `rn-carousel-index` on carousel initialization.
+ if (shouldInitialySlideTo) {
+ scope.carouselIndex = shouldInitialySlideTo;
+ shouldInitialySlideTo = null;
+ }
+ // if deepWatch ON ,manually compare objects to guess the new position
+ if (deepWatch && angular.isArray(newValue)) {
+ var activeElement = oldValue[scope.carouselIndex];
+ var newIndex = getItemIndex(newValue, activeElement, scope.carouselIndex);
+ goToSlide(newIndex, {animate: false});
+ } else {
+ goToSlide(scope.carouselIndex, {animate: false});
+ }
+ }, true);
+ }
+
+ function swipeEnd(coords, event, forceAnimation) {
+ // console.log('swipeEnd', 'scope.carouselIndex', scope.carouselIndex);
+ // Prevent clicks on buttons inside slider to trigger "swipeEnd" event on touchend/mouseup
+ // console.log(iAttributes.rnCarouselOnInfiniteScroll);
+ if (event && !swipeMoved) {
+ return;
+ }
+ unbindMouseUpEvent();
+ pressed = false;
+ swipeMoved = false;
+ destination = startX - coords.x;
+ if (destination===0) {
+ return;
+ }
+ if (locked) {
+ return;
+ }
+ offset += (-destination * 100 / elWidth);
+ if (options.isSequential) {
+ var minMove = options.moveTreshold * elWidth,
+ absMove = -destination,
+ slidesMove = -Math[absMove >= 0 ? 'ceil' : 'floor'](absMove / elWidth),
+ shouldMove = Math.abs(absMove) > minMove;
+
+ if (currentSlides && (slidesMove + scope.carouselIndex) >= currentSlides.length) {
+ slidesMove = currentSlides.length - 1 - scope.carouselIndex;
+ }
+ if ((slidesMove + scope.carouselIndex) < 0) {
+ slidesMove = -scope.carouselIndex;
+ }
+ var moveOffset = shouldMove ? slidesMove : 0;
+
+ destination = (scope.carouselIndex + moveOffset);
+
+ goToSlide(destination);
+ if(iAttributes.rnCarouselOnInfiniteScrollRight!==undefined && slidesMove === 0 && scope.carouselIndex !== 0) {
+ $parse(iAttributes.rnCarouselOnInfiniteScrollRight)(scope)
+ goToSlide(0);
+ }
+ if(iAttributes.rnCarouselOnInfiniteScrollLeft!==undefined && slidesMove === 0 && scope.carouselIndex === 0 && moveOffset === 0) {
+ $parse(iAttributes.rnCarouselOnInfiniteScrollLeft)(scope)
+ goToSlide(currentSlides.length);
+ }
+
+ } else {
+ scope.$apply(function() {
+ scope.carouselIndex = parseInt(-offset / 100, 10);
+ updateBufferIndex();
+ });
+
+ }
+
+ }
+
+ scope.$on('$destroy', function() {
+ unbindMouseUpEvent();
+ });
+
+ scope.carouselBufferIndex = 0;
+ scope.carouselBufferSize = options.bufferSize;
+
+ function updateBufferIndex() {
+ // update and cap te buffer index
+ var bufferIndex = 0;
+ var bufferEdgeSize = (scope.carouselBufferSize - 1) / 2;
+ if (isBuffered) {
+ if (scope.carouselIndex <= bufferEdgeSize) {
+ // first buffer part
+ bufferIndex = 0;
+ } else if (currentSlides && currentSlides.length < scope.carouselBufferSize) {
+ // smaller than buffer
+ bufferIndex = 0;
+ } else if (currentSlides && scope.carouselIndex > currentSlides.length - scope.carouselBufferSize) {
+ // last buffer part
+ bufferIndex = currentSlides.length - scope.carouselBufferSize;
+ } else {
+ // compute buffer start
+ bufferIndex = scope.carouselIndex - bufferEdgeSize;
+ }
+
+ scope.carouselBufferIndex = bufferIndex;
+ $timeout(function() {
+ updateSlidesPosition(offset);
+ }, 0, false);
+ } else {
+ $timeout(function() {
+ updateSlidesPosition(offset);
+ }, 0, false);
+ }
+ }
+
+ function onOrientationChange() {
+ updateContainerWidth();
+ goToSlide();
+ }
+
+ // handle orientation change
+ var winEl = angular.element($window);
+ winEl.bind('orientationchange', onOrientationChange);
+ winEl.bind('resize', onOrientationChange);
+
+ scope.$on('$destroy', function() {
+ unbindMouseUpEvent();
+ winEl.unbind('orientationchange', onOrientationChange);
+ winEl.unbind('resize', onOrientationChange);
+ });
+ };
+ }
+ };
+ }
+ ]);
+})();
+
+
+
+angular.module('angular-carousel.shifty', [])
+
+.factory('Tweenable', function() {
+
+ /*! shifty - v1.3.4 - 2014-10-29 - http://jeremyckahn.github.io/shifty */
+ ;(function (root) {
+
+ /*!
+ * Shifty Core
+ * By Jeremy Kahn - jeremyckahn@gmail.com
+ */
+
+ var Tweenable = (function () {
+
+ 'use strict';
+
+ // Aliases that get defined later in this function
+ var formula;
+
+ // CONSTANTS
+ var DEFAULT_SCHEDULE_FUNCTION;
+ var DEFAULT_EASING = 'linear';
+ var DEFAULT_DURATION = 500;
+ var UPDATE_TIME = 1000 / 60;
+
+ var _now = Date.now
+ ? Date.now
+ : function () {return +new Date();};
+
+ var now = typeof SHIFTY_DEBUG_NOW !== 'undefined' ? SHIFTY_DEBUG_NOW : _now;
+
+ if (typeof window !== 'undefined') {
+ // requestAnimationFrame() shim by Paul Irish (modified for Shifty)
+ // http://paulirish.com/2011/requestanimationframe-for-smart-animating/
+ DEFAULT_SCHEDULE_FUNCTION = window.requestAnimationFrame
+ || window.webkitRequestAnimationFrame
+ || window.oRequestAnimationFrame
+ || window.msRequestAnimationFrame
+ || (window.mozCancelRequestAnimationFrame
+ && window.mozRequestAnimationFrame)
+ || setTimeout;
+ } else {
+ DEFAULT_SCHEDULE_FUNCTION = setTimeout;
+ }
+
+ function noop () {
+ // NOOP!
+ }
+
+ /*!
+ * Handy shortcut for doing a for-in loop. This is not a "normal" each
+ * function, it is optimized for Shifty. The iterator function only receives
+ * the property name, not the value.
+ * @param {Object} obj
+ * @param {Function(string)} fn
+ */
+ function each (obj, fn) {
+ var key;
+ for (key in obj) {
+ if (Object.hasOwnProperty.call(obj, key)) {
+ fn(key);
+ }
+ }
+ }
+
+ /*!
+ * Perform a shallow copy of Object properties.
+ * @param {Object} targetObject The object to copy into
+ * @param {Object} srcObject The object to copy from
+ * @return {Object} A reference to the augmented `targetObj` Object
+ */
+ function shallowCopy (targetObj, srcObj) {
+ each(srcObj, function (prop) {
+ targetObj[prop] = srcObj[prop];
+ });
+
+ return targetObj;
+ }
+
+ /*!
+ * Copies each property from src onto target, but only if the property to
+ * copy to target is undefined.
+ * @param {Object} target Missing properties in this Object are filled in
+ * @param {Object} src
+ */
+ function defaults (target, src) {
+ each(src, function (prop) {
+ if (typeof target[prop] === 'undefined') {
+ target[prop] = src[prop];
+ }
+ });
+ }
+
+ /*!
+ * Calculates the interpolated tween values of an Object for a given
+ * timestamp.
+ * @param {Number} forPosition The position to compute the state for.
+ * @param {Object} currentState Current state properties.
+ * @param {Object} originalState: The original state properties the Object is
+ * tweening from.
+ * @param {Object} targetState: The destination state properties the Object
+ * is tweening to.
+ * @param {number} duration: The length of the tween in milliseconds.
+ * @param {number} timestamp: The UNIX epoch time at which the tween began.
+ * @param {Object} easing: This Object's keys must correspond to the keys in
+ * targetState.
+ */
+ function tweenProps (forPosition, currentState, originalState, targetState,
+ duration, timestamp, easing) {
+ var normalizedPosition = (forPosition - timestamp) / duration;
+
+ var prop;
+ for (prop in currentState) {
+ if (currentState.hasOwnProperty(prop)) {
+ currentState[prop] = tweenProp(originalState[prop],
+ targetState[prop], formula[easing[prop]], normalizedPosition);
+ }
+ }
+
+ return currentState;
+ }
+
+ /*!
+ * Tweens a single property.
+ * @param {number} start The value that the tween started from.
+ * @param {number} end The value that the tween should end at.
+ * @param {Function} easingFunc The easing curve to apply to the tween.
+ * @param {number} position The normalized position (between 0.0 and 1.0) to
+ * calculate the midpoint of 'start' and 'end' against.
+ * @return {number} The tweened value.
+ */
+ function tweenProp (start, end, easingFunc, position) {
+ return start + (end - start) * easingFunc(position);
+ }
+
+ /*!
+ * Applies a filter to Tweenable instance.
+ * @param {Tweenable} tweenable The `Tweenable` instance to call the filter
+ * upon.
+ * @param {String} filterName The name of the filter to apply.
+ */
+ function applyFilter (tweenable, filterName) {
+ var filters = Tweenable.prototype.filter;
+ var args = tweenable._filterArgs;
+
+ each(filters, function (name) {
+ if (typeof filters[name][filterName] !== 'undefined') {
+ filters[name][filterName].apply(tweenable, args);
+ }
+ });
+ }
+
+ var timeoutHandler_endTime;
+ var timeoutHandler_currentTime;
+ var timeoutHandler_isEnded;
+ var timeoutHandler_offset;
+ /*!
+ * Handles the update logic for one step of a tween.
+ * @param {Tweenable} tweenable
+ * @param {number} timestamp
+ * @param {number} duration
+ * @param {Object} currentState
+ * @param {Object} originalState
+ * @param {Object} targetState
+ * @param {Object} easing
+ * @param {Function(Object, *, number)} step
+ * @param {Function(Function,number)}} schedule
+ */
+ function timeoutHandler (tweenable, timestamp, duration, currentState,
+ originalState, targetState, easing, step, schedule) {
+ timeoutHandler_endTime = timestamp + duration;
+ timeoutHandler_currentTime = Math.min(now(), timeoutHandler_endTime);
+ timeoutHandler_isEnded =
+ timeoutHandler_currentTime >= timeoutHandler_endTime;
+
+ timeoutHandler_offset = duration - (
+ timeoutHandler_endTime - timeoutHandler_currentTime);
+
+ if (tweenable.isPlaying() && !timeoutHandler_isEnded) {
+ tweenable._scheduleId = schedule(tweenable._timeoutHandler, UPDATE_TIME);
+
+ applyFilter(tweenable, 'beforeTween');
+ tweenProps(timeoutHandler_currentTime, currentState, originalState,
+ targetState, duration, timestamp, easing);
+ applyFilter(tweenable, 'afterTween');
+
+ step(currentState, tweenable._attachment, timeoutHandler_offset);
+ } else if (timeoutHandler_isEnded) {
+ step(targetState, tweenable._attachment, timeoutHandler_offset);
+ tweenable.stop(true);
+ }
+ }
+
+
+ /*!
+ * Creates a usable easing Object from either a string or another easing
+ * Object. If `easing` is an Object, then this function clones it and fills
+ * in the missing properties with "linear".
+ * @param {Object} fromTweenParams
+ * @param {Object|string} easing
+ */
+ function composeEasingObject (fromTweenParams, easing) {
+ var composedEasing = {};
+
+ if (typeof easing === 'string') {
+ each(fromTweenParams, function (prop) {
+ composedEasing[prop] = easing;
+ });
+ } else {
+ each(fromTweenParams, function (prop) {
+ if (!composedEasing[prop]) {
+ composedEasing[prop] = easing[prop] || DEFAULT_EASING;
+ }
+ });
+ }
+
+ return composedEasing;
+ }
+
+ /**
+ * Tweenable constructor.
+ * @param {Object=} opt_initialState The values that the initial tween should start at if a "from" object is not provided to Tweenable#tween.
+ * @param {Object=} opt_config See Tweenable.prototype.setConfig()
+ * @constructor
+ */
+ function Tweenable (opt_initialState, opt_config) {
+ this._currentState = opt_initialState || {};
+ this._configured = false;
+ this._scheduleFunction = DEFAULT_SCHEDULE_FUNCTION;
+
+ // To prevent unnecessary calls to setConfig do not set default configuration here.
+ // Only set default configuration immediately before tweening if none has been set.
+ if (typeof opt_config !== 'undefined') {
+ this.setConfig(opt_config);
+ }
+ }
+
+ /**
+ * Configure and start a tween.
+ * @param {Object=} opt_config See Tweenable.prototype.setConfig()
+ * @return {Tweenable}
+ */
+ Tweenable.prototype.tween = function (opt_config) {
+ if (this._isTweening) {
+ return this;
+ }
+
+ // Only set default config if no configuration has been set previously and none is provided now.
+ if (opt_config !== undefined || !this._configured) {
+ this.setConfig(opt_config);
+ }
+
+ this._timestamp = now();
+ this._start(this.get(), this._attachment);
+ return this.resume();
+ };
+
+ /**
+ * Sets the tween configuration. `config` may have the following options:
+ *
+ * - __from__ (_Object=_): Starting position. If omitted, the current state is used.
+ * - __to__ (_Object=_): Ending position.
+ * - __duration__ (_number=_): How many milliseconds to animate for.
+ * - __start__ (_Function(Object)_): Function to execute when the tween begins. Receives the state of the tween as the first parameter. Attachment is the second parameter.
+ * - __step__ (_Function(Object, *, number)_): Function to execute on every tick. Receives the state of the tween as the first parameter. Attachment is the second parameter, and the time elapsed since the start of the tween is the third parameter. This function is not called on the final step of the animation, but `finish` is.
+ * - __finish__ (_Function(Object, *)_): Function to execute upon tween completion. Receives the state of the tween as the first parameter. Attachment is the second parameter.
+ * - __easing__ (_Object|string=_): Easing curve name(s) to use for the tween.
+ * - __attachment__ (_Object|string|any=_): Value that is attached to this instance and passed on to the step/start/finish methods.
+ * @param {Object} config
+ * @return {Tweenable}
+ */
+ Tweenable.prototype.setConfig = function (config) {
+ config = config || {};
+ this._configured = true;
+
+ // Attach something to this Tweenable instance (e.g.: a DOM element, an object, a string, etc.);
+ this._attachment = config.attachment;
+
+ // Init the internal state
+ this._pausedAtTime = null;
+ this._scheduleId = null;
+ this._start = config.start || noop;
+ this._step = config.step || noop;
+ this._finish = config.finish || noop;
+ this._duration = config.duration || DEFAULT_DURATION;
+ this._currentState = config.from || this.get();
+ this._originalState = this.get();
+ this._targetState = config.to || this.get();
+
+ // Aliases used below
+ var currentState = this._currentState;
+ var targetState = this._targetState;
+
+ // Ensure that there is always something to tween to.
+ defaults(targetState, currentState);
+
+ this._easing = composeEasingObject(
+ currentState, config.easing || DEFAULT_EASING);
+
+ this._filterArgs =
+ [currentState, this._originalState, targetState, this._easing];
+
+ applyFilter(this, 'tweenCreated');
+ return this;
+ };
+
+ /**
+ * Gets the current state.
+ * @return {Object}
+ */
+ Tweenable.prototype.get = function () {
+ return shallowCopy({}, this._currentState);
+ };
+
+ /**
+ * Sets the current state.
+ * @param {Object} state
+ */
+ Tweenable.prototype.set = function (state) {
+ this._currentState = state;
+ };
+
+ /**
+ * Pauses a tween. Paused tweens can be resumed from the point at which they were paused. This is different than [`stop()`](#stop), as that method causes a tween to start over when it is resumed.
+ * @return {Tweenable}
+ */
+ Tweenable.prototype.pause = function () {
+ this._pausedAtTime = now();
+ this._isPaused = true;
+ return this;
+ };
+
+ /**
+ * Resumes a paused tween.
+ * @return {Tweenable}
+ */
+ Tweenable.prototype.resume = function () {
+ if (this._isPaused) {
+ this._timestamp += now() - this._pausedAtTime;
+ }
+
+ this._isPaused = false;
+ this._isTweening = true;
+
+ var self = this;
+ this._timeoutHandler = function () {
+ timeoutHandler(self, self._timestamp, self._duration, self._currentState,
+ self._originalState, self._targetState, self._easing, self._step,
+ self._scheduleFunction);
+ };
+
+ this._timeoutHandler();
+
+ return this;
+ };
+
+ /**
+ * Move the state of the animation to a specific point in the tween's timeline.
+ * If the animation is not running, this will cause the `step` handlers to be
+ * called.
+ * @param {millisecond} millisecond The millisecond of the animation to seek to.
+ * @return {Tweenable}
+ */
+ Tweenable.prototype.seek = function (millisecond) {
+ this._timestamp = now() - millisecond;
+
+ if (!this.isPlaying()) {
+ this._isTweening = true;
+ this._isPaused = false;
+
+ // If the animation is not running, call timeoutHandler to make sure that
+ // any step handlers are run.
+ timeoutHandler(this, this._timestamp, this._duration, this._currentState,
+ this._originalState, this._targetState, this._easing, this._step,
+ this._scheduleFunction);
+
+ this._timeoutHandler();
+ this.pause();
+ }
+
+ return this;
+ };
+
+ /**
+ * Stops and cancels a tween.
+ * @param {boolean=} gotoEnd If false or omitted, the tween just stops at its current state, and the "finish" handler is not invoked. If true, the tweened object's values are instantly set to the target values, and "finish" is invoked.
+ * @return {Tweenable}
+ */
+ Tweenable.prototype.stop = function (gotoEnd) {
+ this._isTweening = false;
+ this._isPaused = false;
+ this._timeoutHandler = noop;
+
+ (root.cancelAnimationFrame ||
+ root.webkitCancelAnimationFrame ||
+ root.oCancelAnimationFrame ||
+ root.msCancelAnimationFrame ||
+ root.mozCancelRequestAnimationFrame ||
+ root.clearTimeout)(this._scheduleId);
+
+ if (gotoEnd) {
+ shallowCopy(this._currentState, this._targetState);
+ applyFilter(this, 'afterTweenEnd');
+ this._finish.call(this, this._currentState, this._attachment);
+ }
+
+ return this;
+ };
+
+ /**
+ * Returns whether or not a tween is running.
+ * @return {boolean}
+ */
+ Tweenable.prototype.isPlaying = function () {
+ return this._isTweening && !this._isPaused;
+ };
+
+ /**
+ * Sets a custom schedule function.
+ *
+ * If a custom function is not set the default one is used [`requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window.requestAnimationFrame) if available, otherwise [`setTimeout`](https://developer.mozilla.org/en-US/docs/Web/API/Window.setTimeout)).
+ *
+ * @param {Function(Function,number)} scheduleFunction The function to be called to schedule the next frame to be rendered
+ */
+ Tweenable.prototype.setScheduleFunction = function (scheduleFunction) {
+ this._scheduleFunction = scheduleFunction;
+ };
+
+ /**
+ * `delete`s all "own" properties. Call this when the `Tweenable` instance is no longer needed to free memory.
+ */
+ Tweenable.prototype.dispose = function () {
+ var prop;
+ for (prop in this) {
+ if (this.hasOwnProperty(prop)) {
+ delete this[prop];
+ }
+ }
+ };
+
+ /*!
+ * Filters are used for transforming the properties of a tween at various
+ * points in a Tweenable's life cycle. See the README for more info on this.
+ */
+ Tweenable.prototype.filter = {};
+
+ /*!
+ * This object contains all of the tweens available to Shifty. It is extendible - simply attach properties to the Tweenable.prototype.formula Object following the same format at linear.
+ *
+ * `pos` should be a normalized `number` (between 0 and 1).
+ */
+ Tweenable.prototype.formula = {
+ linear: function (pos) {
+ return pos;
+ }
+ };
+
+ formula = Tweenable.prototype.formula;
+
+ shallowCopy(Tweenable, {
+ 'now': now
+ ,'each': each
+ ,'tweenProps': tweenProps
+ ,'tweenProp': tweenProp
+ ,'applyFilter': applyFilter
+ ,'shallowCopy': shallowCopy
+ ,'defaults': defaults
+ ,'composeEasingObject': composeEasingObject
+ });
+
+ root.Tweenable = Tweenable;
+ return Tweenable;
+
+ } ());
+
+ /*!
+ * All equations are adapted from Thomas Fuchs' [Scripty2](https://github.com/madrobby/scripty2/blob/master/src/effects/transitions/penner.js).
+ *
+ * Based on Easing Equations (c) 2003 [Robert Penner](http://www.robertpenner.com/), all rights reserved. This work is [subject to terms](http://www.robertpenner.com/easing_terms_of_use.html).
+ */
+
+ /*!
+ * TERMS OF USE - EASING EQUATIONS
+ * Open source under the BSD License.
+ * Easing Equations (c) 2003 Robert Penner, all rights reserved.
+ */
+
+ ;(function () {
+
+ Tweenable.shallowCopy(Tweenable.prototype.formula, {
+ easeInQuad: function (pos) {
+ return Math.pow(pos, 2);
+ },
+
+ easeOutQuad: function (pos) {
+ return -(Math.pow((pos - 1), 2) - 1);
+ },
+
+ easeInOutQuad: function (pos) {
+ if ((pos /= 0.5) < 1) {return 0.5 * Math.pow(pos,2);}
+ return -0.5 * ((pos -= 2) * pos - 2);
+ },
+
+ easeInCubic: function (pos) {
+ return Math.pow(pos, 3);
+ },
+
+ easeOutCubic: function (pos) {
+ return (Math.pow((pos - 1), 3) + 1);
+ },
+
+ easeInOutCubic: function (pos) {
+ if ((pos /= 0.5) < 1) {return 0.5 * Math.pow(pos,3);}
+ return 0.5 * (Math.pow((pos - 2),3) + 2);
+ },
+
+ easeInQuart: function (pos) {
+ return Math.pow(pos, 4);
+ },
+
+ easeOutQuart: function (pos) {
+ return -(Math.pow((pos - 1), 4) - 1);
+ },
+
+ easeInOutQuart: function (pos) {
+ if ((pos /= 0.5) < 1) {return 0.5 * Math.pow(pos,4);}
+ return -0.5 * ((pos -= 2) * Math.pow(pos,3) - 2);
+ },
+
+ easeInQuint: function (pos) {
+ return Math.pow(pos, 5);
+ },
+
+ easeOutQuint: function (pos) {
+ return (Math.pow((pos - 1), 5) + 1);
+ },
+
+ easeInOutQuint: function (pos) {
+ if ((pos /= 0.5) < 1) {return 0.5 * Math.pow(pos,5);}
+ return 0.5 * (Math.pow((pos - 2),5) + 2);
+ },
+
+ easeInSine: function (pos) {
+ return -Math.cos(pos * (Math.PI / 2)) + 1;
+ },
+
+ easeOutSine: function (pos) {
+ return Math.sin(pos * (Math.PI / 2));
+ },
+
+ easeInOutSine: function (pos) {
+ return (-0.5 * (Math.cos(Math.PI * pos) - 1));
+ },
+
+ easeInExpo: function (pos) {
+ return (pos === 0) ? 0 : Math.pow(2, 10 * (pos - 1));
+ },
+
+ easeOutExpo: function (pos) {
+ return (pos === 1) ? 1 : -Math.pow(2, -10 * pos) + 1;
+ },
+
+ easeInOutExpo: function (pos) {
+ if (pos === 0) {return 0;}
+ if (pos === 1) {return 1;}
+ if ((pos /= 0.5) < 1) {return 0.5 * Math.pow(2,10 * (pos - 1));}
+ return 0.5 * (-Math.pow(2, -10 * --pos) + 2);
+ },
+
+ easeInCirc: function (pos) {
+ return -(Math.sqrt(1 - (pos * pos)) - 1);
+ },
+
+ easeOutCirc: function (pos) {
+ return Math.sqrt(1 - Math.pow((pos - 1), 2));
+ },
+
+ easeInOutCirc: function (pos) {
+ if ((pos /= 0.5) < 1) {return -0.5 * (Math.sqrt(1 - pos * pos) - 1);}
+ return 0.5 * (Math.sqrt(1 - (pos -= 2) * pos) + 1);
+ },
+
+ easeOutBounce: function (pos) {
+ if ((pos) < (1 / 2.75)) {
+ return (7.5625 * pos * pos);
+ } else if (pos < (2 / 2.75)) {
+ return (7.5625 * (pos -= (1.5 / 2.75)) * pos + 0.75);
+ } else if (pos < (2.5 / 2.75)) {
+ return (7.5625 * (pos -= (2.25 / 2.75)) * pos + 0.9375);
+ } else {
+ return (7.5625 * (pos -= (2.625 / 2.75)) * pos + 0.984375);
+ }
+ },
+
+ easeInBack: function (pos) {
+ var s = 1.70158;
+ return (pos) * pos * ((s + 1) * pos - s);
+ },
+
+ easeOutBack: function (pos) {
+ var s = 1.70158;
+ return (pos = pos - 1) * pos * ((s + 1) * pos + s) + 1;
+ },
+
+ easeInOutBack: function (pos) {
+ var s = 1.70158;
+ if ((pos /= 0.5) < 1) {return 0.5 * (pos * pos * (((s *= (1.525)) + 1) * pos - s));}
+ return 0.5 * ((pos -= 2) * pos * (((s *= (1.525)) + 1) * pos + s) + 2);
+ },
+
+ elastic: function (pos) {
+ return -1 * Math.pow(4,-8 * pos) * Math.sin((pos * 6 - 1) * (2 * Math.PI) / 2) + 1;
+ },
+
+ swingFromTo: function (pos) {
+ var s = 1.70158;
+ return ((pos /= 0.5) < 1) ? 0.5 * (pos * pos * (((s *= (1.525)) + 1) * pos - s)) :
+ 0.5 * ((pos -= 2) * pos * (((s *= (1.525)) + 1) * pos + s) + 2);
+ },
+
+ swingFrom: function (pos) {
+ var s = 1.70158;
+ return pos * pos * ((s + 1) * pos - s);
+ },
+
+ swingTo: function (pos) {
+ var s = 1.70158;
+ return (pos -= 1) * pos * ((s + 1) * pos + s) + 1;
+ },
+
+ bounce: function (pos) {
+ if (pos < (1 / 2.75)) {
+ return (7.5625 * pos * pos);
+ } else if (pos < (2 / 2.75)) {
+ return (7.5625 * (pos -= (1.5 / 2.75)) * pos + 0.75);
+ } else if (pos < (2.5 / 2.75)) {
+ return (7.5625 * (pos -= (2.25 / 2.75)) * pos + 0.9375);
+ } else {
+ return (7.5625 * (pos -= (2.625 / 2.75)) * pos + 0.984375);
+ }
+ },
+
+ bouncePast: function (pos) {
+ if (pos < (1 / 2.75)) {
+ return (7.5625 * pos * pos);
+ } else if (pos < (2 / 2.75)) {
+ return 2 - (7.5625 * (pos -= (1.5 / 2.75)) * pos + 0.75);
+ } else if (pos < (2.5 / 2.75)) {
+ return 2 - (7.5625 * (pos -= (2.25 / 2.75)) * pos + 0.9375);
+ } else {
+ return 2 - (7.5625 * (pos -= (2.625 / 2.75)) * pos + 0.984375);
+ }
+ },
+
+ easeFromTo: function (pos) {
+ if ((pos /= 0.5) < 1) {return 0.5 * Math.pow(pos,4);}
+ return -0.5 * ((pos -= 2) * Math.pow(pos,3) - 2);
+ },
+
+ easeFrom: function (pos) {
+ return Math.pow(pos,4);
+ },
+
+ easeTo: function (pos) {
+ return Math.pow(pos,0.25);
+ }
+ });
+
+ }());
+
+ /*!
+ * The Bezier magic in this file is adapted/copied almost wholesale from
+ * [Scripty2](https://github.com/madrobby/scripty2/blob/master/src/effects/transitions/cubic-bezier.js),
+ * which was adapted from Apple code (which probably came from
+ * [here](http://opensource.apple.com/source/WebCore/WebCore-955.66/platform/graphics/UnitBezier.h)).
+ * Special thanks to Apple and Thomas Fuchs for much of this code.
+ */
+
+ /*!
+ * Copyright (c) 2006 Apple Computer, Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * 3. Neither the name of the copyright holder(s) nor the names of any
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+ ;(function () {
+ // port of webkit cubic bezier handling by http://www.netzgesta.de/dev/
+ function cubicBezierAtTime(t,p1x,p1y,p2x,p2y,duration) {
+ var ax = 0,bx = 0,cx = 0,ay = 0,by = 0,cy = 0;
+ function sampleCurveX(t) {return ((ax * t + bx) * t + cx) * t;}
+ function sampleCurveY(t) {return ((ay * t + by) * t + cy) * t;}
+ function sampleCurveDerivativeX(t) {return (3.0 * ax * t + 2.0 * bx) * t + cx;}
+ function solveEpsilon(duration) {return 1.0 / (200.0 * duration);}
+ function solve(x,epsilon) {return sampleCurveY(solveCurveX(x,epsilon));}
+ function fabs(n) {if (n >= 0) {return n;}else {return 0 - n;}}
+ function solveCurveX(x,epsilon) {
+ var t0,t1,t2,x2,d2,i;
+ for (t2 = x, i = 0; i < 8; i++) {x2 = sampleCurveX(t2) - x; if (fabs(x2) < epsilon) {return t2;} d2 = sampleCurveDerivativeX(t2); if (fabs(d2) < 1e-6) {break;} t2 = t2 - x2 / d2;}
+ t0 = 0.0; t1 = 1.0; t2 = x; if (t2 < t0) {return t0;} if (t2 > t1) {return t1;}
+ while (t0 < t1) {x2 = sampleCurveX(t2); if (fabs(x2 - x) < epsilon) {return t2;} if (x > x2) {t0 = t2;}else {t1 = t2;} t2 = (t1 - t0) * 0.5 + t0;}
+ return t2; // Failure.
+ }
+ cx = 3.0 * p1x; bx = 3.0 * (p2x - p1x) - cx; ax = 1.0 - cx - bx; cy = 3.0 * p1y; by = 3.0 * (p2y - p1y) - cy; ay = 1.0 - cy - by;
+ return solve(t, solveEpsilon(duration));
+ }
+ /*!
+ * getCubicBezierTransition(x1, y1, x2, y2) -> Function
+ *
+ * Generates a transition easing function that is compatible
+ * with WebKit's CSS transitions `-webkit-transition-timing-function`
+ * CSS property.
+ *
+ * The W3C has more information about
+ * <a href="http://www.w3.org/TR/css3-transitions/#transition-timing-function_tag">
+ * CSS3 transition timing functions</a>.
+ *
+ * @param {number} x1
+ * @param {number} y1
+ * @param {number} x2
+ * @param {number} y2
+ * @return {function}
+ */
+ function getCubicBezierTransition (x1, y1, x2, y2) {
+ return function (pos) {
+ return cubicBezierAtTime(pos,x1,y1,x2,y2,1);
+ };
+ }
+ // End ported code
+
+ /**
+ * Creates a Bezier easing function and attaches it to `Tweenable.prototype.formula`. This function gives you total control over the easing curve. Matthew Lein's [Ceaser](http://matthewlein.com/ceaser/) is a useful tool for visualizing the curves you can make with this function.
+ *
+ * @param {string} name The name of the easing curve. Overwrites the old easing function on Tweenable.prototype.formula if it exists.
+ * @param {number} x1
+ * @param {number} y1
+ * @param {number} x2
+ * @param {number} y2
+ * @return {function} The easing function that was attached to Tweenable.prototype.formula.
+ */
+ Tweenable.setBezierFunction = function (name, x1, y1, x2, y2) {
+ var cubicBezierTransition = getCubicBezierTransition(x1, y1, x2, y2);
+ cubicBezierTransition.x1 = x1;
+ cubicBezierTransition.y1 = y1;
+ cubicBezierTransition.x2 = x2;
+ cubicBezierTransition.y2 = y2;
+
+ return Tweenable.prototype.formula[name] = cubicBezierTransition;
+ };
+
+
+ /**
+ * `delete`s an easing function from `Tweenable.prototype.formula`. Be careful with this method, as it `delete`s whatever easing formula matches `name` (which means you can delete default Shifty easing functions).
+ *
+ * @param {string} name The name of the easing function to delete.
+ * @return {function}
+ */
+ Tweenable.unsetBezierFunction = function (name) {
+ delete Tweenable.prototype.formula[name];
+ };
+
+ })();
+
+ ;(function () {
+
+ function getInterpolatedValues (
+ from, current, targetState, position, easing) {
+ return Tweenable.tweenProps(
+ position, current, from, targetState, 1, 0, easing);
+ }
+
+ // Fake a Tweenable and patch some internals. This approach allows us to
+ // skip uneccessary processing and object recreation, cutting down on garbage
+ // collection pauses.
+ var mockTweenable = new Tweenable();
+ mockTweenable._filterArgs = [];
+
+ /**
+ * Compute the midpoint of two Objects. This method effectively calculates a specific frame of animation that [Tweenable#tween](shifty.core.js.html#tween) does many times over the course of a tween.
+ *
+ * Example:
+ *
+ * var interpolatedValues = Tweenable.interpolate({
+ * width: '100px',
+ * opacity: 0,
+ * color: '#fff'
+ * }, {
+ * width: '200px',
+ * opacity: 1,
+ * color: '#000'
+ * }, 0.5);
+ *
+ * console.log(interpolatedValues);
+ * // {opacity: 0.5, width: "150px", color: "rgb(127,127,127)"}
+ *
+ * @param {Object} from The starting values to tween from.
+ * @param {Object} targetState The ending values to tween to.
+ * @param {number} position The normalized position value (between 0.0 and 1.0) to interpolate the values between `from` and `to` for. `from` represents 0 and `to` represents `1`.
+ * @param {string|Object} easing The easing curve(s) to calculate the midpoint against. You can reference any easing function attached to `Tweenable.prototype.formula`. If omitted, this defaults to "linear".
+ * @return {Object}
+ */
+ Tweenable.interpolate = function (from, targetState, position, easing) {
+ var current = Tweenable.shallowCopy({}, from);
+ var easingObject = Tweenable.composeEasingObject(
+ from, easing || 'linear');
+
+ mockTweenable.set({});
+
+ // Alias and reuse the _filterArgs array instead of recreating it.
+ var filterArgs = mockTweenable._filterArgs;
+ filterArgs.length = 0;
+ filterArgs[0] = current;
+ filterArgs[1] = from;
+ filterArgs[2] = targetState;
+ filterArgs[3] = easingObject;
+
+ // Any defined value transformation must be applied
+ Tweenable.applyFilter(mockTweenable, 'tweenCreated');
+ Tweenable.applyFilter(mockTweenable, 'beforeTween');
+
+ var interpolatedValues = getInterpolatedValues(
+ from, current, targetState, position, easingObject);
+
+ // Transform values back into their original format
+ Tweenable.applyFilter(mockTweenable, 'afterTween');
+
+ return interpolatedValues;
+ };
+
+ }());
+
+ /**
+ * Adds string interpolation support to Shifty.
+ *
+ * The Token extension allows Shifty to tween numbers inside of strings. Among
+ * other things, this allows you to animate CSS properties. For example, you
+ * can do this:
+ *
+ * var tweenable = new Tweenable();
+ * tweenable.tween({
+ * from: { transform: 'translateX(45px)'},
+ * to: { transform: 'translateX(90xp)'}
+ * });
+ *
+ * ` `
+ * `translateX(45)` will be tweened to `translateX(90)`. To demonstrate:
+ *
+ * var tweenable = new Tweenable();
+ * tweenable.tween({
+ * from: { transform: 'translateX(45px)'},
+ * to: { transform: 'translateX(90px)'},
+ * step: function (state) {
+ * console.log(state.transform);
+ * }
+ * });
+ *
+ * ` `
+ * The above snippet will log something like this in the console:
+ *
+ * translateX(60.3px)
+ * ...
+ * translateX(76.05px)
+ * ...
+ * translateX(90px)
+ *
+ * ` `
+ * Another use for this is animating colors:
+ *
+ * var tweenable = new Tweenable();
+ * tweenable.tween({
+ * from: { color: 'rgb(0,255,0)'},
+ * to: { color: 'rgb(255,0,255)'},
+ * step: function (state) {
+ * console.log(state.color);
+ * }
+ * });
+ *
+ * ` `
+ * The above snippet will log something like this:
+ *
+ * rgb(84,170,84)
+ * ...
+ * rgb(170,84,170)
+ * ...
+ * rgb(255,0,255)
+ *
+ * ` `
+ * This extension also supports hexadecimal colors, in both long (`#ff00ff`)
+ * and short (`#f0f`) forms. Be aware that hexadecimal input values will be
+ * converted into the equivalent RGB output values. This is done to optimize
+ * for performance.
+ *
+ * var tweenable = new Tweenable();
+ * tweenable.tween({
+ * from: { color: '#0f0'},
+ * to: { color: '#f0f'},
+ * step: function (state) {
+ * console.log(state.color);
+ * }
+ * });
+ *
+ * ` `
+ * This snippet will generate the same output as the one before it because
+ * equivalent values were supplied (just in hexadecimal form rather than RGB):
+ *
+ * rgb(84,170,84)
+ * ...
+ * rgb(170,84,170)
+ * ...
+ * rgb(255,0,255)
+ *
+ * ` `
+ * ` `
+ * ## Easing support
+ *
+ * Easing works somewhat differently in the Token extension. This is because
+ * some CSS properties have multiple values in them, and you might need to
+ * tween each value along its own easing curve. A basic example:
+ *
+ * var tweenable = new Tweenable();
+ * tweenable.tween({
+ * from: { transform: 'translateX(0px) translateY(0px)'},
+ * to: { transform: 'translateX(100px) translateY(100px)'},
+ * easing: { transform: 'easeInQuad' },
+ * step: function (state) {
+ * console.log(state.transform);
+ * }
+ * });
+ *
+ * ` `
+ * The above snippet create values like this:
+ *
+ * translateX(11.560000000000002px) translateY(11.560000000000002px)
+ * ...
+ * translateX(46.24000000000001px) translateY(46.24000000000001px)
+ * ...
+ * translateX(100px) translateY(100px)
+ *
+ * ` `
+ * In this case, the values for `translateX` and `translateY` are always the
+ * same for each step of the tween, because they have the same start and end
+ * points and both use the same easing curve. We can also tween `translateX`
+ * and `translateY` along independent curves:
+ *
+ * var tweenable = new Tweenable();
+ * tweenable.tween({
+ * from: { transform: 'translateX(0px) translateY(0px)'},
+ * to: { transform: 'translateX(100px) translateY(100px)'},
+ * easing: { transform: 'easeInQuad bounce' },
+ * step: function (state) {
+ * console.log(state.transform);
+ * }
+ * });
+ *
+ * ` `
+ * The above snippet create values like this:
+ *
+ * translateX(10.89px) translateY(82.355625px)
+ * ...
+ * translateX(44.89000000000001px) translateY(86.73062500000002px)
+ * ...
+ * translateX(100px) translateY(100px)
+ *
+ * ` `
+ * `translateX` and `translateY` are not in sync anymore, because `easeInQuad`
+ * was specified for `translateX` and `bounce` for `translateY`. Mixing and
+ * matching easing curves can make for some interesting motion in your
+ * animations.
+ *
+ * The order of the space-separated easing curves correspond the token values
+ * they apply to. If there are more token values than easing curves listed,
+ * the last easing curve listed is used.
+ */
+ function token () {
+ // Functionality for this extension runs implicitly if it is loaded.
+ } /*!*/
+
+ // token function is defined above only so that dox-foundation sees it as
+ // documentation and renders it. It is never used, and is optimized away at
+ // build time.
+
+ ;(function (Tweenable) {
+
+ /*!
+ * @typedef {{
+ * formatString: string
+ * chunkNames: Array.<string>
+ * }}
+ */
+ var formatManifest;
+
+ // CONSTANTS
+
+ var R_NUMBER_COMPONENT = /(\d|\-|\.)/;
+ var R_FORMAT_CHUNKS = /([^\-0-9\.]+)/g;
+ var R_UNFORMATTED_VALUES = /[0-9.\-]+/g;
+ var R_RGB = new RegExp(
+ 'rgb\\(' + R_UNFORMATTED_VALUES.source +
+ (/,\s*/.source) + R_UNFORMATTED_VALUES.source +
+ (/,\s*/.source) + R_UNFORMATTED_VALUES.source + '\\)', 'g');
+ var R_RGB_PREFIX = /^.*\(/;
+ var R_HEX = /#([0-9]|[a-f]){3,6}/gi;
+ var VALUE_PLACEHOLDER = 'VAL';
+
+ // HELPERS
+
+ var getFormatChunksFrom_accumulator = [];
+ /*!
+ * @param {Array.number} rawValues
+ * @param {string} prefix
+ *
+ * @return {Array.<string>}
+ */
+ function getFormatChunksFrom (rawValues, prefix) {
+ getFormatChunksFrom_accumulator.length = 0;
+
+ var rawValuesLength = rawValues.length;
+ var i;
+
+ for (i = 0; i < rawValuesLength; i++) {
+ getFormatChunksFrom_accumulator.push('_' + prefix + '_' + i);
+ }
+
+ return getFormatChunksFrom_accumulator;
+ }
+
+ /*!
+ * @param {string} formattedString
+ *
+ * @return {string}
+ */
+ function getFormatStringFrom (formattedString) {
+ var chunks = formattedString.match(R_FORMAT_CHUNKS);
+
+ if (!chunks) {
+ // chunks will be null if there were no tokens to parse in
+ // formattedString (for example, if formattedString is '2'). Coerce
+ // chunks to be useful here.
+ chunks = ['', ''];
+
+ // If there is only one chunk, assume that the string is a number
+ // followed by a token...
+ // NOTE: This may be an unwise assumption.
+ } else if (chunks.length === 1 ||
+ // ...or if the string starts with a number component (".", "-", or a
+ // digit)...
+ formattedString[0].match(R_NUMBER_COMPONENT)) {
+ // ...prepend an empty string here to make sure that the formatted number
+ // is properly replaced by VALUE_PLACEHOLDER
+ chunks.unshift('');
+ }
+
+ return chunks.join(VALUE_PLACEHOLDER);
+ }
+
+ /*!
+ * Convert all hex color values within a string to an rgb string.
+ *
+ * @param {Object} stateObject
+ *
+ * @return {Object} The modified obj
+ */
+ function sanitizeObjectForHexProps (stateObject) {
+ Tweenable.each(stateObject, function (prop) {
+ var currentProp = stateObject[prop];
+
+ if (typeof currentProp === 'string' && currentProp.match(R_HEX)) {
+ stateObject[prop] = sanitizeHexChunksToRGB(currentProp);
+ }
+ });
+ }
+
+ /*!
+ * @param {string} str
+ *
+ * @return {string}
+ */
+ function sanitizeHexChunksToRGB (str) {
+ return filterStringChunks(R_HEX, str, convertHexToRGB);
+ }
+
+ /*!
+ * @param {string} hexString
+ *
+ * @return {string}
+ */
+ function convertHexToRGB (hexString) {
+ var rgbArr = hexToRGBArray(hexString);
+ return 'rgb(' + rgbArr[0] + ',' + rgbArr[1] + ',' + rgbArr[2] + ')';
+ }
+
+ var hexToRGBArray_returnArray = [];
+ /*!
+ * Convert a hexadecimal string to an array with three items, one each for
+ * the red, blue, and green decimal values.
+ *
+ * @param {string} hex A hexadecimal string.
+ *
+ * @returns {Array.<number>} The converted Array of RGB values if `hex` is a
+ * valid string, or an Array of three 0's.
+ */
+ function hexToRGBArray (hex) {
+
+ hex = hex.replace(/#/, '');
+
+ // If the string is a shorthand three digit hex notation, normalize it to
+ // the standard six digit notation
+ if (hex.length === 3) {
+ hex = hex.split('');
+ hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
+ }
+
+ hexToRGBArray_returnArray[0] = hexToDec(hex.substr(0, 2));
+ hexToRGBArray_returnArray[1] = hexToDec(hex.substr(2, 2));
+ hexToRGBArray_returnArray[2] = hexToDec(hex.substr(4, 2));
+
+ return hexToRGBArray_returnArray;
+ }
+
+ /*!
+ * Convert a base-16 number to base-10.
+ *
+ * @param {Number|String} hex The value to convert
+ *
+ * @returns {Number} The base-10 equivalent of `hex`.
+ */
+ function hexToDec (hex) {
+ return parseInt(hex, 16);
+ }
+
+ /*!
+ * Runs a filter operation on all chunks of a string that match a RegExp
+ *
+ * @param {RegExp} pattern
+ * @param {string} unfilteredString
+ * @param {function(string)} filter
+ *
+ * @return {string}
+ */
+ function filterStringChunks (pattern, unfilteredString, filter) {
+ var pattenMatches = unfilteredString.match(pattern);
+ var filteredString = unfilteredString.replace(pattern, VALUE_PLACEHOLDER);
+
+ if (pattenMatches) {
+ var pattenMatchesLength = pattenMatches.length;
+ var currentChunk;
+
+ for (var i = 0; i < pattenMatchesLength; i++) {
+ currentChunk = pattenMatches.shift();
+ filteredString = filteredString.replace(
+ VALUE_PLACEHOLDER, filter(currentChunk));
+ }
+ }
+
+ return filteredString;
+ }
+
+ /*!
+ * Check for floating point values within rgb strings and rounds them.
+ *
+ * @param {string} formattedString
+ *
+ * @return {string}
+ */
+ function sanitizeRGBChunks (formattedString) {
+ return filterStringChunks(R_RGB, formattedString, sanitizeRGBChunk);
+ }
+
+ /*!
+ * @param {string} rgbChunk
+ *
+ * @return {string}
+ */
+ function sanitizeRGBChunk (rgbChunk) {
+ var numbers = rgbChunk.match(R_UNFORMATTED_VALUES);
+ var numbersLength = numbers.length;
+ var sanitizedString = rgbChunk.match(R_RGB_PREFIX)[0];
+
+ for (var i = 0; i < numbersLength; i++) {
+ sanitizedString += parseInt(numbers[i], 10) + ',';
+ }
+
+ sanitizedString = sanitizedString.slice(0, -1) + ')';
+
+ return sanitizedString;
+ }
+
+ /*!
+ * @param {Object} stateObject
+ *
+ * @return {Object} An Object of formatManifests that correspond to
+ * the string properties of stateObject
+ */
+ function getFormatManifests (stateObject) {
+ var manifestAccumulator = {};
+
+ Tweenable.each(stateObject, function (prop) {
+ var currentProp = stateObject[prop];
+
+ if (typeof currentProp === 'string') {
+ var rawValues = getValuesFrom(currentProp);
+
+ manifestAccumulator[prop] = {
+ 'formatString': getFormatStringFrom(currentProp)
+ ,'chunkNames': getFormatChunksFrom(rawValues, prop)
+ };
+ }
+ });
+
+ return manifestAccumulator;
+ }
+
+ /*!
+ * @param {Object} stateObject
+ * @param {Object} formatManifests
+ */
+ function expandFormattedProperties (stateObject, formatManifests) {
+ Tweenable.each(formatManifests, function (prop) {
+ var currentProp = stateObject[prop];
+ var rawValues = getValuesFrom(currentProp);
+ var rawValuesLength = rawValues.length;
+
+ for (var i = 0; i < rawValuesLength; i++) {
+ stateObject[formatManifests[prop].chunkNames[i]] = +rawValues[i];
+ }
+
+ delete stateObject[prop];
+ });
+ }
+
+ /*!
+ * @param {Object} stateObject
+ * @param {Object} formatManifests
+ */
+ function collapseFormattedProperties (stateObject, formatManifests) {
+ Tweenable.each(formatManifests, function (prop) {
+ var currentProp = stateObject[prop];
+ var formatChunks = extractPropertyChunks(
+ stateObject, formatManifests[prop].chunkNames);
+ var valuesList = getValuesList(
+ formatChunks, formatManifests[prop].chunkNames);
+ currentProp = getFormattedValues(
+ formatManifests[prop].formatString, valuesList);
+ stateObject[prop] = sanitizeRGBChunks(currentProp);
+ });
+ }
+
+ /*!
+ * @param {Object} stateObject
+ * @param {Array.<string>} chunkNames
+ *
+ * @return {Object} The extracted value chunks.
+ */
+ function extractPropertyChunks (stateObject, chunkNames) {
+ var extractedValues = {};
+ var currentChunkName, chunkNamesLength = chunkNames.length;
+
+ for (var i = 0; i < chunkNamesLength; i++) {
+ currentChunkName = chunkNames[i];
+ extractedValues[currentChunkName] = stateObject[currentChunkName];
+ delete stateObject[currentChunkName];
+ }
+
+ return extractedValues;
+ }
+
+ var getValuesList_accumulator = [];
+ /*!
+ * @param {Object} stateObject
+ * @param {Array.<string>} chunkNames
+ *
+ * @return {Array.<number>}
+ */
+ function getValuesList (stateObject, chunkNames) {
+ getValuesList_accumulator.length = 0;
+ var chunkNamesLength = chunkNames.length;
+
+ for (var i = 0; i < chunkNamesLength; i++) {
+ getValuesList_accumulator.push(stateObject[chunkNames[i]]);
+ }
+
+ return getValuesList_accumulator;
+ }
+
+ /*!
+ * @param {string} formatString
+ * @param {Array.<number>} rawValues
+ *
+ * @return {string}
+ */
+ function getFormattedValues (formatString, rawValues) {
+ var formattedValueString = formatString;
+ var rawValuesLength = rawValues.length;
+
+ for (var i = 0; i < rawValuesLength; i++) {
+ formattedValueString = formattedValueString.replace(
+ VALUE_PLACEHOLDER, +rawValues[i].toFixed(4));
+ }
+
+ return formattedValueString;
+ }
+
+ /*!
+ * Note: It's the duty of the caller to convert the Array elements of the
+ * return value into numbers. This is a performance optimization.
+ *
+ * @param {string} formattedString
+ *
+ * @return {Array.<string>|null}
+ */
+ function getValuesFrom (formattedString) {
+ return formattedString.match(R_UNFORMATTED_VALUES);
+ }
+
+ /*!
+ * @param {Object} easingObject
+ * @param {Object} tokenData
+ */
+ function expandEasingObject (easingObject, tokenData) {
+ Tweenable.each(tokenData, function (prop) {
+ var currentProp = tokenData[prop];
+ var chunkNames = currentProp.chunkNames;
+ var chunkLength = chunkNames.length;
+ var easingChunks = easingObject[prop].split(' ');
+ var lastEasingChunk = easingChunks[easingChunks.length - 1];
+
+ for (var i = 0; i < chunkLength; i++) {
+ easingObject[chunkNames[i]] = easingChunks[i] || lastEasingChunk;
+ }
+
+ delete easingObject[prop];
+ });
+ }
+
+ /*!
+ * @param {Object} easingObject
+ * @param {Object} tokenData
+ */
+ function collapseEasingObject (easingObject, tokenData) {
+ Tweenable.each(tokenData, function (prop) {
+ var currentProp = tokenData[prop];
+ var chunkNames = currentProp.chunkNames;
+ var chunkLength = chunkNames.length;
+ var composedEasingString = '';
+
+ for (var i = 0; i < chunkLength; i++) {
+ composedEasingString += ' ' + easingObject[chunkNames[i]];
+ delete easingObject[chunkNames[i]];
+ }
+
+ easingObject[prop] = composedEasingString.substr(1);
+ });
+ }
+
+ Tweenable.prototype.filter.token = {
+ 'tweenCreated': function (currentState, fromState, toState, easingObject) {
+ sanitizeObjectForHexProps(currentState);
+ sanitizeObjectForHexProps(fromState);
+ sanitizeObjectForHexProps(toState);
+ this._tokenData = getFormatManifests(currentState);
+ },
+
+ 'beforeTween': function (currentState, fromState, toState, easingObject) {
+ expandEasingObject(easingObject, this._tokenData);
+ expandFormattedProperties(currentState, this._tokenData);
+ expandFormattedProperties(fromState, this._tokenData);
+ expandFormattedProperties(toState, this._tokenData);
+ },
+
+ 'afterTween': function (currentState, fromState, toState, easingObject) {
+ collapseFormattedProperties(currentState, this._tokenData);
+ collapseFormattedProperties(fromState, this._tokenData);
+ collapseFormattedProperties(toState, this._tokenData);
+ collapseEasingObject(easingObject, this._tokenData);
+ }
+ };
+
+ } (Tweenable));
+
+ }(window));
+
+ return window.Tweenable;
+});
+
+(function() {
+ "use strict";
+
+ angular.module('angular-carousel')
+
+ .filter('carouselSlice', function() {
+ return function(collection, start, size) {
+ if (angular.isArray(collection)) {
+ return collection.slice(start, start + size);
+ } else if (angular.isObject(collection)) {
+ // dont try to slice collections :)
+ return collection;
+ }
+ };
+ });
+
+})();
+/* jshint ignore:end */
diff --git a/etc/js/angular-circular-navigation.js b/etc/js/angular-circular-navigation.js
new file mode 100644
index 00000000..17488768
--- /dev/null
+++ b/etc/js/angular-circular-navigation.js
@@ -0,0 +1,70 @@
+// PP - Modified to show at right angles
+/* jshint -W041 */
+/* jslint browser: true*/
+/* global cordova,StatusBar,angular,console */
+
+(function () {
+
+ 'use strict';
+
+ /*global define, module, exports, require */
+ // options.isOpen
+
+ /* istanbul ignore next */
+ var angular = window.angular ? window.angular : 'undefined' !== typeof require ? require('angular') : undefined;
+
+ var circular = angular.module('angularCircularNavigation', [])
+ .directive('circular', ['$compile', function ($compile) {
+
+ return {
+ restrict: 'EA',
+ scope: {
+ options: '='
+ },
+ template: '<button ng-click="toggleMenu()" class="cn-button {{options.button.size}}" ng-class="options.button.cssClass" style="background: {{options.button.background ? options.button.background : options.background}}; color: {{options.button.color ? options.button.color :options.color}};">{{options.content}}</button>' +
+ '<div class="cn-wrapper {{options.size}} items-{{options.items.length}}" ng-class="{\'opened-nav\':options.isOpen}"><ul>' +
+ '<li ng-repeat="item in options.items">' +
+ '<a ng-hide="item.empty" ng-click="perform(options, item)" ng-class="{\'is-active\': item.isActive}" class="{{item.cssClass}}" style="background: {{item.background ? item.background : options.background}}; color: {{item.color ? item.color : options.color}};">' +
+ '<span>{{item.content}}</span>' +
+ '</a></li></ul></div>',
+ controller: ['$scope', '$element', '$attrs',
+ function ($scope, $element, $attrs) {
+
+ $scope.toggleMenu = function () {
+ //PP
+ if (typeof $scope.options.button.onclick === 'function')
+ {
+ // PP - console.log ("FUNCTION");
+ //$scope.options.isOpen = !$scope.options.isOpen;
+ $scope.options.button.onclick();
+ }
+ else
+ {
+ // console.log ("NO FUNCTION");
+ $scope.options.isOpen = !$scope.options.isOpen;
+ }
+ };
+
+ $scope.perform = function (options, item) {
+ if (typeof item.onclick === 'function') {
+ item.onclick(options, item);
+ }
+
+ if ($scope.options.toggleOnClick) {
+ $scope.toggleMenu();
+ }
+ };
+
+ }
+ ]
+ };
+ }]);
+
+ /* istanbul ignore next */
+ if (typeof define === 'function' && define.amd) {
+ define('circular', ['angular'], circular);
+ } else if ('undefined' !== typeof exports && 'undefined' !== typeof module) {
+ module.exports = circular;
+ }
+
+})();
diff --git a/etc/js/angular-cookies.js b/etc/js/angular-cookies.js
new file mode 100644
index 00000000..c3a9e1a5
--- /dev/null
+++ b/etc/js/angular-cookies.js
@@ -0,0 +1,330 @@
+/**
+ * @license AngularJS v1.5.11
+ * (c) 2010-2017 Google, Inc. http://angularjs.org
+ * License: MIT
+ */
+(function(window, angular) {'use strict';
+
+/**
+ * @ngdoc module
+ * @name ngCookies
+ * @description
+ *
+ * # ngCookies
+ *
+ * The `ngCookies` module provides a convenient wrapper for reading and writing browser cookies.
+ *
+ *
+ * <div doc-module-components="ngCookies"></div>
+ *
+ * See {@link ngCookies.$cookies `$cookies`} for usage.
+ */
+
+
+angular.module('ngCookies', ['ng']).
+ /**
+ * @ngdoc provider
+ * @name $cookiesProvider
+ * @description
+ * Use `$cookiesProvider` to change the default behavior of the {@link ngCookies.$cookies $cookies} service.
+ * */
+ provider('$cookies', [/** @this */function $CookiesProvider() {
+ /**
+ * @ngdoc property
+ * @name $cookiesProvider#defaults
+ * @description
+ *
+ * Object containing default options to pass when setting cookies.
+ *
+ * The object may have following properties:
+ *
+ * - **path** - `{string}` - The cookie will be available only for this path and its
+ * sub-paths. By default, this is the URL that appears in your `<base>` tag.
+ * - **domain** - `{string}` - The cookie will be available only for this domain and
+ * its sub-domains. For security reasons the user agent will not accept the cookie
+ * if the current domain is not a sub-domain of this domain or equal to it.
+ * - **expires** - `{string|Date}` - String of the form "Wdy, DD Mon YYYY HH:MM:SS GMT"
+ * or a Date object indicating the exact date/time this cookie will expire.
+ * - **secure** - `{boolean}` - If `true`, then the cookie will only be available through a
+ * secured connection.
+ *
+ * Note: By default, the address that appears in your `<base>` tag will be used as the path.
+ * This is important so that cookies will be visible for all routes when html5mode is enabled.
+ *
+ * @example
+ *
+ * ```js
+ * angular.module('cookiesProviderExample', ['ngCookies'])
+ * .config(['$cookiesProvider', function($cookiesProvider) {
+ * // Setting default options
+ * $cookiesProvider.defaults.domain = 'foo.com';
+ * $cookiesProvider.defaults.secure = true;
+ * }]);
+ * ```
+ **/
+ var defaults = this.defaults = {};
+
+ function calcOptions(options) {
+ return options ? angular.extend({}, defaults, options) : defaults;
+ }
+
+ /**
+ * @ngdoc service
+ * @name $cookies
+ *
+ * @description
+ * Provides read/write access to browser's cookies.
+ *
+ * <div class="alert alert-info">
+ * Up until Angular 1.3, `$cookies` exposed properties that represented the
+ * current browser cookie values. In version 1.4, this behavior has changed, and
+ * `$cookies` now provides a standard api of getters, setters etc.
+ * </div>
+ *
+ * Requires the {@link ngCookies `ngCookies`} module to be installed.
+ *
+ * @example
+ *
+ * ```js
+ * angular.module('cookiesExample', ['ngCookies'])
+ * .controller('ExampleController', ['$cookies', function($cookies) {
+ * // Retrieving a cookie
+ * var favoriteCookie = $cookies.get('myFavorite');
+ * // Setting a cookie
+ * $cookies.put('myFavorite', 'oatmeal');
+ * }]);
+ * ```
+ */
+ this.$get = ['$$cookieReader', '$$cookieWriter', function($$cookieReader, $$cookieWriter) {
+ return {
+ /**
+ * @ngdoc method
+ * @name $cookies#get
+ *
+ * @description
+ * Returns the value of given cookie key
+ *
+ * @param {string} key Id to use for lookup.
+ * @returns {string} Raw cookie value.
+ */
+ get: function(key) {
+ return $$cookieReader()[key];
+ },
+
+ /**
+ * @ngdoc method
+ * @name $cookies#getObject
+ *
+ * @description
+ * Returns the deserialized value of given cookie key
+ *
+ * @param {string} key Id to use for lookup.
+ * @returns {Object} Deserialized cookie value.
+ */
+ getObject: function(key) {
+ var value = this.get(key);
+ return value ? angular.fromJson(value) : value;
+ },
+
+ /**
+ * @ngdoc method
+ * @name $cookies#getAll
+ *
+ * @description
+ * Returns a key value object with all the cookies
+ *
+ * @returns {Object} All cookies
+ */
+ getAll: function() {
+ return $$cookieReader();
+ },
+
+ /**
+ * @ngdoc method
+ * @name $cookies#put
+ *
+ * @description
+ * Sets a value for given cookie key
+ *
+ * @param {string} key Id for the `value`.
+ * @param {string} value Raw value to be stored.
+ * @param {Object=} options Options object.
+ * See {@link ngCookies.$cookiesProvider#defaults $cookiesProvider.defaults}
+ */
+ put: function(key, value, options) {
+ $$cookieWriter(key, value, calcOptions(options));
+ },
+
+ /**
+ * @ngdoc method
+ * @name $cookies#putObject
+ *
+ * @description
+ * Serializes and sets a value for given cookie key
+ *
+ * @param {string} key Id for the `value`.
+ * @param {Object} value Value to be stored.
+ * @param {Object=} options Options object.
+ * See {@link ngCookies.$cookiesProvider#defaults $cookiesProvider.defaults}
+ */
+ putObject: function(key, value, options) {
+ this.put(key, angular.toJson(value), options);
+ },
+
+ /**
+ * @ngdoc method
+ * @name $cookies#remove
+ *
+ * @description
+ * Remove given cookie
+ *
+ * @param {string} key Id of the key-value pair to delete.
+ * @param {Object=} options Options object.
+ * See {@link ngCookies.$cookiesProvider#defaults $cookiesProvider.defaults}
+ */
+ remove: function(key, options) {
+ $$cookieWriter(key, undefined, calcOptions(options));
+ }
+ };
+ }];
+ }]);
+
+angular.module('ngCookies').
+/**
+ * @ngdoc service
+ * @name $cookieStore
+ * @deprecated
+ * sinceVersion="v1.4.0"
+ * Please use the {@link ngCookies.$cookies `$cookies`} service instead.
+ *
+ * @requires $cookies
+ *
+ * @description
+ * Provides a key-value (string-object) storage, that is backed by session cookies.
+ * Objects put or retrieved from this storage are automatically serialized or
+ * deserialized by angular's toJson/fromJson.
+ *
+ * Requires the {@link ngCookies `ngCookies`} module to be installed.
+ *
+ * @example
+ *
+ * ```js
+ * angular.module('cookieStoreExample', ['ngCookies'])
+ * .controller('ExampleController', ['$cookieStore', function($cookieStore) {
+ * // Put cookie
+ * $cookieStore.put('myFavorite','oatmeal');
+ * // Get cookie
+ * var favoriteCookie = $cookieStore.get('myFavorite');
+ * // Removing a cookie
+ * $cookieStore.remove('myFavorite');
+ * }]);
+ * ```
+ */
+ factory('$cookieStore', ['$cookies', function($cookies) {
+
+ return {
+ /**
+ * @ngdoc method
+ * @name $cookieStore#get
+ *
+ * @description
+ * Returns the value of given cookie key
+ *
+ * @param {string} key Id to use for lookup.
+ * @returns {Object} Deserialized cookie value, undefined if the cookie does not exist.
+ */
+ get: function(key) {
+ return $cookies.getObject(key);
+ },
+
+ /**
+ * @ngdoc method
+ * @name $cookieStore#put
+ *
+ * @description
+ * Sets a value for given cookie key
+ *
+ * @param {string} key Id for the `value`.
+ * @param {Object} value Value to be stored.
+ */
+ put: function(key, value) {
+ $cookies.putObject(key, value);
+ },
+
+ /**
+ * @ngdoc method
+ * @name $cookieStore#remove
+ *
+ * @description
+ * Remove given cookie
+ *
+ * @param {string} key Id of the key-value pair to delete.
+ */
+ remove: function(key) {
+ $cookies.remove(key);
+ }
+ };
+
+ }]);
+
+/**
+ * @name $$cookieWriter
+ * @requires $document
+ *
+ * @description
+ * This is a private service for writing cookies
+ *
+ * @param {string} name Cookie name
+ * @param {string=} value Cookie value (if undefined, cookie will be deleted)
+ * @param {Object=} options Object with options that need to be stored for the cookie.
+ */
+function $$CookieWriter($document, $log, $browser) {
+ var cookiePath = $browser.baseHref();
+ var rawDocument = $document[0];
+
+ function buildCookieString(name, value, options) {
+ var path, expires;
+ options = options || {};
+ expires = options.expires;
+ path = angular.isDefined(options.path) ? options.path : cookiePath;
+ if (angular.isUndefined(value)) {
+ expires = 'Thu, 01 Jan 1970 00:00:00 GMT';
+ value = '';
+ }
+ if (angular.isString(expires)) {
+ expires = new Date(expires);
+ }
+
+ var str = encodeURIComponent(name) + '=' + encodeURIComponent(value);
+ str += path ? ';path=' + path : '';
+ str += options.domain ? ';domain=' + options.domain : '';
+ str += expires ? ';expires=' + expires.toUTCString() : '';
+ str += options.secure ? ';secure' : '';
+
+ // per http://www.ietf.org/rfc/rfc2109.txt browser must allow at minimum:
+ // - 300 cookies
+ // - 20 cookies per unique domain
+ // - 4096 bytes per cookie
+ var cookieLength = str.length + 1;
+ if (cookieLength > 4096) {
+ $log.warn('Cookie \'' + name +
+ '\' possibly not set or overflowed because it was too large (' +
+ cookieLength + ' > 4096 bytes)!');
+ }
+
+ return str;
+ }
+
+ return function(name, value, options) {
+ rawDocument.cookie = buildCookieString(name, value, options);
+ };
+}
+
+$$CookieWriter.$inject = ['$document', '$log', '$browser'];
+
+angular.module('ngCookies').provider('$$cookieWriter', /** @this */ function $$CookieWriterProvider() {
+ this.$get = $$CookieWriter;
+});
+
+
+})(window, window.angular);
diff --git a/etc/js/angular-ios9-uiwebview.patch.js b/etc/js/angular-ios9-uiwebview.patch.js
new file mode 100644
index 00000000..c52cad82
--- /dev/null
+++ b/etc/js/angular-ios9-uiwebview.patch.js
@@ -0,0 +1,73 @@
+/**
+ * ================== angular-ios9-uiwebview.patch.js v1.1.0 ==================
+ *
+ * This patch works around iOS9 UIWebView regression that causes infinite digest
+ * errors in Angular.
+ *
+ * The patch can be applied to Angular 1.2.0 – 1.4.5. Newer versions of Angular
+ * have the workaround baked in.
+ *
+ * To apply this patch load/bundle this file with your application and add a
+ * dependency on the "ngIOS9Patch" module to your main app module.
+ *
+ * For example:
+ *
+ * ```
+ * angular.module('myApp', ['ngRoute'])`
+ * ```
+ *
+ * becomes
+ *
+ * ```
+ * angular.module('myApp', ['ngRoute', 'ngIOS9UIWebViewPatch'])
+ * ```
+ *
+ *
+ * More info:
+ * - https://openradar.appspot.com/22186109
+ * - https://github.com/angular/angular.js/issues/12241
+ * - https://github.com/driftyco/ionic/issues/4082
+ *
+ *
+ * @license AngularJS
+ * (c) 2010-2015 Google, Inc. http://angularjs.org
+ * License: MIT
+ */
+
+angular.module('ngIOS9UIWebViewPatch', ['ng']).config(function($provide) {
+ $provide.decorator('$browser', ['$delegate', '$window', function($delegate, $window) {
+
+ if (isIOS9UIWebView($window.navigator.userAgent)) {
+ return applyIOS9Shim($delegate);
+ }
+
+ return $delegate;
+
+ function isIOS9UIWebView(userAgent) {
+ return /(iPhone|iPad|iPod).* OS 9_\d/.test(userAgent) && !/Version\/9\./.test(userAgent);
+ }
+
+ function applyIOS9Shim(browser) {
+ var pendingLocationUrl = null;
+ var originalUrlFn= browser.url;
+
+ browser.url = function() {
+ if (arguments.length) {
+ pendingLocationUrl = arguments[0];
+ return originalUrlFn.apply(browser, arguments);
+ }
+
+ return pendingLocationUrl || originalUrlFn.apply(browser, arguments);
+ };
+
+ window.addEventListener('popstate', clearPendingLocationUrl, false);
+ window.addEventListener('hashchange', clearPendingLocationUrl, false);
+
+ function clearPendingLocationUrl() {
+ pendingLocationUrl = null;
+ }
+
+ return browser;
+ }
+ }]);
+});
diff --git a/etc/js/angular-sanitize.js b/etc/js/angular-sanitize.js
new file mode 100644
index 00000000..f4e25237
--- /dev/null
+++ b/etc/js/angular-sanitize.js
@@ -0,0 +1,717 @@
+/**
+ * @license AngularJS v1.5.3
+ * (c) 2010-2016 Google, Inc. http://angularjs.org
+ * License: MIT
+ */
+(function(window, angular, undefined) {'use strict';
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * Any commits to this file should be reviewed with security in mind. *
+ * Changes to this file can potentially create security vulnerabilities. *
+ * An approval from 2 Core members with history of modifying *
+ * this file is required. *
+ * *
+ * Does the change somehow allow for arbitrary javascript to be executed? *
+ * Or allows for someone to change the prototype of built-in objects? *
+ * Or gives undesired access to variables likes document or window? *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+var $sanitizeMinErr = angular.$$minErr('$sanitize');
+
+/**
+ * @ngdoc module
+ * @name ngSanitize
+ * @description
+ *
+ * # ngSanitize
+ *
+ * The `ngSanitize` module provides functionality to sanitize HTML.
+ *
+ *
+ * <div doc-module-components="ngSanitize"></div>
+ *
+ * See {@link ngSanitize.$sanitize `$sanitize`} for usage.
+ */
+
+/**
+ * @ngdoc service
+ * @name $sanitize
+ * @kind function
+ *
+ * @description
+ * Sanitizes an html string by stripping all potentially dangerous tokens.
+ *
+ * The input is sanitized by parsing the HTML into tokens. All safe tokens (from a whitelist) are
+ * then serialized back to properly escaped html string. This means that no unsafe input can make
+ * it into the returned string.
+ *
+ * The whitelist for URL sanitization of attribute values is configured using the functions
+ * `aHrefSanitizationWhitelist` and `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider
+ * `$compileProvider`}.
+ *
+ * The input may also contain SVG markup if this is enabled via {@link $sanitizeProvider}.
+ *
+ * @param {string} html HTML input.
+ * @returns {string} Sanitized HTML.
+ *
+ * @example
+ <example module="sanitizeExample" deps="angular-sanitize.js">
+ <file name="index.html">
+ <script>
+ angular.module('sanitizeExample', ['ngSanitize'])
+ .controller('ExampleController', ['$scope', '$sce', function($scope, $sce) {
+ $scope.snippet =
+ '<p style="color:blue">an html\n' +
+ '<em onmouseover="this.textContent=\'PWN3D!\'">click here</em>\n' +
+ 'snippet</p>';
+ $scope.deliberatelyTrustDangerousSnippet = function() {
+ return $sce.trustAsHtml($scope.snippet);
+ };
+ }]);
+ </script>
+ <div ng-controller="ExampleController">
+ Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea>
+ <table>
+ <tr>
+ <td>Directive</td>
+ <td>How</td>
+ <td>Source</td>
+ <td>Rendered</td>
+ </tr>
+ <tr id="bind-html-with-sanitize">
+ <td>ng-bind-html</td>
+ <td>Automatically uses $sanitize</td>
+ <td><pre>&lt;div ng-bind-html="snippet"&gt;<br/>&lt;/div&gt;</pre></td>
+ <td><div ng-bind-html="snippet"></div></td>
+ </tr>
+ <tr id="bind-html-with-trust">
+ <td>ng-bind-html</td>
+ <td>Bypass $sanitize by explicitly trusting the dangerous value</td>
+ <td>
+ <pre>&lt;div ng-bind-html="deliberatelyTrustDangerousSnippet()"&gt;
+&lt;/div&gt;</pre>
+ </td>
+ <td><div ng-bind-html="deliberatelyTrustDangerousSnippet()"></div></td>
+ </tr>
+ <tr id="bind-default">
+ <td>ng-bind</td>
+ <td>Automatically escapes</td>
+ <td><pre>&lt;div ng-bind="snippet"&gt;<br/>&lt;/div&gt;</pre></td>
+ <td><div ng-bind="snippet"></div></td>
+ </tr>
+ </table>
+ </div>
+ </file>
+ <file name="protractor.js" type="protractor">
+ it('should sanitize the html snippet by default', function() {
+ expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()).
+ toBe('<p>an html\n<em>click here</em>\nsnippet</p>');
+ });
+
+ it('should inline raw snippet if bound to a trusted value', function() {
+ expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).
+ toBe("<p style=\"color:blue\">an html\n" +
+ "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" +
+ "snippet</p>");
+ });
+
+ it('should escape snippet without any filter', function() {
+ expect(element(by.css('#bind-default div')).getInnerHtml()).
+ toBe("&lt;p style=\"color:blue\"&gt;an html\n" +
+ "&lt;em onmouseover=\"this.textContent='PWN3D!'\"&gt;click here&lt;/em&gt;\n" +
+ "snippet&lt;/p&gt;");
+ });
+
+ it('should update', function() {
+ element(by.model('snippet')).clear();
+ element(by.model('snippet')).sendKeys('new <b onclick="alert(1)">text</b>');
+ expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()).
+ toBe('new <b>text</b>');
+ expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).toBe(
+ 'new <b onclick="alert(1)">text</b>');
+ expect(element(by.css('#bind-default div')).getInnerHtml()).toBe(
+ "new &lt;b onclick=\"alert(1)\"&gt;text&lt;/b&gt;");
+ });
+ </file>
+ </example>
+ */
+
+
+/**
+ * @ngdoc provider
+ * @name $sanitizeProvider
+ *
+ * @description
+ * Creates and configures {@link $sanitize} instance.
+ */
+function $SanitizeProvider() {
+ var svgEnabled = false;
+
+ this.$get = ['$$sanitizeUri', function($$sanitizeUri) {
+ if (svgEnabled) {
+ angular.extend(validElements, svgElements);
+ }
+ return function(html) {
+ var buf = [];
+ htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) {
+ return !/^unsafe:/.test($$sanitizeUri(uri, isImage));
+ }));
+ return buf.join('');
+ };
+ }];
+
+
+ /**
+ * @ngdoc method
+ * @name $sanitizeProvider#enableSvg
+ * @kind function
+ *
+ * @description
+ * Enables a subset of svg to be supported by the sanitizer.
+ *
+ * <div class="alert alert-warning">
+ * <p>By enabling this setting without taking other precautions, you might expose your
+ * application to click-hijacking attacks. In these attacks, sanitized svg elements could be positioned
+ * outside of the containing element and be rendered over other elements on the page (e.g. a login
+ * link). Such behavior can then result in phishing incidents.</p>
+ *
+ * <p>To protect against these, explicitly setup `overflow: hidden` css rule for all potential svg
+ * tags within the sanitized content:</p>
+ *
+ * <br>
+ *
+ * <pre><code>
+ * .rootOfTheIncludedContent svg {
+ * overflow: hidden !important;
+ * }
+ * </code></pre>
+ * </div>
+ *
+ * @param {boolean=} regexp New regexp to whitelist urls with.
+ * @returns {boolean|ng.$sanitizeProvider} Returns the currently configured value if called
+ * without an argument or self for chaining otherwise.
+ */
+ this.enableSvg = function(enableSvg) {
+ if (angular.isDefined(enableSvg)) {
+ svgEnabled = enableSvg;
+ return this;
+ } else {
+ return svgEnabled;
+ }
+ };
+}
+
+function sanitizeText(chars) {
+ var buf = [];
+ var writer = htmlSanitizeWriter(buf, angular.noop);
+ writer.chars(chars);
+ return buf.join('');
+}
+
+
+// Regular Expressions for parsing tags and attributes
+var SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
+ // Match everything outside of normal chars and " (quote character)
+ NON_ALPHANUMERIC_REGEXP = /([^\#-~ |!])/g;
+
+
+// Good source of info about elements and attributes
+// http://dev.w3.org/html5/spec/Overview.html#semantics
+// http://simon.html5.org/html-elements
+
+// Safe Void Elements - HTML5
+// http://dev.w3.org/html5/spec/Overview.html#void-elements
+var voidElements = toMap("area,br,col,hr,img,wbr");
+
+// Elements that you can, intentionally, leave open (and which close themselves)
+// http://dev.w3.org/html5/spec/Overview.html#optional-tags
+var optionalEndTagBlockElements = toMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),
+ optionalEndTagInlineElements = toMap("rp,rt"),
+ optionalEndTagElements = angular.extend({},
+ optionalEndTagInlineElements,
+ optionalEndTagBlockElements);
+
+// Safe Block Elements - HTML5
+var blockElements = angular.extend({}, optionalEndTagBlockElements, toMap("address,article," +
+ "aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5," +
+ "h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,section,table,ul"));
+
+// Inline Elements - HTML5
+var inlineElements = angular.extend({}, optionalEndTagInlineElements, toMap("a,abbr,acronym,b," +
+ "bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s," +
+ "samp,small,span,strike,strong,sub,sup,time,tt,u,var"));
+
+// SVG Elements
+// https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Elements
+// Note: the elements animate,animateColor,animateMotion,animateTransform,set are intentionally omitted.
+// They can potentially allow for arbitrary javascript to be executed. See #11290
+var svgElements = toMap("circle,defs,desc,ellipse,font-face,font-face-name,font-face-src,g,glyph," +
+ "hkern,image,linearGradient,line,marker,metadata,missing-glyph,mpath,path,polygon,polyline," +
+ "radialGradient,rect,stop,svg,switch,text,title,tspan");
+
+// Blocked Elements (will be stripped)
+var blockedElements = toMap("script,style");
+
+var validElements = angular.extend({},
+ voidElements,
+ blockElements,
+ inlineElements,
+ optionalEndTagElements);
+
+//Attributes that have href and hence need to be sanitized
+var uriAttrs = toMap("background,cite,href,longdesc,src,xlink:href");
+
+var htmlAttrs = toMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,' +
+ 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,' +
+ 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,' +
+ 'scope,scrolling,shape,size,span,start,summary,tabindex,target,title,type,' +
+ 'valign,value,vspace,width');
+
+// SVG attributes (without "id" and "name" attributes)
+// https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Attributes
+var svgAttrs = toMap('accent-height,accumulate,additive,alphabetic,arabic-form,ascent,' +
+ 'baseProfile,bbox,begin,by,calcMode,cap-height,class,color,color-rendering,content,' +
+ 'cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,font-size,font-stretch,' +
+ 'font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,gradientUnits,hanging,' +
+ 'height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,keySplines,keyTimes,lang,' +
+ 'marker-end,marker-mid,marker-start,markerHeight,markerUnits,markerWidth,mathematical,' +
+ 'max,min,offset,opacity,orient,origin,overline-position,overline-thickness,panose-1,' +
+ 'path,pathLength,points,preserveAspectRatio,r,refX,refY,repeatCount,repeatDur,' +
+ 'requiredExtensions,requiredFeatures,restart,rotate,rx,ry,slope,stemh,stemv,stop-color,' +
+ 'stop-opacity,strikethrough-position,strikethrough-thickness,stroke,stroke-dasharray,' +
+ 'stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,stroke-opacity,' +
+ 'stroke-width,systemLanguage,target,text-anchor,to,transform,type,u1,u2,underline-position,' +
+ 'underline-thickness,unicode,unicode-range,units-per-em,values,version,viewBox,visibility,' +
+ 'width,widths,x,x-height,x1,x2,xlink:actuate,xlink:arcrole,xlink:role,xlink:show,xlink:title,' +
+ 'xlink:type,xml:base,xml:lang,xml:space,xmlns,xmlns:xlink,y,y1,y2,zoomAndPan', true);
+
+var validAttrs = angular.extend({},
+ uriAttrs,
+ svgAttrs,
+ htmlAttrs);
+
+function toMap(str, lowercaseKeys) {
+ var obj = {}, items = str.split(','), i;
+ for (i = 0; i < items.length; i++) {
+ obj[lowercaseKeys ? angular.lowercase(items[i]) : items[i]] = true;
+ }
+ return obj;
+}
+
+var inertBodyElement;
+(function(window) {
+ var doc;
+ if (window.document && window.document.implementation) {
+ doc = window.document.implementation.createHTMLDocument("inert");
+ } else {
+ throw $sanitizeMinErr('noinert', "Can't create an inert html document");
+ }
+ var docElement = doc.documentElement || doc.getDocumentElement();
+ var bodyElements = docElement.getElementsByTagName('body');
+
+ // usually there should be only one body element in the document, but IE doesn't have any, so we need to create one
+ if (bodyElements.length === 1) {
+ inertBodyElement = bodyElements[0];
+ } else {
+ var html = doc.createElement('html');
+ inertBodyElement = doc.createElement('body');
+ html.appendChild(inertBodyElement);
+ doc.appendChild(html);
+ }
+})(window);
+
+/**
+ * @example
+ * htmlParser(htmlString, {
+ * start: function(tag, attrs) {},
+ * end: function(tag) {},
+ * chars: function(text) {},
+ * comment: function(text) {}
+ * });
+ *
+ * @param {string} html string
+ * @param {object} handler
+ */
+function htmlParser(html, handler) {
+ if (html === null || html === undefined) {
+ html = '';
+ } else if (typeof html !== 'string') {
+ html = '' + html;
+ }
+ inertBodyElement.innerHTML = html;
+
+ //mXSS protection
+ var mXSSAttempts = 5;
+ do {
+ if (mXSSAttempts === 0) {
+ throw $sanitizeMinErr('uinput', "Failed to sanitize html because the input is unstable");
+ }
+ mXSSAttempts--;
+
+ // strip custom-namespaced attributes on IE<=11
+ if (document.documentMode <= 11) {
+ stripCustomNsAttrs(inertBodyElement);
+ }
+ html = inertBodyElement.innerHTML; //trigger mXSS
+ inertBodyElement.innerHTML = html;
+ } while (html !== inertBodyElement.innerHTML);
+
+ var node = inertBodyElement.firstChild;
+ while (node) {
+ switch (node.nodeType) {
+ case 1: // ELEMENT_NODE
+ handler.start(node.nodeName.toLowerCase(), attrToMap(node.attributes));
+ break;
+ case 3: // TEXT NODE
+ handler.chars(node.textContent);
+ break;
+ }
+
+ var nextNode;
+ if (!(nextNode = node.firstChild)) {
+ if (node.nodeType == 1) {
+ handler.end(node.nodeName.toLowerCase());
+ }
+ nextNode = node.nextSibling;
+ if (!nextNode) {
+ while (nextNode == null) {
+ node = node.parentNode;
+ if (node === inertBodyElement) break;
+ nextNode = node.nextSibling;
+ if (node.nodeType == 1) {
+ handler.end(node.nodeName.toLowerCase());
+ }
+ }
+ }
+ }
+ node = nextNode;
+ }
+
+ while (node = inertBodyElement.firstChild) {
+ inertBodyElement.removeChild(node);
+ }
+}
+
+function attrToMap(attrs) {
+ var map = {};
+ for (var i = 0, ii = attrs.length; i < ii; i++) {
+ var attr = attrs[i];
+ map[attr.name] = attr.value;
+ }
+ return map;
+}
+
+
+/**
+ * Escapes all potentially dangerous characters, so that the
+ * resulting string can be safely inserted into attribute or
+ * element text.
+ * @param value
+ * @returns {string} escaped text
+ */
+function encodeEntities(value) {
+ return value.
+ replace(/&/g, '&amp;').
+ replace(SURROGATE_PAIR_REGEXP, function(value) {
+ var hi = value.charCodeAt(0);
+ var low = value.charCodeAt(1);
+ return '&#' + (((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000) + ';';
+ }).
+ replace(NON_ALPHANUMERIC_REGEXP, function(value) {
+ return '&#' + value.charCodeAt(0) + ';';
+ }).
+ replace(/</g, '&lt;').
+ replace(/>/g, '&gt;');
+}
+
+/**
+ * create an HTML/XML writer which writes to buffer
+ * @param {Array} buf use buf.join('') to get out sanitized html string
+ * @returns {object} in the form of {
+ * start: function(tag, attrs) {},
+ * end: function(tag) {},
+ * chars: function(text) {},
+ * comment: function(text) {}
+ * }
+ */
+function htmlSanitizeWriter(buf, uriValidator) {
+ var ignoreCurrentElement = false;
+ var out = angular.bind(buf, buf.push);
+ return {
+ start: function(tag, attrs) {
+ tag = angular.lowercase(tag);
+ if (!ignoreCurrentElement && blockedElements[tag]) {
+ ignoreCurrentElement = tag;
+ }
+ if (!ignoreCurrentElement && validElements[tag] === true) {
+ out('<');
+ out(tag);
+ angular.forEach(attrs, function(value, key) {
+ var lkey=angular.lowercase(key);
+ var isImage = (tag === 'img' && lkey === 'src') || (lkey === 'background');
+ if (validAttrs[lkey] === true &&
+ (uriAttrs[lkey] !== true || uriValidator(value, isImage))) {
+ out(' ');
+ out(key);
+ out('="');
+ out(encodeEntities(value));
+ out('"');
+ }
+ });
+ out('>');
+ }
+ },
+ end: function(tag) {
+ tag = angular.lowercase(tag);
+ if (!ignoreCurrentElement && validElements[tag] === true && voidElements[tag] !== true) {
+ out('</');
+ out(tag);
+ out('>');
+ }
+ if (tag == ignoreCurrentElement) {
+ ignoreCurrentElement = false;
+ }
+ },
+ chars: function(chars) {
+ if (!ignoreCurrentElement) {
+ out(encodeEntities(chars));
+ }
+ }
+ };
+}
+
+
+/**
+ * When IE9-11 comes across an unknown namespaced attribute e.g. 'xlink:foo' it adds 'xmlns:ns1' attribute to declare
+ * ns1 namespace and prefixes the attribute with 'ns1' (e.g. 'ns1:xlink:foo'). This is undesirable since we don't want
+ * to allow any of these custom attributes. This method strips them all.
+ *
+ * @param node Root element to process
+ */
+function stripCustomNsAttrs(node) {
+ if (node.nodeType === Node.ELEMENT_NODE) {
+ var attrs = node.attributes;
+ for (var i = 0, l = attrs.length; i < l; i++) {
+ var attrNode = attrs[i];
+ var attrName = attrNode.name.toLowerCase();
+ if (attrName === 'xmlns:ns1' || attrName.indexOf('ns1:') === 0) {
+ node.removeAttributeNode(attrNode);
+ i--;
+ l--;
+ }
+ }
+ }
+
+ var nextNode = node.firstChild;
+ if (nextNode) {
+ stripCustomNsAttrs(nextNode);
+ }
+
+ nextNode = node.nextSibling;
+ if (nextNode) {
+ stripCustomNsAttrs(nextNode);
+ }
+}
+
+
+
+// define ngSanitize module and register $sanitize service
+angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider);
+
+/* global sanitizeText: false */
+
+/**
+ * @ngdoc filter
+ * @name linky
+ * @kind function
+ *
+ * @description
+ * Finds links in text input and turns them into html links. Supports `http/https/ftp/mailto` and
+ * plain email address links.
+ *
+ * Requires the {@link ngSanitize `ngSanitize`} module to be installed.
+ *
+ * @param {string} text Input text.
+ * @param {string} target Window (`_blank|_self|_parent|_top`) or named frame to open links in.
+ * @param {object|function(url)} [attributes] Add custom attributes to the link element.
+ *
+ * Can be one of:
+ *
+ * - `object`: A map of attributes
+ * - `function`: Takes the url as a parameter and returns a map of attributes
+ *
+ * If the map of attributes contains a value for `target`, it overrides the value of
+ * the target parameter.
+ *
+ *
+ * @returns {string} Html-linkified and {@link $sanitize sanitized} text.
+ *
+ * @usage
+ <span ng-bind-html="linky_expression | linky"></span>
+ *
+ * @example
+ <example module="linkyExample" deps="angular-sanitize.js">
+ <file name="index.html">
+ <div ng-controller="ExampleController">
+ Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea>
+ <table>
+ <tr>
+ <th>Filter</th>
+ <th>Source</th>
+ <th>Rendered</th>
+ </tr>
+ <tr id="linky-filter">
+ <td>linky filter</td>
+ <td>
+ <pre>&lt;div ng-bind-html="snippet | linky"&gt;<br>&lt;/div&gt;</pre>
+ </td>
+ <td>
+ <div ng-bind-html="snippet | linky"></div>
+ </td>
+ </tr>
+ <tr id="linky-target">
+ <td>linky target</td>
+ <td>
+ <pre>&lt;div ng-bind-html="snippetWithSingleURL | linky:'_blank'"&gt;<br>&lt;/div&gt;</pre>
+ </td>
+ <td>
+ <div ng-bind-html="snippetWithSingleURL | linky:'_blank'"></div>
+ </td>
+ </tr>
+ <tr id="linky-custom-attributes">
+ <td>linky custom attributes</td>
+ <td>
+ <pre>&lt;div ng-bind-html="snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}"&gt;<br>&lt;/div&gt;</pre>
+ </td>
+ <td>
+ <div ng-bind-html="snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}"></div>
+ </td>
+ </tr>
+ <tr id="escaped-html">
+ <td>no filter</td>
+ <td><pre>&lt;div ng-bind="snippet"&gt;<br>&lt;/div&gt;</pre></td>
+ <td><div ng-bind="snippet"></div></td>
+ </tr>
+ </table>
+ </file>
+ <file name="script.js">
+ angular.module('linkyExample', ['ngSanitize'])
+ .controller('ExampleController', ['$scope', function($scope) {
+ $scope.snippet =
+ 'Pretty text with some links:\n'+
+ 'http://angularjs.org/,\n'+
+ 'mailto:us@somewhere.org,\n'+
+ 'another@somewhere.org,\n'+
+ 'and one more: ftp://127.0.0.1/.';
+ $scope.snippetWithSingleURL = 'http://angularjs.org/';
+ }]);
+ </file>
+ <file name="protractor.js" type="protractor">
+ it('should linkify the snippet with urls', function() {
+ expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
+ toBe('Pretty text with some links: http://angularjs.org/, us@somewhere.org, ' +
+ 'another@somewhere.org, and one more: ftp://127.0.0.1/.');
+ expect(element.all(by.css('#linky-filter a')).count()).toEqual(4);
+ });
+
+ it('should not linkify snippet without the linky filter', function() {
+ expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()).
+ toBe('Pretty text with some links: http://angularjs.org/, mailto:us@somewhere.org, ' +
+ 'another@somewhere.org, and one more: ftp://127.0.0.1/.');
+ expect(element.all(by.css('#escaped-html a')).count()).toEqual(0);
+ });
+
+ it('should update', function() {
+ element(by.model('snippet')).clear();
+ element(by.model('snippet')).sendKeys('new http://link.');
+ expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
+ toBe('new http://link.');
+ expect(element.all(by.css('#linky-filter a')).count()).toEqual(1);
+ expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText())
+ .toBe('new http://link.');
+ });
+
+ it('should work with the target property', function() {
+ expect(element(by.id('linky-target')).
+ element(by.binding("snippetWithSingleURL | linky:'_blank'")).getText()).
+ toBe('http://angularjs.org/');
+ expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank');
+ });
+
+ it('should optionally add custom attributes', function() {
+ expect(element(by.id('linky-custom-attributes')).
+ element(by.binding("snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}")).getText()).
+ toBe('http://angularjs.org/');
+ expect(element(by.css('#linky-custom-attributes a')).getAttribute('rel')).toEqual('nofollow');
+ });
+ </file>
+ </example>
+ */
+angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) {
+ var LINKY_URL_REGEXP =
+ /((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"\u201d\u2019]/i,
+ MAILTO_REGEXP = /^mailto:/i;
+
+ var linkyMinErr = angular.$$minErr('linky');
+ var isString = angular.isString;
+
+ return function(text, target, attributes) {
+ if (text == null || text === '') return text;
+ if (!isString(text)) throw linkyMinErr('notstring', 'Expected string but received: {0}', text);
+
+ var match;
+ var raw = text;
+ var html = [];
+ var url;
+ var i;
+ while ((match = raw.match(LINKY_URL_REGEXP))) {
+ // We can not end in these as they are sometimes found at the end of the sentence
+ url = match[0];
+ // if we did not match ftp/http/www/mailto then assume mailto
+ if (!match[2] && !match[4]) {
+ url = (match[3] ? 'http://' : 'mailto:') + url;
+ }
+ i = match.index;
+ addText(raw.substr(0, i));
+ addLink(url, match[0].replace(MAILTO_REGEXP, ''));
+ raw = raw.substring(i + match[0].length);
+ }
+ addText(raw);
+ return $sanitize(html.join(''));
+
+ function addText(text) {
+ if (!text) {
+ return;
+ }
+ html.push(sanitizeText(text));
+ }
+
+ function addLink(url, text) {
+ var key;
+ html.push('<a ');
+ if (angular.isFunction(attributes)) {
+ attributes = attributes(url);
+ }
+ if (angular.isObject(attributes)) {
+ for (key in attributes) {
+ html.push(key + '="' + attributes[key] + '" ');
+ }
+ } else {
+ attributes = {};
+ }
+ if (angular.isDefined(target) && !('target' in attributes)) {
+ html.push('target="',
+ target,
+ '" ');
+ }
+ html.push('href="',
+ url.replace(/"/g, '&quot;'),
+ '">');
+ addText(text);
+ html.push('</a>');
+ }
+ };
+}]);
+
+
+})(window, window.angular);
diff --git a/etc/js/angular-touch.js b/etc/js/angular-touch.js
new file mode 100644
index 00000000..79b8d5ec
--- /dev/null
+++ b/etc/js/angular-touch.js
@@ -0,0 +1,735 @@
+/**
+ * @license AngularJS v1.5.8
+ * (c) 2010-2016 Google, Inc. http://angularjs.org
+ * License: MIT
+ */
+(function(window, angular) {'use strict';
+
+/* global ngTouchClickDirectiveFactory: false,
+ */
+
+/**
+ * @ngdoc module
+ * @name ngTouch
+ * @description
+ *
+ * # ngTouch
+ *
+ * The `ngTouch` module provides touch events and other helpers for touch-enabled devices.
+ * The implementation is based on jQuery Mobile touch event handling
+ * ([jquerymobile.com](http://jquerymobile.com/)).
+ *
+ *
+ * See {@link ngTouch.$swipe `$swipe`} for usage.
+ *
+ * <div doc-module-components="ngTouch"></div>
+ *
+ */
+
+// define ngTouch module
+/* global -ngTouch */
+var ngTouch = angular.module('ngTouch', []);
+
+ngTouch.provider('$touch', $TouchProvider);
+
+function nodeName_(element) {
+ return angular.lowercase(element.nodeName || (element[0] && element[0].nodeName));
+}
+
+/**
+ * @ngdoc provider
+ * @name $touchProvider
+ *
+ * @description
+ * The `$touchProvider` allows enabling / disabling {@link ngTouch.ngClick ngTouch's ngClick directive}.
+ */
+$TouchProvider.$inject = ['$provide', '$compileProvider'];
+function $TouchProvider($provide, $compileProvider) {
+
+ /**
+ * @ngdoc method
+ * @name $touchProvider#ngClickOverrideEnabled
+ *
+ * @param {boolean=} enabled update the ngClickOverrideEnabled state if provided, otherwise just return the
+ * current ngClickOverrideEnabled state
+ * @returns {*} current value if used as getter or itself (chaining) if used as setter
+ *
+ * @kind function
+ *
+ * @description
+ * Call this method to enable/disable {@link ngTouch.ngClick ngTouch's ngClick directive}. If enabled,
+ * the default ngClick directive will be replaced by a version that eliminates the 300ms delay for
+ * click events on browser for touch-devices.
+ *
+ * The default is `false`.
+ *
+ */
+ var ngClickOverrideEnabled = false;
+ var ngClickDirectiveAdded = false;
+ this.ngClickOverrideEnabled = function(enabled) {
+ if (angular.isDefined(enabled)) {
+
+ if (enabled && !ngClickDirectiveAdded) {
+ ngClickDirectiveAdded = true;
+
+ // Use this to identify the correct directive in the delegate
+ ngTouchClickDirectiveFactory.$$moduleName = 'ngTouch';
+ $compileProvider.directive('ngClick', ngTouchClickDirectiveFactory);
+
+ $provide.decorator('ngClickDirective', ['$delegate', function($delegate) {
+ if (ngClickOverrideEnabled) {
+ // drop the default ngClick directive
+ $delegate.shift();
+ } else {
+ // drop the ngTouch ngClick directive if the override has been re-disabled (because
+ // we cannot de-register added directives)
+ var i = $delegate.length - 1;
+ while (i >= 0) {
+ if ($delegate[i].$$moduleName === 'ngTouch') {
+ $delegate.splice(i, 1);
+ break;
+ }
+ i--;
+ }
+ }
+
+ return $delegate;
+ }]);
+ }
+
+ ngClickOverrideEnabled = enabled;
+ return this;
+ }
+
+ return ngClickOverrideEnabled;
+ };
+
+ /**
+ * @ngdoc service
+ * @name $touch
+ * @kind object
+ *
+ * @description
+ * Provides the {@link ngTouch.$touch#ngClickOverrideEnabled `ngClickOverrideEnabled`} method.
+ *
+ */
+ this.$get = function() {
+ return {
+ /**
+ * @ngdoc method
+ * @name $touch#ngClickOverrideEnabled
+ *
+ * @returns {*} current value of `ngClickOverrideEnabled` set in the {@link ngTouch.$touchProvider $touchProvider},
+ * i.e. if {@link ngTouch.ngClick ngTouch's ngClick} directive is enabled.
+ *
+ * @kind function
+ */
+ ngClickOverrideEnabled: function() {
+ return ngClickOverrideEnabled;
+ }
+ };
+ };
+
+}
+
+/* global ngTouch: false */
+
+ /**
+ * @ngdoc service
+ * @name $swipe
+ *
+ * @description
+ * The `$swipe` service is a service that abstracts the messier details of hold-and-drag swipe
+ * behavior, to make implementing swipe-related directives more convenient.
+ *
+ * Requires the {@link ngTouch `ngTouch`} module to be installed.
+ *
+ * `$swipe` is used by the `ngSwipeLeft` and `ngSwipeRight` directives in `ngTouch`.
+ *
+ * # Usage
+ * The `$swipe` service is an object with a single method: `bind`. `bind` takes an element
+ * which is to be watched for swipes, and an object with four handler functions. See the
+ * documentation for `bind` below.
+ */
+
+ngTouch.factory('$swipe', [function() {
+ // The total distance in any direction before we make the call on swipe vs. scroll.
+ var MOVE_BUFFER_RADIUS = 10;
+
+ var POINTER_EVENTS = {
+ 'mouse': {
+ start: 'mousedown',
+ move: 'mousemove',
+ end: 'mouseup'
+ },
+ 'touch': {
+ start: 'touchstart',
+ move: 'touchmove',
+ end: 'touchend',
+ cancel: 'touchcancel'
+ },
+ 'pointer': {
+ start: 'pointerdown',
+ move: 'pointermove',
+ end: 'pointerup',
+ cancel: 'pointercancel'
+ }
+ };
+
+ function getCoordinates(event) {
+ var originalEvent = event.originalEvent || event;
+ var touches = originalEvent.touches && originalEvent.touches.length ? originalEvent.touches : [originalEvent];
+ var e = (originalEvent.changedTouches && originalEvent.changedTouches[0]) || touches[0];
+
+ return {
+ x: e.clientX,
+ y: e.clientY
+ };
+ }
+
+ function getEvents(pointerTypes, eventType) {
+ var res = [];
+ angular.forEach(pointerTypes, function(pointerType) {
+ var eventName = POINTER_EVENTS[pointerType][eventType];
+ if (eventName) {
+ res.push(eventName);
+ }
+ });
+ return res.join(' ');
+ }
+
+ return {
+ /**
+ * @ngdoc method
+ * @name $swipe#bind
+ *
+ * @description
+ * The main method of `$swipe`. It takes an element to be watched for swipe motions, and an
+ * object containing event handlers.
+ * The pointer types that should be used can be specified via the optional
+ * third argument, which is an array of strings `'mouse'`, `'touch'` and `'pointer'`. By default,
+ * `$swipe` will listen for `mouse`, `touch` and `pointer` events.
+ *
+ * The four events are `start`, `move`, `end`, and `cancel`. `start`, `move`, and `end`
+ * receive as a parameter a coordinates object of the form `{ x: 150, y: 310 }` and the raw
+ * `event`. `cancel` receives the raw `event` as its single parameter.
+ *
+ * `start` is called on either `mousedown`, `touchstart` or `pointerdown`. After this event, `$swipe` is
+ * watching for `touchmove`, `mousemove` or `pointermove` events. These events are ignored until the total
+ * distance moved in either dimension exceeds a small threshold.
+ *
+ * Once this threshold is exceeded, either the horizontal or vertical delta is greater.
+ * - If the horizontal distance is greater, this is a swipe and `move` and `end` events follow.
+ * - If the vertical distance is greater, this is a scroll, and we let the browser take over.
+ * A `cancel` event is sent.
+ *
+ * `move` is called on `mousemove`, `touchmove` and `pointermove` after the above logic has determined that
+ * a swipe is in progress.
+ *
+ * `end` is called when a swipe is successfully completed with a `touchend`, `mouseup` or `pointerup`.
+ *
+ * `cancel` is called either on a `touchcancel` or `pointercancel` from the browser, or when we begin scrolling
+ * as described above.
+ *
+ */
+ bind: function(element, eventHandlers, pointerTypes) {
+ // Absolute total movement, used to control swipe vs. scroll.
+ var totalX, totalY;
+ // Coordinates of the start position.
+ var startCoords;
+ // Last event's position.
+ var lastPos;
+ // Whether a swipe is active.
+ var active = false;
+
+ pointerTypes = pointerTypes || ['mouse', 'touch', 'pointer'];
+ element.on(getEvents(pointerTypes, 'start'), function(event) {
+ startCoords = getCoordinates(event);
+ active = true;
+ totalX = 0;
+ totalY = 0;
+ lastPos = startCoords;
+ eventHandlers['start'] && eventHandlers['start'](startCoords, event);
+ });
+ var events = getEvents(pointerTypes, 'cancel');
+ if (events) {
+ element.on(events, function(event) {
+ active = false;
+ eventHandlers['cancel'] && eventHandlers['cancel'](event);
+ });
+ }
+
+ element.on(getEvents(pointerTypes, 'move'), function(event) {
+ if (!active) return;
+
+ // Android will send a touchcancel if it thinks we're starting to scroll.
+ // So when the total distance (+ or - or both) exceeds 10px in either direction,
+ // we either:
+ // - On totalX > totalY, we send preventDefault() and treat this as a swipe.
+ // - On totalY > totalX, we let the browser handle it as a scroll.
+
+ if (!startCoords) return;
+ var coords = getCoordinates(event);
+
+ totalX += Math.abs(coords.x - lastPos.x);
+ totalY += Math.abs(coords.y - lastPos.y);
+
+ lastPos = coords;
+
+ if (totalX < MOVE_BUFFER_RADIUS && totalY < MOVE_BUFFER_RADIUS) {
+ return;
+ }
+
+ // One of totalX or totalY has exceeded the buffer, so decide on swipe vs. scroll.
+ if (totalY > totalX) {
+ // Allow native scrolling to take over.
+ active = false;
+ eventHandlers['cancel'] && eventHandlers['cancel'](event);
+ return;
+ } else {
+ // Prevent the browser from scrolling.
+ event.preventDefault();
+ eventHandlers['move'] && eventHandlers['move'](coords, event);
+ }
+ });
+
+ element.on(getEvents(pointerTypes, 'end'), function(event) {
+ if (!active) return;
+ active = false;
+ eventHandlers['end'] && eventHandlers['end'](getCoordinates(event), event);
+ });
+ }
+ };
+}]);
+
+/* global ngTouch: false,
+ nodeName_: false
+*/
+
+/**
+ * @ngdoc directive
+ * @name ngClick
+ * @deprecated
+ *
+ * @description
+ * <div class="alert alert-danger">
+ * **DEPRECATION NOTICE**: Beginning with Angular 1.5, this directive is deprecated and by default **disabled**.
+ * The directive will receive no further support and might be removed from future releases.
+ * If you need the directive, you can enable it with the {@link ngTouch.$touchProvider $touchProvider#ngClickOverrideEnabled}
+ * function. We also recommend that you migrate to [FastClick](https://github.com/ftlabs/fastclick).
+ * To learn more about the 300ms delay, this [Telerik article](http://developer.telerik.com/featured/300-ms-click-delay-ios-8/)
+ * gives a good overview.
+ * </div>
+ * A more powerful replacement for the default ngClick designed to be used on touchscreen
+ * devices. Most mobile browsers wait about 300ms after a tap-and-release before sending
+ * the click event. This version handles them immediately, and then prevents the
+ * following click event from propagating.
+ *
+ * Requires the {@link ngTouch `ngTouch`} module to be installed.
+ *
+ * This directive can fall back to using an ordinary click event, and so works on desktop
+ * browsers as well as mobile.
+ *
+ * This directive also sets the CSS class `ng-click-active` while the element is being held
+ * down (by a mouse click or touch) so you can restyle the depressed element if you wish.
+ *
+ * @element ANY
+ * @param {expression} ngClick {@link guide/expression Expression} to evaluate
+ * upon tap. (Event object is available as `$event`)
+ *
+ * @example
+ <example module="ngClickExample" deps="angular-touch.js">
+ <file name="index.html">
+ <button ng-click="count = count + 1" ng-init="count=0">
+ Increment
+ </button>
+ count: {{ count }}
+ </file>
+ <file name="script.js">
+ angular.module('ngClickExample', ['ngTouch']);
+ </file>
+ </example>
+ */
+
+var ngTouchClickDirectiveFactory = ['$parse', '$timeout', '$rootElement',
+ function($parse, $timeout, $rootElement) {
+ var TAP_DURATION = 750; // Shorter than 750ms is a tap, longer is a taphold or drag.
+ var MOVE_TOLERANCE = 12; // 12px seems to work in most mobile browsers.
+ var PREVENT_DURATION = 2500; // 2.5 seconds maximum from preventGhostClick call to click
+ var CLICKBUSTER_THRESHOLD = 25; // 25 pixels in any dimension is the limit for busting clicks.
+
+ var ACTIVE_CLASS_NAME = 'ng-click-active';
+ var lastPreventedTime;
+ var touchCoordinates;
+ var lastLabelClickCoordinates;
+
+
+ // TAP EVENTS AND GHOST CLICKS
+ //
+ // Why tap events?
+ // Mobile browsers detect a tap, then wait a moment (usually ~300ms) to see if you're
+ // double-tapping, and then fire a click event.
+ //
+ // This delay sucks and makes mobile apps feel unresponsive.
+ // So we detect touchstart, touchcancel and touchend ourselves and determine when
+ // the user has tapped on something.
+ //
+ // What happens when the browser then generates a click event?
+ // The browser, of course, also detects the tap and fires a click after a delay. This results in
+ // tapping/clicking twice. We do "clickbusting" to prevent it.
+ //
+ // How does it work?
+ // We attach global touchstart and click handlers, that run during the capture (early) phase.
+ // So the sequence for a tap is:
+ // - global touchstart: Sets an "allowable region" at the point touched.
+ // - element's touchstart: Starts a touch
+ // (- touchcancel ends the touch, no click follows)
+ // - element's touchend: Determines if the tap is valid (didn't move too far away, didn't hold
+ // too long) and fires the user's tap handler. The touchend also calls preventGhostClick().
+ // - preventGhostClick() removes the allowable region the global touchstart created.
+ // - The browser generates a click event.
+ // - The global click handler catches the click, and checks whether it was in an allowable region.
+ // - If preventGhostClick was called, the region will have been removed, the click is busted.
+ // - If the region is still there, the click proceeds normally. Therefore clicks on links and
+ // other elements without ngTap on them work normally.
+ //
+ // This is an ugly, terrible hack!
+ // Yeah, tell me about it. The alternatives are using the slow click events, or making our users
+ // deal with the ghost clicks, so I consider this the least of evils. Fortunately Angular
+ // encapsulates this ugly logic away from the user.
+ //
+ // Why not just put click handlers on the element?
+ // We do that too, just to be sure. If the tap event caused the DOM to change,
+ // it is possible another element is now in that position. To take account for these possibly
+ // distinct elements, the handlers are global and care only about coordinates.
+
+ // Checks if the coordinates are close enough to be within the region.
+ function hit(x1, y1, x2, y2) {
+ return Math.abs(x1 - x2) < CLICKBUSTER_THRESHOLD && Math.abs(y1 - y2) < CLICKBUSTER_THRESHOLD;
+ }
+
+ // Checks a list of allowable regions against a click location.
+ // Returns true if the click should be allowed.
+ // Splices out the allowable region from the list after it has been used.
+ function checkAllowableRegions(touchCoordinates, x, y) {
+ for (var i = 0; i < touchCoordinates.length; i += 2) {
+ if (hit(touchCoordinates[i], touchCoordinates[i + 1], x, y)) {
+ touchCoordinates.splice(i, i + 2);
+ return true; // allowable region
+ }
+ }
+ return false; // No allowable region; bust it.
+ }
+
+ // Global click handler that prevents the click if it's in a bustable zone and preventGhostClick
+ // was called recently.
+ function onClick(event) {
+ if (Date.now() - lastPreventedTime > PREVENT_DURATION) {
+ return; // Too old.
+ }
+
+ var touches = event.touches && event.touches.length ? event.touches : [event];
+ var x = touches[0].clientX;
+ var y = touches[0].clientY;
+ // Work around desktop Webkit quirk where clicking a label will fire two clicks (on the label
+ // and on the input element). Depending on the exact browser, this second click we don't want
+ // to bust has either (0,0), negative coordinates, or coordinates equal to triggering label
+ // click event
+ if (x < 1 && y < 1) {
+ return; // offscreen
+ }
+ if (lastLabelClickCoordinates &&
+ lastLabelClickCoordinates[0] === x && lastLabelClickCoordinates[1] === y) {
+ return; // input click triggered by label click
+ }
+ // reset label click coordinates on first subsequent click
+ if (lastLabelClickCoordinates) {
+ lastLabelClickCoordinates = null;
+ }
+ // remember label click coordinates to prevent click busting of trigger click event on input
+ if (nodeName_(event.target) === 'label') {
+ lastLabelClickCoordinates = [x, y];
+ }
+
+ // Look for an allowable region containing this click.
+ // If we find one, that means it was created by touchstart and not removed by
+ // preventGhostClick, so we don't bust it.
+ if (checkAllowableRegions(touchCoordinates, x, y)) {
+ return;
+ }
+
+ // If we didn't find an allowable region, bust the click.
+ event.stopPropagation();
+ event.preventDefault();
+
+ // Blur focused form elements
+ event.target && event.target.blur && event.target.blur();
+ }
+
+
+ // Global touchstart handler that creates an allowable region for a click event.
+ // This allowable region can be removed by preventGhostClick if we want to bust it.
+ function onTouchStart(event) {
+ var touches = event.touches && event.touches.length ? event.touches : [event];
+ var x = touches[0].clientX;
+ var y = touches[0].clientY;
+ touchCoordinates.push(x, y);
+
+ $timeout(function() {
+ // Remove the allowable region.
+ for (var i = 0; i < touchCoordinates.length; i += 2) {
+ if (touchCoordinates[i] == x && touchCoordinates[i + 1] == y) {
+ touchCoordinates.splice(i, i + 2);
+ return;
+ }
+ }
+ }, PREVENT_DURATION, false);
+ }
+
+ // On the first call, attaches some event handlers. Then whenever it gets called, it creates a
+ // zone around the touchstart where clicks will get busted.
+ function preventGhostClick(x, y) {
+ if (!touchCoordinates) {
+ $rootElement[0].addEventListener('click', onClick, true);
+ $rootElement[0].addEventListener('touchstart', onTouchStart, true);
+ touchCoordinates = [];
+ }
+
+ lastPreventedTime = Date.now();
+
+ checkAllowableRegions(touchCoordinates, x, y);
+ }
+
+ // Actual linking function.
+ return function(scope, element, attr) {
+ var clickHandler = $parse(attr.ngClick),
+ tapping = false,
+ tapElement, // Used to blur the element after a tap.
+ startTime, // Used to check if the tap was held too long.
+ touchStartX,
+ touchStartY;
+
+ function resetState() {
+ tapping = false;
+ element.removeClass(ACTIVE_CLASS_NAME);
+ }
+
+ element.on('touchstart', function(event) {
+ tapping = true;
+ tapElement = event.target ? event.target : event.srcElement; // IE uses srcElement.
+ // Hack for Safari, which can target text nodes instead of containers.
+ if (tapElement.nodeType == 3) {
+ tapElement = tapElement.parentNode;
+ }
+
+ element.addClass(ACTIVE_CLASS_NAME);
+
+ startTime = Date.now();
+
+ // Use jQuery originalEvent
+ var originalEvent = event.originalEvent || event;
+ var touches = originalEvent.touches && originalEvent.touches.length ? originalEvent.touches : [originalEvent];
+ var e = touches[0];
+ touchStartX = e.clientX;
+ touchStartY = e.clientY;
+ });
+
+ element.on('touchcancel', function(event) {
+ resetState();
+ });
+
+ element.on('touchend', function(event) {
+ var diff = Date.now() - startTime;
+
+ // Use jQuery originalEvent
+ var originalEvent = event.originalEvent || event;
+ var touches = (originalEvent.changedTouches && originalEvent.changedTouches.length) ?
+ originalEvent.changedTouches :
+ ((originalEvent.touches && originalEvent.touches.length) ? originalEvent.touches : [originalEvent]);
+ var e = touches[0];
+ var x = e.clientX;
+ var y = e.clientY;
+ var dist = Math.sqrt(Math.pow(x - touchStartX, 2) + Math.pow(y - touchStartY, 2));
+
+ if (tapping && diff < TAP_DURATION && dist < MOVE_TOLERANCE) {
+ // Call preventGhostClick so the clickbuster will catch the corresponding click.
+ preventGhostClick(x, y);
+
+ // Blur the focused element (the button, probably) before firing the callback.
+ // This doesn't work perfectly on Android Chrome, but seems to work elsewhere.
+ // I couldn't get anything to work reliably on Android Chrome.
+ if (tapElement) {
+ tapElement.blur();
+ }
+
+ if (!angular.isDefined(attr.disabled) || attr.disabled === false) {
+ element.triggerHandler('click', [event]);
+ }
+ }
+
+ resetState();
+ });
+
+ // Hack for iOS Safari's benefit. It goes searching for onclick handlers and is liable to click
+ // something else nearby.
+ element.onclick = function(event) { };
+
+ // Actual click handler.
+ // There are three different kinds of clicks, only two of which reach this point.
+ // - On desktop browsers without touch events, their clicks will always come here.
+ // - On mobile browsers, the simulated "fast" click will call this.
+ // - But the browser's follow-up slow click will be "busted" before it reaches this handler.
+ // Therefore it's safe to use this directive on both mobile and desktop.
+ element.on('click', function(event, touchend) {
+ scope.$apply(function() {
+ clickHandler(scope, {$event: (touchend || event)});
+ });
+ });
+
+ element.on('mousedown', function(event) {
+ element.addClass(ACTIVE_CLASS_NAME);
+ });
+
+ element.on('mousemove mouseup', function(event) {
+ element.removeClass(ACTIVE_CLASS_NAME);
+ });
+
+ };
+}];
+
+/* global ngTouch: false */
+
+/**
+ * @ngdoc directive
+ * @name ngSwipeLeft
+ *
+ * @description
+ * Specify custom behavior when an element is swiped to the left on a touchscreen device.
+ * A leftward swipe is a quick, right-to-left slide of the finger.
+ * Though ngSwipeLeft is designed for touch-based devices, it will work with a mouse click and drag
+ * too.
+ *
+ * To disable the mouse click and drag functionality, add `ng-swipe-disable-mouse` to
+ * the `ng-swipe-left` or `ng-swipe-right` DOM Element.
+ *
+ * Requires the {@link ngTouch `ngTouch`} module to be installed.
+ *
+ * @element ANY
+ * @param {expression} ngSwipeLeft {@link guide/expression Expression} to evaluate
+ * upon left swipe. (Event object is available as `$event`)
+ *
+ * @example
+ <example module="ngSwipeLeftExample" deps="angular-touch.js">
+ <file name="index.html">
+ <div ng-show="!showActions" ng-swipe-left="showActions = true">
+ Some list content, like an email in the inbox
+ </div>
+ <div ng-show="showActions" ng-swipe-right="showActions = false">
+ <button ng-click="reply()">Reply</button>
+ <button ng-click="delete()">Delete</button>
+ </div>
+ </file>
+ <file name="script.js">
+ angular.module('ngSwipeLeftExample', ['ngTouch']);
+ </file>
+ </example>
+ */
+
+/**
+ * @ngdoc directive
+ * @name ngSwipeRight
+ *
+ * @description
+ * Specify custom behavior when an element is swiped to the right on a touchscreen device.
+ * A rightward swipe is a quick, left-to-right slide of the finger.
+ * Though ngSwipeRight is designed for touch-based devices, it will work with a mouse click and drag
+ * too.
+ *
+ * Requires the {@link ngTouch `ngTouch`} module to be installed.
+ *
+ * @element ANY
+ * @param {expression} ngSwipeRight {@link guide/expression Expression} to evaluate
+ * upon right swipe. (Event object is available as `$event`)
+ *
+ * @example
+ <example module="ngSwipeRightExample" deps="angular-touch.js">
+ <file name="index.html">
+ <div ng-show="!showActions" ng-swipe-left="showActions = true">
+ Some list content, like an email in the inbox
+ </div>
+ <div ng-show="showActions" ng-swipe-right="showActions = false">
+ <button ng-click="reply()">Reply</button>
+ <button ng-click="delete()">Delete</button>
+ </div>
+ </file>
+ <file name="script.js">
+ angular.module('ngSwipeRightExample', ['ngTouch']);
+ </file>
+ </example>
+ */
+
+function makeSwipeDirective(directiveName, direction, eventName) {
+ ngTouch.directive(directiveName, ['$parse', '$swipe', function($parse, $swipe) {
+ // The maximum vertical delta for a swipe should be less than 75px.
+ var MAX_VERTICAL_DISTANCE = 75;
+ // Vertical distance should not be more than a fraction of the horizontal distance.
+ var MAX_VERTICAL_RATIO = 0.3;
+ // At least a 30px lateral motion is necessary for a swipe.
+ var MIN_HORIZONTAL_DISTANCE = 30;
+
+ return function(scope, element, attr) {
+ var swipeHandler = $parse(attr[directiveName]);
+
+ var startCoords, valid;
+
+ function validSwipe(coords) {
+ // Check that it's within the coordinates.
+ // Absolute vertical distance must be within tolerances.
+ // Horizontal distance, we take the current X - the starting X.
+ // This is negative for leftward swipes and positive for rightward swipes.
+ // After multiplying by the direction (-1 for left, +1 for right), legal swipes
+ // (ie. same direction as the directive wants) will have a positive delta and
+ // illegal ones a negative delta.
+ // Therefore this delta must be positive, and larger than the minimum.
+ if (!startCoords) return false;
+ var deltaY = Math.abs(coords.y - startCoords.y);
+ var deltaX = (coords.x - startCoords.x) * direction;
+ return valid && // Short circuit for already-invalidated swipes.
+ deltaY < MAX_VERTICAL_DISTANCE &&
+ deltaX > 0 &&
+ deltaX > MIN_HORIZONTAL_DISTANCE &&
+ deltaY / deltaX < MAX_VERTICAL_RATIO;
+ }
+
+ var pointerTypes = ['touch'];
+ if (!angular.isDefined(attr['ngSwipeDisableMouse'])) {
+ pointerTypes.push('mouse');
+ }
+ $swipe.bind(element, {
+ 'start': function(coords, event) {
+ startCoords = coords;
+ valid = true;
+ },
+ 'cancel': function(event) {
+ valid = false;
+ },
+ 'end': function(coords, event) {
+ if (validSwipe(coords)) {
+ scope.$apply(function() {
+ element.triggerHandler(eventName);
+ swipeHandler(scope, {$event: event});
+ });
+ }
+ }
+ }, pointerTypes);
+ };
+ }]);
+}
+
+// Left is negative X-coordinate, right is positive.
+makeSwipeDirective('ngSwipeLeft', -1, 'swipeleft');
+makeSwipeDirective('ngSwipeRight', 1, 'swiperight');
+
+
+
+})(window, window.angular);
diff --git a/etc/js/angular-translate-loader-static-files.js b/etc/js/angular-translate-loader-static-files.js
new file mode 100644
index 00000000..bbb2780c
--- /dev/null
+++ b/etc/js/angular-translate-loader-static-files.js
@@ -0,0 +1,107 @@
+/*!
+ * angular-translate - v2.11.1 - 2016-07-17
+ *
+ * Copyright (c) 2016 The angular-translate team, Pascal Precht; Licensed MIT
+ */
+(function (root, factory) {
+ if (typeof define === 'function' && define.amd) {
+ // AMD. Register as an anonymous module unless amdModuleId is set
+ define([], function () {
+ return (factory());
+ });
+ } else if (typeof exports === 'object') {
+ // Node. Does not work with strict CommonJS, but
+ // only CommonJS-like environments that support module.exports,
+ // like Node.
+ module.exports = factory();
+ } else {
+ factory();
+ }
+}(this, function () {
+
+$translateStaticFilesLoader.$inject = ['$q', '$http'];
+angular.module('pascalprecht.translate')
+/**
+ * @ngdoc object
+ * @name pascalprecht.translate.$translateStaticFilesLoader
+ * @requires $q
+ * @requires $http
+ *
+ * @description
+ * Creates a loading function for a typical static file url pattern:
+ * "lang-en_US.json", "lang-de_DE.json", etc. Using this builder,
+ * the response of these urls must be an object of key-value pairs.
+ *
+ * @param {object} options Options object, which gets prefix, suffix and key.
+ */
+.factory('$translateStaticFilesLoader', $translateStaticFilesLoader);
+
+function $translateStaticFilesLoader($q, $http) {
+
+ 'use strict';
+
+ return function (options) {
+
+ if (!options || (!angular.isArray(options.files) && (!angular.isString(options.prefix) || !angular.isString(options.suffix)))) {
+ throw new Error('Couldn\'t load static files, no files and prefix or suffix specified!');
+ }
+
+ if (!options.files) {
+ options.files = [{
+ prefix: options.prefix,
+ suffix: options.suffix
+ }];
+ }
+
+ var load = function (file) {
+ if (!file || (!angular.isString(file.prefix) || !angular.isString(file.suffix))) {
+ throw new Error('Couldn\'t load static file, no prefix or suffix specified!');
+ }
+
+ return $http(angular.extend({
+ url: [
+ file.prefix,
+ options.key,
+ file.suffix
+ ].join(''),
+ method: 'GET',
+ params: ''
+ }, options.$http))
+ .then(function(result) {
+ return result.data;
+ }, function () {
+ return $q.reject(options.key);
+ });
+ };
+
+ var promises = [],
+ length = options.files.length;
+
+ for (var i = 0; i < length; i++) {
+ promises.push(load({
+ prefix: options.files[i].prefix,
+ key: options.key,
+ suffix: options.files[i].suffix
+ }));
+ }
+
+ return $q.all(promises)
+ .then(function (data) {
+ var length = data.length,
+ mergedData = {};
+
+ for (var i = 0; i < length; i++) {
+ for (var key in data[i]) {
+ mergedData[key] = data[i][key];
+ }
+ }
+
+ return mergedData;
+ });
+ };
+}
+
+$translateStaticFilesLoader.displayName = '$translateStaticFilesLoader';
+return 'pascalprecht.translate';
+
+}));
diff --git a/etc/js/angular-translate-storage-cookie.js b/etc/js/angular-translate-storage-cookie.js
new file mode 100644
index 00000000..5b45f3d1
--- /dev/null
+++ b/etc/js/angular-translate-storage-cookie.js
@@ -0,0 +1,97 @@
+/*!
+ * angular-translate - v2.11.1 - 2016-07-17
+ *
+ * Copyright (c) 2016 The angular-translate team, Pascal Precht; Licensed MIT
+ */
+(function (root, factory) {
+ if (typeof define === 'function' && define.amd) {
+ // AMD. Register as an anonymous module unless amdModuleId is set
+ define([], function () {
+ return (factory());
+ });
+ } else if (typeof exports === 'object') {
+ // Node. Does not work with strict CommonJS, but
+ // only CommonJS-like environments that support module.exports,
+ // like Node.
+ module.exports = factory();
+ } else {
+ factory();
+ }
+}(this, function () {
+
+$translateCookieStorageFactory.$inject = ['$cookieStore'];
+angular.module('pascalprecht.translate')
+
+/**
+ * @ngdoc object
+ * @name pascalprecht.translate.$translateCookieStorage
+ * @requires $cookieStore
+ *
+ * @description
+ * Abstraction layer for cookieStore. This service is used when telling angular-translate
+ * to use cookieStore as storage.
+ *
+ */
+ .factory('$translateCookieStorage', $translateCookieStorageFactory);
+
+function $translateCookieStorageFactory($cookieStore) {
+
+ 'use strict';
+
+ var $translateCookieStorage = {
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateCookieStorage#get
+ * @methodOf pascalprecht.translate.$translateCookieStorage
+ *
+ * @description
+ * Returns an item from cookieStorage by given name.
+ *
+ * @param {string} name Item name
+ * @return {string} Value of item name
+ */
+ get: function (name) {
+ return $cookieStore.get(name);
+ },
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateCookieStorage#set
+ * @methodOf pascalprecht.translate.$translateCookieStorage
+ *
+ * @description
+ * Sets an item in cookieStorage by given name.
+ *
+ * @deprecated use #put
+ *
+ * @param {string} name Item name
+ * @param {string} value Item value
+ */
+ set: function (name, value) {
+ $cookieStore.put(name, value);
+ },
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateCookieStorage#put
+ * @methodOf pascalprecht.translate.$translateCookieStorage
+ *
+ * @description
+ * Sets an item in cookieStorage by given name.
+ *
+ * @param {string} name Item name
+ * @param {string} value Item value
+ */
+ put: function (name, value) {
+ $cookieStore.put(name, value);
+ }
+ };
+
+ return $translateCookieStorage;
+}
+
+$translateCookieStorageFactory.displayName = '$translateCookieStorage';
+return 'pascalprecht.translate';
+
+}));
diff --git a/etc/js/angular-translate-storage-local.js b/etc/js/angular-translate-storage-local.js
new file mode 100644
index 00000000..43795b1a
--- /dev/null
+++ b/etc/js/angular-translate-storage-local.js
@@ -0,0 +1,123 @@
+/*!
+ * angular-translate - v2.11.1 - 2016-07-17
+ *
+ * Copyright (c) 2016 The angular-translate team, Pascal Precht; Licensed MIT
+ */
+(function (root, factory) {
+ if (typeof define === 'function' && define.amd) {
+ // AMD. Register as an anonymous module unless amdModuleId is set
+ define([], function () {
+ return (factory());
+ });
+ } else if (typeof exports === 'object') {
+ // Node. Does not work with strict CommonJS, but
+ // only CommonJS-like environments that support module.exports,
+ // like Node.
+ module.exports = factory();
+ } else {
+ factory();
+ }
+}(this, function () {
+
+$translateLocalStorageFactory.$inject = ['$window', '$translateCookieStorage'];
+angular.module('pascalprecht.translate')
+
+/**
+ * @ngdoc object
+ * @name pascalprecht.translate.$translateLocalStorage
+ * @requires $window
+ * @requires $translateCookieStorage
+ *
+ * @description
+ * Abstraction layer for localStorage. This service is used when telling angular-translate
+ * to use localStorage as storage.
+ *
+ */
+.factory('$translateLocalStorage', $translateLocalStorageFactory);
+
+function $translateLocalStorageFactory($window, $translateCookieStorage) {
+
+ 'use strict';
+
+ // Setup adapter
+ var localStorageAdapter = (function(){
+ var langKey;
+ return {
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateLocalStorage#get
+ * @methodOf pascalprecht.translate.$translateLocalStorage
+ *
+ * @description
+ * Returns an item from localStorage by given name.
+ *
+ * @param {string} name Item name
+ * @return {string} Value of item name
+ */
+ get: function (name) {
+ if(!langKey) {
+ langKey = $window.localStorage.getItem(name);
+ }
+
+ return langKey;
+ },
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateLocalStorage#set
+ * @methodOf pascalprecht.translate.$translateLocalStorage
+ *
+ * @description
+ * Sets an item in localStorage by given name.
+ *
+ * @deprecated use #put
+ *
+ * @param {string} name Item name
+ * @param {string} value Item value
+ */
+ set: function (name, value) {
+ langKey=value;
+ $window.localStorage.setItem(name, value);
+ },
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateLocalStorage#put
+ * @methodOf pascalprecht.translate.$translateLocalStorage
+ *
+ * @description
+ * Sets an item in localStorage by given name.
+ *
+ * @param {string} name Item name
+ * @param {string} value Item value
+ */
+ put: function (name, value) {
+ langKey=value;
+ $window.localStorage.setItem(name, value);
+ }
+ };
+ }());
+
+ var hasLocalStorageSupport = 'localStorage' in $window;
+ if (hasLocalStorageSupport) {
+ var testKey = 'pascalprecht.translate.storageTest';
+ try {
+ // this check have to be wrapped within a try/catch because on
+ // a SecurityError: Dom Exception 18 on iOS
+ if ($window.localStorage !== null) {
+ $window.localStorage.setItem(testKey, 'foo');
+ $window.localStorage.removeItem(testKey);
+ hasLocalStorageSupport = true;
+ } else {
+ hasLocalStorageSupport = false;
+ }
+ } catch (e){
+ hasLocalStorageSupport = false;
+ }
+ }
+ var $translateLocalStorage = hasLocalStorageSupport ? localStorageAdapter : $translateCookieStorage;
+ return $translateLocalStorage;
+}
+
+$translateLocalStorageFactory.displayName = '$translateLocalStorageFactory';
+return 'pascalprecht.translate';
+
+}));
diff --git a/etc/js/angular-translate.js b/etc/js/angular-translate.js
new file mode 100644
index 00000000..7901a19e
--- /dev/null
+++ b/etc/js/angular-translate.js
@@ -0,0 +1,3472 @@
+/*!
+ * angular-translate - v2.11.1 - 2016-07-17
+ *
+ * Copyright (c) 2016 The angular-translate team, Pascal Precht; Licensed MIT
+ */
+(function (root, factory) {
+ if (typeof define === 'function' && define.amd) {
+ // AMD. Register as an anonymous module unless amdModuleId is set
+ define([], function () {
+ return (factory());
+ });
+ } else if (typeof exports === 'object') {
+ // Node. Does not work with strict CommonJS, but
+ // only CommonJS-like environments that support module.exports,
+ // like Node.
+ module.exports = factory();
+ } else {
+ factory();
+ }
+}(this, function () {
+
+/**
+ * @ngdoc overview
+ * @name pascalprecht.translate
+ *
+ * @description
+ * The main module which holds everything together.
+ */
+runTranslate.$inject = ['$translate'];
+$translate.$inject = ['$STORAGE_KEY', '$windowProvider', '$translateSanitizationProvider', 'pascalprechtTranslateOverrider'];
+$translateDefaultInterpolation.$inject = ['$interpolate', '$translateSanitization'];
+translateDirective.$inject = ['$translate', '$q', '$interpolate', '$compile', '$parse', '$rootScope'];
+translateCloakDirective.$inject = ['$translate', '$rootScope'];
+translateFilterFactory.$inject = ['$parse', '$translate'];
+$translationCache.$inject = ['$cacheFactory'];
+angular.module('pascalprecht.translate', ['ng'])
+ .run(runTranslate);
+
+function runTranslate($translate) {
+
+ 'use strict';
+
+ var key = $translate.storageKey(),
+ storage = $translate.storage();
+
+ var fallbackFromIncorrectStorageValue = function () {
+ var preferred = $translate.preferredLanguage();
+ if (angular.isString(preferred)) {
+ $translate.use(preferred);
+ // $translate.use() will also remember the language.
+ // So, we don't need to call storage.put() here.
+ } else {
+ storage.put(key, $translate.use());
+ }
+ };
+
+ fallbackFromIncorrectStorageValue.displayName = 'fallbackFromIncorrectStorageValue';
+
+ if (storage) {
+ if (!storage.get(key)) {
+ fallbackFromIncorrectStorageValue();
+ } else {
+ $translate.use(storage.get(key))['catch'](fallbackFromIncorrectStorageValue);
+ }
+ } else if (angular.isString($translate.preferredLanguage())) {
+ $translate.use($translate.preferredLanguage());
+ }
+}
+
+runTranslate.displayName = 'runTranslate';
+
+/**
+ * @ngdoc object
+ * @name pascalprecht.translate.$translateSanitizationProvider
+ *
+ * @description
+ *
+ * Configurations for $translateSanitization
+ */
+angular.module('pascalprecht.translate').provider('$translateSanitization', $translateSanitizationProvider);
+
+function $translateSanitizationProvider () {
+
+ 'use strict';
+
+ var $sanitize,
+ currentStrategy = null, // TODO change to either 'sanitize', 'escape' or ['sanitize', 'escapeParameters'] in 3.0.
+ hasConfiguredStrategy = false,
+ hasShownNoStrategyConfiguredWarning = false,
+ strategies;
+
+ /**
+ * Definition of a sanitization strategy function
+ * @callback StrategyFunction
+ * @param {string|object} value - value to be sanitized (either a string or an interpolated value map)
+ * @param {string} mode - either 'text' for a string (translation) or 'params' for the interpolated params
+ * @return {string|object}
+ */
+
+ /**
+ * @ngdoc property
+ * @name strategies
+ * @propertyOf pascalprecht.translate.$translateSanitizationProvider
+ *
+ * @description
+ * Following strategies are built-in:
+ * <dl>
+ * <dt>sanitize</dt>
+ * <dd>Sanitizes HTML in the translation text using $sanitize</dd>
+ * <dt>escape</dt>
+ * <dd>Escapes HTML in the translation</dd>
+ * <dt>sanitizeParameters</dt>
+ * <dd>Sanitizes HTML in the values of the interpolation parameters using $sanitize</dd>
+ * <dt>escapeParameters</dt>
+ * <dd>Escapes HTML in the values of the interpolation parameters</dd>
+ * <dt>escaped</dt>
+ * <dd>Support legacy strategy name 'escaped' for backwards compatibility (will be removed in 3.0)</dd>
+ * </dl>
+ *
+ */
+
+ strategies = {
+ sanitize: function (value, mode) {
+ if (mode === 'text') {
+ value = htmlSanitizeValue(value);
+ }
+ return value;
+ },
+ escape: function (value, mode) {
+ if (mode === 'text') {
+ value = htmlEscapeValue(value);
+ }
+ return value;
+ },
+ sanitizeParameters: function (value, mode) {
+ if (mode === 'params') {
+ value = mapInterpolationParameters(value, htmlSanitizeValue);
+ }
+ return value;
+ },
+ escapeParameters: function (value, mode) {
+ if (mode === 'params') {
+ value = mapInterpolationParameters(value, htmlEscapeValue);
+ }
+ return value;
+ }
+ };
+ // Support legacy strategy name 'escaped' for backwards compatibility.
+ // TODO should be removed in 3.0
+ strategies.escaped = strategies.escapeParameters;
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateSanitizationProvider#addStrategy
+ * @methodOf pascalprecht.translate.$translateSanitizationProvider
+ *
+ * @description
+ * Adds a sanitization strategy to the list of known strategies.
+ *
+ * @param {string} strategyName - unique key for a strategy
+ * @param {StrategyFunction} strategyFunction - strategy function
+ * @returns {object} this
+ */
+ this.addStrategy = function (strategyName, strategyFunction) {
+ strategies[strategyName] = strategyFunction;
+ return this;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateSanitizationProvider#removeStrategy
+ * @methodOf pascalprecht.translate.$translateSanitizationProvider
+ *
+ * @description
+ * Removes a sanitization strategy from the list of known strategies.
+ *
+ * @param {string} strategyName - unique key for a strategy
+ * @returns {object} this
+ */
+ this.removeStrategy = function (strategyName) {
+ delete strategies[strategyName];
+ return this;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateSanitizationProvider#useStrategy
+ * @methodOf pascalprecht.translate.$translateSanitizationProvider
+ *
+ * @description
+ * Selects a sanitization strategy. When an array is provided the strategies will be executed in order.
+ *
+ * @param {string|StrategyFunction|array} strategy The sanitization strategy / strategies which should be used. Either a name of an existing strategy, a custom strategy function, or an array consisting of multiple names and / or custom functions.
+ * @returns {object} this
+ */
+ this.useStrategy = function (strategy) {
+ hasConfiguredStrategy = true;
+ currentStrategy = strategy;
+ return this;
+ };
+
+ /**
+ * @ngdoc object
+ * @name pascalprecht.translate.$translateSanitization
+ * @requires $injector
+ * @requires $log
+ *
+ * @description
+ * Sanitizes interpolation parameters and translated texts.
+ *
+ */
+ this.$get = ['$injector', '$log', function ($injector, $log) {
+
+ var cachedStrategyMap = {};
+
+ var applyStrategies = function (value, mode, selectedStrategies) {
+ angular.forEach(selectedStrategies, function (selectedStrategy) {
+ if (angular.isFunction(selectedStrategy)) {
+ value = selectedStrategy(value, mode);
+ } else if (angular.isFunction(strategies[selectedStrategy])) {
+ value = strategies[selectedStrategy](value, mode);
+ } else if (angular.isString(strategies[selectedStrategy])) {
+ if (!cachedStrategyMap[strategies[selectedStrategy]]) {
+ try {
+ cachedStrategyMap[strategies[selectedStrategy]] = $injector.get(strategies[selectedStrategy]);
+ } catch (e) {
+ cachedStrategyMap[strategies[selectedStrategy]] = function() {};
+ throw new Error('pascalprecht.translate.$translateSanitization: Unknown sanitization strategy: \'' + selectedStrategy + '\'');
+ }
+ }
+ value = cachedStrategyMap[strategies[selectedStrategy]](value, mode);
+ } else {
+ throw new Error('pascalprecht.translate.$translateSanitization: Unknown sanitization strategy: \'' + selectedStrategy + '\'');
+ }
+ });
+ return value;
+ };
+
+ // TODO: should be removed in 3.0
+ var showNoStrategyConfiguredWarning = function () {
+ if (!hasConfiguredStrategy && !hasShownNoStrategyConfiguredWarning) {
+ $log.warn('pascalprecht.translate.$translateSanitization: No sanitization strategy has been configured. This can have serious security implications. See http://angular-translate.github.io/docs/#/guide/19_security for details.');
+ hasShownNoStrategyConfiguredWarning = true;
+ }
+ };
+
+ if ($injector.has('$sanitize')) {
+ $sanitize = $injector.get('$sanitize');
+ }
+
+ return {
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateSanitization#useStrategy
+ * @methodOf pascalprecht.translate.$translateSanitization
+ *
+ * @description
+ * Selects a sanitization strategy. When an array is provided the strategies will be executed in order.
+ *
+ * @param {string|StrategyFunction|array} strategy The sanitization strategy / strategies which should be used. Either a name of an existing strategy, a custom strategy function, or an array consisting of multiple names and / or custom functions.
+ */
+ useStrategy: (function (self) {
+ return function (strategy) {
+ self.useStrategy(strategy);
+ };
+ })(this),
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateSanitization#sanitize
+ * @methodOf pascalprecht.translate.$translateSanitization
+ *
+ * @description
+ * Sanitizes a value.
+ *
+ * @param {string|object} value The value which should be sanitized.
+ * @param {string} mode The current sanitization mode, either 'params' or 'text'.
+ * @param {string|StrategyFunction|array} [strategy] Optional custom strategy which should be used instead of the currently selected strategy.
+ * @returns {string|object} sanitized value
+ */
+ sanitize: function (value, mode, strategy) {
+ if (!currentStrategy) {
+ showNoStrategyConfiguredWarning();
+ }
+
+ if (arguments.length < 3) {
+ strategy = currentStrategy;
+ }
+
+ if (!strategy) {
+ return value;
+ }
+
+ var selectedStrategies = angular.isArray(strategy) ? strategy : [strategy];
+ return applyStrategies(value, mode, selectedStrategies);
+ }
+ };
+ }];
+
+ var htmlEscapeValue = function (value) {
+ var element = angular.element('<div></div>');
+ element.text(value); // not chainable, see #1044
+ return element.html();
+ };
+
+ var htmlSanitizeValue = function (value) {
+ if (!$sanitize) {
+ throw new Error('pascalprecht.translate.$translateSanitization: Error cannot find $sanitize service. Either include the ngSanitize module (https://docs.angularjs.org/api/ngSanitize) or use a sanitization strategy which does not depend on $sanitize, such as \'escape\'.');
+ }
+ return $sanitize(value);
+ };
+
+ var mapInterpolationParameters = function (value, iteratee, stack) {
+ if (angular.isObject(value)) {
+ var result = angular.isArray(value) ? [] : {};
+
+ if (!stack) {
+ stack = [];
+ } else {
+ if (stack.indexOf(value) > -1) {
+ throw new Error('pascalprecht.translate.$translateSanitization: Error cannot interpolate parameter due recursive object');
+ }
+ }
+
+ stack.push(value);
+ angular.forEach(value, function (propertyValue, propertyKey) {
+
+ /* Skipping function properties. */
+ if (angular.isFunction(propertyValue)) {
+ return;
+ }
+
+ result[propertyKey] = mapInterpolationParameters(propertyValue, iteratee, stack);
+ });
+ stack.splice(-1, 1); // remove last
+
+ return result;
+ } else if (angular.isNumber(value)) {
+ return value;
+ } else {
+ return iteratee(value);
+ }
+ };
+}
+
+/**
+ * @ngdoc object
+ * @name pascalprecht.translate.$translateProvider
+ * @description
+ *
+ * $translateProvider allows developers to register translation-tables, asynchronous loaders
+ * and similar to configure translation behavior directly inside of a module.
+ *
+ */
+angular.module('pascalprecht.translate')
+.constant('pascalprechtTranslateOverrider', {})
+.provider('$translate', $translate);
+
+function $translate($STORAGE_KEY, $windowProvider, $translateSanitizationProvider, pascalprechtTranslateOverrider) {
+
+ 'use strict';
+
+ var $translationTable = {},
+ $preferredLanguage,
+ $availableLanguageKeys = [],
+ $languageKeyAliases,
+ $fallbackLanguage,
+ $fallbackWasString,
+ $uses,
+ $nextLang,
+ $storageFactory,
+ $storageKey = $STORAGE_KEY,
+ $storagePrefix,
+ $missingTranslationHandlerFactory,
+ $interpolationFactory,
+ $interpolatorFactories = [],
+ $loaderFactory,
+ $cloakClassName = 'translate-cloak',
+ $loaderOptions,
+ $notFoundIndicatorLeft,
+ $notFoundIndicatorRight,
+ $postCompilingEnabled = false,
+ $forceAsyncReloadEnabled = false,
+ $nestedObjectDelimeter = '.',
+ $isReady = false,
+ $keepContent = false,
+ loaderCache,
+ directivePriority = 0,
+ statefulFilter = true,
+ postProcessFn,
+ uniformLanguageTagResolver = 'default',
+ languageTagResolver = {
+ 'default': function (tag) {
+ return (tag || '').split('-').join('_');
+ },
+ java: function (tag) {
+ var temp = (tag || '').split('-').join('_');
+ var parts = temp.split('_');
+ return parts.length > 1 ? (parts[0].toLowerCase() + '_' + parts[1].toUpperCase()) : temp;
+ },
+ bcp47: function (tag) {
+ var temp = (tag || '').split('_').join('-');
+ var parts = temp.split('-');
+ return parts.length > 1 ? (parts[0].toLowerCase() + '-' + parts[1].toUpperCase()) : temp;
+ },
+ 'iso639-1': function (tag) {
+ var temp = (tag || '').split('_').join('-');
+ var parts = temp.split('-');
+ return parts[0].toLowerCase();
+ }
+ };
+
+ var version = '2.11.1';
+
+ // tries to determine the browsers language
+ var getFirstBrowserLanguage = function () {
+
+ // internal purpose only
+ if (angular.isFunction(pascalprechtTranslateOverrider.getLocale)) {
+ return pascalprechtTranslateOverrider.getLocale();
+ }
+
+ var nav = $windowProvider.$get().navigator,
+ browserLanguagePropertyKeys = ['language', 'browserLanguage', 'systemLanguage', 'userLanguage'],
+ i,
+ language;
+
+ // support for HTML 5.1 "navigator.languages"
+ if (angular.isArray(nav.languages)) {
+ for (i = 0; i < nav.languages.length; i++) {
+ language = nav.languages[i];
+ if (language && language.length) {
+ return language;
+ }
+ }
+ }
+
+ // support for other well known properties in browsers
+ for (i = 0; i < browserLanguagePropertyKeys.length; i++) {
+ language = nav[browserLanguagePropertyKeys[i]];
+ if (language && language.length) {
+ return language;
+ }
+ }
+
+ return null;
+ };
+ getFirstBrowserLanguage.displayName = 'angular-translate/service: getFirstBrowserLanguage';
+
+ // tries to determine the browsers locale
+ var getLocale = function () {
+ var locale = getFirstBrowserLanguage() || '';
+ if (languageTagResolver[uniformLanguageTagResolver]) {
+ locale = languageTagResolver[uniformLanguageTagResolver](locale);
+ }
+ return locale;
+ };
+ getLocale.displayName = 'angular-translate/service: getLocale';
+
+ /**
+ * @name indexOf
+ * @private
+ *
+ * @description
+ * indexOf polyfill. Kinda sorta.
+ *
+ * @param {array} array Array to search in.
+ * @param {string} searchElement Element to search for.
+ *
+ * @returns {int} Index of search element.
+ */
+ var indexOf = function(array, searchElement) {
+ for (var i = 0, len = array.length; i < len; i++) {
+ if (array[i] === searchElement) {
+ return i;
+ }
+ }
+ return -1;
+ };
+
+ /**
+ * @name trim
+ * @private
+ *
+ * @description
+ * trim polyfill
+ *
+ * @returns {string} The string stripped of whitespace from both ends
+ */
+ var trim = function() {
+ return this.toString().replace(/^\s+|\s+$/g, '');
+ };
+
+ var negotiateLocale = function (preferred) {
+ if(!preferred) {
+ return;
+ }
+
+ var avail = [],
+ locale = angular.lowercase(preferred),
+ i = 0,
+ n = $availableLanguageKeys.length;
+
+ for (; i < n; i++) {
+ avail.push(angular.lowercase($availableLanguageKeys[i]));
+ }
+
+ // Check for an exact match in our list of available keys
+ if (indexOf(avail, locale) > -1) {
+ return preferred;
+ }
+
+ if ($languageKeyAliases) {
+ var alias;
+ for (var langKeyAlias in $languageKeyAliases) {
+ if ($languageKeyAliases.hasOwnProperty(langKeyAlias)) {
+ var hasWildcardKey = false;
+ var hasExactKey = Object.prototype.hasOwnProperty.call($languageKeyAliases, langKeyAlias) &&
+ angular.lowercase(langKeyAlias) === angular.lowercase(preferred);
+
+ if (langKeyAlias.slice(-1) === '*') {
+ hasWildcardKey = langKeyAlias.slice(0, -1) === preferred.slice(0, langKeyAlias.length - 1);
+ }
+ if (hasExactKey || hasWildcardKey) {
+ alias = $languageKeyAliases[langKeyAlias];
+ if (indexOf(avail, angular.lowercase(alias)) > -1) {
+ return alias;
+ }
+ }
+ }
+ }
+ }
+
+ // Check for a language code without region
+ var parts = preferred.split('_');
+
+ if (parts.length > 1 && indexOf(avail, angular.lowercase(parts[0])) > -1) {
+ return parts[0];
+ }
+
+ // If everything fails, return undefined.
+ return;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#translations
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Registers a new translation table for specific language key.
+ *
+ * To register a translation table for specific language, pass a defined language
+ * key as first parameter.
+ *
+ * <pre>
+ * // register translation table for language: 'de_DE'
+ * $translateProvider.translations('de_DE', {
+ * 'GREETING': 'Hallo Welt!'
+ * });
+ *
+ * // register another one
+ * $translateProvider.translations('en_US', {
+ * 'GREETING': 'Hello world!'
+ * });
+ * </pre>
+ *
+ * When registering multiple translation tables for for the same language key,
+ * the actual translation table gets extended. This allows you to define module
+ * specific translation which only get added, once a specific module is loaded in
+ * your app.
+ *
+ * Invoking this method with no arguments returns the translation table which was
+ * registered with no language key. Invoking it with a language key returns the
+ * related translation table.
+ *
+ * @param {string} langKey A language key.
+ * @param {object} translationTable A plain old JavaScript object that represents a translation table.
+ *
+ */
+ var translations = function (langKey, translationTable) {
+
+ if (!langKey && !translationTable) {
+ return $translationTable;
+ }
+
+ if (langKey && !translationTable) {
+ if (angular.isString(langKey)) {
+ return $translationTable[langKey];
+ }
+ } else {
+ if (!angular.isObject($translationTable[langKey])) {
+ $translationTable[langKey] = {};
+ }
+ angular.extend($translationTable[langKey], flatObject(translationTable));
+ }
+ return this;
+ };
+
+ this.translations = translations;
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#cloakClassName
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ *
+ * Let's you change the class name for `translate-cloak` directive.
+ * Default class name is `translate-cloak`.
+ *
+ * @param {string} name translate-cloak class name
+ */
+ this.cloakClassName = function (name) {
+ if (!name) {
+ return $cloakClassName;
+ }
+ $cloakClassName = name;
+ return this;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#nestedObjectDelimeter
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ *
+ * Let's you change the delimiter for namespaced translations.
+ * Default delimiter is `.`.
+ *
+ * @param {string} delimiter namespace separator
+ */
+ this.nestedObjectDelimeter = function (delimiter) {
+ if (!delimiter) {
+ return $nestedObjectDelimeter;
+ }
+ $nestedObjectDelimeter = delimiter;
+ return this;
+ };
+
+ /**
+ * @name flatObject
+ * @private
+ *
+ * @description
+ * Flats an object. This function is used to flatten given translation data with
+ * namespaces, so they are later accessible via dot notation.
+ */
+ var flatObject = function (data, path, result, prevKey) {
+ var key, keyWithPath, keyWithShortPath, val;
+
+ if (!path) {
+ path = [];
+ }
+ if (!result) {
+ result = {};
+ }
+ for (key in data) {
+ if (!Object.prototype.hasOwnProperty.call(data, key)) {
+ continue;
+ }
+ val = data[key];
+ if (angular.isObject(val)) {
+ flatObject(val, path.concat(key), result, key);
+ } else {
+ keyWithPath = path.length ? ('' + path.join($nestedObjectDelimeter) + $nestedObjectDelimeter + key) : key;
+ if(path.length && key === prevKey){
+ // Create shortcut path (foo.bar == foo.bar.bar)
+ keyWithShortPath = '' + path.join($nestedObjectDelimeter);
+ // Link it to original path
+ result[keyWithShortPath] = '@:' + keyWithPath;
+ }
+ result[keyWithPath] = val;
+ }
+ }
+ return result;
+ };
+ flatObject.displayName = 'flatObject';
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#addInterpolation
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Adds interpolation services to angular-translate, so it can manage them.
+ *
+ * @param {object} factory Interpolation service factory
+ */
+ this.addInterpolation = function (factory) {
+ $interpolatorFactories.push(factory);
+ return this;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#useMessageFormatInterpolation
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Tells angular-translate to use interpolation functionality of messageformat.js.
+ * This is useful when having high level pluralization and gender selection.
+ */
+ this.useMessageFormatInterpolation = function () {
+ return this.useInterpolation('$translateMessageFormatInterpolation');
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#useInterpolation
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Tells angular-translate which interpolation style to use as default, application-wide.
+ * Simply pass a factory/service name. The interpolation service has to implement
+ * the correct interface.
+ *
+ * @param {string} factory Interpolation service name.
+ */
+ this.useInterpolation = function (factory) {
+ $interpolationFactory = factory;
+ return this;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#useSanitizeStrategy
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Simply sets a sanitation strategy type.
+ *
+ * @param {string} value Strategy type.
+ */
+ this.useSanitizeValueStrategy = function (value) {
+ $translateSanitizationProvider.useStrategy(value);
+ return this;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#preferredLanguage
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Tells the module which of the registered translation tables to use for translation
+ * at initial startup by passing a language key. Similar to `$translateProvider#use`
+ * only that it says which language to **prefer**.
+ *
+ * @param {string} langKey A language key.
+ */
+ this.preferredLanguage = function(langKey) {
+ if (langKey) {
+ setupPreferredLanguage(langKey);
+ return this;
+ }
+ return $preferredLanguage;
+ };
+ var setupPreferredLanguage = function (langKey) {
+ if (langKey) {
+ $preferredLanguage = langKey;
+ }
+ return $preferredLanguage;
+ };
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#translationNotFoundIndicator
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Sets an indicator which is used when a translation isn't found. E.g. when
+ * setting the indicator as 'X' and one tries to translate a translation id
+ * called `NOT_FOUND`, this will result in `X NOT_FOUND X`.
+ *
+ * Internally this methods sets a left indicator and a right indicator using
+ * `$translateProvider.translationNotFoundIndicatorLeft()` and
+ * `$translateProvider.translationNotFoundIndicatorRight()`.
+ *
+ * **Note**: These methods automatically add a whitespace between the indicators
+ * and the translation id.
+ *
+ * @param {string} indicator An indicator, could be any string.
+ */
+ this.translationNotFoundIndicator = function (indicator) {
+ this.translationNotFoundIndicatorLeft(indicator);
+ this.translationNotFoundIndicatorRight(indicator);
+ return this;
+ };
+
+ /**
+ * ngdoc function
+ * @name pascalprecht.translate.$translateProvider#translationNotFoundIndicatorLeft
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Sets an indicator which is used when a translation isn't found left to the
+ * translation id.
+ *
+ * @param {string} indicator An indicator.
+ */
+ this.translationNotFoundIndicatorLeft = function (indicator) {
+ if (!indicator) {
+ return $notFoundIndicatorLeft;
+ }
+ $notFoundIndicatorLeft = indicator;
+ return this;
+ };
+
+ /**
+ * ngdoc function
+ * @name pascalprecht.translate.$translateProvider#translationNotFoundIndicatorLeft
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Sets an indicator which is used when a translation isn't found right to the
+ * translation id.
+ *
+ * @param {string} indicator An indicator.
+ */
+ this.translationNotFoundIndicatorRight = function (indicator) {
+ if (!indicator) {
+ return $notFoundIndicatorRight;
+ }
+ $notFoundIndicatorRight = indicator;
+ return this;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#fallbackLanguage
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Tells the module which of the registered translation tables to use when missing translations
+ * at initial startup by passing a language key. Similar to `$translateProvider#use`
+ * only that it says which language to **fallback**.
+ *
+ * @param {string||array} langKey A language key.
+ *
+ */
+ this.fallbackLanguage = function (langKey) {
+ fallbackStack(langKey);
+ return this;
+ };
+
+ var fallbackStack = function (langKey) {
+ if (langKey) {
+ if (angular.isString(langKey)) {
+ $fallbackWasString = true;
+ $fallbackLanguage = [ langKey ];
+ } else if (angular.isArray(langKey)) {
+ $fallbackWasString = false;
+ $fallbackLanguage = langKey;
+ }
+ if (angular.isString($preferredLanguage) && indexOf($fallbackLanguage, $preferredLanguage) < 0) {
+ $fallbackLanguage.push($preferredLanguage);
+ }
+
+ return this;
+ } else {
+ if ($fallbackWasString) {
+ return $fallbackLanguage[0];
+ } else {
+ return $fallbackLanguage;
+ }
+ }
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#use
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Set which translation table to use for translation by given language key. When
+ * trying to 'use' a language which isn't provided, it'll throw an error.
+ *
+ * You actually don't have to use this method since `$translateProvider#preferredLanguage`
+ * does the job too.
+ *
+ * @param {string} langKey A language key.
+ */
+ this.use = function (langKey) {
+ if (langKey) {
+ if (!$translationTable[langKey] && (!$loaderFactory)) {
+ // only throw an error, when not loading translation data asynchronously
+ throw new Error('$translateProvider couldn\'t find translationTable for langKey: \'' + langKey + '\'');
+ }
+ $uses = langKey;
+ return this;
+ }
+ return $uses;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#resolveClientLocale
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * This returns the current browser/client's language key. The result is processed with the configured uniform tag resolver.
+ *
+ * @returns {string} the current client/browser language key
+ */
+ this.resolveClientLocale = function () {
+ return getLocale();
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#storageKey
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Tells the module which key must represent the choosed language by a user in the storage.
+ *
+ * @param {string} key A key for the storage.
+ */
+ var storageKey = function(key) {
+ if (!key) {
+ if ($storagePrefix) {
+ return $storagePrefix + $storageKey;
+ }
+ return $storageKey;
+ }
+ $storageKey = key;
+ return this;
+ };
+
+ this.storageKey = storageKey;
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#useUrlLoader
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Tells angular-translate to use `$translateUrlLoader` extension service as loader.
+ *
+ * @param {string} url Url
+ * @param {Object=} options Optional configuration object
+ */
+ this.useUrlLoader = function (url, options) {
+ return this.useLoader('$translateUrlLoader', angular.extend({ url: url }, options));
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#useStaticFilesLoader
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Tells angular-translate to use `$translateStaticFilesLoader` extension service as loader.
+ *
+ * @param {Object=} options Optional configuration object
+ */
+ this.useStaticFilesLoader = function (options) {
+ return this.useLoader('$translateStaticFilesLoader', options);
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#useLoader
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Tells angular-translate to use any other service as loader.
+ *
+ * @param {string} loaderFactory Factory name to use
+ * @param {Object=} options Optional configuration object
+ */
+ this.useLoader = function (loaderFactory, options) {
+ $loaderFactory = loaderFactory;
+ $loaderOptions = options || {};
+ return this;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#useLocalStorage
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Tells angular-translate to use `$translateLocalStorage` service as storage layer.
+ *
+ */
+ this.useLocalStorage = function () {
+ return this.useStorage('$translateLocalStorage');
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#useCookieStorage
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Tells angular-translate to use `$translateCookieStorage` service as storage layer.
+ */
+ this.useCookieStorage = function () {
+ return this.useStorage('$translateCookieStorage');
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#useStorage
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Tells angular-translate to use custom service as storage layer.
+ */
+ this.useStorage = function (storageFactory) {
+ $storageFactory = storageFactory;
+ return this;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#storagePrefix
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Sets prefix for storage key.
+ *
+ * @param {string} prefix Storage key prefix
+ */
+ this.storagePrefix = function (prefix) {
+ if (!prefix) {
+ return prefix;
+ }
+ $storagePrefix = prefix;
+ return this;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#useMissingTranslationHandlerLog
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Tells angular-translate to use built-in log handler when trying to translate
+ * a translation Id which doesn't exist.
+ *
+ * This is actually a shortcut method for `useMissingTranslationHandler()`.
+ *
+ */
+ this.useMissingTranslationHandlerLog = function () {
+ return this.useMissingTranslationHandler('$translateMissingTranslationHandlerLog');
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#useMissingTranslationHandler
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Expects a factory name which later gets instantiated with `$injector`.
+ * This method can be used to tell angular-translate to use a custom
+ * missingTranslationHandler. Just build a factory which returns a function
+ * and expects a translation id as argument.
+ *
+ * Example:
+ * <pre>
+ * app.config(function ($translateProvider) {
+ * $translateProvider.useMissingTranslationHandler('customHandler');
+ * });
+ *
+ * app.factory('customHandler', function (dep1, dep2) {
+ * return function (translationId) {
+ * // something with translationId and dep1 and dep2
+ * };
+ * });
+ * </pre>
+ *
+ * @param {string} factory Factory name
+ */
+ this.useMissingTranslationHandler = function (factory) {
+ $missingTranslationHandlerFactory = factory;
+ return this;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#usePostCompiling
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * If post compiling is enabled, all translated values will be processed
+ * again with AngularJS' $compile.
+ *
+ * Example:
+ * <pre>
+ * app.config(function ($translateProvider) {
+ * $translateProvider.usePostCompiling(true);
+ * });
+ * </pre>
+ *
+ * @param {string} factory Factory name
+ */
+ this.usePostCompiling = function (value) {
+ $postCompilingEnabled = !(!value);
+ return this;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#forceAsyncReload
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * If force async reload is enabled, async loader will always be called
+ * even if $translationTable already contains the language key, adding
+ * possible new entries to the $translationTable.
+ *
+ * Example:
+ * <pre>
+ * app.config(function ($translateProvider) {
+ * $translateProvider.forceAsyncReload(true);
+ * });
+ * </pre>
+ *
+ * @param {boolean} value - valid values are true or false
+ */
+ this.forceAsyncReload = function (value) {
+ $forceAsyncReloadEnabled = !(!value);
+ return this;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#uniformLanguageTag
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Tells angular-translate which language tag should be used as a result when determining
+ * the current browser language.
+ *
+ * This setting must be set before invoking {@link pascalprecht.translate.$translateProvider#methods_determinePreferredLanguage determinePreferredLanguage()}.
+ *
+ * <pre>
+ * $translateProvider
+ * .uniformLanguageTag('bcp47')
+ * .determinePreferredLanguage()
+ * </pre>
+ *
+ * The resolver currently supports:
+ * * default
+ * (traditionally: hyphens will be converted into underscores, i.e. en-US => en_US)
+ * en-US => en_US
+ * en_US => en_US
+ * en-us => en_us
+ * * java
+ * like default, but the second part will be always in uppercase
+ * en-US => en_US
+ * en_US => en_US
+ * en-us => en_US
+ * * BCP 47 (RFC 4646 & 4647)
+ * en-US => en-US
+ * en_US => en-US
+ * en-us => en-US
+ *
+ * See also:
+ * * http://en.wikipedia.org/wiki/IETF_language_tag
+ * * http://www.w3.org/International/core/langtags/
+ * * http://tools.ietf.org/html/bcp47
+ *
+ * @param {string|object} options - options (or standard)
+ * @param {string} options.standard - valid values are 'default', 'bcp47', 'java'
+ */
+ this.uniformLanguageTag = function (options) {
+
+ if (!options) {
+ options = {};
+ } else if (angular.isString(options)) {
+ options = {
+ standard: options
+ };
+ }
+
+ uniformLanguageTagResolver = options.standard;
+
+ return this;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#determinePreferredLanguage
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Tells angular-translate to try to determine on its own which language key
+ * to set as preferred language. When `fn` is given, angular-translate uses it
+ * to determine a language key, otherwise it uses the built-in `getLocale()`
+ * method.
+ *
+ * The `getLocale()` returns a language key in the format `[lang]_[country]` or
+ * `[lang]` depending on what the browser provides.
+ *
+ * Use this method at your own risk, since not all browsers return a valid
+ * locale (see {@link pascalprecht.translate.$translateProvider#methods_uniformLanguageTag uniformLanguageTag()}).
+ *
+ * @param {Function=} fn Function to determine a browser's locale
+ */
+ this.determinePreferredLanguage = function (fn) {
+
+ var locale = (fn && angular.isFunction(fn)) ? fn() : getLocale();
+
+ if (!$availableLanguageKeys.length) {
+ $preferredLanguage = locale;
+ } else {
+ $preferredLanguage = negotiateLocale(locale) || locale;
+ }
+
+ return this;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#registerAvailableLanguageKeys
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Registers a set of language keys the app will work with. Use this method in
+ * combination with
+ * {@link pascalprecht.translate.$translateProvider#determinePreferredLanguage determinePreferredLanguage}.
+ * When available languages keys are registered, angular-translate
+ * tries to find the best fitting language key depending on the browsers locale,
+ * considering your language key convention.
+ *
+ * @param {object} languageKeys Array of language keys the your app will use
+ * @param {object=} aliases Alias map.
+ */
+ this.registerAvailableLanguageKeys = function (languageKeys, aliases) {
+ if (languageKeys) {
+ $availableLanguageKeys = languageKeys;
+ if (aliases) {
+ $languageKeyAliases = aliases;
+ }
+ return this;
+ }
+ return $availableLanguageKeys;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#useLoaderCache
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Registers a cache for internal $http based loaders.
+ * {@link pascalprecht.translate.$translationCache $translationCache}.
+ * When false the cache will be disabled (default). When true or undefined
+ * the cache will be a default (see $cacheFactory). When an object it will
+ * be treat as a cache object itself: the usage is $http({cache: cache})
+ *
+ * @param {object} cache boolean, string or cache-object
+ */
+ this.useLoaderCache = function (cache) {
+ if (cache === false) {
+ // disable cache
+ loaderCache = undefined;
+ } else if (cache === true) {
+ // enable cache using AJS defaults
+ loaderCache = true;
+ } else if (typeof(cache) === 'undefined') {
+ // enable cache using default
+ loaderCache = '$translationCache';
+ } else if (cache) {
+ // enable cache using given one (see $cacheFactory)
+ loaderCache = cache;
+ }
+ return this;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#directivePriority
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Sets the default priority of the translate directive. The standard value is `0`.
+ * Calling this function without an argument will return the current value.
+ *
+ * @param {number} priority for the translate-directive
+ */
+ this.directivePriority = function (priority) {
+ if (priority === undefined) {
+ // getter
+ return directivePriority;
+ } else {
+ // setter with chaining
+ directivePriority = priority;
+ return this;
+ }
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#statefulFilter
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Since AngularJS 1.3, filters which are not stateless (depending at the scope)
+ * have to explicit define this behavior.
+ * Sets whether the translate filter should be stateful or stateless. The standard value is `true`
+ * meaning being stateful.
+ * Calling this function without an argument will return the current value.
+ *
+ * @param {boolean} state - defines the state of the filter
+ */
+ this.statefulFilter = function (state) {
+ if (state === undefined) {
+ // getter
+ return statefulFilter;
+ } else {
+ // setter with chaining
+ statefulFilter = state;
+ return this;
+ }
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#postProcess
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * The post processor will be intercept right after the translation result. It can modify the result.
+ *
+ * @param {object} fn Function or service name (string) to be called after the translation value has been set / resolved. The function itself will enrich every value being processed and then continue the normal resolver process
+ */
+ this.postProcess = function (fn) {
+ if (fn) {
+ postProcessFn = fn;
+ } else {
+ postProcessFn = undefined;
+ }
+ return this;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#keepContent
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * If keepContent is set to true than translate directive will always use innerHTML
+ * as a default translation
+ *
+ * Example:
+ * <pre>
+ * app.config(function ($translateProvider) {
+ * $translateProvider.keepContent(true);
+ * });
+ * </pre>
+ *
+ * @param {boolean} value - valid values are true or false
+ */
+ this.keepContent = function (value) {
+ $keepContent = !(!value);
+ return this;
+ };
+
+ /**
+ * @ngdoc object
+ * @name pascalprecht.translate.$translate
+ * @requires $interpolate
+ * @requires $log
+ * @requires $rootScope
+ * @requires $q
+ *
+ * @description
+ * The `$translate` service is the actual core of angular-translate. It expects a translation id
+ * and optional interpolate parameters to translate contents.
+ *
+ * <pre>
+ * $translate('HEADLINE_TEXT').then(function (translation) {
+ * $scope.translatedText = translation;
+ * });
+ * </pre>
+ *
+ * @param {string|array} translationId A token which represents a translation id
+ * This can be optionally an array of translation ids which
+ * results that the function returns an object where each key
+ * is the translation id and the value the translation.
+ * @param {object=} interpolateParams An object hash for dynamic values
+ * @param {string} interpolationId The id of the interpolation to use
+ * @param {string} defaultTranslationText the optional default translation text that is written as
+ * as default text in case it is not found in any configured language
+ * @param {string} forceLanguage A language to be used instead of the current language
+ * @returns {object} promise
+ */
+ this.$get = [
+ '$log',
+ '$injector',
+ '$rootScope',
+ '$q',
+ function ($log, $injector, $rootScope, $q) {
+
+ var Storage,
+ defaultInterpolator = $injector.get($interpolationFactory || '$translateDefaultInterpolation'),
+ pendingLoader = false,
+ interpolatorHashMap = {},
+ langPromises = {},
+ fallbackIndex,
+ startFallbackIteration;
+
+ var $translate = function (translationId, interpolateParams, interpolationId, defaultTranslationText, forceLanguage) {
+ if (!$uses && $preferredLanguage) {
+ $uses = $preferredLanguage;
+ }
+ var uses = (forceLanguage && forceLanguage !== $uses) ? // we don't want to re-negotiate $uses
+ (negotiateLocale(forceLanguage) || forceLanguage) : $uses;
+
+ // Check forceLanguage is present
+ if (forceLanguage) {
+ loadTranslationsIfMissing(forceLanguage);
+ }
+
+ // Duck detection: If the first argument is an array, a bunch of translations was requested.
+ // The result is an object.
+ if (angular.isArray(translationId)) {
+ // Inspired by Q.allSettled by Kris Kowal
+ // https://github.com/kriskowal/q/blob/b0fa72980717dc202ffc3cbf03b936e10ebbb9d7/q.js#L1553-1563
+ // This transforms all promises regardless resolved or rejected
+ var translateAll = function (translationIds) {
+ var results = {}; // storing the actual results
+ var promises = []; // promises to wait for
+ // Wraps the promise a) being always resolved and b) storing the link id->value
+ var translate = function (translationId) {
+ var deferred = $q.defer();
+ var regardless = function (value) {
+ results[translationId] = value;
+ deferred.resolve([translationId, value]);
+ };
+ // we don't care whether the promise was resolved or rejected; just store the values
+ $translate(translationId, interpolateParams, interpolationId, defaultTranslationText, forceLanguage).then(regardless, regardless);
+ return deferred.promise;
+ };
+ for (var i = 0, c = translationIds.length; i < c; i++) {
+ promises.push(translate(translationIds[i]));
+ }
+ // wait for all (including storing to results)
+ return $q.all(promises).then(function () {
+ // return the results
+ return results;
+ });
+ };
+ return translateAll(translationId);
+ }
+
+ var deferred = $q.defer();
+
+ // trim off any whitespace
+ if (translationId) {
+ translationId = trim.apply(translationId);
+ }
+
+ var promiseToWaitFor = (function () {
+ var promise = $preferredLanguage ?
+ langPromises[$preferredLanguage] :
+ langPromises[uses];
+
+ fallbackIndex = 0;
+
+ if ($storageFactory && !promise) {
+ // looks like there's no pending promise for $preferredLanguage or
+ // $uses. Maybe there's one pending for a language that comes from
+ // storage.
+ var langKey = Storage.get($storageKey);
+ promise = langPromises[langKey];
+
+ if ($fallbackLanguage && $fallbackLanguage.length) {
+ var index = indexOf($fallbackLanguage, langKey);
+ // maybe the language from storage is also defined as fallback language
+ // we increase the fallback language index to not search in that language
+ // as fallback, since it's probably the first used language
+ // in that case the index starts after the first element
+ fallbackIndex = (index === 0) ? 1 : 0;
+
+ // but we can make sure to ALWAYS fallback to preferred language at least
+ if (indexOf($fallbackLanguage, $preferredLanguage) < 0) {
+ $fallbackLanguage.push($preferredLanguage);
+ }
+ }
+ }
+ return promise;
+ }());
+
+ if (!promiseToWaitFor) {
+ // no promise to wait for? okay. Then there's no loader registered
+ // nor is a one pending for language that comes from storage.
+ // We can just translate.
+ determineTranslation(translationId, interpolateParams, interpolationId, defaultTranslationText, uses).then(deferred.resolve, deferred.reject);
+ } else {
+ var promiseResolved = function () {
+ // $uses may have changed while waiting
+ if (!forceLanguage) {
+ uses = $uses;
+ }
+ determineTranslation(translationId, interpolateParams, interpolationId, defaultTranslationText, uses).then(deferred.resolve, deferred.reject);
+ };
+ promiseResolved.displayName = 'promiseResolved';
+
+ promiseToWaitFor['finally'](promiseResolved);
+ }
+ return deferred.promise;
+ };
+
+ /**
+ * @name applyNotFoundIndicators
+ * @private
+ *
+ * @description
+ * Applies not fount indicators to given translation id, if needed.
+ * This function gets only executed, if a translation id doesn't exist,
+ * which is why a translation id is expected as argument.
+ *
+ * @param {string} translationId Translation id.
+ * @returns {string} Same as given translation id but applied with not found
+ * indicators.
+ */
+ var applyNotFoundIndicators = function (translationId) {
+ // applying notFoundIndicators
+ if ($notFoundIndicatorLeft) {
+ translationId = [$notFoundIndicatorLeft, translationId].join(' ');
+ }
+ if ($notFoundIndicatorRight) {
+ translationId = [translationId, $notFoundIndicatorRight].join(' ');
+ }
+ return translationId;
+ };
+
+ /**
+ * @name useLanguage
+ * @private
+ *
+ * @description
+ * Makes actual use of a language by setting a given language key as used
+ * language and informs registered interpolators to also use the given
+ * key as locale.
+ *
+ * @param {string} key Locale key.
+ */
+ var useLanguage = function (key) {
+ $uses = key;
+
+ // make sure to store new language key before triggering success event
+ if ($storageFactory) {
+ Storage.put($translate.storageKey(), $uses);
+ }
+
+ $rootScope.$emit('$translateChangeSuccess', {language: key});
+
+ // inform default interpolator
+ defaultInterpolator.setLocale($uses);
+
+ var eachInterpolator = function (interpolator, id) {
+ interpolatorHashMap[id].setLocale($uses);
+ };
+ eachInterpolator.displayName = 'eachInterpolatorLocaleSetter';
+
+ // inform all others too!
+ angular.forEach(interpolatorHashMap, eachInterpolator);
+ $rootScope.$emit('$translateChangeEnd', {language: key});
+ };
+
+ /**
+ * @name loadAsync
+ * @private
+ *
+ * @description
+ * Kicks of registered async loader using `$injector` and applies existing
+ * loader options. When resolved, it updates translation tables accordingly
+ * or rejects with given language key.
+ *
+ * @param {string} key Language key.
+ * @return {Promise} A promise.
+ */
+ var loadAsync = function (key) {
+ if (!key) {
+ throw 'No language key specified for loading.';
+ }
+
+ var deferred = $q.defer();
+
+ $rootScope.$emit('$translateLoadingStart', {language: key});
+ pendingLoader = true;
+
+ var cache = loaderCache;
+ if (typeof(cache) === 'string') {
+ // getting on-demand instance of loader
+ cache = $injector.get(cache);
+ }
+
+ var loaderOptions = angular.extend({}, $loaderOptions, {
+ key: key,
+ $http: angular.extend({}, {
+ cache: cache
+ }, $loaderOptions.$http)
+ });
+
+ var onLoaderSuccess = function (data) {
+ var translationTable = {};
+ $rootScope.$emit('$translateLoadingSuccess', {language: key});
+
+ if (angular.isArray(data)) {
+ angular.forEach(data, function (table) {
+ angular.extend(translationTable, flatObject(table));
+ });
+ } else {
+ angular.extend(translationTable, flatObject(data));
+ }
+ pendingLoader = false;
+ deferred.resolve({
+ key: key,
+ table: translationTable
+ });
+ $rootScope.$emit('$translateLoadingEnd', {language: key});
+ };
+ onLoaderSuccess.displayName = 'onLoaderSuccess';
+
+ var onLoaderError = function (key) {
+ $rootScope.$emit('$translateLoadingError', {language: key});
+ deferred.reject(key);
+ $rootScope.$emit('$translateLoadingEnd', {language: key});
+ };
+ onLoaderError.displayName = 'onLoaderError';
+
+ $injector.get($loaderFactory)(loaderOptions)
+ .then(onLoaderSuccess, onLoaderError);
+
+ return deferred.promise;
+ };
+
+ if ($storageFactory) {
+ Storage = $injector.get($storageFactory);
+
+ if (!Storage.get || !Storage.put) {
+ throw new Error('Couldn\'t use storage \'' + $storageFactory + '\', missing get() or put() method!');
+ }
+ }
+
+ // if we have additional interpolations that were added via
+ // $translateProvider.addInterpolation(), we have to map'em
+ if ($interpolatorFactories.length) {
+ var eachInterpolationFactory = function (interpolatorFactory) {
+ var interpolator = $injector.get(interpolatorFactory);
+ // setting initial locale for each interpolation service
+ interpolator.setLocale($preferredLanguage || $uses);
+ // make'em recognizable through id
+ interpolatorHashMap[interpolator.getInterpolationIdentifier()] = interpolator;
+ };
+ eachInterpolationFactory.displayName = 'interpolationFactoryAdder';
+
+ angular.forEach($interpolatorFactories, eachInterpolationFactory);
+ }
+
+ /**
+ * @name getTranslationTable
+ * @private
+ *
+ * @description
+ * Returns a promise that resolves to the translation table
+ * or is rejected if an error occurred.
+ *
+ * @param langKey
+ * @returns {Q.promise}
+ */
+ var getTranslationTable = function (langKey) {
+ var deferred = $q.defer();
+ if (Object.prototype.hasOwnProperty.call($translationTable, langKey)) {
+ deferred.resolve($translationTable[langKey]);
+ } else if (langPromises[langKey]) {
+ var onResolve = function (data) {
+ translations(data.key, data.table);
+ deferred.resolve(data.table);
+ };
+ onResolve.displayName = 'translationTableResolver';
+ langPromises[langKey].then(onResolve, deferred.reject);
+ } else {
+ deferred.reject();
+ }
+ return deferred.promise;
+ };
+
+ /**
+ * @name getFallbackTranslation
+ * @private
+ *
+ * @description
+ * Returns a promise that will resolve to the translation
+ * or be rejected if no translation was found for the language.
+ * This function is currently only used for fallback language translation.
+ *
+ * @param langKey The language to translate to.
+ * @param translationId
+ * @param interpolateParams
+ * @param Interpolator
+ * @returns {Q.promise}
+ */
+ var getFallbackTranslation = function (langKey, translationId, interpolateParams, Interpolator) {
+ var deferred = $q.defer();
+
+ var onResolve = function (translationTable) {
+ if (Object.prototype.hasOwnProperty.call(translationTable, translationId)) {
+ Interpolator.setLocale(langKey);
+ var translation = translationTable[translationId];
+ if (translation.substr(0, 2) === '@:') {
+ getFallbackTranslation(langKey, translation.substr(2), interpolateParams, Interpolator)
+ .then(deferred.resolve, deferred.reject);
+ } else {
+ var interpolatedValue = Interpolator.interpolate(translationTable[translationId], interpolateParams);
+ interpolatedValue = applyPostProcessing(translationId, translationTable[translationId], interpolatedValue, interpolateParams, langKey);
+
+ deferred.resolve(interpolatedValue);
+
+ }
+ Interpolator.setLocale($uses);
+ } else {
+ deferred.reject();
+ }
+ };
+ onResolve.displayName = 'fallbackTranslationResolver';
+
+ getTranslationTable(langKey).then(onResolve, deferred.reject);
+
+ return deferred.promise;
+ };
+
+ /**
+ * @name getFallbackTranslationInstant
+ * @private
+ *
+ * @description
+ * Returns a translation
+ * This function is currently only used for fallback language translation.
+ *
+ * @param langKey The language to translate to.
+ * @param translationId
+ * @param interpolateParams
+ * @param Interpolator
+ * @returns {string} translation
+ */
+ var getFallbackTranslationInstant = function (langKey, translationId, interpolateParams, Interpolator) {
+ var result, translationTable = $translationTable[langKey];
+
+ if (translationTable && Object.prototype.hasOwnProperty.call(translationTable, translationId)) {
+ Interpolator.setLocale(langKey);
+ result = Interpolator.interpolate(translationTable[translationId], interpolateParams);
+ result = applyPostProcessing(translationId, translationTable[translationId], result, interpolateParams, langKey);
+ if (result.substr(0, 2) === '@:') {
+ return getFallbackTranslationInstant(langKey, result.substr(2), interpolateParams, Interpolator);
+ }
+ Interpolator.setLocale($uses);
+ }
+
+ return result;
+ };
+
+
+ /**
+ * @name translateByHandler
+ * @private
+ *
+ * Translate by missing translation handler.
+ *
+ * @param translationId
+ * @param interpolateParams
+ * @param defaultTranslationText
+ * @returns translation created by $missingTranslationHandler or translationId is $missingTranslationHandler is
+ * absent
+ */
+ var translateByHandler = function (translationId, interpolateParams, defaultTranslationText) {
+ // If we have a handler factory - we might also call it here to determine if it provides
+ // a default text for a translationid that can't be found anywhere in our tables
+ if ($missingTranslationHandlerFactory) {
+ var resultString = $injector.get($missingTranslationHandlerFactory)(translationId, $uses, interpolateParams, defaultTranslationText);
+ if (resultString !== undefined) {
+ return resultString;
+ } else {
+ return translationId;
+ }
+ } else {
+ return translationId;
+ }
+ };
+
+ /**
+ * @name resolveForFallbackLanguage
+ * @private
+ *
+ * Recursive helper function for fallbackTranslation that will sequentially look
+ * for a translation in the fallbackLanguages starting with fallbackLanguageIndex.
+ *
+ * @param fallbackLanguageIndex
+ * @param translationId
+ * @param interpolateParams
+ * @param Interpolator
+ * @returns {Q.promise} Promise that will resolve to the translation.
+ */
+ var resolveForFallbackLanguage = function (fallbackLanguageIndex, translationId, interpolateParams, Interpolator, defaultTranslationText) {
+ var deferred = $q.defer();
+
+ if (fallbackLanguageIndex < $fallbackLanguage.length) {
+ var langKey = $fallbackLanguage[fallbackLanguageIndex];
+ getFallbackTranslation(langKey, translationId, interpolateParams, Interpolator).then(
+ function (data) {
+ deferred.resolve(data);
+ },
+ function () {
+ // Look in the next fallback language for a translation.
+ // It delays the resolving by passing another promise to resolve.
+ return resolveForFallbackLanguage(fallbackLanguageIndex + 1, translationId, interpolateParams, Interpolator, defaultTranslationText).then(deferred.resolve, deferred.reject);
+ }
+ );
+ } else {
+ // No translation found in any fallback language
+ // if a default translation text is set in the directive, then return this as a result
+ if (defaultTranslationText) {
+ deferred.resolve(defaultTranslationText);
+ } else {
+ // if no default translation is set and an error handler is defined, send it to the handler
+ // and then return the result
+ if ($missingTranslationHandlerFactory) {
+ deferred.resolve(translateByHandler(translationId, interpolateParams));
+ } else {
+ deferred.reject(translateByHandler(translationId, interpolateParams));
+ }
+
+ }
+ }
+ return deferred.promise;
+ };
+
+ /**
+ * @name resolveForFallbackLanguageInstant
+ * @private
+ *
+ * Recursive helper function for fallbackTranslation that will sequentially look
+ * for a translation in the fallbackLanguages starting with fallbackLanguageIndex.
+ *
+ * @param fallbackLanguageIndex
+ * @param translationId
+ * @param interpolateParams
+ * @param Interpolator
+ * @returns {string} translation
+ */
+ var resolveForFallbackLanguageInstant = function (fallbackLanguageIndex, translationId, interpolateParams, Interpolator) {
+ var result;
+
+ if (fallbackLanguageIndex < $fallbackLanguage.length) {
+ var langKey = $fallbackLanguage[fallbackLanguageIndex];
+ result = getFallbackTranslationInstant(langKey, translationId, interpolateParams, Interpolator);
+ if (!result) {
+ result = resolveForFallbackLanguageInstant(fallbackLanguageIndex + 1, translationId, interpolateParams, Interpolator);
+ }
+ }
+ return result;
+ };
+
+ /**
+ * Translates with the usage of the fallback languages.
+ *
+ * @param translationId
+ * @param interpolateParams
+ * @param Interpolator
+ * @returns {Q.promise} Promise, that resolves to the translation.
+ */
+ var fallbackTranslation = function (translationId, interpolateParams, Interpolator, defaultTranslationText) {
+ // Start with the fallbackLanguage with index 0
+ return resolveForFallbackLanguage((startFallbackIteration>0 ? startFallbackIteration : fallbackIndex), translationId, interpolateParams, Interpolator, defaultTranslationText);
+ };
+
+ /**
+ * Translates with the usage of the fallback languages.
+ *
+ * @param translationId
+ * @param interpolateParams
+ * @param Interpolator
+ * @returns {String} translation
+ */
+ var fallbackTranslationInstant = function (translationId, interpolateParams, Interpolator) {
+ // Start with the fallbackLanguage with index 0
+ return resolveForFallbackLanguageInstant((startFallbackIteration>0 ? startFallbackIteration : fallbackIndex), translationId, interpolateParams, Interpolator);
+ };
+
+ var determineTranslation = function (translationId, interpolateParams, interpolationId, defaultTranslationText, uses) {
+
+ var deferred = $q.defer();
+
+ var table = uses ? $translationTable[uses] : $translationTable,
+ Interpolator = (interpolationId) ? interpolatorHashMap[interpolationId] : defaultInterpolator;
+
+ // if the translation id exists, we can just interpolate it
+ if (table && Object.prototype.hasOwnProperty.call(table, translationId)) {
+ var translation = table[translationId];
+
+ // If using link, rerun $translate with linked translationId and return it
+ if (translation.substr(0, 2) === '@:') {
+
+ $translate(translation.substr(2), interpolateParams, interpolationId, defaultTranslationText, uses)
+ .then(deferred.resolve, deferred.reject);
+ } else {
+ //
+ var resolvedTranslation = Interpolator.interpolate(translation, interpolateParams);
+ resolvedTranslation = applyPostProcessing(translationId, translation, resolvedTranslation, interpolateParams, uses);
+ deferred.resolve(resolvedTranslation);
+ }
+ } else {
+ var missingTranslationHandlerTranslation;
+ // for logging purposes only (as in $translateMissingTranslationHandlerLog), value is not returned to promise
+ if ($missingTranslationHandlerFactory && !pendingLoader) {
+ missingTranslationHandlerTranslation = translateByHandler(translationId, interpolateParams, defaultTranslationText);
+ }
+
+ // since we couldn't translate the inital requested translation id,
+ // we try it now with one or more fallback languages, if fallback language(s) is
+ // configured.
+ if (uses && $fallbackLanguage && $fallbackLanguage.length) {
+ fallbackTranslation(translationId, interpolateParams, Interpolator, defaultTranslationText)
+ .then(function (translation) {
+ deferred.resolve(translation);
+ }, function (_translationId) {
+ deferred.reject(applyNotFoundIndicators(_translationId));
+ });
+ } else if ($missingTranslationHandlerFactory && !pendingLoader && missingTranslationHandlerTranslation) {
+ // looks like the requested translation id doesn't exists.
+ // Now, if there is a registered handler for missing translations and no
+ // asyncLoader is pending, we execute the handler
+ if (defaultTranslationText) {
+ deferred.resolve(defaultTranslationText);
+ } else {
+ deferred.resolve(missingTranslationHandlerTranslation);
+ }
+ } else {
+ if (defaultTranslationText) {
+ deferred.resolve(defaultTranslationText);
+ } else {
+ deferred.reject(applyNotFoundIndicators(translationId));
+ }
+ }
+ }
+ return deferred.promise;
+ };
+
+ var determineTranslationInstant = function (translationId, interpolateParams, interpolationId, uses) {
+
+ var result, table = uses ? $translationTable[uses] : $translationTable,
+ Interpolator = defaultInterpolator;
+
+ // if the interpolation id exists use custom interpolator
+ if (interpolatorHashMap && Object.prototype.hasOwnProperty.call(interpolatorHashMap, interpolationId)) {
+ Interpolator = interpolatorHashMap[interpolationId];
+ }
+
+ // if the translation id exists, we can just interpolate it
+ if (table && Object.prototype.hasOwnProperty.call(table, translationId)) {
+ var translation = table[translationId];
+
+ // If using link, rerun $translate with linked translationId and return it
+ if (translation.substr(0, 2) === '@:') {
+ result = determineTranslationInstant(translation.substr(2), interpolateParams, interpolationId, uses);
+ } else {
+ result = Interpolator.interpolate(translation, interpolateParams);
+ result = applyPostProcessing(translationId, translation, result, interpolateParams, uses);
+ }
+ } else {
+ var missingTranslationHandlerTranslation;
+ // for logging purposes only (as in $translateMissingTranslationHandlerLog), value is not returned to promise
+ if ($missingTranslationHandlerFactory && !pendingLoader) {
+ missingTranslationHandlerTranslation = translateByHandler(translationId, interpolateParams);
+ }
+
+ // since we couldn't translate the inital requested translation id,
+ // we try it now with one or more fallback languages, if fallback language(s) is
+ // configured.
+ if (uses && $fallbackLanguage && $fallbackLanguage.length) {
+ fallbackIndex = 0;
+ result = fallbackTranslationInstant(translationId, interpolateParams, Interpolator);
+ } else if ($missingTranslationHandlerFactory && !pendingLoader && missingTranslationHandlerTranslation) {
+ // looks like the requested translation id doesn't exists.
+ // Now, if there is a registered handler for missing translations and no
+ // asyncLoader is pending, we execute the handler
+ result = missingTranslationHandlerTranslation;
+ } else {
+ result = applyNotFoundIndicators(translationId);
+ }
+ }
+
+ return result;
+ };
+
+ var clearNextLangAndPromise = function(key) {
+ if ($nextLang === key) {
+ $nextLang = undefined;
+ }
+ langPromises[key] = undefined;
+ };
+
+ var applyPostProcessing = function (translationId, translation, resolvedTranslation, interpolateParams, uses) {
+ var fn = postProcessFn;
+
+ if (fn) {
+
+ if (typeof(fn) === 'string') {
+ // getting on-demand instance
+ fn = $injector.get(fn);
+ }
+ if (fn) {
+ return fn(translationId, translation, resolvedTranslation, interpolateParams, uses);
+ }
+ }
+
+ return resolvedTranslation;
+ };
+
+ var loadTranslationsIfMissing = function (key) {
+ if (!$translationTable[key] && $loaderFactory && !langPromises[key]) {
+ langPromises[key] = loadAsync(key).then(function (translation) {
+ translations(translation.key, translation.table);
+ return translation;
+ });
+ }
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translate#preferredLanguage
+ * @methodOf pascalprecht.translate.$translate
+ *
+ * @description
+ * Returns the language key for the preferred language.
+ *
+ * @param {string} langKey language String or Array to be used as preferredLanguage (changing at runtime)
+ *
+ * @return {string} preferred language key
+ */
+ $translate.preferredLanguage = function (langKey) {
+ if(langKey) {
+ setupPreferredLanguage(langKey);
+ }
+ return $preferredLanguage;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translate#cloakClassName
+ * @methodOf pascalprecht.translate.$translate
+ *
+ * @description
+ * Returns the configured class name for `translate-cloak` directive.
+ *
+ * @return {string} cloakClassName
+ */
+ $translate.cloakClassName = function () {
+ return $cloakClassName;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translate#nestedObjectDelimeter
+ * @methodOf pascalprecht.translate.$translate
+ *
+ * @description
+ * Returns the configured delimiter for nested namespaces.
+ *
+ * @return {string} nestedObjectDelimeter
+ */
+ $translate.nestedObjectDelimeter = function () {
+ return $nestedObjectDelimeter;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translate#fallbackLanguage
+ * @methodOf pascalprecht.translate.$translate
+ *
+ * @description
+ * Returns the language key for the fallback languages or sets a new fallback stack.
+ *
+ * @param {string=} langKey language String or Array of fallback languages to be used (to change stack at runtime)
+ *
+ * @return {string||array} fallback language key
+ */
+ $translate.fallbackLanguage = function (langKey) {
+ if (langKey !== undefined && langKey !== null) {
+ fallbackStack(langKey);
+
+ // as we might have an async loader initiated and a new translation language might have been defined
+ // we need to add the promise to the stack also. So - iterate.
+ if ($loaderFactory) {
+ if ($fallbackLanguage && $fallbackLanguage.length) {
+ for (var i = 0, len = $fallbackLanguage.length; i < len; i++) {
+ if (!langPromises[$fallbackLanguage[i]]) {
+ langPromises[$fallbackLanguage[i]] = loadAsync($fallbackLanguage[i]);
+ }
+ }
+ }
+ }
+ $translate.use($translate.use());
+ }
+ if ($fallbackWasString) {
+ return $fallbackLanguage[0];
+ } else {
+ return $fallbackLanguage;
+ }
+
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translate#useFallbackLanguage
+ * @methodOf pascalprecht.translate.$translate
+ *
+ * @description
+ * Sets the first key of the fallback language stack to be used for translation.
+ * Therefore all languages in the fallback array BEFORE this key will be skipped!
+ *
+ * @param {string=} langKey Contains the langKey the iteration shall start with. Set to false if you want to
+ * get back to the whole stack
+ */
+ $translate.useFallbackLanguage = function (langKey) {
+ if (langKey !== undefined && langKey !== null) {
+ if (!langKey) {
+ startFallbackIteration = 0;
+ } else {
+ var langKeyPosition = indexOf($fallbackLanguage, langKey);
+ if (langKeyPosition > -1) {
+ startFallbackIteration = langKeyPosition;
+ }
+ }
+
+ }
+
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translate#proposedLanguage
+ * @methodOf pascalprecht.translate.$translate
+ *
+ * @description
+ * Returns the language key of language that is currently loaded asynchronously.
+ *
+ * @return {string} language key
+ */
+ $translate.proposedLanguage = function () {
+ return $nextLang;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translate#storage
+ * @methodOf pascalprecht.translate.$translate
+ *
+ * @description
+ * Returns registered storage.
+ *
+ * @return {object} Storage
+ */
+ $translate.storage = function () {
+ return Storage;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translate#negotiateLocale
+ * @methodOf pascalprecht.translate.$translate
+ *
+ * @description
+ * Returns a language key based on available languages and language aliases. If a
+ * language key cannot be resolved, returns undefined.
+ *
+ * If no or a falsy key is given, returns undefined.
+ *
+ * @param {string} [key] Language key
+ * @return {string|undefined} Language key or undefined if no language key is found.
+ */
+ $translate.negotiateLocale = negotiateLocale;
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translate#use
+ * @methodOf pascalprecht.translate.$translate
+ *
+ * @description
+ * Tells angular-translate which language to use by given language key. This method is
+ * used to change language at runtime. It also takes care of storing the language
+ * key in a configured store to let your app remember the choosed language.
+ *
+ * When trying to 'use' a language which isn't available it tries to load it
+ * asynchronously with registered loaders.
+ *
+ * Returns promise object with loaded language file data or string of the currently used language.
+ *
+ * If no or a falsy key is given it returns the currently used language key.
+ * The returned string will be ```undefined``` if setting up $translate hasn't finished.
+ * @example
+ * $translate.use("en_US").then(function(data){
+ * $scope.text = $translate("HELLO");
+ * });
+ *
+ * @param {string} [key] Language key
+ * @return {object|string} Promise with loaded language data or the language key if a falsy param was given.
+ */
+ $translate.use = function (key) {
+ if (!key) {
+ return $uses;
+ }
+
+ var deferred = $q.defer();
+
+ $rootScope.$emit('$translateChangeStart', {language: key});
+
+ // Try to get the aliased language key
+ var aliasedKey = negotiateLocale(key);
+ // Ensure only registered language keys will be loaded
+ if ($availableLanguageKeys.length > 0 && !aliasedKey) {
+ return $q.reject(key);
+ }
+
+ if (aliasedKey) {
+ key = aliasedKey;
+ }
+
+ // if there isn't a translation table for the language we've requested,
+ // we load it asynchronously
+ $nextLang = key;
+ if (($forceAsyncReloadEnabled || !$translationTable[key]) && $loaderFactory && !langPromises[key]) {
+ langPromises[key] = loadAsync(key).then(function (translation) {
+ translations(translation.key, translation.table);
+ deferred.resolve(translation.key);
+ if ($nextLang === key) {
+ useLanguage(translation.key);
+ }
+ return translation;
+ }, function (key) {
+ $rootScope.$emit('$translateChangeError', {language: key});
+ deferred.reject(key);
+ $rootScope.$emit('$translateChangeEnd', {language: key});
+ return $q.reject(key);
+ });
+ langPromises[key]['finally'](function () {
+ clearNextLangAndPromise(key);
+ });
+ } else if (langPromises[key]) {
+ // we are already loading this asynchronously
+ // resolve our new deferred when the old langPromise is resolved
+ langPromises[key].then(function (translation) {
+ if ($nextLang === translation.key) {
+ useLanguage(translation.key);
+ }
+ deferred.resolve(translation.key);
+ return translation;
+ }, function (key) {
+ // find first available fallback language if that request has failed
+ if (!$uses && $fallbackLanguage && $fallbackLanguage.length > 0) {
+ return $translate.use($fallbackLanguage[0]).then(deferred.resolve, deferred.reject);
+ } else {
+ return deferred.reject(key);
+ }
+ });
+ } else {
+ deferred.resolve(key);
+ useLanguage(key);
+ }
+
+ return deferred.promise;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translate#resolveClientLocale
+ * @methodOf pascalprecht.translate.$translate
+ *
+ * @description
+ * This returns the current browser/client's language key. The result is processed with the configured uniform tag resolver.
+ *
+ * @returns {string} the current client/browser language key
+ */
+ $translate.resolveClientLocale = function () {
+ return getLocale();
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translate#storageKey
+ * @methodOf pascalprecht.translate.$translate
+ *
+ * @description
+ * Returns the key for the storage.
+ *
+ * @return {string} storage key
+ */
+ $translate.storageKey = function () {
+ return storageKey();
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translate#isPostCompilingEnabled
+ * @methodOf pascalprecht.translate.$translate
+ *
+ * @description
+ * Returns whether post compiling is enabled or not
+ *
+ * @return {bool} storage key
+ */
+ $translate.isPostCompilingEnabled = function () {
+ return $postCompilingEnabled;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translate#isForceAsyncReloadEnabled
+ * @methodOf pascalprecht.translate.$translate
+ *
+ * @description
+ * Returns whether force async reload is enabled or not
+ *
+ * @return {boolean} forceAsyncReload value
+ */
+ $translate.isForceAsyncReloadEnabled = function () {
+ return $forceAsyncReloadEnabled;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translate#isKeepContent
+ * @methodOf pascalprecht.translate.$translate
+ *
+ * @description
+ * Returns whether keepContent or not
+ *
+ * @return {boolean} keepContent value
+ */
+ $translate.isKeepContent = function () {
+ return $keepContent;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translate#refresh
+ * @methodOf pascalprecht.translate.$translate
+ *
+ * @description
+ * Refreshes a translation table pointed by the given langKey. If langKey is not specified,
+ * the module will drop all existent translation tables and load new version of those which
+ * are currently in use.
+ *
+ * Refresh means that the module will drop target translation table and try to load it again.
+ *
+ * In case there are no loaders registered the refresh() method will throw an Error.
+ *
+ * If the module is able to refresh translation tables refresh() method will broadcast
+ * $translateRefreshStart and $translateRefreshEnd events.
+ *
+ * @example
+ * // this will drop all currently existent translation tables and reload those which are
+ * // currently in use
+ * $translate.refresh();
+ * // this will refresh a translation table for the en_US language
+ * $translate.refresh('en_US');
+ *
+ * @param {string} langKey A language key of the table, which has to be refreshed
+ *
+ * @return {promise} Promise, which will be resolved in case a translation tables refreshing
+ * process is finished successfully, and reject if not.
+ */
+ $translate.refresh = function (langKey) {
+ if (!$loaderFactory) {
+ throw new Error('Couldn\'t refresh translation table, no loader registered!');
+ }
+
+ var deferred = $q.defer();
+
+ function resolve() {
+ deferred.resolve();
+ $rootScope.$emit('$translateRefreshEnd', {language: langKey});
+ }
+
+ function reject() {
+ deferred.reject();
+ $rootScope.$emit('$translateRefreshEnd', {language: langKey});
+ }
+
+ $rootScope.$emit('$translateRefreshStart', {language: langKey});
+
+ if (!langKey) {
+ // if there's no language key specified we refresh ALL THE THINGS!
+ var tables = [], loadingKeys = {};
+
+ // reload registered fallback languages
+ if ($fallbackLanguage && $fallbackLanguage.length) {
+ for (var i = 0, len = $fallbackLanguage.length; i < len; i++) {
+ tables.push(loadAsync($fallbackLanguage[i]));
+ loadingKeys[$fallbackLanguage[i]] = true;
+ }
+ }
+
+ // reload currently used language
+ if ($uses && !loadingKeys[$uses]) {
+ tables.push(loadAsync($uses));
+ }
+
+ var allTranslationsLoaded = function (tableData) {
+ $translationTable = {};
+ angular.forEach(tableData, function (data) {
+ translations(data.key, data.table);
+ });
+ if ($uses) {
+ useLanguage($uses);
+ }
+ resolve();
+ };
+ allTranslationsLoaded.displayName = 'refreshPostProcessor';
+
+ $q.all(tables).then(allTranslationsLoaded, reject);
+
+ } else if ($translationTable[langKey]) {
+
+ var oneTranslationsLoaded = function (data) {
+ translations(data.key, data.table);
+ if (langKey === $uses) {
+ useLanguage($uses);
+ }
+ resolve();
+ return data;
+ };
+ oneTranslationsLoaded.displayName = 'refreshPostProcessor';
+
+ loadAsync(langKey).then(oneTranslationsLoaded, reject);
+
+ } else {
+ reject();
+ }
+ return deferred.promise;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translate#instant
+ * @methodOf pascalprecht.translate.$translate
+ *
+ * @description
+ * Returns a translation instantly from the internal state of loaded translation. All rules
+ * regarding the current language, the preferred language of even fallback languages will be
+ * used except any promise handling. If a language was not found, an asynchronous loading
+ * will be invoked in the background.
+ *
+ * @param {string|array} translationId A token which represents a translation id
+ * This can be optionally an array of translation ids which
+ * results that the function's promise returns an object where
+ * each key is the translation id and the value the translation.
+ * @param {object} interpolateParams Params
+ * @param {string} interpolationId The id of the interpolation to use
+ * @param {string} forceLanguage A language to be used instead of the current language
+ *
+ * @return {string|object} translation
+ */
+ $translate.instant = function (translationId, interpolateParams, interpolationId, forceLanguage) {
+
+ // we don't want to re-negotiate $uses
+ var uses = (forceLanguage && forceLanguage !== $uses) ? // we don't want to re-negotiate $uses
+ (negotiateLocale(forceLanguage) || forceLanguage) : $uses;
+
+ // Detect undefined and null values to shorten the execution and prevent exceptions
+ if (translationId === null || angular.isUndefined(translationId)) {
+ return translationId;
+ }
+
+ // Check forceLanguage is present
+ if (forceLanguage) {
+ loadTranslationsIfMissing(forceLanguage);
+ }
+
+ // Duck detection: If the first argument is an array, a bunch of translations was requested.
+ // The result is an object.
+ if (angular.isArray(translationId)) {
+ var results = {};
+ for (var i = 0, c = translationId.length; i < c; i++) {
+ results[translationId[i]] = $translate.instant(translationId[i], interpolateParams, interpolationId, forceLanguage);
+ }
+ return results;
+ }
+
+ // We discarded unacceptable values. So we just need to verify if translationId is empty String
+ if (angular.isString(translationId) && translationId.length < 1) {
+ return translationId;
+ }
+
+ // trim off any whitespace
+ if (translationId) {
+ translationId = trim.apply(translationId);
+ }
+
+ var result, possibleLangKeys = [];
+ if ($preferredLanguage) {
+ possibleLangKeys.push($preferredLanguage);
+ }
+ if (uses) {
+ possibleLangKeys.push(uses);
+ }
+ if ($fallbackLanguage && $fallbackLanguage.length) {
+ possibleLangKeys = possibleLangKeys.concat($fallbackLanguage);
+ }
+ for (var j = 0, d = possibleLangKeys.length; j < d; j++) {
+ var possibleLangKey = possibleLangKeys[j];
+ if ($translationTable[possibleLangKey]) {
+ if (typeof $translationTable[possibleLangKey][translationId] !== 'undefined') {
+ result = determineTranslationInstant(translationId, interpolateParams, interpolationId, uses);
+ }
+ }
+ if (typeof result !== 'undefined') {
+ break;
+ }
+ }
+
+ if (!result && result !== '') {
+ if ($notFoundIndicatorLeft || $notFoundIndicatorRight) {
+ result = applyNotFoundIndicators(translationId);
+ } else {
+ // Return translation of default interpolator if not found anything.
+ result = defaultInterpolator.interpolate(translationId, interpolateParams);
+ if ($missingTranslationHandlerFactory && !pendingLoader) {
+ result = translateByHandler(translationId, interpolateParams);
+ }
+ }
+ }
+
+ return result;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translate#versionInfo
+ * @methodOf pascalprecht.translate.$translate
+ *
+ * @description
+ * Returns the current version information for the angular-translate library
+ *
+ * @return {string} angular-translate version
+ */
+ $translate.versionInfo = function () {
+ return version;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translate#loaderCache
+ * @methodOf pascalprecht.translate.$translate
+ *
+ * @description
+ * Returns the defined loaderCache.
+ *
+ * @return {boolean|string|object} current value of loaderCache
+ */
+ $translate.loaderCache = function () {
+ return loaderCache;
+ };
+
+ // internal purpose only
+ $translate.directivePriority = function () {
+ return directivePriority;
+ };
+
+ // internal purpose only
+ $translate.statefulFilter = function () {
+ return statefulFilter;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translate#isReady
+ * @methodOf pascalprecht.translate.$translate
+ *
+ * @description
+ * Returns whether the service is "ready" to translate (i.e. loading 1st language).
+ *
+ * See also {@link pascalprecht.translate.$translate#methods_onReady onReady()}.
+ *
+ * @return {boolean} current value of ready
+ */
+ $translate.isReady = function () {
+ return $isReady;
+ };
+
+ var $onReadyDeferred = $q.defer();
+ $onReadyDeferred.promise.then(function () {
+ $isReady = true;
+ });
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translate#onReady
+ * @methodOf pascalprecht.translate.$translate
+ *
+ * @description
+ * Returns whether the service is "ready" to translate (i.e. loading 1st language).
+ *
+ * See also {@link pascalprecht.translate.$translate#methods_isReady isReady()}.
+ *
+ * @param {Function=} fn Function to invoke when service is ready
+ * @return {object} Promise resolved when service is ready
+ */
+ $translate.onReady = function (fn) {
+ var deferred = $q.defer();
+ if (angular.isFunction(fn)) {
+ deferred.promise.then(fn);
+ }
+ if ($isReady) {
+ deferred.resolve();
+ } else {
+ $onReadyDeferred.promise.then(deferred.resolve);
+ }
+ return deferred.promise;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translate#getAvailableLanguageKeys
+ * @methodOf pascalprecht.translate.$translate
+ *
+ * @description
+ * This function simply returns the registered language keys being defined before in the config phase
+ * With this, an application can use the array to provide a language selection dropdown or similar
+ * without any additional effort
+ *
+ * @returns {object} returns the list of possibly registered language keys and mapping or null if not defined
+ */
+ $translate.getAvailableLanguageKeys = function () {
+ if ($availableLanguageKeys.length > 0) {
+ return $availableLanguageKeys;
+ }
+ return null;
+ };
+
+ // Whenever $translateReady is being fired, this will ensure the state of $isReady
+ var globalOnReadyListener = $rootScope.$on('$translateReady', function () {
+ $onReadyDeferred.resolve();
+ globalOnReadyListener(); // one time only
+ globalOnReadyListener = null;
+ });
+ var globalOnChangeListener = $rootScope.$on('$translateChangeEnd', function () {
+ $onReadyDeferred.resolve();
+ globalOnChangeListener(); // one time only
+ globalOnChangeListener = null;
+ });
+
+ if ($loaderFactory) {
+
+ // If at least one async loader is defined and there are no
+ // (default) translations available we should try to load them.
+ if (angular.equals($translationTable, {})) {
+ if ($translate.use()) {
+ $translate.use($translate.use());
+ }
+ }
+
+ // Also, if there are any fallback language registered, we start
+ // loading them asynchronously as soon as we can.
+ if ($fallbackLanguage && $fallbackLanguage.length) {
+ var processAsyncResult = function (translation) {
+ translations(translation.key, translation.table);
+ $rootScope.$emit('$translateChangeEnd', { language: translation.key });
+ return translation;
+ };
+ for (var i = 0, len = $fallbackLanguage.length; i < len; i++) {
+ var fallbackLanguageId = $fallbackLanguage[i];
+ if ($forceAsyncReloadEnabled || !$translationTable[fallbackLanguageId]) {
+ langPromises[fallbackLanguageId] = loadAsync(fallbackLanguageId).then(processAsyncResult);
+ }
+ }
+ }
+ } else {
+ $rootScope.$emit('$translateReady', { language: $translate.use() });
+ }
+
+ return $translate;
+ }
+ ];
+}
+
+$translate.displayName = 'displayName';
+
+/**
+ * @ngdoc object
+ * @name pascalprecht.translate.$translateDefaultInterpolation
+ * @requires $interpolate
+ *
+ * @description
+ * Uses angular's `$interpolate` services to interpolate strings against some values.
+ *
+ * Be aware to configure a proper sanitization strategy.
+ *
+ * See also:
+ * * {@link pascalprecht.translate.$translateSanitization}
+ *
+ * @return {object} $translateDefaultInterpolation Interpolator service
+ */
+angular.module('pascalprecht.translate').factory('$translateDefaultInterpolation', $translateDefaultInterpolation);
+
+function $translateDefaultInterpolation ($interpolate, $translateSanitization) {
+
+ 'use strict';
+
+ var $translateInterpolator = {},
+ $locale,
+ $identifier = 'default';
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateDefaultInterpolation#setLocale
+ * @methodOf pascalprecht.translate.$translateDefaultInterpolation
+ *
+ * @description
+ * Sets current locale (this is currently not use in this interpolation).
+ *
+ * @param {string} locale Language key or locale.
+ */
+ $translateInterpolator.setLocale = function (locale) {
+ $locale = locale;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateDefaultInterpolation#getInterpolationIdentifier
+ * @methodOf pascalprecht.translate.$translateDefaultInterpolation
+ *
+ * @description
+ * Returns an identifier for this interpolation service.
+ *
+ * @returns {string} $identifier
+ */
+ $translateInterpolator.getInterpolationIdentifier = function () {
+ return $identifier;
+ };
+
+ /**
+ * @deprecated will be removed in 3.0
+ * @see {@link pascalprecht.translate.$translateSanitization}
+ */
+ $translateInterpolator.useSanitizeValueStrategy = function (value) {
+ $translateSanitization.useStrategy(value);
+ return this;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateDefaultInterpolation#interpolate
+ * @methodOf pascalprecht.translate.$translateDefaultInterpolation
+ *
+ * @description
+ * Interpolates given value agains given interpolate params using angulars
+ * `$interpolate` service.
+ *
+ * Since AngularJS 1.5, `value` must not be a string but can be anything input.
+ *
+ * @returns {string} interpolated string.
+ */
+ $translateInterpolator.interpolate = function (value, interpolationParams) {
+ interpolationParams = interpolationParams || {};
+ interpolationParams = $translateSanitization.sanitize(interpolationParams, 'params');
+
+ var interpolatedText;
+ if (angular.isNumber(value)) {
+ // numbers are safe
+ interpolatedText = '' + value;
+ } else if (angular.isString(value)) {
+ // strings must be interpolated (that's the job here)
+ interpolatedText = $interpolate(value)(interpolationParams);
+ interpolatedText = $translateSanitization.sanitize(interpolatedText, 'text');
+ } else {
+ // neither a number or a string, cant interpolate => empty string
+ interpolatedText = '';
+ }
+
+ return interpolatedText;
+ };
+
+ return $translateInterpolator;
+}
+
+$translateDefaultInterpolation.displayName = '$translateDefaultInterpolation';
+
+angular.module('pascalprecht.translate').constant('$STORAGE_KEY', 'NG_TRANSLATE_LANG_KEY');
+
+angular.module('pascalprecht.translate')
+/**
+ * @ngdoc directive
+ * @name pascalprecht.translate.directive:translate
+ * @requires $compile
+ * @requires $filter
+ * @requires $interpolate
+ * @restrict AE
+ *
+ * @description
+ * Translates given translation id either through attribute or DOM content.
+ * Internally it uses `translate` filter to translate translation id. It possible to
+ * pass an optional `translate-values` object literal as string into translation id.
+ *
+ * @param {string=} translate Translation id which could be either string or interpolated string.
+ * @param {string=} translate-values Values to pass into translation id. Can be passed as object literal string or interpolated object.
+ * @param {string=} translate-attr-ATTR translate Translation id and put it into ATTR attribute.
+ * @param {string=} translate-default will be used unless translation was successful
+ * @param {boolean=} translate-compile (default true if present) defines locally activation of {@link pascalprecht.translate.$translateProvider#methods_usePostCompiling}
+ * @param {boolean=} translate-keep-content (default true if present) defines that in case a KEY could not be translated, that the existing content is left in the innerHTML}
+ *
+ * @example
+ <example module="ngView">
+ <file name="index.html">
+ <div ng-controller="TranslateCtrl">
+
+ <pre translate="TRANSLATION_ID"></pre>
+ <pre translate>TRANSLATION_ID</pre>
+ <pre translate translate-attr-title="TRANSLATION_ID"></pre>
+ <pre translate="{{translationId}}"></pre>
+ <pre translate>{{translationId}}</pre>
+ <pre translate="WITH_VALUES" translate-values="{value: 5}"></pre>
+ <pre translate translate-values="{value: 5}">WITH_VALUES</pre>
+ <pre translate="WITH_VALUES" translate-values="{{values}}"></pre>
+ <pre translate translate-values="{{values}}">WITH_VALUES</pre>
+ <pre translate translate-attr-title="WITH_VALUES" translate-values="{{values}}"></pre>
+ <pre translate="WITH_CAMEL_CASE_KEY" translate-value-camel-case-key="Hi"></pre>
+
+ </div>
+ </file>
+ <file name="script.js">
+ angular.module('ngView', ['pascalprecht.translate'])
+
+ .config(function ($translateProvider) {
+
+ $translateProvider.translations('en',{
+ 'TRANSLATION_ID': 'Hello there!',
+ 'WITH_VALUES': 'The following value is dynamic: {{value}}',
+ 'WITH_CAMEL_CASE_KEY': 'The interpolation key is camel cased: {{camelCaseKey}}'
+ }).preferredLanguage('en');
+
+ });
+
+ angular.module('ngView').controller('TranslateCtrl', function ($scope) {
+ $scope.translationId = 'TRANSLATION_ID';
+
+ $scope.values = {
+ value: 78
+ };
+ });
+ </file>
+ <file name="scenario.js">
+ it('should translate', function () {
+ inject(function ($rootScope, $compile) {
+ $rootScope.translationId = 'TRANSLATION_ID';
+
+ element = $compile('<p translate="TRANSLATION_ID"></p>')($rootScope);
+ $rootScope.$digest();
+ expect(element.text()).toBe('Hello there!');
+
+ element = $compile('<p translate="{{translationId}}"></p>')($rootScope);
+ $rootScope.$digest();
+ expect(element.text()).toBe('Hello there!');
+
+ element = $compile('<p translate>TRANSLATION_ID</p>')($rootScope);
+ $rootScope.$digest();
+ expect(element.text()).toBe('Hello there!');
+
+ element = $compile('<p translate>{{translationId}}</p>')($rootScope);
+ $rootScope.$digest();
+ expect(element.text()).toBe('Hello there!');
+
+ element = $compile('<p translate translate-attr-title="TRANSLATION_ID"></p>')($rootScope);
+ $rootScope.$digest();
+ expect(element.attr('title')).toBe('Hello there!');
+
+ element = $compile('<p translate="WITH_CAMEL_CASE_KEY" translate-value-camel-case-key="Hello"></p>')($rootScope);
+ $rootScope.$digest();
+ expect(element.text()).toBe('The interpolation key is camel cased: Hello');
+ });
+ });
+ </file>
+ </example>
+ */
+.directive('translate', translateDirective);
+function translateDirective($translate, $q, $interpolate, $compile, $parse, $rootScope) {
+
+ 'use strict';
+
+ /**
+ * @name trim
+ * @private
+ *
+ * @description
+ * trim polyfill
+ *
+ * @returns {string} The string stripped of whitespace from both ends
+ */
+ var trim = function() {
+ return this.toString().replace(/^\s+|\s+$/g, '');
+ };
+
+ return {
+ restrict: 'AE',
+ scope: true,
+ priority: $translate.directivePriority(),
+ compile: function (tElement, tAttr) {
+
+ var translateValuesExist = (tAttr.translateValues) ?
+ tAttr.translateValues : undefined;
+
+ var translateInterpolation = (tAttr.translateInterpolation) ?
+ tAttr.translateInterpolation : undefined;
+
+ var translateValueExist = tElement[0].outerHTML.match(/translate-value-+/i);
+
+ var interpolateRegExp = '^(.*)(' + $interpolate.startSymbol() + '.*' + $interpolate.endSymbol() + ')(.*)',
+ watcherRegExp = '^(.*)' + $interpolate.startSymbol() + '(.*)' + $interpolate.endSymbol() + '(.*)';
+
+ return function linkFn(scope, iElement, iAttr) {
+
+ scope.interpolateParams = {};
+ scope.preText = '';
+ scope.postText = '';
+ scope.translateNamespace = getTranslateNamespace(scope);
+ var translationIds = {};
+
+ var initInterpolationParams = function (interpolateParams, iAttr, tAttr) {
+ // initial setup
+ if (iAttr.translateValues) {
+ angular.extend(interpolateParams, $parse(iAttr.translateValues)(scope.$parent));
+ }
+ // initially fetch all attributes if existing and fill the params
+ if (translateValueExist) {
+ for (var attr in tAttr) {
+ if (Object.prototype.hasOwnProperty.call(iAttr, attr) && attr.substr(0, 14) === 'translateValue' && attr !== 'translateValues') {
+ var attributeName = angular.lowercase(attr.substr(14, 1)) + attr.substr(15);
+ interpolateParams[attributeName] = tAttr[attr];
+ }
+ }
+ }
+ };
+
+ // Ensures any change of the attribute "translate" containing the id will
+ // be re-stored to the scope's "translationId".
+ // If the attribute has no content, the element's text value (white spaces trimmed off) will be used.
+ var observeElementTranslation = function (translationId) {
+
+ // Remove any old watcher
+ if (angular.isFunction(observeElementTranslation._unwatchOld)) {
+ observeElementTranslation._unwatchOld();
+ observeElementTranslation._unwatchOld = undefined;
+ }
+
+ if (angular.equals(translationId , '') || !angular.isDefined(translationId)) {
+ var iElementText = trim.apply(iElement.text());
+
+ // Resolve translation id by inner html if required
+ var interpolateMatches = iElementText.match(interpolateRegExp);
+ // Interpolate translation id if required
+ if (angular.isArray(interpolateMatches)) {
+ scope.preText = interpolateMatches[1];
+ scope.postText = interpolateMatches[3];
+ translationIds.translate = $interpolate(interpolateMatches[2])(scope.$parent);
+ var watcherMatches = iElementText.match(watcherRegExp);
+ if (angular.isArray(watcherMatches) && watcherMatches[2] && watcherMatches[2].length) {
+ observeElementTranslation._unwatchOld = scope.$watch(watcherMatches[2], function (newValue) {
+ translationIds.translate = newValue;
+ updateTranslations();
+ });
+ }
+ } else {
+ // do not assigne the translation id if it is empty.
+ translationIds.translate = !iElementText ? undefined : iElementText;
+ }
+ } else {
+ translationIds.translate = translationId;
+ }
+ updateTranslations();
+ };
+
+ var observeAttributeTranslation = function (translateAttr) {
+ iAttr.$observe(translateAttr, function (translationId) {
+ translationIds[translateAttr] = translationId;
+ updateTranslations();
+ });
+ };
+
+ // initial setup with values
+ initInterpolationParams(scope.interpolateParams, iAttr, tAttr);
+
+ var firstAttributeChangedEvent = true;
+ iAttr.$observe('translate', function (translationId) {
+ if (typeof translationId === 'undefined') {
+ // case of element "<translate>xyz</translate>"
+ observeElementTranslation('');
+ } else {
+ // case of regular attribute
+ if (translationId !== '' || !firstAttributeChangedEvent) {
+ translationIds.translate = translationId;
+ updateTranslations();
+ }
+ }
+ firstAttributeChangedEvent = false;
+ });
+
+ for (var translateAttr in iAttr) {
+ if (iAttr.hasOwnProperty(translateAttr) && translateAttr.substr(0, 13) === 'translateAttr') {
+ observeAttributeTranslation(translateAttr);
+ }
+ }
+
+ iAttr.$observe('translateDefault', function (value) {
+ scope.defaultText = value;
+ updateTranslations();
+ });
+
+ if (translateValuesExist) {
+ iAttr.$observe('translateValues', function (interpolateParams) {
+ if (interpolateParams) {
+ scope.$parent.$watch(function () {
+ angular.extend(scope.interpolateParams, $parse(interpolateParams)(scope.$parent));
+ });
+ }
+ });
+ }
+
+ if (translateValueExist) {
+ var observeValueAttribute = function (attrName) {
+ iAttr.$observe(attrName, function (value) {
+ var attributeName = angular.lowercase(attrName.substr(14, 1)) + attrName.substr(15);
+ scope.interpolateParams[attributeName] = value;
+ });
+ };
+ for (var attr in iAttr) {
+ if (Object.prototype.hasOwnProperty.call(iAttr, attr) && attr.substr(0, 14) === 'translateValue' && attr !== 'translateValues') {
+ observeValueAttribute(attr);
+ }
+ }
+ }
+
+ // Master update function
+ var updateTranslations = function () {
+ for (var key in translationIds) {
+ if (translationIds.hasOwnProperty(key) && translationIds[key] !== undefined) {
+ updateTranslation(key, translationIds[key], scope, scope.interpolateParams, scope.defaultText, scope.translateNamespace);
+ }
+ }
+ };
+
+ // Put translation processing function outside loop
+ var updateTranslation = function(translateAttr, translationId, scope, interpolateParams, defaultTranslationText, translateNamespace) {
+ if (translationId) {
+ // if translation id starts with '.' and translateNamespace given, prepend namespace
+ if (translateNamespace && translationId.charAt(0) === '.') {
+ translationId = translateNamespace + translationId;
+ }
+
+ $translate(translationId, interpolateParams, translateInterpolation, defaultTranslationText, scope.translateLanguage)
+ .then(function (translation) {
+ applyTranslation(translation, scope, true, translateAttr);
+ }, function (translationId) {
+ applyTranslation(translationId, scope, false, translateAttr);
+ });
+ } else {
+ // as an empty string cannot be translated, we can solve this using successful=false
+ applyTranslation(translationId, scope, false, translateAttr);
+ }
+ };
+
+ var applyTranslation = function (value, scope, successful, translateAttr) {
+ if (!successful) {
+ if (typeof scope.defaultText !== 'undefined') {
+ value = scope.defaultText;
+ }
+ }
+ if (translateAttr === 'translate') {
+ // default translate into innerHTML
+ if (successful || (!successful && !$translate.isKeepContent() && typeof iAttr.translateKeepContent === 'undefined')) {
+ iElement.empty().append(scope.preText + value + scope.postText);
+ }
+ var globallyEnabled = $translate.isPostCompilingEnabled();
+ var locallyDefined = typeof tAttr.translateCompile !== 'undefined';
+ var locallyEnabled = locallyDefined && tAttr.translateCompile !== 'false';
+ if ((globallyEnabled && !locallyDefined) || locallyEnabled) {
+ $compile(iElement.contents())(scope);
+ }
+ } else {
+ // translate attribute
+ var attributeName = iAttr.$attr[translateAttr];
+ if (attributeName.substr(0, 5) === 'data-') {
+ // ensure html5 data prefix is stripped
+ attributeName = attributeName.substr(5);
+ }
+ attributeName = attributeName.substr(15);
+ iElement.attr(attributeName, value);
+ }
+ };
+
+ if (translateValuesExist || translateValueExist || iAttr.translateDefault) {
+ scope.$watch('interpolateParams', updateTranslations, true);
+ }
+
+ // Replaced watcher on translateLanguage with event listener
+ var unbindTranslateLanguage = scope.$on('translateLanguageChanged', updateTranslations);
+
+ // Ensures the text will be refreshed after the current language was changed
+ // w/ $translate.use(...)
+ var unbind = $rootScope.$on('$translateChangeSuccess', updateTranslations);
+
+ // ensure translation will be looked up at least one
+ if (iElement.text().length) {
+ if (iAttr.translate) {
+ observeElementTranslation(iAttr.translate);
+ } else {
+ observeElementTranslation('');
+ }
+ } else if (iAttr.translate) {
+ // ensure attribute will be not skipped
+ observeElementTranslation(iAttr.translate);
+ }
+ updateTranslations();
+ scope.$on('$destroy', function(){
+ unbindTranslateLanguage();
+ unbind();
+ });
+ };
+ }
+ };
+}
+
+/**
+ * Returns the scope's namespace.
+ * @private
+ * @param scope
+ * @returns {string}
+ */
+function getTranslateNamespace(scope) {
+ 'use strict';
+ if (scope.translateNamespace) {
+ return scope.translateNamespace;
+ }
+ if (scope.$parent) {
+ return getTranslateNamespace(scope.$parent);
+ }
+}
+
+translateDirective.displayName = 'translateDirective';
+
+angular.module('pascalprecht.translate')
+/**
+ * @ngdoc directive
+ * @name pascalprecht.translate.directive:translateCloak
+ * @requires $rootScope
+ * @requires $translate
+ * @restrict A
+ *
+ * $description
+ * Adds a `translate-cloak` class name to the given element where this directive
+ * is applied initially and removes it, once a loader has finished loading.
+ *
+ * This directive can be used to prevent initial flickering when loading translation
+ * data asynchronously.
+ *
+ * The class name is defined in
+ * {@link pascalprecht.translate.$translateProvider#cloakClassName $translate.cloakClassName()}.
+ *
+ * @param {string=} translate-cloak If a translationId is provided, it will be used for showing
+ * or hiding the cloak. Basically it relies on the translation
+ * resolve.
+ */
+.directive('translateCloak', translateCloakDirective);
+
+function translateCloakDirective($translate, $rootScope) {
+
+ 'use strict';
+
+ return {
+ compile: function (tElement) {
+ var applyCloak = function () {
+ tElement.addClass($translate.cloakClassName());
+ },
+ removeCloak = function () {
+ tElement.removeClass($translate.cloakClassName());
+ };
+ $translate.onReady(function () {
+ removeCloak();
+ });
+ applyCloak();
+
+ return function linkFn(scope, iElement, iAttr) {
+ if (iAttr.translateCloak && iAttr.translateCloak.length) {
+ // Register a watcher for the defined translation allowing a fine tuned cloak
+ iAttr.$observe('translateCloak', function (translationId) {
+ $translate(translationId).then(removeCloak, applyCloak);
+ });
+ // Register for change events as this is being another indicicator revalidating the cloak)
+ $rootScope.$on('$translateChangeSuccess', function () {
+ $translate(iAttr.translateCloak).then(removeCloak, applyCloak);
+ });
+ }
+ };
+ }
+ };
+}
+
+translateCloakDirective.displayName = 'translateCloakDirective';
+
+angular.module('pascalprecht.translate')
+/**
+ * @ngdoc directive
+ * @name pascalprecht.translate.directive:translateNamespace
+ * @restrict A
+ *
+ * @description
+ * Translates given translation id either through attribute or DOM content.
+ * Internally it uses `translate` filter to translate translation id. It possible to
+ * pass an optional `translate-values` object literal as string into translation id.
+ *
+ * @param {string=} translate namespace name which could be either string or interpolated string.
+ *
+ * @example
+ <example module="ngView">
+ <file name="index.html">
+ <div translate-namespace="CONTENT">
+
+ <div>
+ <h1 translate>.HEADERS.TITLE</h1>
+ <h1 translate>.HEADERS.WELCOME</h1>
+ </div>
+
+ <div translate-namespace=".HEADERS">
+ <h1 translate>.TITLE</h1>
+ <h1 translate>.WELCOME</h1>
+ </div>
+
+ </div>
+ </file>
+ <file name="script.js">
+ angular.module('ngView', ['pascalprecht.translate'])
+
+ .config(function ($translateProvider) {
+
+ $translateProvider.translations('en',{
+ 'TRANSLATION_ID': 'Hello there!',
+ 'CONTENT': {
+ 'HEADERS': {
+ TITLE: 'Title'
+ }
+ },
+ 'CONTENT.HEADERS.WELCOME': 'Welcome'
+ }).preferredLanguage('en');
+
+ });
+
+ </file>
+ </example>
+ */
+.directive('translateNamespace', translateNamespaceDirective);
+
+function translateNamespaceDirective() {
+
+ 'use strict';
+
+ return {
+ restrict: 'A',
+ scope: true,
+ compile: function () {
+ return {
+ pre: function (scope, iElement, iAttrs) {
+ scope.translateNamespace = getTranslateNamespace(scope);
+
+ if (scope.translateNamespace && iAttrs.translateNamespace.charAt(0) === '.') {
+ scope.translateNamespace += iAttrs.translateNamespace;
+ } else {
+ scope.translateNamespace = iAttrs.translateNamespace;
+ }
+ }
+ };
+ }
+ };
+}
+
+/**
+ * Returns the scope's namespace.
+ * @private
+ * @param scope
+ * @returns {string}
+ */
+function getTranslateNamespace(scope) {
+ 'use strict';
+ if (scope.translateNamespace) {
+ return scope.translateNamespace;
+ }
+ if (scope.$parent) {
+ return getTranslateNamespace(scope.$parent);
+ }
+}
+
+translateNamespaceDirective.displayName = 'translateNamespaceDirective';
+
+angular.module('pascalprecht.translate')
+/**
+ * @ngdoc directive
+ * @name pascalprecht.translate.directive:translateLanguage
+ * @restrict A
+ *
+ * @description
+ * Forces the language to the directives in the underlying scope.
+ *
+ * @param {string=} translate language that will be negotiated.
+ *
+ * @example
+ <example module="ngView">
+ <file name="index.html">
+ <div>
+
+ <div>
+ <h1 translate>HELLO</h1>
+ </div>
+
+ <div translate-language="de">
+ <h1 translate>HELLO</h1>
+ </div>
+
+ </div>
+ </file>
+ <file name="script.js">
+ angular.module('ngView', ['pascalprecht.translate'])
+
+ .config(function ($translateProvider) {
+
+ $translateProvider
+ .translations('en',{
+ 'HELLO': 'Hello world!'
+ })
+ .translations('de',{
+ 'HELLO': 'Hallo Welt!'
+ })
+ .preferredLanguage('en');
+
+ });
+
+ </file>
+ </example>
+ */
+.directive('translateLanguage', translateLanguageDirective);
+
+function translateLanguageDirective() {
+
+ 'use strict';
+
+ return {
+ restrict: 'A',
+ scope: true,
+ compile: function () {
+ return function linkFn(scope, iElement, iAttrs) {
+
+ iAttrs.$observe('translateLanguage', function (newTranslateLanguage) {
+ scope.translateLanguage = newTranslateLanguage;
+ });
+
+ scope.$watch('translateLanguage', function(){
+ scope.$broadcast('translateLanguageChanged');
+ });
+ };
+ }
+ };
+}
+
+translateLanguageDirective.displayName = 'translateLanguageDirective';
+
+angular.module('pascalprecht.translate')
+/**
+ * @ngdoc filter
+ * @name pascalprecht.translate.filter:translate
+ * @requires $parse
+ * @requires pascalprecht.translate.$translate
+ * @function
+ *
+ * @description
+ * Uses `$translate` service to translate contents. Accepts interpolate parameters
+ * to pass dynamized values though translation.
+ *
+ * @param {string} translationId A translation id to be translated.
+ * @param {*=} interpolateParams Optional object literal (as hash or string) to pass values into translation.
+ *
+ * @returns {string} Translated text.
+ *
+ * @example
+ <example module="ngView">
+ <file name="index.html">
+ <div ng-controller="TranslateCtrl">
+
+ <pre>{{ 'TRANSLATION_ID' | translate }}</pre>
+ <pre>{{ translationId | translate }}</pre>
+ <pre>{{ 'WITH_VALUES' | translate:'{value: 5}' }}</pre>
+ <pre>{{ 'WITH_VALUES' | translate:values }}</pre>
+
+ </div>
+ </file>
+ <file name="script.js">
+ angular.module('ngView', ['pascalprecht.translate'])
+
+ .config(function ($translateProvider) {
+
+ $translateProvider.translations('en', {
+ 'TRANSLATION_ID': 'Hello there!',
+ 'WITH_VALUES': 'The following value is dynamic: {{value}}'
+ });
+ $translateProvider.preferredLanguage('en');
+
+ });
+
+ angular.module('ngView').controller('TranslateCtrl', function ($scope) {
+ $scope.translationId = 'TRANSLATION_ID';
+
+ $scope.values = {
+ value: 78
+ };
+ });
+ </file>
+ </example>
+ */
+.filter('translate', translateFilterFactory);
+
+function translateFilterFactory($parse, $translate) {
+
+ 'use strict';
+
+ var translateFilter = function (translationId, interpolateParams, interpolation, forceLanguage) {
+ if (!angular.isObject(interpolateParams)) {
+ interpolateParams = $parse(interpolateParams)(this);
+ }
+
+ return $translate.instant(translationId, interpolateParams, interpolation, forceLanguage);
+ };
+
+ if ($translate.statefulFilter()) {
+ translateFilter.$stateful = true;
+ }
+
+ return translateFilter;
+}
+
+translateFilterFactory.displayName = 'translateFilterFactory';
+
+angular.module('pascalprecht.translate')
+
+/**
+ * @ngdoc object
+ * @name pascalprecht.translate.$translationCache
+ * @requires $cacheFactory
+ *
+ * @description
+ * The first time a translation table is used, it is loaded in the translation cache for quick retrieval. You
+ * can load translation tables directly into the cache by consuming the
+ * `$translationCache` service directly.
+ *
+ * @return {object} $cacheFactory object.
+ */
+ .factory('$translationCache', $translationCache);
+
+function $translationCache($cacheFactory) {
+
+ 'use strict';
+
+ return $cacheFactory('translations');
+}
+
+$translationCache.displayName = '$translationCache';
+return 'pascalprecht.translate';
+
+}));
diff --git a/etc/js/angular-wizard.js b/etc/js/angular-wizard.js
new file mode 100644
index 00000000..b7923b73
--- /dev/null
+++ b/etc/js/angular-wizard.js
@@ -0,0 +1,446 @@
+/**
+ * Easy to use Wizard library for AngularJS
+ * @version v0.6.0 - 2015-12-31 * @link https://github.com/mgonto/angular-wizard
+ * @author Martin Gontovnikas <martin@gon.to>
+ * @license MIT License, http://www.opensource.org/licenses/MIT
+ */
+angular.module('templates-angularwizard', ['step.html', 'wizard.html']);
+
+angular.module("step.html", []).run(["$templateCache", function($templateCache) {
+ $templateCache.put("step.html",
+ "<section ng-show=\"selected\" ng-class=\"{current: selected, done: completed}\" class=\"step\" ng-transclude>\n" +
+ "</section>");
+}]);
+
+angular.module("wizard.html", []).run(["$templateCache", function($templateCache) {
+ $templateCache.put("wizard.html",
+ "<div>\n" +
+ " <div class=\"steps\" ng-transclude></div>\n" +
+ " <ul class=\"steps-indicator steps-{{getEnabledSteps().length}}\" ng-if=\"!hideIndicators\">\n" +
+ " <li ng-class=\"{default: !step.completed && !step.selected, current: step.selected && !step.completed, done: step.completed && !step.selected, editing: step.selected && step.completed}\" ng-repeat=\"step in getEnabledSteps()\">\n" +
+ " <a ng-click=\"goTo(step)\">{{step.title || step.wzTitle}}</a>\n" +
+ " </li>\n" +
+ " </ul>\n" +
+ "</div>\n" +
+ "");
+}]);
+
+angular.module('mgo-angular-wizard', ['templates-angularwizard']);
+
+angular.module('mgo-angular-wizard').directive('wzStep', function() {
+ return {
+ restrict: 'EA',
+ replace: true,
+ transclude: true,
+ scope: {
+ wzTitle: '@',
+ canenter : '=',
+ canexit : '=',
+ disabled: '@?wzDisabled',
+ description: '@',
+ wzData: '='
+ },
+ require: '^wizard',
+ templateUrl: function(element, attributes) {
+ return attributes.template || "step.html";
+ },
+ link: function($scope, $element, $attrs, wizard) {
+ $scope.title = $scope.wzTitle;
+ wizard.addStep($scope);
+ }
+ };
+});
+
+//wizard directive
+angular.module('mgo-angular-wizard').directive('wizard', function() {
+ return {
+ restrict: 'EA',
+ replace: true,
+ transclude: true,
+ scope: {
+ currentStep: '=',
+ onFinish: '&',
+ hideIndicators: '=',
+ editMode: '=',
+ name: '@'
+ },
+ templateUrl: function(element, attributes) {
+ return attributes.template || "wizard.html";
+ },
+
+ //controller for wizard directive, treat this just like an angular controller
+ controller: ['$scope', '$element', '$log', 'WizardHandler', '$q', function($scope, $element, $log, WizardHandler, $q) {
+ //this variable allows directive to load without having to pass any step validation
+ var firstRun = true;
+ //creating instance of wizard, passing this as second argument allows access to functions attached to this via Service
+ WizardHandler.addWizard($scope.name || WizardHandler.defaultName, this);
+
+ $scope.$on('$destroy', function() {
+ WizardHandler.removeWizard($scope.name || WizardHandler.defaultName);
+ });
+
+ //steps array where all the scopes of each step are added
+ $scope.steps = [];
+
+ var stepIdx = function(step) {
+ var idx = 0;
+ var res = -1;
+ angular.forEach($scope.getEnabledSteps(), function(currStep) {
+ if (currStep === step) {
+ res = idx;
+ }
+ idx++;
+ });
+ return res;
+ };
+
+ var stepByTitle = function(titleToFind) {
+ var foundStep = null;
+ angular.forEach($scope.getEnabledSteps(), function(step) {
+ if (step.wzTitle === titleToFind) {
+ foundStep = step;
+ }
+ });
+ return foundStep;
+ };
+
+ //access to context object for step validation
+ $scope.context = {};
+
+ //watching changes to currentStep
+ $scope.$watch('currentStep', function(step) {
+ //checking to make sure currentStep is truthy value
+ if (!step) return;
+ //setting stepTitle equal to current step title or default title
+ var stepTitle = $scope.selectedStep.wzTitle;
+ if ($scope.selectedStep && stepTitle !== $scope.currentStep) {
+ //invoking goTo() with step title as argument
+ $scope.goTo(stepByTitle($scope.currentStep));
+ }
+
+ });
+
+ //watching steps array length and editMode value, if edit module is undefined or null the nothing is done
+ //if edit mode is truthy, then all steps are marked as completed
+ $scope.$watch('[editMode, steps.length]', function() {
+ var editMode = $scope.editMode;
+ if (angular.isUndefined(editMode) || (editMode === null)) return;
+
+ if (editMode) {
+ angular.forEach($scope.getEnabledSteps(), function(step) {
+ step.completed = true;
+ });
+ } else {
+ var completedStepsIndex = $scope.currentStepNumber() - 1;
+ angular.forEach($scope.getEnabledSteps(), function(step, stepIndex) {
+ if(stepIndex >= completedStepsIndex) {
+ step.completed = false;
+ }
+ });
+ }
+ }, true);
+
+ //called each time step directive is loaded
+ this.addStep = function(step) {
+ //pushing the scope of directive onto step array
+ $scope.steps.push(step);
+ //if this is first step being pushed then goTo that first step
+ if ($scope.getEnabledSteps().length === 1) {
+ //goTo first step
+ $scope.goTo($scope.getEnabledSteps()[0]);
+ }
+ };
+
+ this.context = $scope.context;
+
+ $scope.getStepNumber = function(step) {
+ return stepIdx(step) + 1;
+ };
+
+ $scope.goTo = function(step) {
+ //if this is the first time the wizard is loading it bi-passes step validation
+ if(firstRun){
+ //deselect all steps so you can set fresh below
+ unselectAll();
+ $scope.selectedStep = step;
+ //making sure current step is not undefined
+ if (!angular.isUndefined($scope.currentStep)) {
+ $scope.currentStep = step.wzTitle;
+ }
+ //setting selected step to argument passed into goTo()
+ step.selected = true;
+ //emit event upwards with data on goTo() invoktion
+ $scope.$emit('wizard:stepChanged', {step: step, index: stepIdx(step)});
+ //setting variable to false so all other step changes must pass validation
+ firstRun = false;
+ } else {
+ //createing variables to capture current state that goTo() was invoked from and allow booleans
+ var thisStep;
+ //getting data for step you are transitioning out of
+ if($scope.currentStepNumber() > 0){
+ thisStep = $scope.currentStepNumber() - 1;
+ } else if ($scope.currentStepNumber() === 0){
+ thisStep = 0;
+ }
+ //$log.log('steps[thisStep] Data: ', $scope.getEnabledSteps()[thisStep].canexit);
+ $q.all([canExitStep($scope.getEnabledSteps()[thisStep], step), canEnterStep(step)]).then(function(data) {
+ if(data[0] && data[1]){
+ //deselect all steps so you can set fresh below
+ unselectAll();
+
+ //$log.log('value for canExit argument: ', $scope.currentStep.canexit);
+ $scope.selectedStep = step;
+ //making sure current step is not undefined
+ if(!angular.isUndefined($scope.currentStep)){
+ $scope.currentStep = step.wzTitle;
+ }
+ //setting selected step to argument passed into goTo()
+ step.selected = true;
+ //emit event upwards with data on goTo() invoktion
+ $scope.$emit('wizard:stepChanged', {step: step, index: stepIdx(step)});
+ //$log.log('current step number: ', $scope.currentStepNumber());
+ }
+ });
+ }
+ };
+
+ function canEnterStep(step) {
+ var defer,
+ canEnter;
+ //If no validation function is provided, allow the user to enter the step
+ if(step.canenter === undefined){
+ return true;
+ }
+ //If canenter is a boolean value instead of a function, return the value
+ if(typeof step.canenter === 'boolean'){
+ return step.canenter;
+ }
+ //Check to see if the canenter function is a promise which needs to be returned
+ canEnter = step.canenter($scope.context);
+ if(angular.isFunction(canEnter.then)){
+ defer = $q.defer();
+ canEnter.then(function(response){
+ defer.resolve(response);
+ });
+ return defer.promise;
+ } else {
+ return canEnter === true;
+ }
+ }
+
+ function canExitStep(step, stepTo) {
+ var defer,
+ canExit;
+ //Exiting the step should be allowed if no validation function was provided or if the user is moving backwards
+ if(typeof(step.canexit) === 'undefined' || $scope.getStepNumber(stepTo) < $scope.currentStepNumber()){
+ return true;
+ }
+ //If canexit is a boolean value instead of a function, return the value
+ if(typeof step.canexit === 'boolean'){
+ return step.canexit;
+ }
+ //Check to see if the canexit function is a promise which needs to be returned
+ canExit = step.canexit($scope.context);
+ if(angular.isFunction(canExit.then)){
+ defer = $q.defer();
+ canExit.then(function(response){
+ defer.resolve(response);
+ });
+ return defer.promise;
+ } else {
+ return canExit === true;
+ }
+ }
+
+ $scope.currentStepNumber = function() {
+ //retreive current step number
+ return stepIdx($scope.selectedStep) + 1;
+ };
+
+ $scope.getEnabledSteps = function() {
+ return $scope.steps.filter(function(step){
+ return step.disabled !== 'true';
+ });
+ };
+
+ //unSelect All Steps
+ function unselectAll() {
+ //traverse steps array and set each "selected" property to false
+ angular.forEach($scope.getEnabledSteps(), function (step) {
+ step.selected = false;
+ });
+ //set selectedStep variable to null
+ $scope.selectedStep = null;
+ }
+
+ //ALL METHODS ATTACHED TO this ARE ACCESSIBLE VIA WizardHandler.wizard().methodName()
+
+ this.currentStepTitle = function(){
+ return $scope.selectedStep.wzTitle;
+ };
+
+ this.currentStepDescription = function(){
+ return $scope.selectedStep.description;
+ };
+
+ this.currentStep = function(){
+ return $scope.selectedStep;
+ };
+
+ this.totalStepCount = function() {
+ return $scope.getEnabledSteps().length;
+ }
+
+ //Access to enabled steps from outside
+ this.getEnabledSteps = function(){
+ return $scope.getEnabledSteps();
+ };
+
+ //Access to current step number from outside
+ this.currentStepNumber = function(){
+ return $scope.currentStepNumber();
+ };
+ //method used for next button within step
+ this.next = function(callback) {
+ var enabledSteps = $scope.getEnabledSteps();
+ //setting variable equal to step you were on when next() was invoked
+ var index = stepIdx($scope.selectedStep);
+ //checking to see if callback is a function
+ if(angular.isFunction(callback)){
+ if(callback()){
+ if (index === enabledSteps.length - 1) {
+ this.finish();
+ } else {
+ //invoking goTo() with step number next in line
+ $scope.goTo(enabledSteps[index + 1]);
+ }
+ } else {
+ return;
+ }
+ }
+ if (!callback) {
+ //completed property set on scope which is used to add class/remove class from progress bar
+ $scope.selectedStep.completed = true;
+ }
+ //checking to see if this is the last step. If it is next behaves the same as finish()
+ if (index === enabledSteps.length - 1) {
+ this.finish();
+ } else {
+ //invoking goTo() with step number next in line
+ $scope.goTo(enabledSteps[index + 1]);
+ }
+
+ };
+
+ //used to traverse to any step, step number placed as argument
+ this.goTo = function(step) {
+ var enabledSteps = $scope.getEnabledSteps();
+ var stepTo;
+ //checking that step is a Number
+ if (angular.isNumber(step)) {
+ stepTo = enabledSteps[step];
+ } else {
+ //finding the step associated with the title entered as goTo argument
+ stepTo = stepByTitle(step);
+ }
+ //going to step
+ $scope.goTo(stepTo);
+ };
+
+ //calls finish() which calls onFinish() which is declared on an attribute and linked to controller via wizard directive.
+ this.finish = function() {
+ if ($scope.onFinish) {
+ $scope.onFinish();
+ }
+ };
+
+ this.previous = function() {
+ //getting index of current step
+ var index = stepIdx($scope.selectedStep);
+ //ensuring you aren't trying to go back from the first step
+ if (index === 0) {
+ throw new Error("Can't go back. It's already in step 0");
+ } else {
+ //go back one step from current step
+ $scope.goTo($scope.getEnabledSteps()[index - 1]);
+ }
+ };
+
+ //cancel is alias for previous.
+ this.cancel = function() {
+ //getting index of current step
+ var index = stepIdx($scope.selectedStep);
+ //ensuring you aren't trying to go back from the first step
+ if (index === 0) {
+ throw new Error("Can't go back. It's already in step 0");
+ } else {
+ //go back one step from current step
+ $scope.goTo($scope.getEnabledSteps()[0]);
+ }
+ };
+
+ //reset
+ this.reset = function(){
+ //traverse steps array and set each "completed" property to false
+ angular.forEach($scope.getEnabledSteps(), function (step) {
+ step.completed = false;
+ });
+ //go to first step
+ this.goTo(0);
+ };
+ }]
+ };
+});
+function wizardButtonDirective(action) {
+ angular.module('mgo-angular-wizard')
+ .directive(action, function() {
+ return {
+ restrict: 'A',
+ replace: false,
+ require: '^wizard',
+ link: function($scope, $element, $attrs, wizard) {
+
+ $element.on("click", function(e) {
+ e.preventDefault();
+ $scope.$apply(function() {
+ $scope.$eval($attrs[action]);
+ wizard[action.replace("wz", "").toLowerCase()]();
+ });
+ });
+ }
+ };
+ });
+}
+
+wizardButtonDirective('wzNext');
+wizardButtonDirective('wzPrevious');
+wizardButtonDirective('wzFinish');
+wizardButtonDirective('wzCancel');
+wizardButtonDirective('wzReset');
+
+angular.module('mgo-angular-wizard').factory('WizardHandler', function() {
+ var service = {};
+
+ var wizards = {};
+
+ service.defaultName = "defaultWizard";
+
+ service.addWizard = function(name, wizard) {
+ wizards[name] = wizard;
+ };
+
+ service.removeWizard = function(name) {
+ delete wizards[name];
+ };
+
+ service.wizard = function(name) {
+ var nameToUse = name;
+ if (!name) {
+ nameToUse = service.defaultName;
+ }
+
+ return wizards[nameToUse];
+ };
+
+ return service;
+});
diff --git a/etc/js/crypto-js.js b/etc/js/crypto-js.js
new file mode 100644
index 00000000..17f2b116
--- /dev/null
+++ b/etc/js/crypto-js.js
@@ -0,0 +1,5988 @@
+;(function (root, factory) {
+ if (typeof exports === "object") {
+ // CommonJS
+ module.exports = exports = factory();
+ }
+ else if (typeof define === "function" && define.amd) {
+ // AMD
+ define([], factory);
+ }
+ else {
+ // Global (browser)
+ root.CryptoJS = factory();
+ }
+}(this, function () {
+
+ /**
+ * CryptoJS core components.
+ */
+ var CryptoJS = CryptoJS || (function (Math, undefined) {
+ /*
+ * Local polyfil of Object.create
+ */
+ var create = Object.create || (function () {
+ function F() {};
+
+ return function (obj) {
+ var subtype;
+
+ F.prototype = obj;
+
+ subtype = new F();
+
+ F.prototype = null;
+
+ return subtype;
+ };
+ }())
+
+ /**
+ * CryptoJS namespace.
+ */
+ var C = {};
+
+ /**
+ * Library namespace.
+ */
+ var C_lib = C.lib = {};
+
+ /**
+ * Base object for prototypal inheritance.
+ */
+ var Base = C_lib.Base = (function () {
+
+
+ return {
+ /**
+ * Creates a new object that inherits from this object.
+ *
+ * @param {Object} overrides Properties to copy into the new object.
+ *
+ * @return {Object} The new object.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var MyType = CryptoJS.lib.Base.extend({
+ * field: 'value',
+ *
+ * method: function () {
+ * }
+ * });
+ */
+ extend: function (overrides) {
+ // Spawn
+ var subtype = create(this);
+
+ // Augment
+ if (overrides) {
+ subtype.mixIn(overrides);
+ }
+
+ // Create default initializer
+ if (!subtype.hasOwnProperty('init') || this.init === subtype.init) {
+ subtype.init = function () {
+ subtype.$super.init.apply(this, arguments);
+ };
+ }
+
+ // Initializer's prototype is the subtype object
+ subtype.init.prototype = subtype;
+
+ // Reference supertype
+ subtype.$super = this;
+
+ return subtype;
+ },
+
+ /**
+ * Extends this object and runs the init method.
+ * Arguments to create() will be passed to init().
+ *
+ * @return {Object} The new object.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var instance = MyType.create();
+ */
+ create: function () {
+ var instance = this.extend();
+ instance.init.apply(instance, arguments);
+
+ return instance;
+ },
+
+ /**
+ * Initializes a newly created object.
+ * Override this method to add some logic when your objects are created.
+ *
+ * @example
+ *
+ * var MyType = CryptoJS.lib.Base.extend({
+ * init: function () {
+ * // ...
+ * }
+ * });
+ */
+ init: function () {
+ },
+
+ /**
+ * Copies properties into this object.
+ *
+ * @param {Object} properties The properties to mix in.
+ *
+ * @example
+ *
+ * MyType.mixIn({
+ * field: 'value'
+ * });
+ */
+ mixIn: function (properties) {
+ for (var propertyName in properties) {
+ if (properties.hasOwnProperty(propertyName)) {
+ this[propertyName] = properties[propertyName];
+ }
+ }
+
+ // IE won't copy toString using the loop above
+ if (properties.hasOwnProperty('toString')) {
+ this.toString = properties.toString;
+ }
+ },
+
+ /**
+ * Creates a copy of this object.
+ *
+ * @return {Object} The clone.
+ *
+ * @example
+ *
+ * var clone = instance.clone();
+ */
+ clone: function () {
+ return this.init.prototype.extend(this);
+ }
+ };
+ }());
+
+ /**
+ * An array of 32-bit words.
+ *
+ * @property {Array} words The array of 32-bit words.
+ * @property {number} sigBytes The number of significant bytes in this word array.
+ */
+ var WordArray = C_lib.WordArray = Base.extend({
+ /**
+ * Initializes a newly created word array.
+ *
+ * @param {Array} words (Optional) An array of 32-bit words.
+ * @param {number} sigBytes (Optional) The number of significant bytes in the words.
+ *
+ * @example
+ *
+ * var wordArray = CryptoJS.lib.WordArray.create();
+ * var wordArray = CryptoJS.lib.WordArray.create([0x00010203, 0x04050607]);
+ * var wordArray = CryptoJS.lib.WordArray.create([0x00010203, 0x04050607], 6);
+ */
+ init: function (words, sigBytes) {
+ words = this.words = words || [];
+
+ if (sigBytes != undefined) {
+ this.sigBytes = sigBytes;
+ } else {
+ this.sigBytes = words.length * 4;
+ }
+ },
+
+ /**
+ * Converts this word array to a string.
+ *
+ * @param {Encoder} encoder (Optional) The encoding strategy to use. Default: CryptoJS.enc.Hex
+ *
+ * @return {string} The stringified word array.
+ *
+ * @example
+ *
+ * var string = wordArray + '';
+ * var string = wordArray.toString();
+ * var string = wordArray.toString(CryptoJS.enc.Utf8);
+ */
+ toString: function (encoder) {
+ return (encoder || Hex).stringify(this);
+ },
+
+ /**
+ * Concatenates a word array to this word array.
+ *
+ * @param {WordArray} wordArray The word array to append.
+ *
+ * @return {WordArray} This word array.
+ *
+ * @example
+ *
+ * wordArray1.concat(wordArray2);
+ */
+ concat: function (wordArray) {
+ // Shortcuts
+ var thisWords = this.words;
+ var thatWords = wordArray.words;
+ var thisSigBytes = this.sigBytes;
+ var thatSigBytes = wordArray.sigBytes;
+
+ // Clamp excess bits
+ this.clamp();
+
+ // Concat
+ if (thisSigBytes % 4) {
+ // Copy one byte at a time
+ for (var i = 0; i < thatSigBytes; i++) {
+ var thatByte = (thatWords[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
+ thisWords[(thisSigBytes + i) >>> 2] |= thatByte << (24 - ((thisSigBytes + i) % 4) * 8);
+ }
+ } else {
+ // Copy one word at a time
+ for (var i = 0; i < thatSigBytes; i += 4) {
+ thisWords[(thisSigBytes + i) >>> 2] = thatWords[i >>> 2];
+ }
+ }
+ this.sigBytes += thatSigBytes;
+
+ // Chainable
+ return this;
+ },
+
+ /**
+ * Removes insignificant bits.
+ *
+ * @example
+ *
+ * wordArray.clamp();
+ */
+ clamp: function () {
+ // Shortcuts
+ var words = this.words;
+ var sigBytes = this.sigBytes;
+
+ // Clamp
+ words[sigBytes >>> 2] &= 0xffffffff << (32 - (sigBytes % 4) * 8);
+ words.length = Math.ceil(sigBytes / 4);
+ },
+
+ /**
+ * Creates a copy of this word array.
+ *
+ * @return {WordArray} The clone.
+ *
+ * @example
+ *
+ * var clone = wordArray.clone();
+ */
+ clone: function () {
+ var clone = Base.clone.call(this);
+ clone.words = this.words.slice(0);
+
+ return clone;
+ },
+
+ /**
+ * Creates a word array filled with random bytes.
+ *
+ * @param {number} nBytes The number of random bytes to generate.
+ *
+ * @return {WordArray} The random word array.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var wordArray = CryptoJS.lib.WordArray.random(16);
+ */
+ random: function (nBytes) {
+ var words = [];
+
+ var r = (function (m_w) {
+ var m_w = m_w;
+ var m_z = 0x3ade68b1;
+ var mask = 0xffffffff;
+
+ return function () {
+ m_z = (0x9069 * (m_z & 0xFFFF) + (m_z >> 0x10)) & mask;
+ m_w = (0x4650 * (m_w & 0xFFFF) + (m_w >> 0x10)) & mask;
+ var result = ((m_z << 0x10) + m_w) & mask;
+ result /= 0x100000000;
+ result += 0.5;
+ return result * (Math.random() > .5 ? 1 : -1);
+ }
+ });
+
+ for (var i = 0, rcache; i < nBytes; i += 4) {
+ var _r = r((rcache || Math.random()) * 0x100000000);
+
+ rcache = _r() * 0x3ade67b7;
+ words.push((_r() * 0x100000000) | 0);
+ }
+
+ return new WordArray.init(words, nBytes);
+ }
+ });
+
+ /**
+ * Encoder namespace.
+ */
+ var C_enc = C.enc = {};
+
+ /**
+ * Hex encoding strategy.
+ */
+ var Hex = C_enc.Hex = {
+ /**
+ * Converts a word array to a hex string.
+ *
+ * @param {WordArray} wordArray The word array.
+ *
+ * @return {string} The hex string.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var hexString = CryptoJS.enc.Hex.stringify(wordArray);
+ */
+ stringify: function (wordArray) {
+ // Shortcuts
+ var words = wordArray.words;
+ var sigBytes = wordArray.sigBytes;
+
+ // Convert
+ var hexChars = [];
+ for (var i = 0; i < sigBytes; i++) {
+ var bite = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
+ hexChars.push((bite >>> 4).toString(16));
+ hexChars.push((bite & 0x0f).toString(16));
+ }
+
+ return hexChars.join('');
+ },
+
+ /**
+ * Converts a hex string to a word array.
+ *
+ * @param {string} hexStr The hex string.
+ *
+ * @return {WordArray} The word array.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var wordArray = CryptoJS.enc.Hex.parse(hexString);
+ */
+ parse: function (hexStr) {
+ // Shortcut
+ var hexStrLength = hexStr.length;
+
+ // Convert
+ var words = [];
+ for (var i = 0; i < hexStrLength; i += 2) {
+ words[i >>> 3] |= parseInt(hexStr.substr(i, 2), 16) << (24 - (i % 8) * 4);
+ }
+
+ return new WordArray.init(words, hexStrLength / 2);
+ }
+ };
+
+ /**
+ * Latin1 encoding strategy.
+ */
+ var Latin1 = C_enc.Latin1 = {
+ /**
+ * Converts a word array to a Latin1 string.
+ *
+ * @param {WordArray} wordArray The word array.
+ *
+ * @return {string} The Latin1 string.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var latin1String = CryptoJS.enc.Latin1.stringify(wordArray);
+ */
+ stringify: function (wordArray) {
+ // Shortcuts
+ var words = wordArray.words;
+ var sigBytes = wordArray.sigBytes;
+
+ // Convert
+ var latin1Chars = [];
+ for (var i = 0; i < sigBytes; i++) {
+ var bite = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
+ latin1Chars.push(String.fromCharCode(bite));
+ }
+
+ return latin1Chars.join('');
+ },
+
+ /**
+ * Converts a Latin1 string to a word array.
+ *
+ * @param {string} latin1Str The Latin1 string.
+ *
+ * @return {WordArray} The word array.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var wordArray = CryptoJS.enc.Latin1.parse(latin1String);
+ */
+ parse: function (latin1Str) {
+ // Shortcut
+ var latin1StrLength = latin1Str.length;
+
+ // Convert
+ var words = [];
+ for (var i = 0; i < latin1StrLength; i++) {
+ words[i >>> 2] |= (latin1Str.charCodeAt(i) & 0xff) << (24 - (i % 4) * 8);
+ }
+
+ return new WordArray.init(words, latin1StrLength);
+ }
+ };
+
+ /**
+ * UTF-8 encoding strategy.
+ */
+ var Utf8 = C_enc.Utf8 = {
+ /**
+ * Converts a word array to a UTF-8 string.
+ *
+ * @param {WordArray} wordArray The word array.
+ *
+ * @return {string} The UTF-8 string.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var utf8String = CryptoJS.enc.Utf8.stringify(wordArray);
+ */
+ stringify: function (wordArray) {
+ try {
+ return decodeURIComponent(escape(Latin1.stringify(wordArray)));
+ } catch (e) {
+ throw new Error('Malformed UTF-8 data');
+ }
+ },
+
+ /**
+ * Converts a UTF-8 string to a word array.
+ *
+ * @param {string} utf8Str The UTF-8 string.
+ *
+ * @return {WordArray} The word array.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var wordArray = CryptoJS.enc.Utf8.parse(utf8String);
+ */
+ parse: function (utf8Str) {
+ return Latin1.parse(unescape(encodeURIComponent(utf8Str)));
+ }
+ };
+
+ /**
+ * Abstract buffered block algorithm template.
+ *
+ * The property blockSize must be implemented in a concrete subtype.
+ *
+ * @property {number} _minBufferSize The number of blocks that should be kept unprocessed in the buffer. Default: 0
+ */
+ var BufferedBlockAlgorithm = C_lib.BufferedBlockAlgorithm = Base.extend({
+ /**
+ * Resets this block algorithm's data buffer to its initial state.
+ *
+ * @example
+ *
+ * bufferedBlockAlgorithm.reset();
+ */
+ reset: function () {
+ // Initial values
+ this._data = new WordArray.init();
+ this._nDataBytes = 0;
+ },
+
+ /**
+ * Adds new data to this block algorithm's buffer.
+ *
+ * @param {WordArray|string} data The data to append. Strings are converted to a WordArray using UTF-8.
+ *
+ * @example
+ *
+ * bufferedBlockAlgorithm._append('data');
+ * bufferedBlockAlgorithm._append(wordArray);
+ */
+ _append: function (data) {
+ // Convert string to WordArray, else assume WordArray already
+ if (typeof data == 'string') {
+ data = Utf8.parse(data);
+ }
+
+ // Append
+ this._data.concat(data);
+ this._nDataBytes += data.sigBytes;
+ },
+
+ /**
+ * Processes available data blocks.
+ *
+ * This method invokes _doProcessBlock(offset), which must be implemented by a concrete subtype.
+ *
+ * @param {boolean} doFlush Whether all blocks and partial blocks should be processed.
+ *
+ * @return {WordArray} The processed data.
+ *
+ * @example
+ *
+ * var processedData = bufferedBlockAlgorithm._process();
+ * var processedData = bufferedBlockAlgorithm._process(!!'flush');
+ */
+ _process: function (doFlush) {
+ // Shortcuts
+ var data = this._data;
+ var dataWords = data.words;
+ var dataSigBytes = data.sigBytes;
+ var blockSize = this.blockSize;
+ var blockSizeBytes = blockSize * 4;
+
+ // Count blocks ready
+ var nBlocksReady = dataSigBytes / blockSizeBytes;
+ if (doFlush) {
+ // Round up to include partial blocks
+ nBlocksReady = Math.ceil(nBlocksReady);
+ } else {
+ // Round down to include only full blocks,
+ // less the number of blocks that must remain in the buffer
+ nBlocksReady = Math.max((nBlocksReady | 0) - this._minBufferSize, 0);
+ }
+
+ // Count words ready
+ var nWordsReady = nBlocksReady * blockSize;
+
+ // Count bytes ready
+ var nBytesReady = Math.min(nWordsReady * 4, dataSigBytes);
+
+ // Process blocks
+ if (nWordsReady) {
+ for (var offset = 0; offset < nWordsReady; offset += blockSize) {
+ // Perform concrete-algorithm logic
+ this._doProcessBlock(dataWords, offset);
+ }
+
+ // Remove processed words
+ var processedWords = dataWords.splice(0, nWordsReady);
+ data.sigBytes -= nBytesReady;
+ }
+
+ // Return processed words
+ return new WordArray.init(processedWords, nBytesReady);
+ },
+
+ /**
+ * Creates a copy of this object.
+ *
+ * @return {Object} The clone.
+ *
+ * @example
+ *
+ * var clone = bufferedBlockAlgorithm.clone();
+ */
+ clone: function () {
+ var clone = Base.clone.call(this);
+ clone._data = this._data.clone();
+
+ return clone;
+ },
+
+ _minBufferSize: 0
+ });
+
+ /**
+ * Abstract hasher template.
+ *
+ * @property {number} blockSize The number of 32-bit words this hasher operates on. Default: 16 (512 bits)
+ */
+ var Hasher = C_lib.Hasher = BufferedBlockAlgorithm.extend({
+ /**
+ * Configuration options.
+ */
+ cfg: Base.extend(),
+
+ /**
+ * Initializes a newly created hasher.
+ *
+ * @param {Object} cfg (Optional) The configuration options to use for this hash computation.
+ *
+ * @example
+ *
+ * var hasher = CryptoJS.algo.SHA256.create();
+ */
+ init: function (cfg) {
+ // Apply config defaults
+ this.cfg = this.cfg.extend(cfg);
+
+ // Set initial values
+ this.reset();
+ },
+
+ /**
+ * Resets this hasher to its initial state.
+ *
+ * @example
+ *
+ * hasher.reset();
+ */
+ reset: function () {
+ // Reset data buffer
+ BufferedBlockAlgorithm.reset.call(this);
+
+ // Perform concrete-hasher logic
+ this._doReset();
+ },
+
+ /**
+ * Updates this hasher with a message.
+ *
+ * @param {WordArray|string} messageUpdate The message to append.
+ *
+ * @return {Hasher} This hasher.
+ *
+ * @example
+ *
+ * hasher.update('message');
+ * hasher.update(wordArray);
+ */
+ update: function (messageUpdate) {
+ // Append
+ this._append(messageUpdate);
+
+ // Update the hash
+ this._process();
+
+ // Chainable
+ return this;
+ },
+
+ /**
+ * Finalizes the hash computation.
+ * Note that the finalize operation is effectively a destructive, read-once operation.
+ *
+ * @param {WordArray|string} messageUpdate (Optional) A final message update.
+ *
+ * @return {WordArray} The hash.
+ *
+ * @example
+ *
+ * var hash = hasher.finalize();
+ * var hash = hasher.finalize('message');
+ * var hash = hasher.finalize(wordArray);
+ */
+ finalize: function (messageUpdate) {
+ // Final message update
+ if (messageUpdate) {
+ this._append(messageUpdate);
+ }
+
+ // Perform concrete-hasher logic
+ var hash = this._doFinalize();
+
+ return hash;
+ },
+
+ blockSize: 512/32,
+
+ /**
+ * Creates a shortcut function to a hasher's object interface.
+ *
+ * @param {Hasher} hasher The hasher to create a helper for.
+ *
+ * @return {Function} The shortcut function.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var SHA256 = CryptoJS.lib.Hasher._createHelper(CryptoJS.algo.SHA256);
+ */
+ _createHelper: function (hasher) {
+ return function (message, cfg) {
+ return new hasher.init(cfg).finalize(message);
+ };
+ },
+
+ /**
+ * Creates a shortcut function to the HMAC's object interface.
+ *
+ * @param {Hasher} hasher The hasher to use in this HMAC helper.
+ *
+ * @return {Function} The shortcut function.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var HmacSHA256 = CryptoJS.lib.Hasher._createHmacHelper(CryptoJS.algo.SHA256);
+ */
+ _createHmacHelper: function (hasher) {
+ return function (message, key) {
+ return new C_algo.HMAC.init(hasher, key).finalize(message);
+ };
+ }
+ });
+
+ /**
+ * Algorithm namespace.
+ */
+ var C_algo = C.algo = {};
+
+ return C;
+ }(Math));
+
+
+ (function () {
+ // Shortcuts
+ var C = CryptoJS;
+ var C_lib = C.lib;
+ var WordArray = C_lib.WordArray;
+ var C_enc = C.enc;
+
+ /**
+ * Base64 encoding strategy.
+ */
+ var Base64 = C_enc.Base64 = {
+ /**
+ * Converts a word array to a Base64 string.
+ *
+ * @param {WordArray} wordArray The word array.
+ *
+ * @return {string} The Base64 string.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var base64String = CryptoJS.enc.Base64.stringify(wordArray);
+ */
+ stringify: function (wordArray) {
+ // Shortcuts
+ var words = wordArray.words;
+ var sigBytes = wordArray.sigBytes;
+ var map = this._map;
+
+ // Clamp excess bits
+ wordArray.clamp();
+
+ // Convert
+ var base64Chars = [];
+ for (var i = 0; i < sigBytes; i += 3) {
+ var byte1 = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
+ var byte2 = (words[(i + 1) >>> 2] >>> (24 - ((i + 1) % 4) * 8)) & 0xff;
+ var byte3 = (words[(i + 2) >>> 2] >>> (24 - ((i + 2) % 4) * 8)) & 0xff;
+
+ var triplet = (byte1 << 16) | (byte2 << 8) | byte3;
+
+ for (var j = 0; (j < 4) && (i + j * 0.75 < sigBytes); j++) {
+ base64Chars.push(map.charAt((triplet >>> (6 * (3 - j))) & 0x3f));
+ }
+ }
+
+ // Add padding
+ var paddingChar = map.charAt(64);
+ if (paddingChar) {
+ while (base64Chars.length % 4) {
+ base64Chars.push(paddingChar);
+ }
+ }
+
+ return base64Chars.join('');
+ },
+
+ /**
+ * Converts a Base64 string to a word array.
+ *
+ * @param {string} base64Str The Base64 string.
+ *
+ * @return {WordArray} The word array.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var wordArray = CryptoJS.enc.Base64.parse(base64String);
+ */
+ parse: function (base64Str) {
+ // Shortcuts
+ var base64StrLength = base64Str.length;
+ var map = this._map;
+ var reverseMap = this._reverseMap;
+
+ if (!reverseMap) {
+ reverseMap = this._reverseMap = [];
+ for (var j = 0; j < map.length; j++) {
+ reverseMap[map.charCodeAt(j)] = j;
+ }
+ }
+
+ // Ignore padding
+ var paddingChar = map.charAt(64);
+ if (paddingChar) {
+ var paddingIndex = base64Str.indexOf(paddingChar);
+ if (paddingIndex !== -1) {
+ base64StrLength = paddingIndex;
+ }
+ }
+
+ // Convert
+ return parseLoop(base64Str, base64StrLength, reverseMap);
+
+ },
+
+ _map: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
+ };
+
+ function parseLoop(base64Str, base64StrLength, reverseMap) {
+ var words = [];
+ var nBytes = 0;
+ for (var i = 0; i < base64StrLength; i++) {
+ if (i % 4) {
+ var bits1 = reverseMap[base64Str.charCodeAt(i - 1)] << ((i % 4) * 2);
+ var bits2 = reverseMap[base64Str.charCodeAt(i)] >>> (6 - (i % 4) * 2);
+ words[nBytes >>> 2] |= (bits1 | bits2) << (24 - (nBytes % 4) * 8);
+ nBytes++;
+ }
+ }
+ return WordArray.create(words, nBytes);
+ }
+ }());
+
+
+ (function (Math) {
+ // Shortcuts
+ var C = CryptoJS;
+ var C_lib = C.lib;
+ var WordArray = C_lib.WordArray;
+ var Hasher = C_lib.Hasher;
+ var C_algo = C.algo;
+
+ // Constants table
+ var T = [];
+
+ // Compute constants
+ (function () {
+ for (var i = 0; i < 64; i++) {
+ T[i] = (Math.abs(Math.sin(i + 1)) * 0x100000000) | 0;
+ }
+ }());
+
+ /**
+ * MD5 hash algorithm.
+ */
+ var MD5 = C_algo.MD5 = Hasher.extend({
+ _doReset: function () {
+ this._hash = new WordArray.init([
+ 0x67452301, 0xefcdab89,
+ 0x98badcfe, 0x10325476
+ ]);
+ },
+
+ _doProcessBlock: function (M, offset) {
+ // Swap endian
+ for (var i = 0; i < 16; i++) {
+ // Shortcuts
+ var offset_i = offset + i;
+ var M_offset_i = M[offset_i];
+
+ M[offset_i] = (
+ (((M_offset_i << 8) | (M_offset_i >>> 24)) & 0x00ff00ff) |
+ (((M_offset_i << 24) | (M_offset_i >>> 8)) & 0xff00ff00)
+ );
+ }
+
+ // Shortcuts
+ var H = this._hash.words;
+
+ var M_offset_0 = M[offset + 0];
+ var M_offset_1 = M[offset + 1];
+ var M_offset_2 = M[offset + 2];
+ var M_offset_3 = M[offset + 3];
+ var M_offset_4 = M[offset + 4];
+ var M_offset_5 = M[offset + 5];
+ var M_offset_6 = M[offset + 6];
+ var M_offset_7 = M[offset + 7];
+ var M_offset_8 = M[offset + 8];
+ var M_offset_9 = M[offset + 9];
+ var M_offset_10 = M[offset + 10];
+ var M_offset_11 = M[offset + 11];
+ var M_offset_12 = M[offset + 12];
+ var M_offset_13 = M[offset + 13];
+ var M_offset_14 = M[offset + 14];
+ var M_offset_15 = M[offset + 15];
+
+ // Working varialbes
+ var a = H[0];
+ var b = H[1];
+ var c = H[2];
+ var d = H[3];
+
+ // Computation
+ a = FF(a, b, c, d, M_offset_0, 7, T[0]);
+ d = FF(d, a, b, c, M_offset_1, 12, T[1]);
+ c = FF(c, d, a, b, M_offset_2, 17, T[2]);
+ b = FF(b, c, d, a, M_offset_3, 22, T[3]);
+ a = FF(a, b, c, d, M_offset_4, 7, T[4]);
+ d = FF(d, a, b, c, M_offset_5, 12, T[5]);
+ c = FF(c, d, a, b, M_offset_6, 17, T[6]);
+ b = FF(b, c, d, a, M_offset_7, 22, T[7]);
+ a = FF(a, b, c, d, M_offset_8, 7, T[8]);
+ d = FF(d, a, b, c, M_offset_9, 12, T[9]);
+ c = FF(c, d, a, b, M_offset_10, 17, T[10]);
+ b = FF(b, c, d, a, M_offset_11, 22, T[11]);
+ a = FF(a, b, c, d, M_offset_12, 7, T[12]);
+ d = FF(d, a, b, c, M_offset_13, 12, T[13]);
+ c = FF(c, d, a, b, M_offset_14, 17, T[14]);
+ b = FF(b, c, d, a, M_offset_15, 22, T[15]);
+
+ a = GG(a, b, c, d, M_offset_1, 5, T[16]);
+ d = GG(d, a, b, c, M_offset_6, 9, T[17]);
+ c = GG(c, d, a, b, M_offset_11, 14, T[18]);
+ b = GG(b, c, d, a, M_offset_0, 20, T[19]);
+ a = GG(a, b, c, d, M_offset_5, 5, T[20]);
+ d = GG(d, a, b, c, M_offset_10, 9, T[21]);
+ c = GG(c, d, a, b, M_offset_15, 14, T[22]);
+ b = GG(b, c, d, a, M_offset_4, 20, T[23]);
+ a = GG(a, b, c, d, M_offset_9, 5, T[24]);
+ d = GG(d, a, b, c, M_offset_14, 9, T[25]);
+ c = GG(c, d, a, b, M_offset_3, 14, T[26]);
+ b = GG(b, c, d, a, M_offset_8, 20, T[27]);
+ a = GG(a, b, c, d, M_offset_13, 5, T[28]);
+ d = GG(d, a, b, c, M_offset_2, 9, T[29]);
+ c = GG(c, d, a, b, M_offset_7, 14, T[30]);
+ b = GG(b, c, d, a, M_offset_12, 20, T[31]);
+
+ a = HH(a, b, c, d, M_offset_5, 4, T[32]);
+ d = HH(d, a, b, c, M_offset_8, 11, T[33]);
+ c = HH(c, d, a, b, M_offset_11, 16, T[34]);
+ b = HH(b, c, d, a, M_offset_14, 23, T[35]);
+ a = HH(a, b, c, d, M_offset_1, 4, T[36]);
+ d = HH(d, a, b, c, M_offset_4, 11, T[37]);
+ c = HH(c, d, a, b, M_offset_7, 16, T[38]);
+ b = HH(b, c, d, a, M_offset_10, 23, T[39]);
+ a = HH(a, b, c, d, M_offset_13, 4, T[40]);
+ d = HH(d, a, b, c, M_offset_0, 11, T[41]);
+ c = HH(c, d, a, b, M_offset_3, 16, T[42]);
+ b = HH(b, c, d, a, M_offset_6, 23, T[43]);
+ a = HH(a, b, c, d, M_offset_9, 4, T[44]);
+ d = HH(d, a, b, c, M_offset_12, 11, T[45]);
+ c = HH(c, d, a, b, M_offset_15, 16, T[46]);
+ b = HH(b, c, d, a, M_offset_2, 23, T[47]);
+
+ a = II(a, b, c, d, M_offset_0, 6, T[48]);
+ d = II(d, a, b, c, M_offset_7, 10, T[49]);
+ c = II(c, d, a, b, M_offset_14, 15, T[50]);
+ b = II(b, c, d, a, M_offset_5, 21, T[51]);
+ a = II(a, b, c, d, M_offset_12, 6, T[52]);
+ d = II(d, a, b, c, M_offset_3, 10, T[53]);
+ c = II(c, d, a, b, M_offset_10, 15, T[54]);
+ b = II(b, c, d, a, M_offset_1, 21, T[55]);
+ a = II(a, b, c, d, M_offset_8, 6, T[56]);
+ d = II(d, a, b, c, M_offset_15, 10, T[57]);
+ c = II(c, d, a, b, M_offset_6, 15, T[58]);
+ b = II(b, c, d, a, M_offset_13, 21, T[59]);
+ a = II(a, b, c, d, M_offset_4, 6, T[60]);
+ d = II(d, a, b, c, M_offset_11, 10, T[61]);
+ c = II(c, d, a, b, M_offset_2, 15, T[62]);
+ b = II(b, c, d, a, M_offset_9, 21, T[63]);
+
+ // Intermediate hash value
+ H[0] = (H[0] + a) | 0;
+ H[1] = (H[1] + b) | 0;
+ H[2] = (H[2] + c) | 0;
+ H[3] = (H[3] + d) | 0;
+ },
+
+ _doFinalize: function () {
+ // Shortcuts
+ var data = this._data;
+ var dataWords = data.words;
+
+ var nBitsTotal = this._nDataBytes * 8;
+ var nBitsLeft = data.sigBytes * 8;
+
+ // Add padding
+ dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32);
+
+ var nBitsTotalH = Math.floor(nBitsTotal / 0x100000000);
+ var nBitsTotalL = nBitsTotal;
+ dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 15] = (
+ (((nBitsTotalH << 8) | (nBitsTotalH >>> 24)) & 0x00ff00ff) |
+ (((nBitsTotalH << 24) | (nBitsTotalH >>> 8)) & 0xff00ff00)
+ );
+ dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 14] = (
+ (((nBitsTotalL << 8) | (nBitsTotalL >>> 24)) & 0x00ff00ff) |
+ (((nBitsTotalL << 24) | (nBitsTotalL >>> 8)) & 0xff00ff00)
+ );
+
+ data.sigBytes = (dataWords.length + 1) * 4;
+
+ // Hash final blocks
+ this._process();
+
+ // Shortcuts
+ var hash = this._hash;
+ var H = hash.words;
+
+ // Swap endian
+ for (var i = 0; i < 4; i++) {
+ // Shortcut
+ var H_i = H[i];
+
+ H[i] = (((H_i << 8) | (H_i >>> 24)) & 0x00ff00ff) |
+ (((H_i << 24) | (H_i >>> 8)) & 0xff00ff00);
+ }
+
+ // Return final computed hash
+ return hash;
+ },
+
+ clone: function () {
+ var clone = Hasher.clone.call(this);
+ clone._hash = this._hash.clone();
+
+ return clone;
+ }
+ });
+
+ function FF(a, b, c, d, x, s, t) {
+ var n = a + ((b & c) | (~b & d)) + x + t;
+ return ((n << s) | (n >>> (32 - s))) + b;
+ }
+
+ function GG(a, b, c, d, x, s, t) {
+ var n = a + ((b & d) | (c & ~d)) + x + t;
+ return ((n << s) | (n >>> (32 - s))) + b;
+ }
+
+ function HH(a, b, c, d, x, s, t) {
+ var n = a + (b ^ c ^ d) + x + t;
+ return ((n << s) | (n >>> (32 - s))) + b;
+ }
+
+ function II(a, b, c, d, x, s, t) {
+ var n = a + (c ^ (b | ~d)) + x + t;
+ return ((n << s) | (n >>> (32 - s))) + b;
+ }
+
+ /**
+ * Shortcut function to the hasher's object interface.
+ *
+ * @param {WordArray|string} message The message to hash.
+ *
+ * @return {WordArray} The hash.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var hash = CryptoJS.MD5('message');
+ * var hash = CryptoJS.MD5(wordArray);
+ */
+ C.MD5 = Hasher._createHelper(MD5);
+
+ /**
+ * Shortcut function to the HMAC's object interface.
+ *
+ * @param {WordArray|string} message The message to hash.
+ * @param {WordArray|string} key The secret key.
+ *
+ * @return {WordArray} The HMAC.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var hmac = CryptoJS.HmacMD5(message, key);
+ */
+ C.HmacMD5 = Hasher._createHmacHelper(MD5);
+ }(Math));
+
+
+ (function () {
+ // Shortcuts
+ var C = CryptoJS;
+ var C_lib = C.lib;
+ var WordArray = C_lib.WordArray;
+ var Hasher = C_lib.Hasher;
+ var C_algo = C.algo;
+
+ // Reusable object
+ var W = [];
+
+ /**
+ * SHA-1 hash algorithm.
+ */
+ var SHA1 = C_algo.SHA1 = Hasher.extend({
+ _doReset: function () {
+ this._hash = new WordArray.init([
+ 0x67452301, 0xefcdab89,
+ 0x98badcfe, 0x10325476,
+ 0xc3d2e1f0
+ ]);
+ },
+
+ _doProcessBlock: function (M, offset) {
+ // Shortcut
+ var H = this._hash.words;
+
+ // Working variables
+ var a = H[0];
+ var b = H[1];
+ var c = H[2];
+ var d = H[3];
+ var e = H[4];
+
+ // Computation
+ for (var i = 0; i < 80; i++) {
+ if (i < 16) {
+ W[i] = M[offset + i] | 0;
+ } else {
+ var n = W[i - 3] ^ W[i - 8] ^ W[i - 14] ^ W[i - 16];
+ W[i] = (n << 1) | (n >>> 31);
+ }
+
+ var t = ((a << 5) | (a >>> 27)) + e + W[i];
+ if (i < 20) {
+ t += ((b & c) | (~b & d)) + 0x5a827999;
+ } else if (i < 40) {
+ t += (b ^ c ^ d) + 0x6ed9eba1;
+ } else if (i < 60) {
+ t += ((b & c) | (b & d) | (c & d)) - 0x70e44324;
+ } else /* if (i < 80) */ {
+ t += (b ^ c ^ d) - 0x359d3e2a;
+ }
+
+ e = d;
+ d = c;
+ c = (b << 30) | (b >>> 2);
+ b = a;
+ a = t;
+ }
+
+ // Intermediate hash value
+ H[0] = (H[0] + a) | 0;
+ H[1] = (H[1] + b) | 0;
+ H[2] = (H[2] + c) | 0;
+ H[3] = (H[3] + d) | 0;
+ H[4] = (H[4] + e) | 0;
+ },
+
+ _doFinalize: function () {
+ // Shortcuts
+ var data = this._data;
+ var dataWords = data.words;
+
+ var nBitsTotal = this._nDataBytes * 8;
+ var nBitsLeft = data.sigBytes * 8;
+
+ // Add padding
+ dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32);
+ dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 14] = Math.floor(nBitsTotal / 0x100000000);
+ dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 15] = nBitsTotal;
+ data.sigBytes = dataWords.length * 4;
+
+ // Hash final blocks
+ this._process();
+
+ // Return final computed hash
+ return this._hash;
+ },
+
+ clone: function () {
+ var clone = Hasher.clone.call(this);
+ clone._hash = this._hash.clone();
+
+ return clone;
+ }
+ });
+
+ /**
+ * Shortcut function to the hasher's object interface.
+ *
+ * @param {WordArray|string} message The message to hash.
+ *
+ * @return {WordArray} The hash.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var hash = CryptoJS.SHA1('message');
+ * var hash = CryptoJS.SHA1(wordArray);
+ */
+ C.SHA1 = Hasher._createHelper(SHA1);
+
+ /**
+ * Shortcut function to the HMAC's object interface.
+ *
+ * @param {WordArray|string} message The message to hash.
+ * @param {WordArray|string} key The secret key.
+ *
+ * @return {WordArray} The HMAC.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var hmac = CryptoJS.HmacSHA1(message, key);
+ */
+ C.HmacSHA1 = Hasher._createHmacHelper(SHA1);
+ }());
+
+
+ (function (Math) {
+ // Shortcuts
+ var C = CryptoJS;
+ var C_lib = C.lib;
+ var WordArray = C_lib.WordArray;
+ var Hasher = C_lib.Hasher;
+ var C_algo = C.algo;
+
+ // Initialization and round constants tables
+ var H = [];
+ var K = [];
+
+ // Compute constants
+ (function () {
+ function isPrime(n) {
+ var sqrtN = Math.sqrt(n);
+ for (var factor = 2; factor <= sqrtN; factor++) {
+ if (!(n % factor)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ function getFractionalBits(n) {
+ return ((n - (n | 0)) * 0x100000000) | 0;
+ }
+
+ var n = 2;
+ var nPrime = 0;
+ while (nPrime < 64) {
+ if (isPrime(n)) {
+ if (nPrime < 8) {
+ H[nPrime] = getFractionalBits(Math.pow(n, 1 / 2));
+ }
+ K[nPrime] = getFractionalBits(Math.pow(n, 1 / 3));
+
+ nPrime++;
+ }
+
+ n++;
+ }
+ }());
+
+ // Reusable object
+ var W = [];
+
+ /**
+ * SHA-256 hash algorithm.
+ */
+ var SHA256 = C_algo.SHA256 = Hasher.extend({
+ _doReset: function () {
+ this._hash = new WordArray.init(H.slice(0));
+ },
+
+ _doProcessBlock: function (M, offset) {
+ // Shortcut
+ var H = this._hash.words;
+
+ // Working variables
+ var a = H[0];
+ var b = H[1];
+ var c = H[2];
+ var d = H[3];
+ var e = H[4];
+ var f = H[5];
+ var g = H[6];
+ var h = H[7];
+
+ // Computation
+ for (var i = 0; i < 64; i++) {
+ if (i < 16) {
+ W[i] = M[offset + i] | 0;
+ } else {
+ var gamma0x = W[i - 15];
+ var gamma0 = ((gamma0x << 25) | (gamma0x >>> 7)) ^
+ ((gamma0x << 14) | (gamma0x >>> 18)) ^
+ (gamma0x >>> 3);
+
+ var gamma1x = W[i - 2];
+ var gamma1 = ((gamma1x << 15) | (gamma1x >>> 17)) ^
+ ((gamma1x << 13) | (gamma1x >>> 19)) ^
+ (gamma1x >>> 10);
+
+ W[i] = gamma0 + W[i - 7] + gamma1 + W[i - 16];
+ }
+
+ var ch = (e & f) ^ (~e & g);
+ var maj = (a & b) ^ (a & c) ^ (b & c);
+
+ var sigma0 = ((a << 30) | (a >>> 2)) ^ ((a << 19) | (a >>> 13)) ^ ((a << 10) | (a >>> 22));
+ var sigma1 = ((e << 26) | (e >>> 6)) ^ ((e << 21) | (e >>> 11)) ^ ((e << 7) | (e >>> 25));
+
+ var t1 = h + sigma1 + ch + K[i] + W[i];
+ var t2 = sigma0 + maj;
+
+ h = g;
+ g = f;
+ f = e;
+ e = (d + t1) | 0;
+ d = c;
+ c = b;
+ b = a;
+ a = (t1 + t2) | 0;
+ }
+
+ // Intermediate hash value
+ H[0] = (H[0] + a) | 0;
+ H[1] = (H[1] + b) | 0;
+ H[2] = (H[2] + c) | 0;
+ H[3] = (H[3] + d) | 0;
+ H[4] = (H[4] + e) | 0;
+ H[5] = (H[5] + f) | 0;
+ H[6] = (H[6] + g) | 0;
+ H[7] = (H[7] + h) | 0;
+ },
+
+ _doFinalize: function () {
+ // Shortcuts
+ var data = this._data;
+ var dataWords = data.words;
+
+ var nBitsTotal = this._nDataBytes * 8;
+ var nBitsLeft = data.sigBytes * 8;
+
+ // Add padding
+ dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32);
+ dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 14] = Math.floor(nBitsTotal / 0x100000000);
+ dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 15] = nBitsTotal;
+ data.sigBytes = dataWords.length * 4;
+
+ // Hash final blocks
+ this._process();
+
+ // Return final computed hash
+ return this._hash;
+ },
+
+ clone: function () {
+ var clone = Hasher.clone.call(this);
+ clone._hash = this._hash.clone();
+
+ return clone;
+ }
+ });
+
+ /**
+ * Shortcut function to the hasher's object interface.
+ *
+ * @param {WordArray|string} message The message to hash.
+ *
+ * @return {WordArray} The hash.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var hash = CryptoJS.SHA256('message');
+ * var hash = CryptoJS.SHA256(wordArray);
+ */
+ C.SHA256 = Hasher._createHelper(SHA256);
+
+ /**
+ * Shortcut function to the HMAC's object interface.
+ *
+ * @param {WordArray|string} message The message to hash.
+ * @param {WordArray|string} key The secret key.
+ *
+ * @return {WordArray} The HMAC.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var hmac = CryptoJS.HmacSHA256(message, key);
+ */
+ C.HmacSHA256 = Hasher._createHmacHelper(SHA256);
+ }(Math));
+
+
+ (function () {
+ // Shortcuts
+ var C = CryptoJS;
+ var C_lib = C.lib;
+ var WordArray = C_lib.WordArray;
+ var C_enc = C.enc;
+
+ /**
+ * UTF-16 BE encoding strategy.
+ */
+ var Utf16BE = C_enc.Utf16 = C_enc.Utf16BE = {
+ /**
+ * Converts a word array to a UTF-16 BE string.
+ *
+ * @param {WordArray} wordArray The word array.
+ *
+ * @return {string} The UTF-16 BE string.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var utf16String = CryptoJS.enc.Utf16.stringify(wordArray);
+ */
+ stringify: function (wordArray) {
+ // Shortcuts
+ var words = wordArray.words;
+ var sigBytes = wordArray.sigBytes;
+
+ // Convert
+ var utf16Chars = [];
+ for (var i = 0; i < sigBytes; i += 2) {
+ var codePoint = (words[i >>> 2] >>> (16 - (i % 4) * 8)) & 0xffff;
+ utf16Chars.push(String.fromCharCode(codePoint));
+ }
+
+ return utf16Chars.join('');
+ },
+
+ /**
+ * Converts a UTF-16 BE string to a word array.
+ *
+ * @param {string} utf16Str The UTF-16 BE string.
+ *
+ * @return {WordArray} The word array.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var wordArray = CryptoJS.enc.Utf16.parse(utf16String);
+ */
+ parse: function (utf16Str) {
+ // Shortcut
+ var utf16StrLength = utf16Str.length;
+
+ // Convert
+ var words = [];
+ for (var i = 0; i < utf16StrLength; i++) {
+ words[i >>> 1] |= utf16Str.charCodeAt(i) << (16 - (i % 2) * 16);
+ }
+
+ return WordArray.create(words, utf16StrLength * 2);
+ }
+ };
+
+ /**
+ * UTF-16 LE encoding strategy.
+ */
+ C_enc.Utf16LE = {
+ /**
+ * Converts a word array to a UTF-16 LE string.
+ *
+ * @param {WordArray} wordArray The word array.
+ *
+ * @return {string} The UTF-16 LE string.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var utf16Str = CryptoJS.enc.Utf16LE.stringify(wordArray);
+ */
+ stringify: function (wordArray) {
+ // Shortcuts
+ var words = wordArray.words;
+ var sigBytes = wordArray.sigBytes;
+
+ // Convert
+ var utf16Chars = [];
+ for (var i = 0; i < sigBytes; i += 2) {
+ var codePoint = swapEndian((words[i >>> 2] >>> (16 - (i % 4) * 8)) & 0xffff);
+ utf16Chars.push(String.fromCharCode(codePoint));
+ }
+
+ return utf16Chars.join('');
+ },
+
+ /**
+ * Converts a UTF-16 LE string to a word array.
+ *
+ * @param {string} utf16Str The UTF-16 LE string.
+ *
+ * @return {WordArray} The word array.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var wordArray = CryptoJS.enc.Utf16LE.parse(utf16Str);
+ */
+ parse: function (utf16Str) {
+ // Shortcut
+ var utf16StrLength = utf16Str.length;
+
+ // Convert
+ var words = [];
+ for (var i = 0; i < utf16StrLength; i++) {
+ words[i >>> 1] |= swapEndian(utf16Str.charCodeAt(i) << (16 - (i % 2) * 16));
+ }
+
+ return WordArray.create(words, utf16StrLength * 2);
+ }
+ };
+
+ function swapEndian(word) {
+ return ((word << 8) & 0xff00ff00) | ((word >>> 8) & 0x00ff00ff);
+ }
+ }());
+
+
+ (function () {
+ // Check if typed arrays are supported
+ if (typeof ArrayBuffer != 'function') {
+ return;
+ }
+
+ // Shortcuts
+ var C = CryptoJS;
+ var C_lib = C.lib;
+ var WordArray = C_lib.WordArray;
+
+ // Reference original init
+ var superInit = WordArray.init;
+
+ // Augment WordArray.init to handle typed arrays
+ var subInit = WordArray.init = function (typedArray) {
+ // Convert buffers to uint8
+ if (typedArray instanceof ArrayBuffer) {
+ typedArray = new Uint8Array(typedArray);
+ }
+
+ // Convert other array views to uint8
+ if (
+ typedArray instanceof Int8Array ||
+ (typeof Uint8ClampedArray !== "undefined" && typedArray instanceof Uint8ClampedArray) ||
+ typedArray instanceof Int16Array ||
+ typedArray instanceof Uint16Array ||
+ typedArray instanceof Int32Array ||
+ typedArray instanceof Uint32Array ||
+ typedArray instanceof Float32Array ||
+ typedArray instanceof Float64Array
+ ) {
+ typedArray = new Uint8Array(typedArray.buffer, typedArray.byteOffset, typedArray.byteLength);
+ }
+
+ // Handle Uint8Array
+ if (typedArray instanceof Uint8Array) {
+ // Shortcut
+ var typedArrayByteLength = typedArray.byteLength;
+
+ // Extract bytes
+ var words = [];
+ for (var i = 0; i < typedArrayByteLength; i++) {
+ words[i >>> 2] |= typedArray[i] << (24 - (i % 4) * 8);
+ }
+
+ // Initialize this word array
+ superInit.call(this, words, typedArrayByteLength);
+ } else {
+ // Else call normal init
+ superInit.apply(this, arguments);
+ }
+ };
+
+ subInit.prototype = WordArray;
+ }());
+
+
+ /** @preserve
+ (c) 2012 by Cédric Mesnil. All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+ - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+ - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+ (function (Math) {
+ // Shortcuts
+ var C = CryptoJS;
+ var C_lib = C.lib;
+ var WordArray = C_lib.WordArray;
+ var Hasher = C_lib.Hasher;
+ var C_algo = C.algo;
+
+ // Constants table
+ var _zl = WordArray.create([
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
+ 7, 4, 13, 1, 10, 6, 15, 3, 12, 0, 9, 5, 2, 14, 11, 8,
+ 3, 10, 14, 4, 9, 15, 8, 1, 2, 7, 0, 6, 13, 11, 5, 12,
+ 1, 9, 11, 10, 0, 8, 12, 4, 13, 3, 7, 15, 14, 5, 6, 2,
+ 4, 0, 5, 9, 7, 12, 2, 10, 14, 1, 3, 8, 11, 6, 15, 13]);
+ var _zr = WordArray.create([
+ 5, 14, 7, 0, 9, 2, 11, 4, 13, 6, 15, 8, 1, 10, 3, 12,
+ 6, 11, 3, 7, 0, 13, 5, 10, 14, 15, 8, 12, 4, 9, 1, 2,
+ 15, 5, 1, 3, 7, 14, 6, 9, 11, 8, 12, 2, 10, 0, 4, 13,
+ 8, 6, 4, 1, 3, 11, 15, 0, 5, 12, 2, 13, 9, 7, 10, 14,
+ 12, 15, 10, 4, 1, 5, 8, 7, 6, 2, 13, 14, 0, 3, 9, 11]);
+ var _sl = WordArray.create([
+ 11, 14, 15, 12, 5, 8, 7, 9, 11, 13, 14, 15, 6, 7, 9, 8,
+ 7, 6, 8, 13, 11, 9, 7, 15, 7, 12, 15, 9, 11, 7, 13, 12,
+ 11, 13, 6, 7, 14, 9, 13, 15, 14, 8, 13, 6, 5, 12, 7, 5,
+ 11, 12, 14, 15, 14, 15, 9, 8, 9, 14, 5, 6, 8, 6, 5, 12,
+ 9, 15, 5, 11, 6, 8, 13, 12, 5, 12, 13, 14, 11, 8, 5, 6 ]);
+ var _sr = WordArray.create([
+ 8, 9, 9, 11, 13, 15, 15, 5, 7, 7, 8, 11, 14, 14, 12, 6,
+ 9, 13, 15, 7, 12, 8, 9, 11, 7, 7, 12, 7, 6, 15, 13, 11,
+ 9, 7, 15, 11, 8, 6, 6, 14, 12, 13, 5, 14, 13, 13, 7, 5,
+ 15, 5, 8, 11, 14, 14, 6, 14, 6, 9, 12, 9, 12, 5, 15, 8,
+ 8, 5, 12, 9, 12, 5, 14, 6, 8, 13, 6, 5, 15, 13, 11, 11 ]);
+
+ var _hl = WordArray.create([ 0x00000000, 0x5A827999, 0x6ED9EBA1, 0x8F1BBCDC, 0xA953FD4E]);
+ var _hr = WordArray.create([ 0x50A28BE6, 0x5C4DD124, 0x6D703EF3, 0x7A6D76E9, 0x00000000]);
+
+ /**
+ * RIPEMD160 hash algorithm.
+ */
+ var RIPEMD160 = C_algo.RIPEMD160 = Hasher.extend({
+ _doReset: function () {
+ this._hash = WordArray.create([0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0]);
+ },
+
+ _doProcessBlock: function (M, offset) {
+
+ // Swap endian
+ for (var i = 0; i < 16; i++) {
+ // Shortcuts
+ var offset_i = offset + i;
+ var M_offset_i = M[offset_i];
+
+ // Swap
+ M[offset_i] = (
+ (((M_offset_i << 8) | (M_offset_i >>> 24)) & 0x00ff00ff) |
+ (((M_offset_i << 24) | (M_offset_i >>> 8)) & 0xff00ff00)
+ );
+ }
+ // Shortcut
+ var H = this._hash.words;
+ var hl = _hl.words;
+ var hr = _hr.words;
+ var zl = _zl.words;
+ var zr = _zr.words;
+ var sl = _sl.words;
+ var sr = _sr.words;
+
+ // Working variables
+ var al, bl, cl, dl, el;
+ var ar, br, cr, dr, er;
+
+ ar = al = H[0];
+ br = bl = H[1];
+ cr = cl = H[2];
+ dr = dl = H[3];
+ er = el = H[4];
+ // Computation
+ var t;
+ for (var i = 0; i < 80; i += 1) {
+ t = (al + M[offset+zl[i]])|0;
+ if (i<16){
+ t += f1(bl,cl,dl) + hl[0];
+ } else if (i<32) {
+ t += f2(bl,cl,dl) + hl[1];
+ } else if (i<48) {
+ t += f3(bl,cl,dl) + hl[2];
+ } else if (i<64) {
+ t += f4(bl,cl,dl) + hl[3];
+ } else {// if (i<80) {
+ t += f5(bl,cl,dl) + hl[4];
+ }
+ t = t|0;
+ t = rotl(t,sl[i]);
+ t = (t+el)|0;
+ al = el;
+ el = dl;
+ dl = rotl(cl, 10);
+ cl = bl;
+ bl = t;
+
+ t = (ar + M[offset+zr[i]])|0;
+ if (i<16){
+ t += f5(br,cr,dr) + hr[0];
+ } else if (i<32) {
+ t += f4(br,cr,dr) + hr[1];
+ } else if (i<48) {
+ t += f3(br,cr,dr) + hr[2];
+ } else if (i<64) {
+ t += f2(br,cr,dr) + hr[3];
+ } else {// if (i<80) {
+ t += f1(br,cr,dr) + hr[4];
+ }
+ t = t|0;
+ t = rotl(t,sr[i]) ;
+ t = (t+er)|0;
+ ar = er;
+ er = dr;
+ dr = rotl(cr, 10);
+ cr = br;
+ br = t;
+ }
+ // Intermediate hash value
+ t = (H[1] + cl + dr)|0;
+ H[1] = (H[2] + dl + er)|0;
+ H[2] = (H[3] + el + ar)|0;
+ H[3] = (H[4] + al + br)|0;
+ H[4] = (H[0] + bl + cr)|0;
+ H[0] = t;
+ },
+
+ _doFinalize: function () {
+ // Shortcuts
+ var data = this._data;
+ var dataWords = data.words;
+
+ var nBitsTotal = this._nDataBytes * 8;
+ var nBitsLeft = data.sigBytes * 8;
+
+ // Add padding
+ dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32);
+ dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 14] = (
+ (((nBitsTotal << 8) | (nBitsTotal >>> 24)) & 0x00ff00ff) |
+ (((nBitsTotal << 24) | (nBitsTotal >>> 8)) & 0xff00ff00)
+ );
+ data.sigBytes = (dataWords.length + 1) * 4;
+
+ // Hash final blocks
+ this._process();
+
+ // Shortcuts
+ var hash = this._hash;
+ var H = hash.words;
+
+ // Swap endian
+ for (var i = 0; i < 5; i++) {
+ // Shortcut
+ var H_i = H[i];
+
+ // Swap
+ H[i] = (((H_i << 8) | (H_i >>> 24)) & 0x00ff00ff) |
+ (((H_i << 24) | (H_i >>> 8)) & 0xff00ff00);
+ }
+
+ // Return final computed hash
+ return hash;
+ },
+
+ clone: function () {
+ var clone = Hasher.clone.call(this);
+ clone._hash = this._hash.clone();
+
+ return clone;
+ }
+ });
+
+
+ function f1(x, y, z) {
+ return ((x) ^ (y) ^ (z));
+
+ }
+
+ function f2(x, y, z) {
+ return (((x)&(y)) | ((~x)&(z)));
+ }
+
+ function f3(x, y, z) {
+ return (((x) | (~(y))) ^ (z));
+ }
+
+ function f4(x, y, z) {
+ return (((x) & (z)) | ((y)&(~(z))));
+ }
+
+ function f5(x, y, z) {
+ return ((x) ^ ((y) |(~(z))));
+
+ }
+
+ function rotl(x,n) {
+ return (x<<n) | (x>>>(32-n));
+ }
+
+
+ /**
+ * Shortcut function to the hasher's object interface.
+ *
+ * @param {WordArray|string} message The message to hash.
+ *
+ * @return {WordArray} The hash.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var hash = CryptoJS.RIPEMD160('message');
+ * var hash = CryptoJS.RIPEMD160(wordArray);
+ */
+ C.RIPEMD160 = Hasher._createHelper(RIPEMD160);
+
+ /**
+ * Shortcut function to the HMAC's object interface.
+ *
+ * @param {WordArray|string} message The message to hash.
+ * @param {WordArray|string} key The secret key.
+ *
+ * @return {WordArray} The HMAC.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var hmac = CryptoJS.HmacRIPEMD160(message, key);
+ */
+ C.HmacRIPEMD160 = Hasher._createHmacHelper(RIPEMD160);
+ }(Math));
+
+
+ (function () {
+ // Shortcuts
+ var C = CryptoJS;
+ var C_lib = C.lib;
+ var Base = C_lib.Base;
+ var C_enc = C.enc;
+ var Utf8 = C_enc.Utf8;
+ var C_algo = C.algo;
+
+ /**
+ * HMAC algorithm.
+ */
+ var HMAC = C_algo.HMAC = Base.extend({
+ /**
+ * Initializes a newly created HMAC.
+ *
+ * @param {Hasher} hasher The hash algorithm to use.
+ * @param {WordArray|string} key The secret key.
+ *
+ * @example
+ *
+ * var hmacHasher = CryptoJS.algo.HMAC.create(CryptoJS.algo.SHA256, key);
+ */
+ init: function (hasher, key) {
+ // Init hasher
+ hasher = this._hasher = new hasher.init();
+
+ // Convert string to WordArray, else assume WordArray already
+ if (typeof key == 'string') {
+ key = Utf8.parse(key);
+ }
+
+ // Shortcuts
+ var hasherBlockSize = hasher.blockSize;
+ var hasherBlockSizeBytes = hasherBlockSize * 4;
+
+ // Allow arbitrary length keys
+ if (key.sigBytes > hasherBlockSizeBytes) {
+ key = hasher.finalize(key);
+ }
+
+ // Clamp excess bits
+ key.clamp();
+
+ // Clone key for inner and outer pads
+ var oKey = this._oKey = key.clone();
+ var iKey = this._iKey = key.clone();
+
+ // Shortcuts
+ var oKeyWords = oKey.words;
+ var iKeyWords = iKey.words;
+
+ // XOR keys with pad constants
+ for (var i = 0; i < hasherBlockSize; i++) {
+ oKeyWords[i] ^= 0x5c5c5c5c;
+ iKeyWords[i] ^= 0x36363636;
+ }
+ oKey.sigBytes = iKey.sigBytes = hasherBlockSizeBytes;
+
+ // Set initial values
+ this.reset();
+ },
+
+ /**
+ * Resets this HMAC to its initial state.
+ *
+ * @example
+ *
+ * hmacHasher.reset();
+ */
+ reset: function () {
+ // Shortcut
+ var hasher = this._hasher;
+
+ // Reset
+ hasher.reset();
+ hasher.update(this._iKey);
+ },
+
+ /**
+ * Updates this HMAC with a message.
+ *
+ * @param {WordArray|string} messageUpdate The message to append.
+ *
+ * @return {HMAC} This HMAC instance.
+ *
+ * @example
+ *
+ * hmacHasher.update('message');
+ * hmacHasher.update(wordArray);
+ */
+ update: function (messageUpdate) {
+ this._hasher.update(messageUpdate);
+
+ // Chainable
+ return this;
+ },
+
+ /**
+ * Finalizes the HMAC computation.
+ * Note that the finalize operation is effectively a destructive, read-once operation.
+ *
+ * @param {WordArray|string} messageUpdate (Optional) A final message update.
+ *
+ * @return {WordArray} The HMAC.
+ *
+ * @example
+ *
+ * var hmac = hmacHasher.finalize();
+ * var hmac = hmacHasher.finalize('message');
+ * var hmac = hmacHasher.finalize(wordArray);
+ */
+ finalize: function (messageUpdate) {
+ // Shortcut
+ var hasher = this._hasher;
+
+ // Compute HMAC
+ var innerHash = hasher.finalize(messageUpdate);
+ hasher.reset();
+ var hmac = hasher.finalize(this._oKey.clone().concat(innerHash));
+
+ return hmac;
+ }
+ });
+ }());
+
+
+ (function () {
+ // Shortcuts
+ var C = CryptoJS;
+ var C_lib = C.lib;
+ var Base = C_lib.Base;
+ var WordArray = C_lib.WordArray;
+ var C_algo = C.algo;
+ var SHA1 = C_algo.SHA1;
+ var HMAC = C_algo.HMAC;
+
+ /**
+ * Password-Based Key Derivation Function 2 algorithm.
+ */
+ var PBKDF2 = C_algo.PBKDF2 = Base.extend({
+ /**
+ * Configuration options.
+ *
+ * @property {number} keySize The key size in words to generate. Default: 4 (128 bits)
+ * @property {Hasher} hasher The hasher to use. Default: SHA1
+ * @property {number} iterations The number of iterations to perform. Default: 1
+ */
+ cfg: Base.extend({
+ keySize: 128/32,
+ hasher: SHA1,
+ iterations: 1
+ }),
+
+ /**
+ * Initializes a newly created key derivation function.
+ *
+ * @param {Object} cfg (Optional) The configuration options to use for the derivation.
+ *
+ * @example
+ *
+ * var kdf = CryptoJS.algo.PBKDF2.create();
+ * var kdf = CryptoJS.algo.PBKDF2.create({ keySize: 8 });
+ * var kdf = CryptoJS.algo.PBKDF2.create({ keySize: 8, iterations: 1000 });
+ */
+ init: function (cfg) {
+ this.cfg = this.cfg.extend(cfg);
+ },
+
+ /**
+ * Computes the Password-Based Key Derivation Function 2.
+ *
+ * @param {WordArray|string} password The password.
+ * @param {WordArray|string} salt A salt.
+ *
+ * @return {WordArray} The derived key.
+ *
+ * @example
+ *
+ * var key = kdf.compute(password, salt);
+ */
+ compute: function (password, salt) {
+ // Shortcut
+ var cfg = this.cfg;
+
+ // Init HMAC
+ var hmac = HMAC.create(cfg.hasher, password);
+
+ // Initial values
+ var derivedKey = WordArray.create();
+ var blockIndex = WordArray.create([0x00000001]);
+
+ // Shortcuts
+ var derivedKeyWords = derivedKey.words;
+ var blockIndexWords = blockIndex.words;
+ var keySize = cfg.keySize;
+ var iterations = cfg.iterations;
+
+ // Generate key
+ while (derivedKeyWords.length < keySize) {
+ var block = hmac.update(salt).finalize(blockIndex);
+ hmac.reset();
+
+ // Shortcuts
+ var blockWords = block.words;
+ var blockWordsLength = blockWords.length;
+
+ // Iterations
+ var intermediate = block;
+ for (var i = 1; i < iterations; i++) {
+ intermediate = hmac.finalize(intermediate);
+ hmac.reset();
+
+ // Shortcut
+ var intermediateWords = intermediate.words;
+
+ // XOR intermediate with block
+ for (var j = 0; j < blockWordsLength; j++) {
+ blockWords[j] ^= intermediateWords[j];
+ }
+ }
+
+ derivedKey.concat(block);
+ blockIndexWords[0]++;
+ }
+ derivedKey.sigBytes = keySize * 4;
+
+ return derivedKey;
+ }
+ });
+
+ /**
+ * Computes the Password-Based Key Derivation Function 2.
+ *
+ * @param {WordArray|string} password The password.
+ * @param {WordArray|string} salt A salt.
+ * @param {Object} cfg (Optional) The configuration options to use for this computation.
+ *
+ * @return {WordArray} The derived key.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var key = CryptoJS.PBKDF2(password, salt);
+ * var key = CryptoJS.PBKDF2(password, salt, { keySize: 8 });
+ * var key = CryptoJS.PBKDF2(password, salt, { keySize: 8, iterations: 1000 });
+ */
+ C.PBKDF2 = function (password, salt, cfg) {
+ return PBKDF2.create(cfg).compute(password, salt);
+ };
+ }());
+
+
+ (function () {
+ // Shortcuts
+ var C = CryptoJS;
+ var C_lib = C.lib;
+ var Base = C_lib.Base;
+ var WordArray = C_lib.WordArray;
+ var C_algo = C.algo;
+ var MD5 = C_algo.MD5;
+
+ /**
+ * This key derivation function is meant to conform with EVP_BytesToKey.
+ * www.openssl.org/docs/crypto/EVP_BytesToKey.html
+ */
+ var EvpKDF = C_algo.EvpKDF = Base.extend({
+ /**
+ * Configuration options.
+ *
+ * @property {number} keySize The key size in words to generate. Default: 4 (128 bits)
+ * @property {Hasher} hasher The hash algorithm to use. Default: MD5
+ * @property {number} iterations The number of iterations to perform. Default: 1
+ */
+ cfg: Base.extend({
+ keySize: 128/32,
+ hasher: MD5,
+ iterations: 1
+ }),
+
+ /**
+ * Initializes a newly created key derivation function.
+ *
+ * @param {Object} cfg (Optional) The configuration options to use for the derivation.
+ *
+ * @example
+ *
+ * var kdf = CryptoJS.algo.EvpKDF.create();
+ * var kdf = CryptoJS.algo.EvpKDF.create({ keySize: 8 });
+ * var kdf = CryptoJS.algo.EvpKDF.create({ keySize: 8, iterations: 1000 });
+ */
+ init: function (cfg) {
+ this.cfg = this.cfg.extend(cfg);
+ },
+
+ /**
+ * Derives a key from a password.
+ *
+ * @param {WordArray|string} password The password.
+ * @param {WordArray|string} salt A salt.
+ *
+ * @return {WordArray} The derived key.
+ *
+ * @example
+ *
+ * var key = kdf.compute(password, salt);
+ */
+ compute: function (password, salt) {
+ // Shortcut
+ var cfg = this.cfg;
+
+ // Init hasher
+ var hasher = cfg.hasher.create();
+
+ // Initial values
+ var derivedKey = WordArray.create();
+
+ // Shortcuts
+ var derivedKeyWords = derivedKey.words;
+ var keySize = cfg.keySize;
+ var iterations = cfg.iterations;
+
+ // Generate key
+ while (derivedKeyWords.length < keySize) {
+ if (block) {
+ hasher.update(block);
+ }
+ var block = hasher.update(password).finalize(salt);
+ hasher.reset();
+
+ // Iterations
+ for (var i = 1; i < iterations; i++) {
+ block = hasher.finalize(block);
+ hasher.reset();
+ }
+
+ derivedKey.concat(block);
+ }
+ derivedKey.sigBytes = keySize * 4;
+
+ return derivedKey;
+ }
+ });
+
+ /**
+ * Derives a key from a password.
+ *
+ * @param {WordArray|string} password The password.
+ * @param {WordArray|string} salt A salt.
+ * @param {Object} cfg (Optional) The configuration options to use for this computation.
+ *
+ * @return {WordArray} The derived key.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var key = CryptoJS.EvpKDF(password, salt);
+ * var key = CryptoJS.EvpKDF(password, salt, { keySize: 8 });
+ * var key = CryptoJS.EvpKDF(password, salt, { keySize: 8, iterations: 1000 });
+ */
+ C.EvpKDF = function (password, salt, cfg) {
+ return EvpKDF.create(cfg).compute(password, salt);
+ };
+ }());
+
+
+ (function () {
+ // Shortcuts
+ var C = CryptoJS;
+ var C_lib = C.lib;
+ var WordArray = C_lib.WordArray;
+ var C_algo = C.algo;
+ var SHA256 = C_algo.SHA256;
+
+ /**
+ * SHA-224 hash algorithm.
+ */
+ var SHA224 = C_algo.SHA224 = SHA256.extend({
+ _doReset: function () {
+ this._hash = new WordArray.init([
+ 0xc1059ed8, 0x367cd507, 0x3070dd17, 0xf70e5939,
+ 0xffc00b31, 0x68581511, 0x64f98fa7, 0xbefa4fa4
+ ]);
+ },
+
+ _doFinalize: function () {
+ var hash = SHA256._doFinalize.call(this);
+
+ hash.sigBytes -= 4;
+
+ return hash;
+ }
+ });
+
+ /**
+ * Shortcut function to the hasher's object interface.
+ *
+ * @param {WordArray|string} message The message to hash.
+ *
+ * @return {WordArray} The hash.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var hash = CryptoJS.SHA224('message');
+ * var hash = CryptoJS.SHA224(wordArray);
+ */
+ C.SHA224 = SHA256._createHelper(SHA224);
+
+ /**
+ * Shortcut function to the HMAC's object interface.
+ *
+ * @param {WordArray|string} message The message to hash.
+ * @param {WordArray|string} key The secret key.
+ *
+ * @return {WordArray} The HMAC.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var hmac = CryptoJS.HmacSHA224(message, key);
+ */
+ C.HmacSHA224 = SHA256._createHmacHelper(SHA224);
+ }());
+
+
+ (function (undefined) {
+ // Shortcuts
+ var C = CryptoJS;
+ var C_lib = C.lib;
+ var Base = C_lib.Base;
+ var X32WordArray = C_lib.WordArray;
+
+ /**
+ * x64 namespace.
+ */
+ var C_x64 = C.x64 = {};
+
+ /**
+ * A 64-bit word.
+ */
+ var X64Word = C_x64.Word = Base.extend({
+ /**
+ * Initializes a newly created 64-bit word.
+ *
+ * @param {number} high The high 32 bits.
+ * @param {number} low The low 32 bits.
+ *
+ * @example
+ *
+ * var x64Word = CryptoJS.x64.Word.create(0x00010203, 0x04050607);
+ */
+ init: function (high, low) {
+ this.high = high;
+ this.low = low;
+ }
+
+ /**
+ * Bitwise NOTs this word.
+ *
+ * @return {X64Word} A new x64-Word object after negating.
+ *
+ * @example
+ *
+ * var negated = x64Word.not();
+ */
+ // not: function () {
+ // var high = ~this.high;
+ // var low = ~this.low;
+
+ // return X64Word.create(high, low);
+ // },
+
+ /**
+ * Bitwise ANDs this word with the passed word.
+ *
+ * @param {X64Word} word The x64-Word to AND with this word.
+ *
+ * @return {X64Word} A new x64-Word object after ANDing.
+ *
+ * @example
+ *
+ * var anded = x64Word.and(anotherX64Word);
+ */
+ // and: function (word) {
+ // var high = this.high & word.high;
+ // var low = this.low & word.low;
+
+ // return X64Word.create(high, low);
+ // },
+
+ /**
+ * Bitwise ORs this word with the passed word.
+ *
+ * @param {X64Word} word The x64-Word to OR with this word.
+ *
+ * @return {X64Word} A new x64-Word object after ORing.
+ *
+ * @example
+ *
+ * var ored = x64Word.or(anotherX64Word);
+ */
+ // or: function (word) {
+ // var high = this.high | word.high;
+ // var low = this.low | word.low;
+
+ // return X64Word.create(high, low);
+ // },
+
+ /**
+ * Bitwise XORs this word with the passed word.
+ *
+ * @param {X64Word} word The x64-Word to XOR with this word.
+ *
+ * @return {X64Word} A new x64-Word object after XORing.
+ *
+ * @example
+ *
+ * var xored = x64Word.xor(anotherX64Word);
+ */
+ // xor: function (word) {
+ // var high = this.high ^ word.high;
+ // var low = this.low ^ word.low;
+
+ // return X64Word.create(high, low);
+ // },
+
+ /**
+ * Shifts this word n bits to the left.
+ *
+ * @param {number} n The number of bits to shift.
+ *
+ * @return {X64Word} A new x64-Word object after shifting.
+ *
+ * @example
+ *
+ * var shifted = x64Word.shiftL(25);
+ */
+ // shiftL: function (n) {
+ // if (n < 32) {
+ // var high = (this.high << n) | (this.low >>> (32 - n));
+ // var low = this.low << n;
+ // } else {
+ // var high = this.low << (n - 32);
+ // var low = 0;
+ // }
+
+ // return X64Word.create(high, low);
+ // },
+
+ /**
+ * Shifts this word n bits to the right.
+ *
+ * @param {number} n The number of bits to shift.
+ *
+ * @return {X64Word} A new x64-Word object after shifting.
+ *
+ * @example
+ *
+ * var shifted = x64Word.shiftR(7);
+ */
+ // shiftR: function (n) {
+ // if (n < 32) {
+ // var low = (this.low >>> n) | (this.high << (32 - n));
+ // var high = this.high >>> n;
+ // } else {
+ // var low = this.high >>> (n - 32);
+ // var high = 0;
+ // }
+
+ // return X64Word.create(high, low);
+ // },
+
+ /**
+ * Rotates this word n bits to the left.
+ *
+ * @param {number} n The number of bits to rotate.
+ *
+ * @return {X64Word} A new x64-Word object after rotating.
+ *
+ * @example
+ *
+ * var rotated = x64Word.rotL(25);
+ */
+ // rotL: function (n) {
+ // return this.shiftL(n).or(this.shiftR(64 - n));
+ // },
+
+ /**
+ * Rotates this word n bits to the right.
+ *
+ * @param {number} n The number of bits to rotate.
+ *
+ * @return {X64Word} A new x64-Word object after rotating.
+ *
+ * @example
+ *
+ * var rotated = x64Word.rotR(7);
+ */
+ // rotR: function (n) {
+ // return this.shiftR(n).or(this.shiftL(64 - n));
+ // },
+
+ /**
+ * Adds this word with the passed word.
+ *
+ * @param {X64Word} word The x64-Word to add with this word.
+ *
+ * @return {X64Word} A new x64-Word object after adding.
+ *
+ * @example
+ *
+ * var added = x64Word.add(anotherX64Word);
+ */
+ // add: function (word) {
+ // var low = (this.low + word.low) | 0;
+ // var carry = (low >>> 0) < (this.low >>> 0) ? 1 : 0;
+ // var high = (this.high + word.high + carry) | 0;
+
+ // return X64Word.create(high, low);
+ // }
+ });
+
+ /**
+ * An array of 64-bit words.
+ *
+ * @property {Array} words The array of CryptoJS.x64.Word objects.
+ * @property {number} sigBytes The number of significant bytes in this word array.
+ */
+ var X64WordArray = C_x64.WordArray = Base.extend({
+ /**
+ * Initializes a newly created word array.
+ *
+ * @param {Array} words (Optional) An array of CryptoJS.x64.Word objects.
+ * @param {number} sigBytes (Optional) The number of significant bytes in the words.
+ *
+ * @example
+ *
+ * var wordArray = CryptoJS.x64.WordArray.create();
+ *
+ * var wordArray = CryptoJS.x64.WordArray.create([
+ * CryptoJS.x64.Word.create(0x00010203, 0x04050607),
+ * CryptoJS.x64.Word.create(0x18191a1b, 0x1c1d1e1f)
+ * ]);
+ *
+ * var wordArray = CryptoJS.x64.WordArray.create([
+ * CryptoJS.x64.Word.create(0x00010203, 0x04050607),
+ * CryptoJS.x64.Word.create(0x18191a1b, 0x1c1d1e1f)
+ * ], 10);
+ */
+ init: function (words, sigBytes) {
+ words = this.words = words || [];
+
+ if (sigBytes != undefined) {
+ this.sigBytes = sigBytes;
+ } else {
+ this.sigBytes = words.length * 8;
+ }
+ },
+
+ /**
+ * Converts this 64-bit word array to a 32-bit word array.
+ *
+ * @return {CryptoJS.lib.WordArray} This word array's data as a 32-bit word array.
+ *
+ * @example
+ *
+ * var x32WordArray = x64WordArray.toX32();
+ */
+ toX32: function () {
+ // Shortcuts
+ var x64Words = this.words;
+ var x64WordsLength = x64Words.length;
+
+ // Convert
+ var x32Words = [];
+ for (var i = 0; i < x64WordsLength; i++) {
+ var x64Word = x64Words[i];
+ x32Words.push(x64Word.high);
+ x32Words.push(x64Word.low);
+ }
+
+ return X32WordArray.create(x32Words, this.sigBytes);
+ },
+
+ /**
+ * Creates a copy of this word array.
+ *
+ * @return {X64WordArray} The clone.
+ *
+ * @example
+ *
+ * var clone = x64WordArray.clone();
+ */
+ clone: function () {
+ var clone = Base.clone.call(this);
+
+ // Clone "words" array
+ var words = clone.words = this.words.slice(0);
+
+ // Clone each X64Word object
+ var wordsLength = words.length;
+ for (var i = 0; i < wordsLength; i++) {
+ words[i] = words[i].clone();
+ }
+
+ return clone;
+ }
+ });
+ }());
+
+
+ (function (Math) {
+ // Shortcuts
+ var C = CryptoJS;
+ var C_lib = C.lib;
+ var WordArray = C_lib.WordArray;
+ var Hasher = C_lib.Hasher;
+ var C_x64 = C.x64;
+ var X64Word = C_x64.Word;
+ var C_algo = C.algo;
+
+ // Constants tables
+ var RHO_OFFSETS = [];
+ var PI_INDEXES = [];
+ var ROUND_CONSTANTS = [];
+
+ // Compute Constants
+ (function () {
+ // Compute rho offset constants
+ var x = 1, y = 0;
+ for (var t = 0; t < 24; t++) {
+ RHO_OFFSETS[x + 5 * y] = ((t + 1) * (t + 2) / 2) % 64;
+
+ var newX = y % 5;
+ var newY = (2 * x + 3 * y) % 5;
+ x = newX;
+ y = newY;
+ }
+
+ // Compute pi index constants
+ for (var x = 0; x < 5; x++) {
+ for (var y = 0; y < 5; y++) {
+ PI_INDEXES[x + 5 * y] = y + ((2 * x + 3 * y) % 5) * 5;
+ }
+ }
+
+ // Compute round constants
+ var LFSR = 0x01;
+ for (var i = 0; i < 24; i++) {
+ var roundConstantMsw = 0;
+ var roundConstantLsw = 0;
+
+ for (var j = 0; j < 7; j++) {
+ if (LFSR & 0x01) {
+ var bitPosition = (1 << j) - 1;
+ if (bitPosition < 32) {
+ roundConstantLsw ^= 1 << bitPosition;
+ } else /* if (bitPosition >= 32) */ {
+ roundConstantMsw ^= 1 << (bitPosition - 32);
+ }
+ }
+
+ // Compute next LFSR
+ if (LFSR & 0x80) {
+ // Primitive polynomial over GF(2): x^8 + x^6 + x^5 + x^4 + 1
+ LFSR = (LFSR << 1) ^ 0x71;
+ } else {
+ LFSR <<= 1;
+ }
+ }
+
+ ROUND_CONSTANTS[i] = X64Word.create(roundConstantMsw, roundConstantLsw);
+ }
+ }());
+
+ // Reusable objects for temporary values
+ var T = [];
+ (function () {
+ for (var i = 0; i < 25; i++) {
+ T[i] = X64Word.create();
+ }
+ }());
+
+ /**
+ * SHA-3 hash algorithm.
+ */
+ var SHA3 = C_algo.SHA3 = Hasher.extend({
+ /**
+ * Configuration options.
+ *
+ * @property {number} outputLength
+ * The desired number of bits in the output hash.
+ * Only values permitted are: 224, 256, 384, 512.
+ * Default: 512
+ */
+ cfg: Hasher.cfg.extend({
+ outputLength: 512
+ }),
+
+ _doReset: function () {
+ var state = this._state = []
+ for (var i = 0; i < 25; i++) {
+ state[i] = new X64Word.init();
+ }
+
+ this.blockSize = (1600 - 2 * this.cfg.outputLength) / 32;
+ },
+
+ _doProcessBlock: function (M, offset) {
+ // Shortcuts
+ var state = this._state;
+ var nBlockSizeLanes = this.blockSize / 2;
+
+ // Absorb
+ for (var i = 0; i < nBlockSizeLanes; i++) {
+ // Shortcuts
+ var M2i = M[offset + 2 * i];
+ var M2i1 = M[offset + 2 * i + 1];
+
+ // Swap endian
+ M2i = (
+ (((M2i << 8) | (M2i >>> 24)) & 0x00ff00ff) |
+ (((M2i << 24) | (M2i >>> 8)) & 0xff00ff00)
+ );
+ M2i1 = (
+ (((M2i1 << 8) | (M2i1 >>> 24)) & 0x00ff00ff) |
+ (((M2i1 << 24) | (M2i1 >>> 8)) & 0xff00ff00)
+ );
+
+ // Absorb message into state
+ var lane = state[i];
+ lane.high ^= M2i1;
+ lane.low ^= M2i;
+ }
+
+ // Rounds
+ for (var round = 0; round < 24; round++) {
+ // Theta
+ for (var x = 0; x < 5; x++) {
+ // Mix column lanes
+ var tMsw = 0, tLsw = 0;
+ for (var y = 0; y < 5; y++) {
+ var lane = state[x + 5 * y];
+ tMsw ^= lane.high;
+ tLsw ^= lane.low;
+ }
+
+ // Temporary values
+ var Tx = T[x];
+ Tx.high = tMsw;
+ Tx.low = tLsw;
+ }
+ for (var x = 0; x < 5; x++) {
+ // Shortcuts
+ var Tx4 = T[(x + 4) % 5];
+ var Tx1 = T[(x + 1) % 5];
+ var Tx1Msw = Tx1.high;
+ var Tx1Lsw = Tx1.low;
+
+ // Mix surrounding columns
+ var tMsw = Tx4.high ^ ((Tx1Msw << 1) | (Tx1Lsw >>> 31));
+ var tLsw = Tx4.low ^ ((Tx1Lsw << 1) | (Tx1Msw >>> 31));
+ for (var y = 0; y < 5; y++) {
+ var lane = state[x + 5 * y];
+ lane.high ^= tMsw;
+ lane.low ^= tLsw;
+ }
+ }
+
+ // Rho Pi
+ for (var laneIndex = 1; laneIndex < 25; laneIndex++) {
+ // Shortcuts
+ var lane = state[laneIndex];
+ var laneMsw = lane.high;
+ var laneLsw = lane.low;
+ var rhoOffset = RHO_OFFSETS[laneIndex];
+
+ // Rotate lanes
+ if (rhoOffset < 32) {
+ var tMsw = (laneMsw << rhoOffset) | (laneLsw >>> (32 - rhoOffset));
+ var tLsw = (laneLsw << rhoOffset) | (laneMsw >>> (32 - rhoOffset));
+ } else /* if (rhoOffset >= 32) */ {
+ var tMsw = (laneLsw << (rhoOffset - 32)) | (laneMsw >>> (64 - rhoOffset));
+ var tLsw = (laneMsw << (rhoOffset - 32)) | (laneLsw >>> (64 - rhoOffset));
+ }
+
+ // Transpose lanes
+ var TPiLane = T[PI_INDEXES[laneIndex]];
+ TPiLane.high = tMsw;
+ TPiLane.low = tLsw;
+ }
+
+ // Rho pi at x = y = 0
+ var T0 = T[0];
+ var state0 = state[0];
+ T0.high = state0.high;
+ T0.low = state0.low;
+
+ // Chi
+ for (var x = 0; x < 5; x++) {
+ for (var y = 0; y < 5; y++) {
+ // Shortcuts
+ var laneIndex = x + 5 * y;
+ var lane = state[laneIndex];
+ var TLane = T[laneIndex];
+ var Tx1Lane = T[((x + 1) % 5) + 5 * y];
+ var Tx2Lane = T[((x + 2) % 5) + 5 * y];
+
+ // Mix rows
+ lane.high = TLane.high ^ (~Tx1Lane.high & Tx2Lane.high);
+ lane.low = TLane.low ^ (~Tx1Lane.low & Tx2Lane.low);
+ }
+ }
+
+ // Iota
+ var lane = state[0];
+ var roundConstant = ROUND_CONSTANTS[round];
+ lane.high ^= roundConstant.high;
+ lane.low ^= roundConstant.low;;
+ }
+ },
+
+ _doFinalize: function () {
+ // Shortcuts
+ var data = this._data;
+ var dataWords = data.words;
+ var nBitsTotal = this._nDataBytes * 8;
+ var nBitsLeft = data.sigBytes * 8;
+ var blockSizeBits = this.blockSize * 32;
+
+ // Add padding
+ dataWords[nBitsLeft >>> 5] |= 0x1 << (24 - nBitsLeft % 32);
+ dataWords[((Math.ceil((nBitsLeft + 1) / blockSizeBits) * blockSizeBits) >>> 5) - 1] |= 0x80;
+ data.sigBytes = dataWords.length * 4;
+
+ // Hash final blocks
+ this._process();
+
+ // Shortcuts
+ var state = this._state;
+ var outputLengthBytes = this.cfg.outputLength / 8;
+ var outputLengthLanes = outputLengthBytes / 8;
+
+ // Squeeze
+ var hashWords = [];
+ for (var i = 0; i < outputLengthLanes; i++) {
+ // Shortcuts
+ var lane = state[i];
+ var laneMsw = lane.high;
+ var laneLsw = lane.low;
+
+ // Swap endian
+ laneMsw = (
+ (((laneMsw << 8) | (laneMsw >>> 24)) & 0x00ff00ff) |
+ (((laneMsw << 24) | (laneMsw >>> 8)) & 0xff00ff00)
+ );
+ laneLsw = (
+ (((laneLsw << 8) | (laneLsw >>> 24)) & 0x00ff00ff) |
+ (((laneLsw << 24) | (laneLsw >>> 8)) & 0xff00ff00)
+ );
+
+ // Squeeze state to retrieve hash
+ hashWords.push(laneLsw);
+ hashWords.push(laneMsw);
+ }
+
+ // Return final computed hash
+ return new WordArray.init(hashWords, outputLengthBytes);
+ },
+
+ clone: function () {
+ var clone = Hasher.clone.call(this);
+
+ var state = clone._state = this._state.slice(0);
+ for (var i = 0; i < 25; i++) {
+ state[i] = state[i].clone();
+ }
+
+ return clone;
+ }
+ });
+
+ /**
+ * Shortcut function to the hasher's object interface.
+ *
+ * @param {WordArray|string} message The message to hash.
+ *
+ * @return {WordArray} The hash.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var hash = CryptoJS.SHA3('message');
+ * var hash = CryptoJS.SHA3(wordArray);
+ */
+ C.SHA3 = Hasher._createHelper(SHA3);
+
+ /**
+ * Shortcut function to the HMAC's object interface.
+ *
+ * @param {WordArray|string} message The message to hash.
+ * @param {WordArray|string} key The secret key.
+ *
+ * @return {WordArray} The HMAC.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var hmac = CryptoJS.HmacSHA3(message, key);
+ */
+ C.HmacSHA3 = Hasher._createHmacHelper(SHA3);
+ }(Math));
+
+
+ (function () {
+ // Shortcuts
+ var C = CryptoJS;
+ var C_lib = C.lib;
+ var Hasher = C_lib.Hasher;
+ var C_x64 = C.x64;
+ var X64Word = C_x64.Word;
+ var X64WordArray = C_x64.WordArray;
+ var C_algo = C.algo;
+
+ function X64Word_create() {
+ return X64Word.create.apply(X64Word, arguments);
+ }
+
+ // Constants
+ var K = [
+ X64Word_create(0x428a2f98, 0xd728ae22), X64Word_create(0x71374491, 0x23ef65cd),
+ X64Word_create(0xb5c0fbcf, 0xec4d3b2f), X64Word_create(0xe9b5dba5, 0x8189dbbc),
+ X64Word_create(0x3956c25b, 0xf348b538), X64Word_create(0x59f111f1, 0xb605d019),
+ X64Word_create(0x923f82a4, 0xaf194f9b), X64Word_create(0xab1c5ed5, 0xda6d8118),
+ X64Word_create(0xd807aa98, 0xa3030242), X64Word_create(0x12835b01, 0x45706fbe),
+ X64Word_create(0x243185be, 0x4ee4b28c), X64Word_create(0x550c7dc3, 0xd5ffb4e2),
+ X64Word_create(0x72be5d74, 0xf27b896f), X64Word_create(0x80deb1fe, 0x3b1696b1),
+ X64Word_create(0x9bdc06a7, 0x25c71235), X64Word_create(0xc19bf174, 0xcf692694),
+ X64Word_create(0xe49b69c1, 0x9ef14ad2), X64Word_create(0xefbe4786, 0x384f25e3),
+ X64Word_create(0x0fc19dc6, 0x8b8cd5b5), X64Word_create(0x240ca1cc, 0x77ac9c65),
+ X64Word_create(0x2de92c6f, 0x592b0275), X64Word_create(0x4a7484aa, 0x6ea6e483),
+ X64Word_create(0x5cb0a9dc, 0xbd41fbd4), X64Word_create(0x76f988da, 0x831153b5),
+ X64Word_create(0x983e5152, 0xee66dfab), X64Word_create(0xa831c66d, 0x2db43210),
+ X64Word_create(0xb00327c8, 0x98fb213f), X64Word_create(0xbf597fc7, 0xbeef0ee4),
+ X64Word_create(0xc6e00bf3, 0x3da88fc2), X64Word_create(0xd5a79147, 0x930aa725),
+ X64Word_create(0x06ca6351, 0xe003826f), X64Word_create(0x14292967, 0x0a0e6e70),
+ X64Word_create(0x27b70a85, 0x46d22ffc), X64Word_create(0x2e1b2138, 0x5c26c926),
+ X64Word_create(0x4d2c6dfc, 0x5ac42aed), X64Word_create(0x53380d13, 0x9d95b3df),
+ X64Word_create(0x650a7354, 0x8baf63de), X64Word_create(0x766a0abb, 0x3c77b2a8),
+ X64Word_create(0x81c2c92e, 0x47edaee6), X64Word_create(0x92722c85, 0x1482353b),
+ X64Word_create(0xa2bfe8a1, 0x4cf10364), X64Word_create(0xa81a664b, 0xbc423001),
+ X64Word_create(0xc24b8b70, 0xd0f89791), X64Word_create(0xc76c51a3, 0x0654be30),
+ X64Word_create(0xd192e819, 0xd6ef5218), X64Word_create(0xd6990624, 0x5565a910),
+ X64Word_create(0xf40e3585, 0x5771202a), X64Word_create(0x106aa070, 0x32bbd1b8),
+ X64Word_create(0x19a4c116, 0xb8d2d0c8), X64Word_create(0x1e376c08, 0x5141ab53),
+ X64Word_create(0x2748774c, 0xdf8eeb99), X64Word_create(0x34b0bcb5, 0xe19b48a8),
+ X64Word_create(0x391c0cb3, 0xc5c95a63), X64Word_create(0x4ed8aa4a, 0xe3418acb),
+ X64Word_create(0x5b9cca4f, 0x7763e373), X64Word_create(0x682e6ff3, 0xd6b2b8a3),
+ X64Word_create(0x748f82ee, 0x5defb2fc), X64Word_create(0x78a5636f, 0x43172f60),
+ X64Word_create(0x84c87814, 0xa1f0ab72), X64Word_create(0x8cc70208, 0x1a6439ec),
+ X64Word_create(0x90befffa, 0x23631e28), X64Word_create(0xa4506ceb, 0xde82bde9),
+ X64Word_create(0xbef9a3f7, 0xb2c67915), X64Word_create(0xc67178f2, 0xe372532b),
+ X64Word_create(0xca273ece, 0xea26619c), X64Word_create(0xd186b8c7, 0x21c0c207),
+ X64Word_create(0xeada7dd6, 0xcde0eb1e), X64Word_create(0xf57d4f7f, 0xee6ed178),
+ X64Word_create(0x06f067aa, 0x72176fba), X64Word_create(0x0a637dc5, 0xa2c898a6),
+ X64Word_create(0x113f9804, 0xbef90dae), X64Word_create(0x1b710b35, 0x131c471b),
+ X64Word_create(0x28db77f5, 0x23047d84), X64Word_create(0x32caab7b, 0x40c72493),
+ X64Word_create(0x3c9ebe0a, 0x15c9bebc), X64Word_create(0x431d67c4, 0x9c100d4c),
+ X64Word_create(0x4cc5d4be, 0xcb3e42b6), X64Word_create(0x597f299c, 0xfc657e2a),
+ X64Word_create(0x5fcb6fab, 0x3ad6faec), X64Word_create(0x6c44198c, 0x4a475817)
+ ];
+
+ // Reusable objects
+ var W = [];
+ (function () {
+ for (var i = 0; i < 80; i++) {
+ W[i] = X64Word_create();
+ }
+ }());
+
+ /**
+ * SHA-512 hash algorithm.
+ */
+ var SHA512 = C_algo.SHA512 = Hasher.extend({
+ _doReset: function () {
+ this._hash = new X64WordArray.init([
+ new X64Word.init(0x6a09e667, 0xf3bcc908), new X64Word.init(0xbb67ae85, 0x84caa73b),
+ new X64Word.init(0x3c6ef372, 0xfe94f82b), new X64Word.init(0xa54ff53a, 0x5f1d36f1),
+ new X64Word.init(0x510e527f, 0xade682d1), new X64Word.init(0x9b05688c, 0x2b3e6c1f),
+ new X64Word.init(0x1f83d9ab, 0xfb41bd6b), new X64Word.init(0x5be0cd19, 0x137e2179)
+ ]);
+ },
+
+ _doProcessBlock: function (M, offset) {
+ // Shortcuts
+ var H = this._hash.words;
+
+ var H0 = H[0];
+ var H1 = H[1];
+ var H2 = H[2];
+ var H3 = H[3];
+ var H4 = H[4];
+ var H5 = H[5];
+ var H6 = H[6];
+ var H7 = H[7];
+
+ var H0h = H0.high;
+ var H0l = H0.low;
+ var H1h = H1.high;
+ var H1l = H1.low;
+ var H2h = H2.high;
+ var H2l = H2.low;
+ var H3h = H3.high;
+ var H3l = H3.low;
+ var H4h = H4.high;
+ var H4l = H4.low;
+ var H5h = H5.high;
+ var H5l = H5.low;
+ var H6h = H6.high;
+ var H6l = H6.low;
+ var H7h = H7.high;
+ var H7l = H7.low;
+
+ // Working variables
+ var ah = H0h;
+ var al = H0l;
+ var bh = H1h;
+ var bl = H1l;
+ var ch = H2h;
+ var cl = H2l;
+ var dh = H3h;
+ var dl = H3l;
+ var eh = H4h;
+ var el = H4l;
+ var fh = H5h;
+ var fl = H5l;
+ var gh = H6h;
+ var gl = H6l;
+ var hh = H7h;
+ var hl = H7l;
+
+ // Rounds
+ for (var i = 0; i < 80; i++) {
+ // Shortcut
+ var Wi = W[i];
+
+ // Extend message
+ if (i < 16) {
+ var Wih = Wi.high = M[offset + i * 2] | 0;
+ var Wil = Wi.low = M[offset + i * 2 + 1] | 0;
+ } else {
+ // Gamma0
+ var gamma0x = W[i - 15];
+ var gamma0xh = gamma0x.high;
+ var gamma0xl = gamma0x.low;
+ var gamma0h = ((gamma0xh >>> 1) | (gamma0xl << 31)) ^ ((gamma0xh >>> 8) | (gamma0xl << 24)) ^ (gamma0xh >>> 7);
+ var gamma0l = ((gamma0xl >>> 1) | (gamma0xh << 31)) ^ ((gamma0xl >>> 8) | (gamma0xh << 24)) ^ ((gamma0xl >>> 7) | (gamma0xh << 25));
+
+ // Gamma1
+ var gamma1x = W[i - 2];
+ var gamma1xh = gamma1x.high;
+ var gamma1xl = gamma1x.low;
+ var gamma1h = ((gamma1xh >>> 19) | (gamma1xl << 13)) ^ ((gamma1xh << 3) | (gamma1xl >>> 29)) ^ (gamma1xh >>> 6);
+ var gamma1l = ((gamma1xl >>> 19) | (gamma1xh << 13)) ^ ((gamma1xl << 3) | (gamma1xh >>> 29)) ^ ((gamma1xl >>> 6) | (gamma1xh << 26));
+
+ // W[i] = gamma0 + W[i - 7] + gamma1 + W[i - 16]
+ var Wi7 = W[i - 7];
+ var Wi7h = Wi7.high;
+ var Wi7l = Wi7.low;
+
+ var Wi16 = W[i - 16];
+ var Wi16h = Wi16.high;
+ var Wi16l = Wi16.low;
+
+ var Wil = gamma0l + Wi7l;
+ var Wih = gamma0h + Wi7h + ((Wil >>> 0) < (gamma0l >>> 0) ? 1 : 0);
+ var Wil = Wil + gamma1l;
+ var Wih = Wih + gamma1h + ((Wil >>> 0) < (gamma1l >>> 0) ? 1 : 0);
+ var Wil = Wil + Wi16l;
+ var Wih = Wih + Wi16h + ((Wil >>> 0) < (Wi16l >>> 0) ? 1 : 0);
+
+ Wi.high = Wih;
+ Wi.low = Wil;
+ }
+
+ var chh = (eh & fh) ^ (~eh & gh);
+ var chl = (el & fl) ^ (~el & gl);
+ var majh = (ah & bh) ^ (ah & ch) ^ (bh & ch);
+ var majl = (al & bl) ^ (al & cl) ^ (bl & cl);
+
+ var sigma0h = ((ah >>> 28) | (al << 4)) ^ ((ah << 30) | (al >>> 2)) ^ ((ah << 25) | (al >>> 7));
+ var sigma0l = ((al >>> 28) | (ah << 4)) ^ ((al << 30) | (ah >>> 2)) ^ ((al << 25) | (ah >>> 7));
+ var sigma1h = ((eh >>> 14) | (el << 18)) ^ ((eh >>> 18) | (el << 14)) ^ ((eh << 23) | (el >>> 9));
+ var sigma1l = ((el >>> 14) | (eh << 18)) ^ ((el >>> 18) | (eh << 14)) ^ ((el << 23) | (eh >>> 9));
+
+ // t1 = h + sigma1 + ch + K[i] + W[i]
+ var Ki = K[i];
+ var Kih = Ki.high;
+ var Kil = Ki.low;
+
+ var t1l = hl + sigma1l;
+ var t1h = hh + sigma1h + ((t1l >>> 0) < (hl >>> 0) ? 1 : 0);
+ var t1l = t1l + chl;
+ var t1h = t1h + chh + ((t1l >>> 0) < (chl >>> 0) ? 1 : 0);
+ var t1l = t1l + Kil;
+ var t1h = t1h + Kih + ((t1l >>> 0) < (Kil >>> 0) ? 1 : 0);
+ var t1l = t1l + Wil;
+ var t1h = t1h + Wih + ((t1l >>> 0) < (Wil >>> 0) ? 1 : 0);
+
+ // t2 = sigma0 + maj
+ var t2l = sigma0l + majl;
+ var t2h = sigma0h + majh + ((t2l >>> 0) < (sigma0l >>> 0) ? 1 : 0);
+
+ // Update working variables
+ hh = gh;
+ hl = gl;
+ gh = fh;
+ gl = fl;
+ fh = eh;
+ fl = el;
+ el = (dl + t1l) | 0;
+ eh = (dh + t1h + ((el >>> 0) < (dl >>> 0) ? 1 : 0)) | 0;
+ dh = ch;
+ dl = cl;
+ ch = bh;
+ cl = bl;
+ bh = ah;
+ bl = al;
+ al = (t1l + t2l) | 0;
+ ah = (t1h + t2h + ((al >>> 0) < (t1l >>> 0) ? 1 : 0)) | 0;
+ }
+
+ // Intermediate hash value
+ H0l = H0.low = (H0l + al);
+ H0.high = (H0h + ah + ((H0l >>> 0) < (al >>> 0) ? 1 : 0));
+ H1l = H1.low = (H1l + bl);
+ H1.high = (H1h + bh + ((H1l >>> 0) < (bl >>> 0) ? 1 : 0));
+ H2l = H2.low = (H2l + cl);
+ H2.high = (H2h + ch + ((H2l >>> 0) < (cl >>> 0) ? 1 : 0));
+ H3l = H3.low = (H3l + dl);
+ H3.high = (H3h + dh + ((H3l >>> 0) < (dl >>> 0) ? 1 : 0));
+ H4l = H4.low = (H4l + el);
+ H4.high = (H4h + eh + ((H4l >>> 0) < (el >>> 0) ? 1 : 0));
+ H5l = H5.low = (H5l + fl);
+ H5.high = (H5h + fh + ((H5l >>> 0) < (fl >>> 0) ? 1 : 0));
+ H6l = H6.low = (H6l + gl);
+ H6.high = (H6h + gh + ((H6l >>> 0) < (gl >>> 0) ? 1 : 0));
+ H7l = H7.low = (H7l + hl);
+ H7.high = (H7h + hh + ((H7l >>> 0) < (hl >>> 0) ? 1 : 0));
+ },
+
+ _doFinalize: function () {
+ // Shortcuts
+ var data = this._data;
+ var dataWords = data.words;
+
+ var nBitsTotal = this._nDataBytes * 8;
+ var nBitsLeft = data.sigBytes * 8;
+
+ // Add padding
+ dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32);
+ dataWords[(((nBitsLeft + 128) >>> 10) << 5) + 30] = Math.floor(nBitsTotal / 0x100000000);
+ dataWords[(((nBitsLeft + 128) >>> 10) << 5) + 31] = nBitsTotal;
+ data.sigBytes = dataWords.length * 4;
+
+ // Hash final blocks
+ this._process();
+
+ // Convert hash to 32-bit word array before returning
+ var hash = this._hash.toX32();
+
+ // Return final computed hash
+ return hash;
+ },
+
+ clone: function () {
+ var clone = Hasher.clone.call(this);
+ clone._hash = this._hash.clone();
+
+ return clone;
+ },
+
+ blockSize: 1024/32
+ });
+
+ /**
+ * Shortcut function to the hasher's object interface.
+ *
+ * @param {WordArray|string} message The message to hash.
+ *
+ * @return {WordArray} The hash.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var hash = CryptoJS.SHA512('message');
+ * var hash = CryptoJS.SHA512(wordArray);
+ */
+ C.SHA512 = Hasher._createHelper(SHA512);
+
+ /**
+ * Shortcut function to the HMAC's object interface.
+ *
+ * @param {WordArray|string} message The message to hash.
+ * @param {WordArray|string} key The secret key.
+ *
+ * @return {WordArray} The HMAC.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var hmac = CryptoJS.HmacSHA512(message, key);
+ */
+ C.HmacSHA512 = Hasher._createHmacHelper(SHA512);
+ }());
+
+
+ (function () {
+ // Shortcuts
+ var C = CryptoJS;
+ var C_x64 = C.x64;
+ var X64Word = C_x64.Word;
+ var X64WordArray = C_x64.WordArray;
+ var C_algo = C.algo;
+ var SHA512 = C_algo.SHA512;
+
+ /**
+ * SHA-384 hash algorithm.
+ */
+ var SHA384 = C_algo.SHA384 = SHA512.extend({
+ _doReset: function () {
+ this._hash = new X64WordArray.init([
+ new X64Word.init(0xcbbb9d5d, 0xc1059ed8), new X64Word.init(0x629a292a, 0x367cd507),
+ new X64Word.init(0x9159015a, 0x3070dd17), new X64Word.init(0x152fecd8, 0xf70e5939),
+ new X64Word.init(0x67332667, 0xffc00b31), new X64Word.init(0x8eb44a87, 0x68581511),
+ new X64Word.init(0xdb0c2e0d, 0x64f98fa7), new X64Word.init(0x47b5481d, 0xbefa4fa4)
+ ]);
+ },
+
+ _doFinalize: function () {
+ var hash = SHA512._doFinalize.call(this);
+
+ hash.sigBytes -= 16;
+
+ return hash;
+ }
+ });
+
+ /**
+ * Shortcut function to the hasher's object interface.
+ *
+ * @param {WordArray|string} message The message to hash.
+ *
+ * @return {WordArray} The hash.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var hash = CryptoJS.SHA384('message');
+ * var hash = CryptoJS.SHA384(wordArray);
+ */
+ C.SHA384 = SHA512._createHelper(SHA384);
+
+ /**
+ * Shortcut function to the HMAC's object interface.
+ *
+ * @param {WordArray|string} message The message to hash.
+ * @param {WordArray|string} key The secret key.
+ *
+ * @return {WordArray} The HMAC.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var hmac = CryptoJS.HmacSHA384(message, key);
+ */
+ C.HmacSHA384 = SHA512._createHmacHelper(SHA384);
+ }());
+
+
+ /**
+ * Cipher core components.
+ */
+ CryptoJS.lib.Cipher || (function (undefined) {
+ // Shortcuts
+ var C = CryptoJS;
+ var C_lib = C.lib;
+ var Base = C_lib.Base;
+ var WordArray = C_lib.WordArray;
+ var BufferedBlockAlgorithm = C_lib.BufferedBlockAlgorithm;
+ var C_enc = C.enc;
+ var Utf8 = C_enc.Utf8;
+ var Base64 = C_enc.Base64;
+ var C_algo = C.algo;
+ var EvpKDF = C_algo.EvpKDF;
+
+ /**
+ * Abstract base cipher template.
+ *
+ * @property {number} keySize This cipher's key size. Default: 4 (128 bits)
+ * @property {number} ivSize This cipher's IV size. Default: 4 (128 bits)
+ * @property {number} _ENC_XFORM_MODE A constant representing encryption mode.
+ * @property {number} _DEC_XFORM_MODE A constant representing decryption mode.
+ */
+ var Cipher = C_lib.Cipher = BufferedBlockAlgorithm.extend({
+ /**
+ * Configuration options.
+ *
+ * @property {WordArray} iv The IV to use for this operation.
+ */
+ cfg: Base.extend(),
+
+ /**
+ * Creates this cipher in encryption mode.
+ *
+ * @param {WordArray} key The key.
+ * @param {Object} cfg (Optional) The configuration options to use for this operation.
+ *
+ * @return {Cipher} A cipher instance.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var cipher = CryptoJS.algo.AES.createEncryptor(keyWordArray, { iv: ivWordArray });
+ */
+ createEncryptor: function (key, cfg) {
+ return this.create(this._ENC_XFORM_MODE, key, cfg);
+ },
+
+ /**
+ * Creates this cipher in decryption mode.
+ *
+ * @param {WordArray} key The key.
+ * @param {Object} cfg (Optional) The configuration options to use for this operation.
+ *
+ * @return {Cipher} A cipher instance.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var cipher = CryptoJS.algo.AES.createDecryptor(keyWordArray, { iv: ivWordArray });
+ */
+ createDecryptor: function (key, cfg) {
+ return this.create(this._DEC_XFORM_MODE, key, cfg);
+ },
+
+ /**
+ * Initializes a newly created cipher.
+ *
+ * @param {number} xformMode Either the encryption or decryption transormation mode constant.
+ * @param {WordArray} key The key.
+ * @param {Object} cfg (Optional) The configuration options to use for this operation.
+ *
+ * @example
+ *
+ * var cipher = CryptoJS.algo.AES.create(CryptoJS.algo.AES._ENC_XFORM_MODE, keyWordArray, { iv: ivWordArray });
+ */
+ init: function (xformMode, key, cfg) {
+ // Apply config defaults
+ this.cfg = this.cfg.extend(cfg);
+
+ // Store transform mode and key
+ this._xformMode = xformMode;
+ this._key = key;
+
+ // Set initial values
+ this.reset();
+ },
+
+ /**
+ * Resets this cipher to its initial state.
+ *
+ * @example
+ *
+ * cipher.reset();
+ */
+ reset: function () {
+ // Reset data buffer
+ BufferedBlockAlgorithm.reset.call(this);
+
+ // Perform concrete-cipher logic
+ this._doReset();
+ },
+
+ /**
+ * Adds data to be encrypted or decrypted.
+ *
+ * @param {WordArray|string} dataUpdate The data to encrypt or decrypt.
+ *
+ * @return {WordArray} The data after processing.
+ *
+ * @example
+ *
+ * var encrypted = cipher.process('data');
+ * var encrypted = cipher.process(wordArray);
+ */
+ process: function (dataUpdate) {
+ // Append
+ this._append(dataUpdate);
+
+ // Process available blocks
+ return this._process();
+ },
+
+ /**
+ * Finalizes the encryption or decryption process.
+ * Note that the finalize operation is effectively a destructive, read-once operation.
+ *
+ * @param {WordArray|string} dataUpdate The final data to encrypt or decrypt.
+ *
+ * @return {WordArray} The data after final processing.
+ *
+ * @example
+ *
+ * var encrypted = cipher.finalize();
+ * var encrypted = cipher.finalize('data');
+ * var encrypted = cipher.finalize(wordArray);
+ */
+ finalize: function (dataUpdate) {
+ // Final data update
+ if (dataUpdate) {
+ this._append(dataUpdate);
+ }
+
+ // Perform concrete-cipher logic
+ var finalProcessedData = this._doFinalize();
+
+ return finalProcessedData;
+ },
+
+ keySize: 128/32,
+
+ ivSize: 128/32,
+
+ _ENC_XFORM_MODE: 1,
+
+ _DEC_XFORM_MODE: 2,
+
+ /**
+ * Creates shortcut functions to a cipher's object interface.
+ *
+ * @param {Cipher} cipher The cipher to create a helper for.
+ *
+ * @return {Object} An object with encrypt and decrypt shortcut functions.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var AES = CryptoJS.lib.Cipher._createHelper(CryptoJS.algo.AES);
+ */
+ _createHelper: (function () {
+ function selectCipherStrategy(key) {
+ if (typeof key == 'string') {
+ return PasswordBasedCipher;
+ } else {
+ return SerializableCipher;
+ }
+ }
+
+ return function (cipher) {
+ return {
+ encrypt: function (message, key, cfg) {
+ return selectCipherStrategy(key).encrypt(cipher, message, key, cfg);
+ },
+
+ decrypt: function (ciphertext, key, cfg) {
+ return selectCipherStrategy(key).decrypt(cipher, ciphertext, key, cfg);
+ }
+ };
+ };
+ }())
+ });
+
+ /**
+ * Abstract base stream cipher template.
+ *
+ * @property {number} blockSize The number of 32-bit words this cipher operates on. Default: 1 (32 bits)
+ */
+ var StreamCipher = C_lib.StreamCipher = Cipher.extend({
+ _doFinalize: function () {
+ // Process partial blocks
+ var finalProcessedBlocks = this._process(!!'flush');
+
+ return finalProcessedBlocks;
+ },
+
+ blockSize: 1
+ });
+
+ /**
+ * Mode namespace.
+ */
+ var C_mode = C.mode = {};
+
+ /**
+ * Abstract base block cipher mode template.
+ */
+ var BlockCipherMode = C_lib.BlockCipherMode = Base.extend({
+ /**
+ * Creates this mode for encryption.
+ *
+ * @param {Cipher} cipher A block cipher instance.
+ * @param {Array} iv The IV words.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var mode = CryptoJS.mode.CBC.createEncryptor(cipher, iv.words);
+ */
+ createEncryptor: function (cipher, iv) {
+ return this.Encryptor.create(cipher, iv);
+ },
+
+ /**
+ * Creates this mode for decryption.
+ *
+ * @param {Cipher} cipher A block cipher instance.
+ * @param {Array} iv The IV words.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var mode = CryptoJS.mode.CBC.createDecryptor(cipher, iv.words);
+ */
+ createDecryptor: function (cipher, iv) {
+ return this.Decryptor.create(cipher, iv);
+ },
+
+ /**
+ * Initializes a newly created mode.
+ *
+ * @param {Cipher} cipher A block cipher instance.
+ * @param {Array} iv The IV words.
+ *
+ * @example
+ *
+ * var mode = CryptoJS.mode.CBC.Encryptor.create(cipher, iv.words);
+ */
+ init: function (cipher, iv) {
+ this._cipher = cipher;
+ this._iv = iv;
+ }
+ });
+
+ /**
+ * Cipher Block Chaining mode.
+ */
+ var CBC = C_mode.CBC = (function () {
+ /**
+ * Abstract base CBC mode.
+ */
+ var CBC = BlockCipherMode.extend();
+
+ /**
+ * CBC encryptor.
+ */
+ CBC.Encryptor = CBC.extend({
+ /**
+ * Processes the data block at offset.
+ *
+ * @param {Array} words The data words to operate on.
+ * @param {number} offset The offset where the block starts.
+ *
+ * @example
+ *
+ * mode.processBlock(data.words, offset);
+ */
+ processBlock: function (words, offset) {
+ // Shortcuts
+ var cipher = this._cipher;
+ var blockSize = cipher.blockSize;
+
+ // XOR and encrypt
+ xorBlock.call(this, words, offset, blockSize);
+ cipher.encryptBlock(words, offset);
+
+ // Remember this block to use with next block
+ this._prevBlock = words.slice(offset, offset + blockSize);
+ }
+ });
+
+ /**
+ * CBC decryptor.
+ */
+ CBC.Decryptor = CBC.extend({
+ /**
+ * Processes the data block at offset.
+ *
+ * @param {Array} words The data words to operate on.
+ * @param {number} offset The offset where the block starts.
+ *
+ * @example
+ *
+ * mode.processBlock(data.words, offset);
+ */
+ processBlock: function (words, offset) {
+ // Shortcuts
+ var cipher = this._cipher;
+ var blockSize = cipher.blockSize;
+
+ // Remember this block to use with next block
+ var thisBlock = words.slice(offset, offset + blockSize);
+
+ // Decrypt and XOR
+ cipher.decryptBlock(words, offset);
+ xorBlock.call(this, words, offset, blockSize);
+
+ // This block becomes the previous block
+ this._prevBlock = thisBlock;
+ }
+ });
+
+ function xorBlock(words, offset, blockSize) {
+ // Shortcut
+ var iv = this._iv;
+
+ // Choose mixing block
+ if (iv) {
+ var block = iv;
+
+ // Remove IV for subsequent blocks
+ this._iv = undefined;
+ } else {
+ var block = this._prevBlock;
+ }
+
+ // XOR blocks
+ for (var i = 0; i < blockSize; i++) {
+ words[offset + i] ^= block[i];
+ }
+ }
+
+ return CBC;
+ }());
+
+ /**
+ * Padding namespace.
+ */
+ var C_pad = C.pad = {};
+
+ /**
+ * PKCS #5/7 padding strategy.
+ */
+ var Pkcs7 = C_pad.Pkcs7 = {
+ /**
+ * Pads data using the algorithm defined in PKCS #5/7.
+ *
+ * @param {WordArray} data The data to pad.
+ * @param {number} blockSize The multiple that the data should be padded to.
+ *
+ * @static
+ *
+ * @example
+ *
+ * CryptoJS.pad.Pkcs7.pad(wordArray, 4);
+ */
+ pad: function (data, blockSize) {
+ // Shortcut
+ var blockSizeBytes = blockSize * 4;
+
+ // Count padding bytes
+ var nPaddingBytes = blockSizeBytes - data.sigBytes % blockSizeBytes;
+
+ // Create padding word
+ var paddingWord = (nPaddingBytes << 24) | (nPaddingBytes << 16) | (nPaddingBytes << 8) | nPaddingBytes;
+
+ // Create padding
+ var paddingWords = [];
+ for (var i = 0; i < nPaddingBytes; i += 4) {
+ paddingWords.push(paddingWord);
+ }
+ var padding = WordArray.create(paddingWords, nPaddingBytes);
+
+ // Add padding
+ data.concat(padding);
+ },
+
+ /**
+ * Unpads data that had been padded using the algorithm defined in PKCS #5/7.
+ *
+ * @param {WordArray} data The data to unpad.
+ *
+ * @static
+ *
+ * @example
+ *
+ * CryptoJS.pad.Pkcs7.unpad(wordArray);
+ */
+ unpad: function (data) {
+ // Get number of padding bytes from last byte
+ var nPaddingBytes = data.words[(data.sigBytes - 1) >>> 2] & 0xff;
+
+ // Remove padding
+ data.sigBytes -= nPaddingBytes;
+ }
+ };
+
+ /**
+ * Abstract base block cipher template.
+ *
+ * @property {number} blockSize The number of 32-bit words this cipher operates on. Default: 4 (128 bits)
+ */
+ var BlockCipher = C_lib.BlockCipher = Cipher.extend({
+ /**
+ * Configuration options.
+ *
+ * @property {Mode} mode The block mode to use. Default: CBC
+ * @property {Padding} padding The padding strategy to use. Default: Pkcs7
+ */
+ cfg: Cipher.cfg.extend({
+ mode: CBC,
+ padding: Pkcs7
+ }),
+
+ reset: function () {
+ // Reset cipher
+ Cipher.reset.call(this);
+
+ // Shortcuts
+ var cfg = this.cfg;
+ var iv = cfg.iv;
+ var mode = cfg.mode;
+
+ // Reset block mode
+ if (this._xformMode == this._ENC_XFORM_MODE) {
+ var modeCreator = mode.createEncryptor;
+ } else /* if (this._xformMode == this._DEC_XFORM_MODE) */ {
+ var modeCreator = mode.createDecryptor;
+ // Keep at least one block in the buffer for unpadding
+ this._minBufferSize = 1;
+ }
+
+ if (this._mode && this._mode.__creator == modeCreator) {
+ this._mode.init(this, iv && iv.words);
+ } else {
+ this._mode = modeCreator.call(mode, this, iv && iv.words);
+ this._mode.__creator = modeCreator;
+ }
+ },
+
+ _doProcessBlock: function (words, offset) {
+ this._mode.processBlock(words, offset);
+ },
+
+ _doFinalize: function () {
+ // Shortcut
+ var padding = this.cfg.padding;
+
+ // Finalize
+ if (this._xformMode == this._ENC_XFORM_MODE) {
+ // Pad data
+ padding.pad(this._data, this.blockSize);
+
+ // Process final blocks
+ var finalProcessedBlocks = this._process(!!'flush');
+ } else /* if (this._xformMode == this._DEC_XFORM_MODE) */ {
+ // Process final blocks
+ var finalProcessedBlocks = this._process(!!'flush');
+
+ // Unpad data
+ padding.unpad(finalProcessedBlocks);
+ }
+
+ return finalProcessedBlocks;
+ },
+
+ blockSize: 128/32
+ });
+
+ /**
+ * A collection of cipher parameters.
+ *
+ * @property {WordArray} ciphertext The raw ciphertext.
+ * @property {WordArray} key The key to this ciphertext.
+ * @property {WordArray} iv The IV used in the ciphering operation.
+ * @property {WordArray} salt The salt used with a key derivation function.
+ * @property {Cipher} algorithm The cipher algorithm.
+ * @property {Mode} mode The block mode used in the ciphering operation.
+ * @property {Padding} padding The padding scheme used in the ciphering operation.
+ * @property {number} blockSize The block size of the cipher.
+ * @property {Format} formatter The default formatting strategy to convert this cipher params object to a string.
+ */
+ var CipherParams = C_lib.CipherParams = Base.extend({
+ /**
+ * Initializes a newly created cipher params object.
+ *
+ * @param {Object} cipherParams An object with any of the possible cipher parameters.
+ *
+ * @example
+ *
+ * var cipherParams = CryptoJS.lib.CipherParams.create({
+ * ciphertext: ciphertextWordArray,
+ * key: keyWordArray,
+ * iv: ivWordArray,
+ * salt: saltWordArray,
+ * algorithm: CryptoJS.algo.AES,
+ * mode: CryptoJS.mode.CBC,
+ * padding: CryptoJS.pad.PKCS7,
+ * blockSize: 4,
+ * formatter: CryptoJS.format.OpenSSL
+ * });
+ */
+ init: function (cipherParams) {
+ this.mixIn(cipherParams);
+ },
+
+ /**
+ * Converts this cipher params object to a string.
+ *
+ * @param {Format} formatter (Optional) The formatting strategy to use.
+ *
+ * @return {string} The stringified cipher params.
+ *
+ * @throws Error If neither the formatter nor the default formatter is set.
+ *
+ * @example
+ *
+ * var string = cipherParams + '';
+ * var string = cipherParams.toString();
+ * var string = cipherParams.toString(CryptoJS.format.OpenSSL);
+ */
+ toString: function (formatter) {
+ return (formatter || this.formatter).stringify(this);
+ }
+ });
+
+ /**
+ * Format namespace.
+ */
+ var C_format = C.format = {};
+
+ /**
+ * OpenSSL formatting strategy.
+ */
+ var OpenSSLFormatter = C_format.OpenSSL = {
+ /**
+ * Converts a cipher params object to an OpenSSL-compatible string.
+ *
+ * @param {CipherParams} cipherParams The cipher params object.
+ *
+ * @return {string} The OpenSSL-compatible string.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var openSSLString = CryptoJS.format.OpenSSL.stringify(cipherParams);
+ */
+ stringify: function (cipherParams) {
+ // Shortcuts
+ var ciphertext = cipherParams.ciphertext;
+ var salt = cipherParams.salt;
+
+ // Format
+ if (salt) {
+ var wordArray = WordArray.create([0x53616c74, 0x65645f5f]).concat(salt).concat(ciphertext);
+ } else {
+ var wordArray = ciphertext;
+ }
+
+ return wordArray.toString(Base64);
+ },
+
+ /**
+ * Converts an OpenSSL-compatible string to a cipher params object.
+ *
+ * @param {string} openSSLStr The OpenSSL-compatible string.
+ *
+ * @return {CipherParams} The cipher params object.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var cipherParams = CryptoJS.format.OpenSSL.parse(openSSLString);
+ */
+ parse: function (openSSLStr) {
+ // Parse base64
+ var ciphertext = Base64.parse(openSSLStr);
+
+ // Shortcut
+ var ciphertextWords = ciphertext.words;
+
+ // Test for salt
+ if (ciphertextWords[0] == 0x53616c74 && ciphertextWords[1] == 0x65645f5f) {
+ // Extract salt
+ var salt = WordArray.create(ciphertextWords.slice(2, 4));
+
+ // Remove salt from ciphertext
+ ciphertextWords.splice(0, 4);
+ ciphertext.sigBytes -= 16;
+ }
+
+ return CipherParams.create({ ciphertext: ciphertext, salt: salt });
+ }
+ };
+
+ /**
+ * A cipher wrapper that returns ciphertext as a serializable cipher params object.
+ */
+ var SerializableCipher = C_lib.SerializableCipher = Base.extend({
+ /**
+ * Configuration options.
+ *
+ * @property {Formatter} format The formatting strategy to convert cipher param objects to and from a string. Default: OpenSSL
+ */
+ cfg: Base.extend({
+ format: OpenSSLFormatter
+ }),
+
+ /**
+ * Encrypts a message.
+ *
+ * @param {Cipher} cipher The cipher algorithm to use.
+ * @param {WordArray|string} message The message to encrypt.
+ * @param {WordArray} key The key.
+ * @param {Object} cfg (Optional) The configuration options to use for this operation.
+ *
+ * @return {CipherParams} A cipher params object.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var ciphertextParams = CryptoJS.lib.SerializableCipher.encrypt(CryptoJS.algo.AES, message, key);
+ * var ciphertextParams = CryptoJS.lib.SerializableCipher.encrypt(CryptoJS.algo.AES, message, key, { iv: iv });
+ * var ciphertextParams = CryptoJS.lib.SerializableCipher.encrypt(CryptoJS.algo.AES, message, key, { iv: iv, format: CryptoJS.format.OpenSSL });
+ */
+ encrypt: function (cipher, message, key, cfg) {
+ // Apply config defaults
+ cfg = this.cfg.extend(cfg);
+
+ // Encrypt
+ var encryptor = cipher.createEncryptor(key, cfg);
+ var ciphertext = encryptor.finalize(message);
+
+ // Shortcut
+ var cipherCfg = encryptor.cfg;
+
+ // Create and return serializable cipher params
+ return CipherParams.create({
+ ciphertext: ciphertext,
+ key: key,
+ iv: cipherCfg.iv,
+ algorithm: cipher,
+ mode: cipherCfg.mode,
+ padding: cipherCfg.padding,
+ blockSize: cipher.blockSize,
+ formatter: cfg.format
+ });
+ },
+
+ /**
+ * Decrypts serialized ciphertext.
+ *
+ * @param {Cipher} cipher The cipher algorithm to use.
+ * @param {CipherParams|string} ciphertext The ciphertext to decrypt.
+ * @param {WordArray} key The key.
+ * @param {Object} cfg (Optional) The configuration options to use for this operation.
+ *
+ * @return {WordArray} The plaintext.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var plaintext = CryptoJS.lib.SerializableCipher.decrypt(CryptoJS.algo.AES, formattedCiphertext, key, { iv: iv, format: CryptoJS.format.OpenSSL });
+ * var plaintext = CryptoJS.lib.SerializableCipher.decrypt(CryptoJS.algo.AES, ciphertextParams, key, { iv: iv, format: CryptoJS.format.OpenSSL });
+ */
+ decrypt: function (cipher, ciphertext, key, cfg) {
+ // Apply config defaults
+ cfg = this.cfg.extend(cfg);
+
+ // Convert string to CipherParams
+ ciphertext = this._parse(ciphertext, cfg.format);
+
+ // Decrypt
+ var plaintext = cipher.createDecryptor(key, cfg).finalize(ciphertext.ciphertext);
+
+ return plaintext;
+ },
+
+ /**
+ * Converts serialized ciphertext to CipherParams,
+ * else assumed CipherParams already and returns ciphertext unchanged.
+ *
+ * @param {CipherParams|string} ciphertext The ciphertext.
+ * @param {Formatter} format The formatting strategy to use to parse serialized ciphertext.
+ *
+ * @return {CipherParams} The unserialized ciphertext.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var ciphertextParams = CryptoJS.lib.SerializableCipher._parse(ciphertextStringOrParams, format);
+ */
+ _parse: function (ciphertext, format) {
+ if (typeof ciphertext == 'string') {
+ return format.parse(ciphertext, this);
+ } else {
+ return ciphertext;
+ }
+ }
+ });
+
+ /**
+ * Key derivation function namespace.
+ */
+ var C_kdf = C.kdf = {};
+
+ /**
+ * OpenSSL key derivation function.
+ */
+ var OpenSSLKdf = C_kdf.OpenSSL = {
+ /**
+ * Derives a key and IV from a password.
+ *
+ * @param {string} password The password to derive from.
+ * @param {number} keySize The size in words of the key to generate.
+ * @param {number} ivSize The size in words of the IV to generate.
+ * @param {WordArray|string} salt (Optional) A 64-bit salt to use. If omitted, a salt will be generated randomly.
+ *
+ * @return {CipherParams} A cipher params object with the key, IV, and salt.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var derivedParams = CryptoJS.kdf.OpenSSL.execute('Password', 256/32, 128/32);
+ * var derivedParams = CryptoJS.kdf.OpenSSL.execute('Password', 256/32, 128/32, 'saltsalt');
+ */
+ execute: function (password, keySize, ivSize, salt) {
+ // Generate random salt
+ if (!salt) {
+ salt = WordArray.random(64/8);
+ }
+
+ // Derive key and IV
+ var key = EvpKDF.create({ keySize: keySize + ivSize }).compute(password, salt);
+
+ // Separate key and IV
+ var iv = WordArray.create(key.words.slice(keySize), ivSize * 4);
+ key.sigBytes = keySize * 4;
+
+ // Return params
+ return CipherParams.create({ key: key, iv: iv, salt: salt });
+ }
+ };
+
+ /**
+ * A serializable cipher wrapper that derives the key from a password,
+ * and returns ciphertext as a serializable cipher params object.
+ */
+ var PasswordBasedCipher = C_lib.PasswordBasedCipher = SerializableCipher.extend({
+ /**
+ * Configuration options.
+ *
+ * @property {KDF} kdf The key derivation function to use to generate a key and IV from a password. Default: OpenSSL
+ */
+ cfg: SerializableCipher.cfg.extend({
+ kdf: OpenSSLKdf
+ }),
+
+ /**
+ * Encrypts a message using a password.
+ *
+ * @param {Cipher} cipher The cipher algorithm to use.
+ * @param {WordArray|string} message The message to encrypt.
+ * @param {string} password The password.
+ * @param {Object} cfg (Optional) The configuration options to use for this operation.
+ *
+ * @return {CipherParams} A cipher params object.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var ciphertextParams = CryptoJS.lib.PasswordBasedCipher.encrypt(CryptoJS.algo.AES, message, 'password');
+ * var ciphertextParams = CryptoJS.lib.PasswordBasedCipher.encrypt(CryptoJS.algo.AES, message, 'password', { format: CryptoJS.format.OpenSSL });
+ */
+ encrypt: function (cipher, message, password, cfg) {
+ // Apply config defaults
+ cfg = this.cfg.extend(cfg);
+
+ // Derive key and other params
+ var derivedParams = cfg.kdf.execute(password, cipher.keySize, cipher.ivSize);
+
+ // Add IV to config
+ cfg.iv = derivedParams.iv;
+
+ // Encrypt
+ var ciphertext = SerializableCipher.encrypt.call(this, cipher, message, derivedParams.key, cfg);
+
+ // Mix in derived params
+ ciphertext.mixIn(derivedParams);
+
+ return ciphertext;
+ },
+
+ /**
+ * Decrypts serialized ciphertext using a password.
+ *
+ * @param {Cipher} cipher The cipher algorithm to use.
+ * @param {CipherParams|string} ciphertext The ciphertext to decrypt.
+ * @param {string} password The password.
+ * @param {Object} cfg (Optional) The configuration options to use for this operation.
+ *
+ * @return {WordArray} The plaintext.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var plaintext = CryptoJS.lib.PasswordBasedCipher.decrypt(CryptoJS.algo.AES, formattedCiphertext, 'password', { format: CryptoJS.format.OpenSSL });
+ * var plaintext = CryptoJS.lib.PasswordBasedCipher.decrypt(CryptoJS.algo.AES, ciphertextParams, 'password', { format: CryptoJS.format.OpenSSL });
+ */
+ decrypt: function (cipher, ciphertext, password, cfg) {
+ // Apply config defaults
+ cfg = this.cfg.extend(cfg);
+
+ // Convert string to CipherParams
+ ciphertext = this._parse(ciphertext, cfg.format);
+
+ // Derive key and other params
+ var derivedParams = cfg.kdf.execute(password, cipher.keySize, cipher.ivSize, ciphertext.salt);
+
+ // Add IV to config
+ cfg.iv = derivedParams.iv;
+
+ // Decrypt
+ var plaintext = SerializableCipher.decrypt.call(this, cipher, ciphertext, derivedParams.key, cfg);
+
+ return plaintext;
+ }
+ });
+ }());
+
+
+ /**
+ * Cipher Feedback block mode.
+ */
+ CryptoJS.mode.CFB = (function () {
+ var CFB = CryptoJS.lib.BlockCipherMode.extend();
+
+ CFB.Encryptor = CFB.extend({
+ processBlock: function (words, offset) {
+ // Shortcuts
+ var cipher = this._cipher;
+ var blockSize = cipher.blockSize;
+
+ generateKeystreamAndEncrypt.call(this, words, offset, blockSize, cipher);
+
+ // Remember this block to use with next block
+ this._prevBlock = words.slice(offset, offset + blockSize);
+ }
+ });
+
+ CFB.Decryptor = CFB.extend({
+ processBlock: function (words, offset) {
+ // Shortcuts
+ var cipher = this._cipher;
+ var blockSize = cipher.blockSize;
+
+ // Remember this block to use with next block
+ var thisBlock = words.slice(offset, offset + blockSize);
+
+ generateKeystreamAndEncrypt.call(this, words, offset, blockSize, cipher);
+
+ // This block becomes the previous block
+ this._prevBlock = thisBlock;
+ }
+ });
+
+ function generateKeystreamAndEncrypt(words, offset, blockSize, cipher) {
+ // Shortcut
+ var iv = this._iv;
+
+ // Generate keystream
+ if (iv) {
+ var keystream = iv.slice(0);
+
+ // Remove IV for subsequent blocks
+ this._iv = undefined;
+ } else {
+ var keystream = this._prevBlock;
+ }
+ cipher.encryptBlock(keystream, 0);
+
+ // Encrypt
+ for (var i = 0; i < blockSize; i++) {
+ words[offset + i] ^= keystream[i];
+ }
+ }
+
+ return CFB;
+ }());
+
+
+ /**
+ * Electronic Codebook block mode.
+ */
+ CryptoJS.mode.ECB = (function () {
+ var ECB = CryptoJS.lib.BlockCipherMode.extend();
+
+ ECB.Encryptor = ECB.extend({
+ processBlock: function (words, offset) {
+ this._cipher.encryptBlock(words, offset);
+ }
+ });
+
+ ECB.Decryptor = ECB.extend({
+ processBlock: function (words, offset) {
+ this._cipher.decryptBlock(words, offset);
+ }
+ });
+
+ return ECB;
+ }());
+
+
+ /**
+ * ANSI X.923 padding strategy.
+ */
+ CryptoJS.pad.AnsiX923 = {
+ pad: function (data, blockSize) {
+ // Shortcuts
+ var dataSigBytes = data.sigBytes;
+ var blockSizeBytes = blockSize * 4;
+
+ // Count padding bytes
+ var nPaddingBytes = blockSizeBytes - dataSigBytes % blockSizeBytes;
+
+ // Compute last byte position
+ var lastBytePos = dataSigBytes + nPaddingBytes - 1;
+
+ // Pad
+ data.clamp();
+ data.words[lastBytePos >>> 2] |= nPaddingBytes << (24 - (lastBytePos % 4) * 8);
+ data.sigBytes += nPaddingBytes;
+ },
+
+ unpad: function (data) {
+ // Get number of padding bytes from last byte
+ var nPaddingBytes = data.words[(data.sigBytes - 1) >>> 2] & 0xff;
+
+ // Remove padding
+ data.sigBytes -= nPaddingBytes;
+ }
+ };
+
+
+ /**
+ * ISO 10126 padding strategy.
+ */
+ CryptoJS.pad.Iso10126 = {
+ pad: function (data, blockSize) {
+ // Shortcut
+ var blockSizeBytes = blockSize * 4;
+
+ // Count padding bytes
+ var nPaddingBytes = blockSizeBytes - data.sigBytes % blockSizeBytes;
+
+ // Pad
+ data.concat(CryptoJS.lib.WordArray.random(nPaddingBytes - 1)).
+ concat(CryptoJS.lib.WordArray.create([nPaddingBytes << 24], 1));
+ },
+
+ unpad: function (data) {
+ // Get number of padding bytes from last byte
+ var nPaddingBytes = data.words[(data.sigBytes - 1) >>> 2] & 0xff;
+
+ // Remove padding
+ data.sigBytes -= nPaddingBytes;
+ }
+ };
+
+
+ /**
+ * ISO/IEC 9797-1 Padding Method 2.
+ */
+ CryptoJS.pad.Iso97971 = {
+ pad: function (data, blockSize) {
+ // Add 0x80 byte
+ data.concat(CryptoJS.lib.WordArray.create([0x80000000], 1));
+
+ // Zero pad the rest
+ CryptoJS.pad.ZeroPadding.pad(data, blockSize);
+ },
+
+ unpad: function (data) {
+ // Remove zero padding
+ CryptoJS.pad.ZeroPadding.unpad(data);
+
+ // Remove one more byte -- the 0x80 byte
+ data.sigBytes--;
+ }
+ };
+
+
+ /**
+ * Output Feedback block mode.
+ */
+ CryptoJS.mode.OFB = (function () {
+ var OFB = CryptoJS.lib.BlockCipherMode.extend();
+
+ var Encryptor = OFB.Encryptor = OFB.extend({
+ processBlock: function (words, offset) {
+ // Shortcuts
+ var cipher = this._cipher
+ var blockSize = cipher.blockSize;
+ var iv = this._iv;
+ var keystream = this._keystream;
+
+ // Generate keystream
+ if (iv) {
+ keystream = this._keystream = iv.slice(0);
+
+ // Remove IV for subsequent blocks
+ this._iv = undefined;
+ }
+ cipher.encryptBlock(keystream, 0);
+
+ // Encrypt
+ for (var i = 0; i < blockSize; i++) {
+ words[offset + i] ^= keystream[i];
+ }
+ }
+ });
+
+ OFB.Decryptor = Encryptor;
+
+ return OFB;
+ }());
+
+
+ /**
+ * A noop padding strategy.
+ */
+ CryptoJS.pad.NoPadding = {
+ pad: function () {
+ },
+
+ unpad: function () {
+ }
+ };
+
+
+ (function (undefined) {
+ // Shortcuts
+ var C = CryptoJS;
+ var C_lib = C.lib;
+ var CipherParams = C_lib.CipherParams;
+ var C_enc = C.enc;
+ var Hex = C_enc.Hex;
+ var C_format = C.format;
+
+ var HexFormatter = C_format.Hex = {
+ /**
+ * Converts the ciphertext of a cipher params object to a hexadecimally encoded string.
+ *
+ * @param {CipherParams} cipherParams The cipher params object.
+ *
+ * @return {string} The hexadecimally encoded string.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var hexString = CryptoJS.format.Hex.stringify(cipherParams);
+ */
+ stringify: function (cipherParams) {
+ return cipherParams.ciphertext.toString(Hex);
+ },
+
+ /**
+ * Converts a hexadecimally encoded ciphertext string to a cipher params object.
+ *
+ * @param {string} input The hexadecimally encoded string.
+ *
+ * @return {CipherParams} The cipher params object.
+ *
+ * @static
+ *
+ * @example
+ *
+ * var cipherParams = CryptoJS.format.Hex.parse(hexString);
+ */
+ parse: function (input) {
+ var ciphertext = Hex.parse(input);
+ return CipherParams.create({ ciphertext: ciphertext });
+ }
+ };
+ }());
+
+
+ (function () {
+ // Shortcuts
+ var C = CryptoJS;
+ var C_lib = C.lib;
+ var BlockCipher = C_lib.BlockCipher;
+ var C_algo = C.algo;
+
+ // Lookup tables
+ var SBOX = [];
+ var INV_SBOX = [];
+ var SUB_MIX_0 = [];
+ var SUB_MIX_1 = [];
+ var SUB_MIX_2 = [];
+ var SUB_MIX_3 = [];
+ var INV_SUB_MIX_0 = [];
+ var INV_SUB_MIX_1 = [];
+ var INV_SUB_MIX_2 = [];
+ var INV_SUB_MIX_3 = [];
+
+ // Compute lookup tables
+ (function () {
+ // Compute double table
+ var d = [];
+ for (var i = 0; i < 256; i++) {
+ if (i < 128) {
+ d[i] = i << 1;
+ } else {
+ d[i] = (i << 1) ^ 0x11b;
+ }
+ }
+
+ // Walk GF(2^8)
+ var x = 0;
+ var xi = 0;
+ for (var i = 0; i < 256; i++) {
+ // Compute sbox
+ var sx = xi ^ (xi << 1) ^ (xi << 2) ^ (xi << 3) ^ (xi << 4);
+ sx = (sx >>> 8) ^ (sx & 0xff) ^ 0x63;
+ SBOX[x] = sx;
+ INV_SBOX[sx] = x;
+
+ // Compute multiplication
+ var x2 = d[x];
+ var x4 = d[x2];
+ var x8 = d[x4];
+
+ // Compute sub bytes, mix columns tables
+ var t = (d[sx] * 0x101) ^ (sx * 0x1010100);
+ SUB_MIX_0[x] = (t << 24) | (t >>> 8);
+ SUB_MIX_1[x] = (t << 16) | (t >>> 16);
+ SUB_MIX_2[x] = (t << 8) | (t >>> 24);
+ SUB_MIX_3[x] = t;
+
+ // Compute inv sub bytes, inv mix columns tables
+ var t = (x8 * 0x1010101) ^ (x4 * 0x10001) ^ (x2 * 0x101) ^ (x * 0x1010100);
+ INV_SUB_MIX_0[sx] = (t << 24) | (t >>> 8);
+ INV_SUB_MIX_1[sx] = (t << 16) | (t >>> 16);
+ INV_SUB_MIX_2[sx] = (t << 8) | (t >>> 24);
+ INV_SUB_MIX_3[sx] = t;
+
+ // Compute next counter
+ if (!x) {
+ x = xi = 1;
+ } else {
+ x = x2 ^ d[d[d[x8 ^ x2]]];
+ xi ^= d[d[xi]];
+ }
+ }
+ }());
+
+ // Precomputed Rcon lookup
+ var RCON = [0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36];
+
+ /**
+ * AES block cipher algorithm.
+ */
+ var AES = C_algo.AES = BlockCipher.extend({
+ _doReset: function () {
+ // Skip reset of nRounds has been set before and key did not change
+ if (this._nRounds && this._keyPriorReset === this._key) {
+ return;
+ }
+
+ // Shortcuts
+ var key = this._keyPriorReset = this._key;
+ var keyWords = key.words;
+ var keySize = key.sigBytes / 4;
+
+ // Compute number of rounds
+ var nRounds = this._nRounds = keySize + 6;
+
+ // Compute number of key schedule rows
+ var ksRows = (nRounds + 1) * 4;
+
+ // Compute key schedule
+ var keySchedule = this._keySchedule = [];
+ for (var ksRow = 0; ksRow < ksRows; ksRow++) {
+ if (ksRow < keySize) {
+ keySchedule[ksRow] = keyWords[ksRow];
+ } else {
+ var t = keySchedule[ksRow - 1];
+
+ if (!(ksRow % keySize)) {
+ // Rot word
+ t = (t << 8) | (t >>> 24);
+
+ // Sub word
+ t = (SBOX[t >>> 24] << 24) | (SBOX[(t >>> 16) & 0xff] << 16) | (SBOX[(t >>> 8) & 0xff] << 8) | SBOX[t & 0xff];
+
+ // Mix Rcon
+ t ^= RCON[(ksRow / keySize) | 0] << 24;
+ } else if (keySize > 6 && ksRow % keySize == 4) {
+ // Sub word
+ t = (SBOX[t >>> 24] << 24) | (SBOX[(t >>> 16) & 0xff] << 16) | (SBOX[(t >>> 8) & 0xff] << 8) | SBOX[t & 0xff];
+ }
+
+ keySchedule[ksRow] = keySchedule[ksRow - keySize] ^ t;
+ }
+ }
+
+ // Compute inv key schedule
+ var invKeySchedule = this._invKeySchedule = [];
+ for (var invKsRow = 0; invKsRow < ksRows; invKsRow++) {
+ var ksRow = ksRows - invKsRow;
+
+ if (invKsRow % 4) {
+ var t = keySchedule[ksRow];
+ } else {
+ var t = keySchedule[ksRow - 4];
+ }
+
+ if (invKsRow < 4 || ksRow <= 4) {
+ invKeySchedule[invKsRow] = t;
+ } else {
+ invKeySchedule[invKsRow] = INV_SUB_MIX_0[SBOX[t >>> 24]] ^ INV_SUB_MIX_1[SBOX[(t >>> 16) & 0xff]] ^
+ INV_SUB_MIX_2[SBOX[(t >>> 8) & 0xff]] ^ INV_SUB_MIX_3[SBOX[t & 0xff]];
+ }
+ }
+ },
+
+ encryptBlock: function (M, offset) {
+ this._doCryptBlock(M, offset, this._keySchedule, SUB_MIX_0, SUB_MIX_1, SUB_MIX_2, SUB_MIX_3, SBOX);
+ },
+
+ decryptBlock: function (M, offset) {
+ // Swap 2nd and 4th rows
+ var t = M[offset + 1];
+ M[offset + 1] = M[offset + 3];
+ M[offset + 3] = t;
+
+ this._doCryptBlock(M, offset, this._invKeySchedule, INV_SUB_MIX_0, INV_SUB_MIX_1, INV_SUB_MIX_2, INV_SUB_MIX_3, INV_SBOX);
+
+ // Inv swap 2nd and 4th rows
+ var t = M[offset + 1];
+ M[offset + 1] = M[offset + 3];
+ M[offset + 3] = t;
+ },
+
+ _doCryptBlock: function (M, offset, keySchedule, SUB_MIX_0, SUB_MIX_1, SUB_MIX_2, SUB_MIX_3, SBOX) {
+ // Shortcut
+ var nRounds = this._nRounds;
+
+ // Get input, add round key
+ var s0 = M[offset] ^ keySchedule[0];
+ var s1 = M[offset + 1] ^ keySchedule[1];
+ var s2 = M[offset + 2] ^ keySchedule[2];
+ var s3 = M[offset + 3] ^ keySchedule[3];
+
+ // Key schedule row counter
+ var ksRow = 4;
+
+ // Rounds
+ for (var round = 1; round < nRounds; round++) {
+ // Shift rows, sub bytes, mix columns, add round key
+ var t0 = SUB_MIX_0[s0 >>> 24] ^ SUB_MIX_1[(s1 >>> 16) & 0xff] ^ SUB_MIX_2[(s2 >>> 8) & 0xff] ^ SUB_MIX_3[s3 & 0xff] ^ keySchedule[ksRow++];
+ var t1 = SUB_MIX_0[s1 >>> 24] ^ SUB_MIX_1[(s2 >>> 16) & 0xff] ^ SUB_MIX_2[(s3 >>> 8) & 0xff] ^ SUB_MIX_3[s0 & 0xff] ^ keySchedule[ksRow++];
+ var t2 = SUB_MIX_0[s2 >>> 24] ^ SUB_MIX_1[(s3 >>> 16) & 0xff] ^ SUB_MIX_2[(s0 >>> 8) & 0xff] ^ SUB_MIX_3[s1 & 0xff] ^ keySchedule[ksRow++];
+ var t3 = SUB_MIX_0[s3 >>> 24] ^ SUB_MIX_1[(s0 >>> 16) & 0xff] ^ SUB_MIX_2[(s1 >>> 8) & 0xff] ^ SUB_MIX_3[s2 & 0xff] ^ keySchedule[ksRow++];
+
+ // Update state
+ s0 = t0;
+ s1 = t1;
+ s2 = t2;
+ s3 = t3;
+ }
+
+ // Shift rows, sub bytes, add round key
+ var t0 = ((SBOX[s0 >>> 24] << 24) | (SBOX[(s1 >>> 16) & 0xff] << 16) | (SBOX[(s2 >>> 8) & 0xff] << 8) | SBOX[s3 & 0xff]) ^ keySchedule[ksRow++];
+ var t1 = ((SBOX[s1 >>> 24] << 24) | (SBOX[(s2 >>> 16) & 0xff] << 16) | (SBOX[(s3 >>> 8) & 0xff] << 8) | SBOX[s0 & 0xff]) ^ keySchedule[ksRow++];
+ var t2 = ((SBOX[s2 >>> 24] << 24) | (SBOX[(s3 >>> 16) & 0xff] << 16) | (SBOX[(s0 >>> 8) & 0xff] << 8) | SBOX[s1 & 0xff]) ^ keySchedule[ksRow++];
+ var t3 = ((SBOX[s3 >>> 24] << 24) | (SBOX[(s0 >>> 16) & 0xff] << 16) | (SBOX[(s1 >>> 8) & 0xff] << 8) | SBOX[s2 & 0xff]) ^ keySchedule[ksRow++];
+
+ // Set output
+ M[offset] = t0;
+ M[offset + 1] = t1;
+ M[offset + 2] = t2;
+ M[offset + 3] = t3;
+ },
+
+ keySize: 256/32
+ });
+
+ /**
+ * Shortcut functions to the cipher's object interface.
+ *
+ * @example
+ *
+ * var ciphertext = CryptoJS.AES.encrypt(message, key, cfg);
+ * var plaintext = CryptoJS.AES.decrypt(ciphertext, key, cfg);
+ */
+ C.AES = BlockCipher._createHelper(AES);
+ }());
+
+
+ (function () {
+ // Shortcuts
+ var C = CryptoJS;
+ var C_lib = C.lib;
+ var WordArray = C_lib.WordArray;
+ var BlockCipher = C_lib.BlockCipher;
+ var C_algo = C.algo;
+
+ // Permuted Choice 1 constants
+ var PC1 = [
+ 57, 49, 41, 33, 25, 17, 9, 1,
+ 58, 50, 42, 34, 26, 18, 10, 2,
+ 59, 51, 43, 35, 27, 19, 11, 3,
+ 60, 52, 44, 36, 63, 55, 47, 39,
+ 31, 23, 15, 7, 62, 54, 46, 38,
+ 30, 22, 14, 6, 61, 53, 45, 37,
+ 29, 21, 13, 5, 28, 20, 12, 4
+ ];
+
+ // Permuted Choice 2 constants
+ var PC2 = [
+ 14, 17, 11, 24, 1, 5,
+ 3, 28, 15, 6, 21, 10,
+ 23, 19, 12, 4, 26, 8,
+ 16, 7, 27, 20, 13, 2,
+ 41, 52, 31, 37, 47, 55,
+ 30, 40, 51, 45, 33, 48,
+ 44, 49, 39, 56, 34, 53,
+ 46, 42, 50, 36, 29, 32
+ ];
+
+ // Cumulative bit shift constants
+ var BIT_SHIFTS = [1, 2, 4, 6, 8, 10, 12, 14, 15, 17, 19, 21, 23, 25, 27, 28];
+
+ // SBOXes and round permutation constants
+ var SBOX_P = [
+ {
+ 0x0: 0x808200,
+ 0x10000000: 0x8000,
+ 0x20000000: 0x808002,
+ 0x30000000: 0x2,
+ 0x40000000: 0x200,
+ 0x50000000: 0x808202,
+ 0x60000000: 0x800202,
+ 0x70000000: 0x800000,
+ 0x80000000: 0x202,
+ 0x90000000: 0x800200,
+ 0xa0000000: 0x8200,
+ 0xb0000000: 0x808000,
+ 0xc0000000: 0x8002,
+ 0xd0000000: 0x800002,
+ 0xe0000000: 0x0,
+ 0xf0000000: 0x8202,
+ 0x8000000: 0x0,
+ 0x18000000: 0x808202,
+ 0x28000000: 0x8202,
+ 0x38000000: 0x8000,
+ 0x48000000: 0x808200,
+ 0x58000000: 0x200,
+ 0x68000000: 0x808002,
+ 0x78000000: 0x2,
+ 0x88000000: 0x800200,
+ 0x98000000: 0x8200,
+ 0xa8000000: 0x808000,
+ 0xb8000000: 0x800202,
+ 0xc8000000: 0x800002,
+ 0xd8000000: 0x8002,
+ 0xe8000000: 0x202,
+ 0xf8000000: 0x800000,
+ 0x1: 0x8000,
+ 0x10000001: 0x2,
+ 0x20000001: 0x808200,
+ 0x30000001: 0x800000,
+ 0x40000001: 0x808002,
+ 0x50000001: 0x8200,
+ 0x60000001: 0x200,
+ 0x70000001: 0x800202,
+ 0x80000001: 0x808202,
+ 0x90000001: 0x808000,
+ 0xa0000001: 0x800002,
+ 0xb0000001: 0x8202,
+ 0xc0000001: 0x202,
+ 0xd0000001: 0x800200,
+ 0xe0000001: 0x8002,
+ 0xf0000001: 0x0,
+ 0x8000001: 0x808202,
+ 0x18000001: 0x808000,
+ 0x28000001: 0x800000,
+ 0x38000001: 0x200,
+ 0x48000001: 0x8000,
+ 0x58000001: 0x800002,
+ 0x68000001: 0x2,
+ 0x78000001: 0x8202,
+ 0x88000001: 0x8002,
+ 0x98000001: 0x800202,
+ 0xa8000001: 0x202,
+ 0xb8000001: 0x808200,
+ 0xc8000001: 0x800200,
+ 0xd8000001: 0x0,
+ 0xe8000001: 0x8200,
+ 0xf8000001: 0x808002
+ },
+ {
+ 0x0: 0x40084010,
+ 0x1000000: 0x4000,
+ 0x2000000: 0x80000,
+ 0x3000000: 0x40080010,
+ 0x4000000: 0x40000010,
+ 0x5000000: 0x40084000,
+ 0x6000000: 0x40004000,
+ 0x7000000: 0x10,
+ 0x8000000: 0x84000,
+ 0x9000000: 0x40004010,
+ 0xa000000: 0x40000000,
+ 0xb000000: 0x84010,
+ 0xc000000: 0x80010,
+ 0xd000000: 0x0,
+ 0xe000000: 0x4010,
+ 0xf000000: 0x40080000,
+ 0x800000: 0x40004000,
+ 0x1800000: 0x84010,
+ 0x2800000: 0x10,
+ 0x3800000: 0x40004010,
+ 0x4800000: 0x40084010,
+ 0x5800000: 0x40000000,
+ 0x6800000: 0x80000,
+ 0x7800000: 0x40080010,
+ 0x8800000: 0x80010,
+ 0x9800000: 0x0,
+ 0xa800000: 0x4000,
+ 0xb800000: 0x40080000,
+ 0xc800000: 0x40000010,
+ 0xd800000: 0x84000,
+ 0xe800000: 0x40084000,
+ 0xf800000: 0x4010,
+ 0x10000000: 0x0,
+ 0x11000000: 0x40080010,
+ 0x12000000: 0x40004010,
+ 0x13000000: 0x40084000,
+ 0x14000000: 0x40080000,
+ 0x15000000: 0x10,
+ 0x16000000: 0x84010,
+ 0x17000000: 0x4000,
+ 0x18000000: 0x4010,
+ 0x19000000: 0x80000,
+ 0x1a000000: 0x80010,
+ 0x1b000000: 0x40000010,
+ 0x1c000000: 0x84000,
+ 0x1d000000: 0x40004000,
+ 0x1e000000: 0x40000000,
+ 0x1f000000: 0x40084010,
+ 0x10800000: 0x84010,
+ 0x11800000: 0x80000,
+ 0x12800000: 0x40080000,
+ 0x13800000: 0x4000,
+ 0x14800000: 0x40004000,
+ 0x15800000: 0x40084010,
+ 0x16800000: 0x10,
+ 0x17800000: 0x40000000,
+ 0x18800000: 0x40084000,
+ 0x19800000: 0x40000010,
+ 0x1a800000: 0x40004010,
+ 0x1b800000: 0x80010,
+ 0x1c800000: 0x0,
+ 0x1d800000: 0x4010,
+ 0x1e800000: 0x40080010,
+ 0x1f800000: 0x84000
+ },
+ {
+ 0x0: 0x104,
+ 0x100000: 0x0,
+ 0x200000: 0x4000100,
+ 0x300000: 0x10104,
+ 0x400000: 0x10004,
+ 0x500000: 0x4000004,
+ 0x600000: 0x4010104,
+ 0x700000: 0x4010000,
+ 0x800000: 0x4000000,
+ 0x900000: 0x4010100,
+ 0xa00000: 0x10100,
+ 0xb00000: 0x4010004,
+ 0xc00000: 0x4000104,
+ 0xd00000: 0x10000,
+ 0xe00000: 0x4,
+ 0xf00000: 0x100,
+ 0x80000: 0x4010100,
+ 0x180000: 0x4010004,
+ 0x280000: 0x0,
+ 0x380000: 0x4000100,
+ 0x480000: 0x4000004,
+ 0x580000: 0x10000,
+ 0x680000: 0x10004,
+ 0x780000: 0x104,
+ 0x880000: 0x4,
+ 0x980000: 0x100,
+ 0xa80000: 0x4010000,
+ 0xb80000: 0x10104,
+ 0xc80000: 0x10100,
+ 0xd80000: 0x4000104,
+ 0xe80000: 0x4010104,
+ 0xf80000: 0x4000000,
+ 0x1000000: 0x4010100,
+ 0x1100000: 0x10004,
+ 0x1200000: 0x10000,
+ 0x1300000: 0x4000100,
+ 0x1400000: 0x100,
+ 0x1500000: 0x4010104,
+ 0x1600000: 0x4000004,
+ 0x1700000: 0x0,
+ 0x1800000: 0x4000104,
+ 0x1900000: 0x4000000,
+ 0x1a00000: 0x4,
+ 0x1b00000: 0x10100,
+ 0x1c00000: 0x4010000,
+ 0x1d00000: 0x104,
+ 0x1e00000: 0x10104,
+ 0x1f00000: 0x4010004,
+ 0x1080000: 0x4000000,
+ 0x1180000: 0x104,
+ 0x1280000: 0x4010100,
+ 0x1380000: 0x0,
+ 0x1480000: 0x10004,
+ 0x1580000: 0x4000100,
+ 0x1680000: 0x100,
+ 0x1780000: 0x4010004,
+ 0x1880000: 0x10000,
+ 0x1980000: 0x4010104,
+ 0x1a80000: 0x10104,
+ 0x1b80000: 0x4000004,
+ 0x1c80000: 0x4000104,
+ 0x1d80000: 0x4010000,
+ 0x1e80000: 0x4,
+ 0x1f80000: 0x10100
+ },
+ {
+ 0x0: 0x80401000,
+ 0x10000: 0x80001040,
+ 0x20000: 0x401040,
+ 0x30000: 0x80400000,
+ 0x40000: 0x0,
+ 0x50000: 0x401000,
+ 0x60000: 0x80000040,
+ 0x70000: 0x400040,
+ 0x80000: 0x80000000,
+ 0x90000: 0x400000,
+ 0xa0000: 0x40,
+ 0xb0000: 0x80001000,
+ 0xc0000: 0x80400040,
+ 0xd0000: 0x1040,
+ 0xe0000: 0x1000,
+ 0xf0000: 0x80401040,
+ 0x8000: 0x80001040,
+ 0x18000: 0x40,
+ 0x28000: 0x80400040,
+ 0x38000: 0x80001000,
+ 0x48000: 0x401000,
+ 0x58000: 0x80401040,
+ 0x68000: 0x0,
+ 0x78000: 0x80400000,
+ 0x88000: 0x1000,
+ 0x98000: 0x80401000,
+ 0xa8000: 0x400000,
+ 0xb8000: 0x1040,
+ 0xc8000: 0x80000000,
+ 0xd8000: 0x400040,
+ 0xe8000: 0x401040,
+ 0xf8000: 0x80000040,
+ 0x100000: 0x400040,
+ 0x110000: 0x401000,
+ 0x120000: 0x80000040,
+ 0x130000: 0x0,
+ 0x140000: 0x1040,
+ 0x150000: 0x80400040,
+ 0x160000: 0x80401000,
+ 0x170000: 0x80001040,
+ 0x180000: 0x80401040,
+ 0x190000: 0x80000000,
+ 0x1a0000: 0x80400000,
+ 0x1b0000: 0x401040,
+ 0x1c0000: 0x80001000,
+ 0x1d0000: 0x400000,
+ 0x1e0000: 0x40,
+ 0x1f0000: 0x1000,
+ 0x108000: 0x80400000,
+ 0x118000: 0x80401040,
+ 0x128000: 0x0,
+ 0x138000: 0x401000,
+ 0x148000: 0x400040,
+ 0x158000: 0x80000000,
+ 0x168000: 0x80001040,
+ 0x178000: 0x40,
+ 0x188000: 0x80000040,
+ 0x198000: 0x1000,
+ 0x1a8000: 0x80001000,
+ 0x1b8000: 0x80400040,
+ 0x1c8000: 0x1040,
+ 0x1d8000: 0x80401000,
+ 0x1e8000: 0x400000,
+ 0x1f8000: 0x401040
+ },
+ {
+ 0x0: 0x80,
+ 0x1000: 0x1040000,
+ 0x2000: 0x40000,
+ 0x3000: 0x20000000,
+ 0x4000: 0x20040080,
+ 0x5000: 0x1000080,
+ 0x6000: 0x21000080,
+ 0x7000: 0x40080,
+ 0x8000: 0x1000000,
+ 0x9000: 0x20040000,
+ 0xa000: 0x20000080,
+ 0xb000: 0x21040080,
+ 0xc000: 0x21040000,
+ 0xd000: 0x0,
+ 0xe000: 0x1040080,
+ 0xf000: 0x21000000,
+ 0x800: 0x1040080,
+ 0x1800: 0x21000080,
+ 0x2800: 0x80,
+ 0x3800: 0x1040000,
+ 0x4800: 0x40000,
+ 0x5800: 0x20040080,
+ 0x6800: 0x21040000,
+ 0x7800: 0x20000000,
+ 0x8800: 0x20040000,
+ 0x9800: 0x0,
+ 0xa800: 0x21040080,
+ 0xb800: 0x1000080,
+ 0xc800: 0x20000080,
+ 0xd800: 0x21000000,
+ 0xe800: 0x1000000,
+ 0xf800: 0x40080,
+ 0x10000: 0x40000,
+ 0x11000: 0x80,
+ 0x12000: 0x20000000,
+ 0x13000: 0x21000080,
+ 0x14000: 0x1000080,
+ 0x15000: 0x21040000,
+ 0x16000: 0x20040080,
+ 0x17000: 0x1000000,
+ 0x18000: 0x21040080,
+ 0x19000: 0x21000000,
+ 0x1a000: 0x1040000,
+ 0x1b000: 0x20040000,
+ 0x1c000: 0x40080,
+ 0x1d000: 0x20000080,
+ 0x1e000: 0x0,
+ 0x1f000: 0x1040080,
+ 0x10800: 0x21000080,
+ 0x11800: 0x1000000,
+ 0x12800: 0x1040000,
+ 0x13800: 0x20040080,
+ 0x14800: 0x20000000,
+ 0x15800: 0x1040080,
+ 0x16800: 0x80,
+ 0x17800: 0x21040000,
+ 0x18800: 0x40080,
+ 0x19800: 0x21040080,
+ 0x1a800: 0x0,
+ 0x1b800: 0x21000000,
+ 0x1c800: 0x1000080,
+ 0x1d800: 0x40000,
+ 0x1e800: 0x20040000,
+ 0x1f800: 0x20000080
+ },
+ {
+ 0x0: 0x10000008,
+ 0x100: 0x2000,
+ 0x200: 0x10200000,
+ 0x300: 0x10202008,
+ 0x400: 0x10002000,
+ 0x500: 0x200000,
+ 0x600: 0x200008,
+ 0x700: 0x10000000,
+ 0x800: 0x0,
+ 0x900: 0x10002008,
+ 0xa00: 0x202000,
+ 0xb00: 0x8,
+ 0xc00: 0x10200008,
+ 0xd00: 0x202008,
+ 0xe00: 0x2008,
+ 0xf00: 0x10202000,
+ 0x80: 0x10200000,
+ 0x180: 0x10202008,
+ 0x280: 0x8,
+ 0x380: 0x200000,
+ 0x480: 0x202008,
+ 0x580: 0x10000008,
+ 0x680: 0x10002000,
+ 0x780: 0x2008,
+ 0x880: 0x200008,
+ 0x980: 0x2000,
+ 0xa80: 0x10002008,
+ 0xb80: 0x10200008,
+ 0xc80: 0x0,
+ 0xd80: 0x10202000,
+ 0xe80: 0x202000,
+ 0xf80: 0x10000000,
+ 0x1000: 0x10002000,
+ 0x1100: 0x10200008,
+ 0x1200: 0x10202008,
+ 0x1300: 0x2008,
+ 0x1400: 0x200000,
+ 0x1500: 0x10000000,
+ 0x1600: 0x10000008,
+ 0x1700: 0x202000,
+ 0x1800: 0x202008,
+ 0x1900: 0x0,
+ 0x1a00: 0x8,
+ 0x1b00: 0x10200000,
+ 0x1c00: 0x2000,
+ 0x1d00: 0x10002008,
+ 0x1e00: 0x10202000,
+ 0x1f00: 0x200008,
+ 0x1080: 0x8,
+ 0x1180: 0x202000,
+ 0x1280: 0x200000,
+ 0x1380: 0x10000008,
+ 0x1480: 0x10002000,
+ 0x1580: 0x2008,
+ 0x1680: 0x10202008,
+ 0x1780: 0x10200000,
+ 0x1880: 0x10202000,
+ 0x1980: 0x10200008,
+ 0x1a80: 0x2000,
+ 0x1b80: 0x202008,
+ 0x1c80: 0x200008,
+ 0x1d80: 0x0,
+ 0x1e80: 0x10000000,
+ 0x1f80: 0x10002008
+ },
+ {
+ 0x0: 0x100000,
+ 0x10: 0x2000401,
+ 0x20: 0x400,
+ 0x30: 0x100401,
+ 0x40: 0x2100401,
+ 0x50: 0x0,
+ 0x60: 0x1,
+ 0x70: 0x2100001,
+ 0x80: 0x2000400,
+ 0x90: 0x100001,
+ 0xa0: 0x2000001,
+ 0xb0: 0x2100400,
+ 0xc0: 0x2100000,
+ 0xd0: 0x401,
+ 0xe0: 0x100400,
+ 0xf0: 0x2000000,
+ 0x8: 0x2100001,
+ 0x18: 0x0,
+ 0x28: 0x2000401,
+ 0x38: 0x2100400,
+ 0x48: 0x100000,
+ 0x58: 0x2000001,
+ 0x68: 0x2000000,
+ 0x78: 0x401,
+ 0x88: 0x100401,
+ 0x98: 0x2000400,
+ 0xa8: 0x2100000,
+ 0xb8: 0x100001,
+ 0xc8: 0x400,
+ 0xd8: 0x2100401,
+ 0xe8: 0x1,
+ 0xf8: 0x100400,
+ 0x100: 0x2000000,
+ 0x110: 0x100000,
+ 0x120: 0x2000401,
+ 0x130: 0x2100001,
+ 0x140: 0x100001,
+ 0x150: 0x2000400,
+ 0x160: 0x2100400,
+ 0x170: 0x100401,
+ 0x180: 0x401,
+ 0x190: 0x2100401,
+ 0x1a0: 0x100400,
+ 0x1b0: 0x1,
+ 0x1c0: 0x0,
+ 0x1d0: 0x2100000,
+ 0x1e0: 0x2000001,
+ 0x1f0: 0x400,
+ 0x108: 0x100400,
+ 0x118: 0x2000401,
+ 0x128: 0x2100001,
+ 0x138: 0x1,
+ 0x148: 0x2000000,
+ 0x158: 0x100000,
+ 0x168: 0x401,
+ 0x178: 0x2100400,
+ 0x188: 0x2000001,
+ 0x198: 0x2100000,
+ 0x1a8: 0x0,
+ 0x1b8: 0x2100401,
+ 0x1c8: 0x100401,
+ 0x1d8: 0x400,
+ 0x1e8: 0x2000400,
+ 0x1f8: 0x100001
+ },
+ {
+ 0x0: 0x8000820,
+ 0x1: 0x20000,
+ 0x2: 0x8000000,
+ 0x3: 0x20,
+ 0x4: 0x20020,
+ 0x5: 0x8020820,
+ 0x6: 0x8020800,
+ 0x7: 0x800,
+ 0x8: 0x8020000,
+ 0x9: 0x8000800,
+ 0xa: 0x20800,
+ 0xb: 0x8020020,
+ 0xc: 0x820,
+ 0xd: 0x0,
+ 0xe: 0x8000020,
+ 0xf: 0x20820,
+ 0x80000000: 0x800,
+ 0x80000001: 0x8020820,
+ 0x80000002: 0x8000820,
+ 0x80000003: 0x8000000,
+ 0x80000004: 0x8020000,
+ 0x80000005: 0x20800,
+ 0x80000006: 0x20820,
+ 0x80000007: 0x20,
+ 0x80000008: 0x8000020,
+ 0x80000009: 0x820,
+ 0x8000000a: 0x20020,
+ 0x8000000b: 0x8020800,
+ 0x8000000c: 0x0,
+ 0x8000000d: 0x8020020,
+ 0x8000000e: 0x8000800,
+ 0x8000000f: 0x20000,
+ 0x10: 0x20820,
+ 0x11: 0x8020800,
+ 0x12: 0x20,
+ 0x13: 0x800,
+ 0x14: 0x8000800,
+ 0x15: 0x8000020,
+ 0x16: 0x8020020,
+ 0x17: 0x20000,
+ 0x18: 0x0,
+ 0x19: 0x20020,
+ 0x1a: 0x8020000,
+ 0x1b: 0x8000820,
+ 0x1c: 0x8020820,
+ 0x1d: 0x20800,
+ 0x1e: 0x820,
+ 0x1f: 0x8000000,
+ 0x80000010: 0x20000,
+ 0x80000011: 0x800,
+ 0x80000012: 0x8020020,
+ 0x80000013: 0x20820,
+ 0x80000014: 0x20,
+ 0x80000015: 0x8020000,
+ 0x80000016: 0x8000000,
+ 0x80000017: 0x8000820,
+ 0x80000018: 0x8020820,
+ 0x80000019: 0x8000020,
+ 0x8000001a: 0x8000800,
+ 0x8000001b: 0x0,
+ 0x8000001c: 0x20800,
+ 0x8000001d: 0x820,
+ 0x8000001e: 0x20020,
+ 0x8000001f: 0x8020800
+ }
+ ];
+
+ // Masks that select the SBOX input
+ var SBOX_MASK = [
+ 0xf8000001, 0x1f800000, 0x01f80000, 0x001f8000,
+ 0x0001f800, 0x00001f80, 0x000001f8, 0x8000001f
+ ];
+
+ /**
+ * DES block cipher algorithm.
+ */
+ var DES = C_algo.DES = BlockCipher.extend({
+ _doReset: function () {
+ // Shortcuts
+ var key = this._key;
+ var keyWords = key.words;
+
+ // Select 56 bits according to PC1
+ var keyBits = [];
+ for (var i = 0; i < 56; i++) {
+ var keyBitPos = PC1[i] - 1;
+ keyBits[i] = (keyWords[keyBitPos >>> 5] >>> (31 - keyBitPos % 32)) & 1;
+ }
+
+ // Assemble 16 subkeys
+ var subKeys = this._subKeys = [];
+ for (var nSubKey = 0; nSubKey < 16; nSubKey++) {
+ // Create subkey
+ var subKey = subKeys[nSubKey] = [];
+
+ // Shortcut
+ var bitShift = BIT_SHIFTS[nSubKey];
+
+ // Select 48 bits according to PC2
+ for (var i = 0; i < 24; i++) {
+ // Select from the left 28 key bits
+ subKey[(i / 6) | 0] |= keyBits[((PC2[i] - 1) + bitShift) % 28] << (31 - i % 6);
+
+ // Select from the right 28 key bits
+ subKey[4 + ((i / 6) | 0)] |= keyBits[28 + (((PC2[i + 24] - 1) + bitShift) % 28)] << (31 - i % 6);
+ }
+
+ // Since each subkey is applied to an expanded 32-bit input,
+ // the subkey can be broken into 8 values scaled to 32-bits,
+ // which allows the key to be used without expansion
+ subKey[0] = (subKey[0] << 1) | (subKey[0] >>> 31);
+ for (var i = 1; i < 7; i++) {
+ subKey[i] = subKey[i] >>> ((i - 1) * 4 + 3);
+ }
+ subKey[7] = (subKey[7] << 5) | (subKey[7] >>> 27);
+ }
+
+ // Compute inverse subkeys
+ var invSubKeys = this._invSubKeys = [];
+ for (var i = 0; i < 16; i++) {
+ invSubKeys[i] = subKeys[15 - i];
+ }
+ },
+
+ encryptBlock: function (M, offset) {
+ this._doCryptBlock(M, offset, this._subKeys);
+ },
+
+ decryptBlock: function (M, offset) {
+ this._doCryptBlock(M, offset, this._invSubKeys);
+ },
+
+ _doCryptBlock: function (M, offset, subKeys) {
+ // Get input
+ this._lBlock = M[offset];
+ this._rBlock = M[offset + 1];
+
+ // Initial permutation
+ exchangeLR.call(this, 4, 0x0f0f0f0f);
+ exchangeLR.call(this, 16, 0x0000ffff);
+ exchangeRL.call(this, 2, 0x33333333);
+ exchangeRL.call(this, 8, 0x00ff00ff);
+ exchangeLR.call(this, 1, 0x55555555);
+
+ // Rounds
+ for (var round = 0; round < 16; round++) {
+ // Shortcuts
+ var subKey = subKeys[round];
+ var lBlock = this._lBlock;
+ var rBlock = this._rBlock;
+
+ // Feistel function
+ var f = 0;
+ for (var i = 0; i < 8; i++) {
+ f |= SBOX_P[i][((rBlock ^ subKey[i]) & SBOX_MASK[i]) >>> 0];
+ }
+ this._lBlock = rBlock;
+ this._rBlock = lBlock ^ f;
+ }
+
+ // Undo swap from last round
+ var t = this._lBlock;
+ this._lBlock = this._rBlock;
+ this._rBlock = t;
+
+ // Final permutation
+ exchangeLR.call(this, 1, 0x55555555);
+ exchangeRL.call(this, 8, 0x00ff00ff);
+ exchangeRL.call(this, 2, 0x33333333);
+ exchangeLR.call(this, 16, 0x0000ffff);
+ exchangeLR.call(this, 4, 0x0f0f0f0f);
+
+ // Set output
+ M[offset] = this._lBlock;
+ M[offset + 1] = this._rBlock;
+ },
+
+ keySize: 64/32,
+
+ ivSize: 64/32,
+
+ blockSize: 64/32
+ });
+
+ // Swap bits across the left and right words
+ function exchangeLR(offset, mask) {
+ var t = ((this._lBlock >>> offset) ^ this._rBlock) & mask;
+ this._rBlock ^= t;
+ this._lBlock ^= t << offset;
+ }
+
+ function exchangeRL(offset, mask) {
+ var t = ((this._rBlock >>> offset) ^ this._lBlock) & mask;
+ this._lBlock ^= t;
+ this._rBlock ^= t << offset;
+ }
+
+ /**
+ * Shortcut functions to the cipher's object interface.
+ *
+ * @example
+ *
+ * var ciphertext = CryptoJS.DES.encrypt(message, key, cfg);
+ * var plaintext = CryptoJS.DES.decrypt(ciphertext, key, cfg);
+ */
+ C.DES = BlockCipher._createHelper(DES);
+
+ /**
+ * Triple-DES block cipher algorithm.
+ */
+ var TripleDES = C_algo.TripleDES = BlockCipher.extend({
+ _doReset: function () {
+ // Shortcuts
+ var key = this._key;
+ var keyWords = key.words;
+
+ // Create DES instances
+ this._des1 = DES.createEncryptor(WordArray.create(keyWords.slice(0, 2)));
+ this._des2 = DES.createEncryptor(WordArray.create(keyWords.slice(2, 4)));
+ this._des3 = DES.createEncryptor(WordArray.create(keyWords.slice(4, 6)));
+ },
+
+ encryptBlock: function (M, offset) {
+ this._des1.encryptBlock(M, offset);
+ this._des2.decryptBlock(M, offset);
+ this._des3.encryptBlock(M, offset);
+ },
+
+ decryptBlock: function (M, offset) {
+ this._des3.decryptBlock(M, offset);
+ this._des2.encryptBlock(M, offset);
+ this._des1.decryptBlock(M, offset);
+ },
+
+ keySize: 192/32,
+
+ ivSize: 64/32,
+
+ blockSize: 64/32
+ });
+
+ /**
+ * Shortcut functions to the cipher's object interface.
+ *
+ * @example
+ *
+ * var ciphertext = CryptoJS.TripleDES.encrypt(message, key, cfg);
+ * var plaintext = CryptoJS.TripleDES.decrypt(ciphertext, key, cfg);
+ */
+ C.TripleDES = BlockCipher._createHelper(TripleDES);
+ }());
+
+
+ (function () {
+ // Shortcuts
+ var C = CryptoJS;
+ var C_lib = C.lib;
+ var StreamCipher = C_lib.StreamCipher;
+ var C_algo = C.algo;
+
+ /**
+ * RC4 stream cipher algorithm.
+ */
+ var RC4 = C_algo.RC4 = StreamCipher.extend({
+ _doReset: function () {
+ // Shortcuts
+ var key = this._key;
+ var keyWords = key.words;
+ var keySigBytes = key.sigBytes;
+
+ // Init sbox
+ var S = this._S = [];
+ for (var i = 0; i < 256; i++) {
+ S[i] = i;
+ }
+
+ // Key setup
+ for (var i = 0, j = 0; i < 256; i++) {
+ var keyByteIndex = i % keySigBytes;
+ var keyByte = (keyWords[keyByteIndex >>> 2] >>> (24 - (keyByteIndex % 4) * 8)) & 0xff;
+
+ j = (j + S[i] + keyByte) % 256;
+
+ // Swap
+ var t = S[i];
+ S[i] = S[j];
+ S[j] = t;
+ }
+
+ // Counters
+ this._i = this._j = 0;
+ },
+
+ _doProcessBlock: function (M, offset) {
+ M[offset] ^= generateKeystreamWord.call(this);
+ },
+
+ keySize: 256/32,
+
+ ivSize: 0
+ });
+
+ function generateKeystreamWord() {
+ // Shortcuts
+ var S = this._S;
+ var i = this._i;
+ var j = this._j;
+
+ // Generate keystream word
+ var keystreamWord = 0;
+ for (var n = 0; n < 4; n++) {
+ i = (i + 1) % 256;
+ j = (j + S[i]) % 256;
+
+ // Swap
+ var t = S[i];
+ S[i] = S[j];
+ S[j] = t;
+
+ keystreamWord |= S[(S[i] + S[j]) % 256] << (24 - n * 8);
+ }
+
+ // Update counters
+ this._i = i;
+ this._j = j;
+
+ return keystreamWord;
+ }
+
+ /**
+ * Shortcut functions to the cipher's object interface.
+ *
+ * @example
+ *
+ * var ciphertext = CryptoJS.RC4.encrypt(message, key, cfg);
+ * var plaintext = CryptoJS.RC4.decrypt(ciphertext, key, cfg);
+ */
+ C.RC4 = StreamCipher._createHelper(RC4);
+
+ /**
+ * Modified RC4 stream cipher algorithm.
+ */
+ var RC4Drop = C_algo.RC4Drop = RC4.extend({
+ /**
+ * Configuration options.
+ *
+ * @property {number} drop The number of keystream words to drop. Default 192
+ */
+ cfg: RC4.cfg.extend({
+ drop: 192
+ }),
+
+ _doReset: function () {
+ RC4._doReset.call(this);
+
+ // Drop
+ for (var i = this.cfg.drop; i > 0; i--) {
+ generateKeystreamWord.call(this);
+ }
+ }
+ });
+
+ /**
+ * Shortcut functions to the cipher's object interface.
+ *
+ * @example
+ *
+ * var ciphertext = CryptoJS.RC4Drop.encrypt(message, key, cfg);
+ * var plaintext = CryptoJS.RC4Drop.decrypt(ciphertext, key, cfg);
+ */
+ C.RC4Drop = StreamCipher._createHelper(RC4Drop);
+ }());
+
+
+ /** @preserve
+ * Counter block mode compatible with Dr Brian Gladman fileenc.c
+ * derived from CryptoJS.mode.CTR
+ * Jan Hruby jhruby.web@gmail.com
+ */
+ CryptoJS.mode.CTRGladman = (function () {
+ var CTRGladman = CryptoJS.lib.BlockCipherMode.extend();
+
+ function incWord(word)
+ {
+ if (((word >> 24) & 0xff) === 0xff) { //overflow
+ var b1 = (word >> 16)&0xff;
+ var b2 = (word >> 8)&0xff;
+ var b3 = word & 0xff;
+
+ if (b1 === 0xff) // overflow b1
+ {
+ b1 = 0;
+ if (b2 === 0xff)
+ {
+ b2 = 0;
+ if (b3 === 0xff)
+ {
+ b3 = 0;
+ }
+ else
+ {
+ ++b3;
+ }
+ }
+ else
+ {
+ ++b2;
+ }
+ }
+ else
+ {
+ ++b1;
+ }
+
+ word = 0;
+ word += (b1 << 16);
+ word += (b2 << 8);
+ word += b3;
+ }
+ else
+ {
+ word += (0x01 << 24);
+ }
+ return word;
+ }
+
+ function incCounter(counter)
+ {
+ if ((counter[0] = incWord(counter[0])) === 0)
+ {
+ // encr_data in fileenc.c from Dr Brian Gladman's counts only with DWORD j < 8
+ counter[1] = incWord(counter[1]);
+ }
+ return counter;
+ }
+
+ var Encryptor = CTRGladman.Encryptor = CTRGladman.extend({
+ processBlock: function (words, offset) {
+ // Shortcuts
+ var cipher = this._cipher
+ var blockSize = cipher.blockSize;
+ var iv = this._iv;
+ var counter = this._counter;
+
+ // Generate keystream
+ if (iv) {
+ counter = this._counter = iv.slice(0);
+
+ // Remove IV for subsequent blocks
+ this._iv = undefined;
+ }
+
+ incCounter(counter);
+
+ var keystream = counter.slice(0);
+ cipher.encryptBlock(keystream, 0);
+
+ // Encrypt
+ for (var i = 0; i < blockSize; i++) {
+ words[offset + i] ^= keystream[i];
+ }
+ }
+ });
+
+ CTRGladman.Decryptor = Encryptor;
+
+ return CTRGladman;
+ }());
+
+
+
+
+ (function () {
+ // Shortcuts
+ var C = CryptoJS;
+ var C_lib = C.lib;
+ var StreamCipher = C_lib.StreamCipher;
+ var C_algo = C.algo;
+
+ // Reusable objects
+ var S = [];
+ var C_ = [];
+ var G = [];
+
+ /**
+ * Rabbit stream cipher algorithm
+ */
+ var Rabbit = C_algo.Rabbit = StreamCipher.extend({
+ _doReset: function () {
+ // Shortcuts
+ var K = this._key.words;
+ var iv = this.cfg.iv;
+
+ // Swap endian
+ for (var i = 0; i < 4; i++) {
+ K[i] = (((K[i] << 8) | (K[i] >>> 24)) & 0x00ff00ff) |
+ (((K[i] << 24) | (K[i] >>> 8)) & 0xff00ff00);
+ }
+
+ // Generate initial state values
+ var X = this._X = [
+ K[0], (K[3] << 16) | (K[2] >>> 16),
+ K[1], (K[0] << 16) | (K[3] >>> 16),
+ K[2], (K[1] << 16) | (K[0] >>> 16),
+ K[3], (K[2] << 16) | (K[1] >>> 16)
+ ];
+
+ // Generate initial counter values
+ var C = this._C = [
+ (K[2] << 16) | (K[2] >>> 16), (K[0] & 0xffff0000) | (K[1] & 0x0000ffff),
+ (K[3] << 16) | (K[3] >>> 16), (K[1] & 0xffff0000) | (K[2] & 0x0000ffff),
+ (K[0] << 16) | (K[0] >>> 16), (K[2] & 0xffff0000) | (K[3] & 0x0000ffff),
+ (K[1] << 16) | (K[1] >>> 16), (K[3] & 0xffff0000) | (K[0] & 0x0000ffff)
+ ];
+
+ // Carry bit
+ this._b = 0;
+
+ // Iterate the system four times
+ for (var i = 0; i < 4; i++) {
+ nextState.call(this);
+ }
+
+ // Modify the counters
+ for (var i = 0; i < 8; i++) {
+ C[i] ^= X[(i + 4) & 7];
+ }
+
+ // IV setup
+ if (iv) {
+ // Shortcuts
+ var IV = iv.words;
+ var IV_0 = IV[0];
+ var IV_1 = IV[1];
+
+ // Generate four subvectors
+ var i0 = (((IV_0 << 8) | (IV_0 >>> 24)) & 0x00ff00ff) | (((IV_0 << 24) | (IV_0 >>> 8)) & 0xff00ff00);
+ var i2 = (((IV_1 << 8) | (IV_1 >>> 24)) & 0x00ff00ff) | (((IV_1 << 24) | (IV_1 >>> 8)) & 0xff00ff00);
+ var i1 = (i0 >>> 16) | (i2 & 0xffff0000);
+ var i3 = (i2 << 16) | (i0 & 0x0000ffff);
+
+ // Modify counter values
+ C[0] ^= i0;
+ C[1] ^= i1;
+ C[2] ^= i2;
+ C[3] ^= i3;
+ C[4] ^= i0;
+ C[5] ^= i1;
+ C[6] ^= i2;
+ C[7] ^= i3;
+
+ // Iterate the system four times
+ for (var i = 0; i < 4; i++) {
+ nextState.call(this);
+ }
+ }
+ },
+
+ _doProcessBlock: function (M, offset) {
+ // Shortcut
+ var X = this._X;
+
+ // Iterate the system
+ nextState.call(this);
+
+ // Generate four keystream words
+ S[0] = X[0] ^ (X[5] >>> 16) ^ (X[3] << 16);
+ S[1] = X[2] ^ (X[7] >>> 16) ^ (X[5] << 16);
+ S[2] = X[4] ^ (X[1] >>> 16) ^ (X[7] << 16);
+ S[3] = X[6] ^ (X[3] >>> 16) ^ (X[1] << 16);
+
+ for (var i = 0; i < 4; i++) {
+ // Swap endian
+ S[i] = (((S[i] << 8) | (S[i] >>> 24)) & 0x00ff00ff) |
+ (((S[i] << 24) | (S[i] >>> 8)) & 0xff00ff00);
+
+ // Encrypt
+ M[offset + i] ^= S[i];
+ }
+ },
+
+ blockSize: 128/32,
+
+ ivSize: 64/32
+ });
+
+ function nextState() {
+ // Shortcuts
+ var X = this._X;
+ var C = this._C;
+
+ // Save old counter values
+ for (var i = 0; i < 8; i++) {
+ C_[i] = C[i];
+ }
+
+ // Calculate new counter values
+ C[0] = (C[0] + 0x4d34d34d + this._b) | 0;
+ C[1] = (C[1] + 0xd34d34d3 + ((C[0] >>> 0) < (C_[0] >>> 0) ? 1 : 0)) | 0;
+ C[2] = (C[2] + 0x34d34d34 + ((C[1] >>> 0) < (C_[1] >>> 0) ? 1 : 0)) | 0;
+ C[3] = (C[3] + 0x4d34d34d + ((C[2] >>> 0) < (C_[2] >>> 0) ? 1 : 0)) | 0;
+ C[4] = (C[4] + 0xd34d34d3 + ((C[3] >>> 0) < (C_[3] >>> 0) ? 1 : 0)) | 0;
+ C[5] = (C[5] + 0x34d34d34 + ((C[4] >>> 0) < (C_[4] >>> 0) ? 1 : 0)) | 0;
+ C[6] = (C[6] + 0x4d34d34d + ((C[5] >>> 0) < (C_[5] >>> 0) ? 1 : 0)) | 0;
+ C[7] = (C[7] + 0xd34d34d3 + ((C[6] >>> 0) < (C_[6] >>> 0) ? 1 : 0)) | 0;
+ this._b = (C[7] >>> 0) < (C_[7] >>> 0) ? 1 : 0;
+
+ // Calculate the g-values
+ for (var i = 0; i < 8; i++) {
+ var gx = X[i] + C[i];
+
+ // Construct high and low argument for squaring
+ var ga = gx & 0xffff;
+ var gb = gx >>> 16;
+
+ // Calculate high and low result of squaring
+ var gh = ((((ga * ga) >>> 17) + ga * gb) >>> 15) + gb * gb;
+ var gl = (((gx & 0xffff0000) * gx) | 0) + (((gx & 0x0000ffff) * gx) | 0);
+
+ // High XOR low
+ G[i] = gh ^ gl;
+ }
+
+ // Calculate new state values
+ X[0] = (G[0] + ((G[7] << 16) | (G[7] >>> 16)) + ((G[6] << 16) | (G[6] >>> 16))) | 0;
+ X[1] = (G[1] + ((G[0] << 8) | (G[0] >>> 24)) + G[7]) | 0;
+ X[2] = (G[2] + ((G[1] << 16) | (G[1] >>> 16)) + ((G[0] << 16) | (G[0] >>> 16))) | 0;
+ X[3] = (G[3] + ((G[2] << 8) | (G[2] >>> 24)) + G[1]) | 0;
+ X[4] = (G[4] + ((G[3] << 16) | (G[3] >>> 16)) + ((G[2] << 16) | (G[2] >>> 16))) | 0;
+ X[5] = (G[5] + ((G[4] << 8) | (G[4] >>> 24)) + G[3]) | 0;
+ X[6] = (G[6] + ((G[5] << 16) | (G[5] >>> 16)) + ((G[4] << 16) | (G[4] >>> 16))) | 0;
+ X[7] = (G[7] + ((G[6] << 8) | (G[6] >>> 24)) + G[5]) | 0;
+ }
+
+ /**
+ * Shortcut functions to the cipher's object interface.
+ *
+ * @example
+ *
+ * var ciphertext = CryptoJS.Rabbit.encrypt(message, key, cfg);
+ * var plaintext = CryptoJS.Rabbit.decrypt(ciphertext, key, cfg);
+ */
+ C.Rabbit = StreamCipher._createHelper(Rabbit);
+ }());
+
+
+ /**
+ * Counter block mode.
+ */
+ CryptoJS.mode.CTR = (function () {
+ var CTR = CryptoJS.lib.BlockCipherMode.extend();
+
+ var Encryptor = CTR.Encryptor = CTR.extend({
+ processBlock: function (words, offset) {
+ // Shortcuts
+ var cipher = this._cipher
+ var blockSize = cipher.blockSize;
+ var iv = this._iv;
+ var counter = this._counter;
+
+ // Generate keystream
+ if (iv) {
+ counter = this._counter = iv.slice(0);
+
+ // Remove IV for subsequent blocks
+ this._iv = undefined;
+ }
+ var keystream = counter.slice(0);
+ cipher.encryptBlock(keystream, 0);
+
+ // Increment counter
+ counter[blockSize - 1] = (counter[blockSize - 1] + 1) | 0
+
+ // Encrypt
+ for (var i = 0; i < blockSize; i++) {
+ words[offset + i] ^= keystream[i];
+ }
+ }
+ });
+
+ CTR.Decryptor = Encryptor;
+
+ return CTR;
+ }());
+
+
+ (function () {
+ // Shortcuts
+ var C = CryptoJS;
+ var C_lib = C.lib;
+ var StreamCipher = C_lib.StreamCipher;
+ var C_algo = C.algo;
+
+ // Reusable objects
+ var S = [];
+ var C_ = [];
+ var G = [];
+
+ /**
+ * Rabbit stream cipher algorithm.
+ *
+ * This is a legacy version that neglected to convert the key to little-endian.
+ * This error doesn't affect the cipher's security,
+ * but it does affect its compatibility with other implementations.
+ */
+ var RabbitLegacy = C_algo.RabbitLegacy = StreamCipher.extend({
+ _doReset: function () {
+ // Shortcuts
+ var K = this._key.words;
+ var iv = this.cfg.iv;
+
+ // Generate initial state values
+ var X = this._X = [
+ K[0], (K[3] << 16) | (K[2] >>> 16),
+ K[1], (K[0] << 16) | (K[3] >>> 16),
+ K[2], (K[1] << 16) | (K[0] >>> 16),
+ K[3], (K[2] << 16) | (K[1] >>> 16)
+ ];
+
+ // Generate initial counter values
+ var C = this._C = [
+ (K[2] << 16) | (K[2] >>> 16), (K[0] & 0xffff0000) | (K[1] & 0x0000ffff),
+ (K[3] << 16) | (K[3] >>> 16), (K[1] & 0xffff0000) | (K[2] & 0x0000ffff),
+ (K[0] << 16) | (K[0] >>> 16), (K[2] & 0xffff0000) | (K[3] & 0x0000ffff),
+ (K[1] << 16) | (K[1] >>> 16), (K[3] & 0xffff0000) | (K[0] & 0x0000ffff)
+ ];
+
+ // Carry bit
+ this._b = 0;
+
+ // Iterate the system four times
+ for (var i = 0; i < 4; i++) {
+ nextState.call(this);
+ }
+
+ // Modify the counters
+ for (var i = 0; i < 8; i++) {
+ C[i] ^= X[(i + 4) & 7];
+ }
+
+ // IV setup
+ if (iv) {
+ // Shortcuts
+ var IV = iv.words;
+ var IV_0 = IV[0];
+ var IV_1 = IV[1];
+
+ // Generate four subvectors
+ var i0 = (((IV_0 << 8) | (IV_0 >>> 24)) & 0x00ff00ff) | (((IV_0 << 24) | (IV_0 >>> 8)) & 0xff00ff00);
+ var i2 = (((IV_1 << 8) | (IV_1 >>> 24)) & 0x00ff00ff) | (((IV_1 << 24) | (IV_1 >>> 8)) & 0xff00ff00);
+ var i1 = (i0 >>> 16) | (i2 & 0xffff0000);
+ var i3 = (i2 << 16) | (i0 & 0x0000ffff);
+
+ // Modify counter values
+ C[0] ^= i0;
+ C[1] ^= i1;
+ C[2] ^= i2;
+ C[3] ^= i3;
+ C[4] ^= i0;
+ C[5] ^= i1;
+ C[6] ^= i2;
+ C[7] ^= i3;
+
+ // Iterate the system four times
+ for (var i = 0; i < 4; i++) {
+ nextState.call(this);
+ }
+ }
+ },
+
+ _doProcessBlock: function (M, offset) {
+ // Shortcut
+ var X = this._X;
+
+ // Iterate the system
+ nextState.call(this);
+
+ // Generate four keystream words
+ S[0] = X[0] ^ (X[5] >>> 16) ^ (X[3] << 16);
+ S[1] = X[2] ^ (X[7] >>> 16) ^ (X[5] << 16);
+ S[2] = X[4] ^ (X[1] >>> 16) ^ (X[7] << 16);
+ S[3] = X[6] ^ (X[3] >>> 16) ^ (X[1] << 16);
+
+ for (var i = 0; i < 4; i++) {
+ // Swap endian
+ S[i] = (((S[i] << 8) | (S[i] >>> 24)) & 0x00ff00ff) |
+ (((S[i] << 24) | (S[i] >>> 8)) & 0xff00ff00);
+
+ // Encrypt
+ M[offset + i] ^= S[i];
+ }
+ },
+
+ blockSize: 128/32,
+
+ ivSize: 64/32
+ });
+
+ function nextState() {
+ // Shortcuts
+ var X = this._X;
+ var C = this._C;
+
+ // Save old counter values
+ for (var i = 0; i < 8; i++) {
+ C_[i] = C[i];
+ }
+
+ // Calculate new counter values
+ C[0] = (C[0] + 0x4d34d34d + this._b) | 0;
+ C[1] = (C[1] + 0xd34d34d3 + ((C[0] >>> 0) < (C_[0] >>> 0) ? 1 : 0)) | 0;
+ C[2] = (C[2] + 0x34d34d34 + ((C[1] >>> 0) < (C_[1] >>> 0) ? 1 : 0)) | 0;
+ C[3] = (C[3] + 0x4d34d34d + ((C[2] >>> 0) < (C_[2] >>> 0) ? 1 : 0)) | 0;
+ C[4] = (C[4] + 0xd34d34d3 + ((C[3] >>> 0) < (C_[3] >>> 0) ? 1 : 0)) | 0;
+ C[5] = (C[5] + 0x34d34d34 + ((C[4] >>> 0) < (C_[4] >>> 0) ? 1 : 0)) | 0;
+ C[6] = (C[6] + 0x4d34d34d + ((C[5] >>> 0) < (C_[5] >>> 0) ? 1 : 0)) | 0;
+ C[7] = (C[7] + 0xd34d34d3 + ((C[6] >>> 0) < (C_[6] >>> 0) ? 1 : 0)) | 0;
+ this._b = (C[7] >>> 0) < (C_[7] >>> 0) ? 1 : 0;
+
+ // Calculate the g-values
+ for (var i = 0; i < 8; i++) {
+ var gx = X[i] + C[i];
+
+ // Construct high and low argument for squaring
+ var ga = gx & 0xffff;
+ var gb = gx >>> 16;
+
+ // Calculate high and low result of squaring
+ var gh = ((((ga * ga) >>> 17) + ga * gb) >>> 15) + gb * gb;
+ var gl = (((gx & 0xffff0000) * gx) | 0) + (((gx & 0x0000ffff) * gx) | 0);
+
+ // High XOR low
+ G[i] = gh ^ gl;
+ }
+
+ // Calculate new state values
+ X[0] = (G[0] + ((G[7] << 16) | (G[7] >>> 16)) + ((G[6] << 16) | (G[6] >>> 16))) | 0;
+ X[1] = (G[1] + ((G[0] << 8) | (G[0] >>> 24)) + G[7]) | 0;
+ X[2] = (G[2] + ((G[1] << 16) | (G[1] >>> 16)) + ((G[0] << 16) | (G[0] >>> 16))) | 0;
+ X[3] = (G[3] + ((G[2] << 8) | (G[2] >>> 24)) + G[1]) | 0;
+ X[4] = (G[4] + ((G[3] << 16) | (G[3] >>> 16)) + ((G[2] << 16) | (G[2] >>> 16))) | 0;
+ X[5] = (G[5] + ((G[4] << 8) | (G[4] >>> 24)) + G[3]) | 0;
+ X[6] = (G[6] + ((G[5] << 16) | (G[5] >>> 16)) + ((G[4] << 16) | (G[4] >>> 16))) | 0;
+ X[7] = (G[7] + ((G[6] << 8) | (G[6] >>> 24)) + G[5]) | 0;
+ }
+
+ /**
+ * Shortcut functions to the cipher's object interface.
+ *
+ * @example
+ *
+ * var ciphertext = CryptoJS.RabbitLegacy.encrypt(message, key, cfg);
+ * var plaintext = CryptoJS.RabbitLegacy.decrypt(ciphertext, key, cfg);
+ */
+ C.RabbitLegacy = StreamCipher._createHelper(RabbitLegacy);
+ }());
+
+
+ /**
+ * Zero padding strategy.
+ */
+ CryptoJS.pad.ZeroPadding = {
+ pad: function (data, blockSize) {
+ // Shortcut
+ var blockSizeBytes = blockSize * 4;
+
+ // Pad
+ data.clamp();
+ data.sigBytes += blockSizeBytes - ((data.sigBytes % blockSizeBytes) || blockSizeBytes);
+ },
+
+ unpad: function (data) {
+ // Shortcut
+ var dataWords = data.words;
+
+ // Unpad
+ var i = data.sigBytes - 1;
+ while (!((dataWords[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff)) {
+ i--;
+ }
+ data.sigBytes = i + 1;
+ }
+ };
+
+
+ return CryptoJS;
+
+})); \ No newline at end of file
diff --git a/etc/js/draggabilly.pkgd.js b/etc/js/draggabilly.pkgd.js
new file mode 100644
index 00000000..bef3db35
--- /dev/null
+++ b/etc/js/draggabilly.pkgd.js
@@ -0,0 +1,1530 @@
+/*!
+ * Draggabilly PACKAGED v2.1.0
+ * Make that shiz draggable
+ * http://draggabilly.desandro.com
+ * MIT license
+ */
+
+/**
+ * Bridget makes jQuery widgets
+ * v2.0.0
+ * MIT license
+ */
+
+/* jshint browser: true, strict: true, undef: true, unused: true */
+
+( function( window, factory ) {
+
+ /* globals define: false, module: false, require: false */
+
+ if ( typeof define == 'function' && define.amd ) {
+ // AMD
+ define( 'jquery-bridget/jquery-bridget',[ 'jquery' ], function( jQuery ) {
+ factory( window, jQuery );
+ });
+ } else if ( typeof module == 'object' && module.exports ) {
+ // CommonJS
+ module.exports = factory(
+ window,
+ require('jquery')
+ );
+ } else {
+ // browser global
+ window.jQueryBridget = factory(
+ window,
+ window.jQuery
+ );
+ }
+
+}( window, function factory( window, jQuery ) {
+
+
+// ----- utils ----- //
+
+var arraySlice = Array.prototype.slice;
+
+// helper function for logging errors
+// $.error breaks jQuery chaining
+var console = window.console;
+var logError = typeof console == 'undefined' ? function() {} :
+ function( message ) {
+ console.error( message );
+ };
+
+// ----- jQueryBridget ----- //
+
+function jQueryBridget( namespace, PluginClass, $ ) {
+ $ = $ || jQuery || window.jQuery;
+ if ( !$ ) {
+ return;
+ }
+
+ // add option method -> $().plugin('option', {...})
+ if ( !PluginClass.prototype.option ) {
+ // option setter
+ PluginClass.prototype.option = function( opts ) {
+ // bail out if not an object
+ if ( !$.isPlainObject( opts ) ){
+ return;
+ }
+ this.options = $.extend( true, this.options, opts );
+ };
+ }
+
+ // make jQuery plugin
+ $.fn[ namespace ] = function( arg0 /*, arg1 */ ) {
+ if ( typeof arg0 == 'string' ) {
+ // method call $().plugin( 'methodName', { options } )
+ // shift arguments by 1
+ var args = arraySlice.call( arguments, 1 );
+ return methodCall( this, arg0, args );
+ }
+ // just $().plugin({ options })
+ plainCall( this, arg0 );
+ return this;
+ };
+
+ // $().plugin('methodName')
+ function methodCall( $elems, methodName, args ) {
+ var returnValue;
+ var pluginMethodStr = '$().' + namespace + '("' + methodName + '")';
+
+ $elems.each( function( i, elem ) {
+ // get instance
+ var instance = $.data( elem, namespace );
+ if ( !instance ) {
+ logError( namespace + ' not initialized. Cannot call methods, i.e. ' +
+ pluginMethodStr );
+ return;
+ }
+
+ var method = instance[ methodName ];
+ if ( !method || methodName.charAt(0) == '_' ) {
+ logError( pluginMethodStr + ' is not a valid method' );
+ return;
+ }
+
+ // apply method, get return value
+ var value = method.apply( instance, args );
+ // set return value if value is returned, use only first value
+ returnValue = returnValue === undefined ? value : returnValue;
+ });
+
+ return returnValue !== undefined ? returnValue : $elems;
+ }
+
+ function plainCall( $elems, options ) {
+ $elems.each( function( i, elem ) {
+ var instance = $.data( elem, namespace );
+ if ( instance ) {
+ // set options & init
+ instance.option( options );
+ instance._init();
+ } else {
+ // initialize new instance
+ instance = new PluginClass( elem, options );
+ $.data( elem, namespace, instance );
+ }
+ });
+ }
+
+ updateJQuery( $ );
+
+}
+
+// ----- updateJQuery ----- //
+
+// set $.bridget for v1 backwards compatibility
+function updateJQuery( $ ) {
+ if ( !$ || ( $ && $.bridget ) ) {
+ return;
+ }
+ $.bridget = jQueryBridget;
+}
+
+updateJQuery( jQuery || window.jQuery );
+
+// ----- ----- //
+
+return jQueryBridget;
+
+}));
+
+/*!
+ * getSize v2.0.2
+ * measure size of elements
+ * MIT license
+ */
+
+/*jshint browser: true, strict: true, undef: true, unused: true */
+/*global define: false, module: false, console: false */
+
+( function( window, factory ) {
+
+
+ if ( typeof define == 'function' && define.amd ) {
+ // AMD
+ define( 'get-size/get-size',[],function() {
+ return factory();
+ });
+ } else if ( typeof module == 'object' && module.exports ) {
+ // CommonJS
+ module.exports = factory();
+ } else {
+ // browser global
+ window.getSize = factory();
+ }
+
+})( window, function factory() {
+
+
+// -------------------------- helpers -------------------------- //
+
+// get a number from a string, not a percentage
+function getStyleSize( value ) {
+ var num = parseFloat( value );
+ // not a percent like '100%', and a number
+ var isValid = value.indexOf('%') == -1 && !isNaN( num );
+ return isValid && num;
+}
+
+function noop() {}
+
+var logError = typeof console == 'undefined' ? noop :
+ function( message ) {
+ console.error( message );
+ };
+
+// -------------------------- measurements -------------------------- //
+
+var measurements = [
+ 'paddingLeft',
+ 'paddingRight',
+ 'paddingTop',
+ 'paddingBottom',
+ 'marginLeft',
+ 'marginRight',
+ 'marginTop',
+ 'marginBottom',
+ 'borderLeftWidth',
+ 'borderRightWidth',
+ 'borderTopWidth',
+ 'borderBottomWidth'
+];
+
+var measurementsLength = measurements.length;
+
+function getZeroSize() {
+ var size = {
+ width: 0,
+ height: 0,
+ innerWidth: 0,
+ innerHeight: 0,
+ outerWidth: 0,
+ outerHeight: 0
+ };
+ for ( var i=0; i < measurementsLength; i++ ) {
+ var measurement = measurements[i];
+ size[ measurement ] = 0;
+ }
+ return size;
+}
+
+// -------------------------- getStyle -------------------------- //
+
+/**
+ * getStyle, get style of element, check for Firefox bug
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=548397
+ */
+function getStyle( elem ) {
+ var style = getComputedStyle( elem );
+ if ( !style ) {
+ logError( 'Style returned ' + style +
+ '. Are you running this code in a hidden iframe on Firefox? ' +
+ 'See http://bit.ly/getsizebug1' );
+ }
+ return style;
+}
+
+// -------------------------- setup -------------------------- //
+
+var isSetup = false;
+
+var isBoxSizeOuter;
+
+/**
+ * setup
+ * check isBoxSizerOuter
+ * do on first getSize() rather than on page load for Firefox bug
+ */
+function setup() {
+ // setup once
+ if ( isSetup ) {
+ return;
+ }
+ isSetup = true;
+
+ // -------------------------- box sizing -------------------------- //
+
+ /**
+ * WebKit measures the outer-width on style.width on border-box elems
+ * IE & Firefox<29 measures the inner-width
+ */
+ var div = document.createElement('div');
+ div.style.width = '200px';
+ div.style.padding = '1px 2px 3px 4px';
+ div.style.borderStyle = 'solid';
+ div.style.borderWidth = '1px 2px 3px 4px';
+ div.style.boxSizing = 'border-box';
+
+ var body = document.body || document.documentElement;
+ body.appendChild( div );
+ var style = getStyle( div );
+
+ getSize.isBoxSizeOuter = isBoxSizeOuter = getStyleSize( style.width ) == 200;
+ body.removeChild( div );
+
+}
+
+// -------------------------- getSize -------------------------- //
+
+function getSize( elem ) {
+ setup();
+
+ // use querySeletor if elem is string
+ if ( typeof elem == 'string' ) {
+ elem = document.querySelector( elem );
+ }
+
+ // do not proceed on non-objects
+ if ( !elem || typeof elem != 'object' || !elem.nodeType ) {
+ return;
+ }
+
+ var style = getStyle( elem );
+
+ // if hidden, everything is 0
+ if ( style.display == 'none' ) {
+ return getZeroSize();
+ }
+
+ var size = {};
+ size.width = elem.offsetWidth;
+ size.height = elem.offsetHeight;
+
+ var isBorderBox = size.isBorderBox = style.boxSizing == 'border-box';
+
+ // get all measurements
+ for ( var i=0; i < measurementsLength; i++ ) {
+ var measurement = measurements[i];
+ var value = style[ measurement ];
+ var num = parseFloat( value );
+ // any 'auto', 'medium' value will be 0
+ size[ measurement ] = !isNaN( num ) ? num : 0;
+ }
+
+ var paddingWidth = size.paddingLeft + size.paddingRight;
+ var paddingHeight = size.paddingTop + size.paddingBottom;
+ var marginWidth = size.marginLeft + size.marginRight;
+ var marginHeight = size.marginTop + size.marginBottom;
+ var borderWidth = size.borderLeftWidth + size.borderRightWidth;
+ var borderHeight = size.borderTopWidth + size.borderBottomWidth;
+
+ var isBorderBoxSizeOuter = isBorderBox && isBoxSizeOuter;
+
+ // overwrite width and height if we can get it from style
+ var styleWidth = getStyleSize( style.width );
+ if ( styleWidth !== false ) {
+ size.width = styleWidth +
+ // add padding and border unless it's already including it
+ ( isBorderBoxSizeOuter ? 0 : paddingWidth + borderWidth );
+ }
+
+ var styleHeight = getStyleSize( style.height );
+ if ( styleHeight !== false ) {
+ size.height = styleHeight +
+ // add padding and border unless it's already including it
+ ( isBorderBoxSizeOuter ? 0 : paddingHeight + borderHeight );
+ }
+
+ size.innerWidth = size.width - ( paddingWidth + borderWidth );
+ size.innerHeight = size.height - ( paddingHeight + borderHeight );
+
+ size.outerWidth = size.width + marginWidth;
+ size.outerHeight = size.height + marginHeight;
+
+ return size;
+}
+
+return getSize;
+
+});
+
+/**
+ * EvEmitter v1.0.1
+ * Lil' event emitter
+ * MIT License
+ */
+
+/* jshint unused: true, undef: true, strict: true */
+
+( function( global, factory ) {
+ // universal module definition
+ /* jshint strict: false */ /* globals define, module */
+ if ( typeof define == 'function' && define.amd ) {
+ // AMD - RequireJS
+ define( 'ev-emitter/ev-emitter',factory );
+ } else if ( typeof module == 'object' && module.exports ) {
+ // CommonJS - Browserify, Webpack
+ module.exports = factory();
+ } else {
+ // Browser globals
+ global.EvEmitter = factory();
+ }
+
+}( this, function() {
+
+
+
+function EvEmitter() {}
+
+var proto = EvEmitter.prototype;
+
+proto.on = function( eventName, listener ) {
+ if ( !eventName || !listener ) {
+ return;
+ }
+ // set events hash
+ var events = this._events = this._events || {};
+ // set listeners array
+ var listeners = events[ eventName ] = events[ eventName ] || [];
+ // only add once
+ if ( listeners.indexOf( listener ) == -1 ) {
+ listeners.push( listener );
+ }
+
+ return this;
+};
+
+proto.once = function( eventName, listener ) {
+ if ( !eventName || !listener ) {
+ return;
+ }
+ // add event
+ this.on( eventName, listener );
+ // set once flag
+ // set onceEvents hash
+ var onceEvents = this._onceEvents = this._onceEvents || {};
+ // set onceListeners array
+ var onceListeners = onceEvents[ eventName ] = onceEvents[ eventName ] || [];
+ // set flag
+ onceListeners[ listener ] = true;
+
+ return this;
+};
+
+proto.off = function( eventName, listener ) {
+ var listeners = this._events && this._events[ eventName ];
+ if ( !listeners || !listeners.length ) {
+ return;
+ }
+ var index = listeners.indexOf( listener );
+ if ( index != -1 ) {
+ listeners.splice( index, 1 );
+ }
+
+ return this;
+};
+
+proto.emitEvent = function( eventName, args ) {
+ var listeners = this._events && this._events[ eventName ];
+ if ( !listeners || !listeners.length ) {
+ return;
+ }
+ var i = 0;
+ var listener = listeners[i];
+ args = args || [];
+ // once stuff
+ var onceListeners = this._onceEvents && this._onceEvents[ eventName ];
+
+ while ( listener ) {
+ var isOnce = onceListeners && onceListeners[ listener ];
+ if ( isOnce ) {
+ // remove listener
+ // remove before trigger to prevent recursion
+ this.off( eventName, listener );
+ // unset once flag
+ delete onceListeners[ listener ];
+ }
+ // trigger listener
+ listener.apply( this, args );
+ // get next listener
+ i += isOnce ? 0 : 1;
+ listener = listeners[i];
+ }
+
+ return this;
+};
+
+return EvEmitter;
+
+}));
+
+/*!
+ * Unipointer v2.1.0
+ * base class for doing one thing with pointer event
+ * MIT license
+ */
+
+/*jshint browser: true, undef: true, unused: true, strict: true */
+
+( function( window, factory ) {
+ // universal module definition
+ /* jshint strict: false */ /*global define, module, require */
+ if ( typeof define == 'function' && define.amd ) {
+ // AMD
+ define( 'unipointer/unipointer',[
+ 'ev-emitter/ev-emitter'
+ ], function( EvEmitter ) {
+ return factory( window, EvEmitter );
+ });
+ } else if ( typeof module == 'object' && module.exports ) {
+ // CommonJS
+ module.exports = factory(
+ window,
+ require('ev-emitter')
+ );
+ } else {
+ // browser global
+ window.Unipointer = factory(
+ window,
+ window.EvEmitter
+ );
+ }
+
+}( window, function factory( window, EvEmitter ) {
+
+
+
+function noop() {}
+
+function Unipointer() {}
+
+// inherit EvEmitter
+var proto = Unipointer.prototype = Object.create( EvEmitter.prototype );
+
+proto.bindStartEvent = function( elem ) {
+ this._bindStartEvent( elem, true );
+};
+
+proto.unbindStartEvent = function( elem ) {
+ this._bindStartEvent( elem, false );
+};
+
+/**
+ * works as unbinder, as you can ._bindStart( false ) to unbind
+ * @param {Boolean} isBind - will unbind if falsey
+ */
+proto._bindStartEvent = function( elem, isBind ) {
+ // munge isBind, default to true
+ isBind = isBind === undefined ? true : !!isBind;
+ var bindMethod = isBind ? 'addEventListener' : 'removeEventListener';
+
+ if ( window.navigator.pointerEnabled ) {
+ // W3C Pointer Events, IE11. See https://coderwall.com/p/mfreca
+ elem[ bindMethod ]( 'pointerdown', this );
+ } else if ( window.navigator.msPointerEnabled ) {
+ // IE10 Pointer Events
+ elem[ bindMethod ]( 'MSPointerDown', this );
+ } else {
+ // listen for both, for devices like Chrome Pixel
+ elem[ bindMethod ]( 'mousedown', this );
+ elem[ bindMethod ]( 'touchstart', this );
+ }
+};
+
+// trigger handler methods for events
+proto.handleEvent = function( event ) {
+ var method = 'on' + event.type;
+ if ( this[ method ] ) {
+ this[ method ]( event );
+ }
+};
+
+// returns the touch that we're keeping track of
+proto.getTouch = function( touches ) {
+ for ( var i=0; i < touches.length; i++ ) {
+ var touch = touches[i];
+ if ( touch.identifier == this.pointerIdentifier ) {
+ return touch;
+ }
+ }
+};
+
+// ----- start event ----- //
+
+proto.onmousedown = function( event ) {
+ // dismiss clicks from right or middle buttons
+ var button = event.button;
+ if ( button && ( button !== 0 && button !== 1 ) ) {
+ return;
+ }
+ this._pointerDown( event, event );
+};
+
+proto.ontouchstart = function( event ) {
+ this._pointerDown( event, event.changedTouches[0] );
+};
+
+proto.onMSPointerDown =
+proto.onpointerdown = function( event ) {
+ this._pointerDown( event, event );
+};
+
+/**
+ * pointer start
+ * @param {Event} event
+ * @param {Event or Touch} pointer
+ */
+proto._pointerDown = function( event, pointer ) {
+ // dismiss other pointers
+ if ( this.isPointerDown ) {
+ return;
+ }
+
+ this.isPointerDown = true;
+ // save pointer identifier to match up touch events
+ this.pointerIdentifier = pointer.pointerId !== undefined ?
+ // pointerId for pointer events, touch.indentifier for touch events
+ pointer.pointerId : pointer.identifier;
+
+ this.pointerDown( event, pointer );
+};
+
+proto.pointerDown = function( event, pointer ) {
+ this._bindPostStartEvents( event );
+ this.emitEvent( 'pointerDown', [ event, pointer ] );
+};
+
+// hash of events to be bound after start event
+var postStartEvents = {
+ mousedown: [ 'mousemove', 'mouseup' ],
+ touchstart: [ 'touchmove', 'touchend', 'touchcancel' ],
+ pointerdown: [ 'pointermove', 'pointerup', 'pointercancel' ],
+ MSPointerDown: [ 'MSPointerMove', 'MSPointerUp', 'MSPointerCancel' ]
+};
+
+proto._bindPostStartEvents = function( event ) {
+ if ( !event ) {
+ return;
+ }
+ // get proper events to match start event
+ var events = postStartEvents[ event.type ];
+ // bind events to node
+ events.forEach( function( eventName ) {
+ window.addEventListener( eventName, this );
+ }, this );
+ // save these arguments
+ this._boundPointerEvents = events;
+};
+
+proto._unbindPostStartEvents = function() {
+ // check for _boundEvents, in case dragEnd triggered twice (old IE8 bug)
+ if ( !this._boundPointerEvents ) {
+ return;
+ }
+ this._boundPointerEvents.forEach( function( eventName ) {
+ window.removeEventListener( eventName, this );
+ }, this );
+
+ delete this._boundPointerEvents;
+};
+
+// ----- move event ----- //
+
+proto.onmousemove = function( event ) {
+ this._pointerMove( event, event );
+};
+
+proto.onMSPointerMove =
+proto.onpointermove = function( event ) {
+ if ( event.pointerId == this.pointerIdentifier ) {
+ this._pointerMove( event, event );
+ }
+};
+
+proto.ontouchmove = function( event ) {
+ var touch = this.getTouch( event.changedTouches );
+ if ( touch ) {
+ this._pointerMove( event, touch );
+ }
+};
+
+/**
+ * pointer move
+ * @param {Event} event
+ * @param {Event or Touch} pointer
+ * @private
+ */
+proto._pointerMove = function( event, pointer ) {
+ this.pointerMove( event, pointer );
+};
+
+// public
+proto.pointerMove = function( event, pointer ) {
+ this.emitEvent( 'pointerMove', [ event, pointer ] );
+};
+
+// ----- end event ----- //
+
+
+proto.onmouseup = function( event ) {
+ this._pointerUp( event, event );
+};
+
+proto.onMSPointerUp =
+proto.onpointerup = function( event ) {
+ if ( event.pointerId == this.pointerIdentifier ) {
+ this._pointerUp( event, event );
+ }
+};
+
+proto.ontouchend = function( event ) {
+ var touch = this.getTouch( event.changedTouches );
+ if ( touch ) {
+ this._pointerUp( event, touch );
+ }
+};
+
+/**
+ * pointer up
+ * @param {Event} event
+ * @param {Event or Touch} pointer
+ * @private
+ */
+proto._pointerUp = function( event, pointer ) {
+ this._pointerDone();
+ this.pointerUp( event, pointer );
+};
+
+// public
+proto.pointerUp = function( event, pointer ) {
+ this.emitEvent( 'pointerUp', [ event, pointer ] );
+};
+
+// ----- pointer done ----- //
+
+// triggered on pointer up & pointer cancel
+proto._pointerDone = function() {
+ // reset properties
+ this.isPointerDown = false;
+ delete this.pointerIdentifier;
+ // remove events
+ this._unbindPostStartEvents();
+ this.pointerDone();
+};
+
+proto.pointerDone = noop;
+
+// ----- pointer cancel ----- //
+
+proto.onMSPointerCancel =
+proto.onpointercancel = function( event ) {
+ if ( event.pointerId == this.pointerIdentifier ) {
+ this._pointerCancel( event, event );
+ }
+};
+
+proto.ontouchcancel = function( event ) {
+ var touch = this.getTouch( event.changedTouches );
+ if ( touch ) {
+ this._pointerCancel( event, touch );
+ }
+};
+
+/**
+ * pointer cancel
+ * @param {Event} event
+ * @param {Event or Touch} pointer
+ * @private
+ */
+proto._pointerCancel = function( event, pointer ) {
+ this._pointerDone();
+ this.pointerCancel( event, pointer );
+};
+
+// public
+proto.pointerCancel = function( event, pointer ) {
+ this.emitEvent( 'pointerCancel', [ event, pointer ] );
+};
+
+// ----- ----- //
+
+// utility function for getting x/y coords from event
+Unipointer.getPointerPoint = function( pointer ) {
+ return {
+ x: pointer.pageX,
+ y: pointer.pageY
+ };
+};
+
+// ----- ----- //
+
+return Unipointer;
+
+}));
+
+/*!
+ * Unidragger v2.1.0
+ * Draggable base class
+ * MIT license
+ */
+
+/*jshint browser: true, unused: true, undef: true, strict: true */
+
+( function( window, factory ) {
+ // universal module definition
+ /*jshint strict: false */ /*globals define, module, require */
+
+ if ( typeof define == 'function' && define.amd ) {
+ // AMD
+ define( 'unidragger/unidragger',[
+ 'unipointer/unipointer'
+ ], function( Unipointer ) {
+ return factory( window, Unipointer );
+ });
+ } else if ( typeof module == 'object' && module.exports ) {
+ // CommonJS
+ module.exports = factory(
+ window,
+ require('unipointer')
+ );
+ } else {
+ // browser global
+ window.Unidragger = factory(
+ window,
+ window.Unipointer
+ );
+ }
+
+}( window, function factory( window, Unipointer ) {
+
+
+
+// ----- ----- //
+
+function noop() {}
+
+// -------------------------- Unidragger -------------------------- //
+
+function Unidragger() {}
+
+// inherit Unipointer & EvEmitter
+var proto = Unidragger.prototype = Object.create( Unipointer.prototype );
+
+// ----- bind start ----- //
+
+proto.bindHandles = function() {
+ this._bindHandles( true );
+};
+
+proto.unbindHandles = function() {
+ this._bindHandles( false );
+};
+
+var navigator = window.navigator;
+/**
+ * works as unbinder, as you can .bindHandles( false ) to unbind
+ * @param {Boolean} isBind - will unbind if falsey
+ */
+proto._bindHandles = function( isBind ) {
+ // munge isBind, default to true
+ isBind = isBind === undefined ? true : !!isBind;
+ // extra bind logic
+ var binderExtra;
+ if ( navigator.pointerEnabled ) {
+ binderExtra = function( handle ) {
+ // disable scrolling on the element
+ handle.style.touchAction = isBind ? 'none' : '';
+ };
+ } else if ( navigator.msPointerEnabled ) {
+ binderExtra = function( handle ) {
+ // disable scrolling on the element
+ handle.style.msTouchAction = isBind ? 'none' : '';
+ };
+ } else {
+ binderExtra = noop;
+ }
+ // bind each handle
+ var bindMethod = isBind ? 'addEventListener' : 'removeEventListener';
+ for ( var i=0; i < this.handles.length; i++ ) {
+ var handle = this.handles[i];
+ this._bindStartEvent( handle, isBind );
+ binderExtra( handle );
+ handle[ bindMethod ]( 'click', this );
+ }
+};
+
+// ----- start event ----- //
+
+/**
+ * pointer start
+ * @param {Event} event
+ * @param {Event or Touch} pointer
+ */
+proto.pointerDown = function( event, pointer ) {
+ // dismiss range sliders
+ if ( event.target.nodeName == 'INPUT' && event.target.type == 'range' ) {
+ // reset pointerDown logic
+ this.isPointerDown = false;
+ delete this.pointerIdentifier;
+ return;
+ }
+
+ this._dragPointerDown( event, pointer );
+ // kludge to blur focused inputs in dragger
+ var focused = document.activeElement;
+ if ( focused && focused.blur ) {
+ focused.blur();
+ }
+ // bind move and end events
+ this._bindPostStartEvents( event );
+ this.emitEvent( 'pointerDown', [ event, pointer ] );
+};
+
+// base pointer down logic
+proto._dragPointerDown = function( event, pointer ) {
+ // track to see when dragging starts
+ this.pointerDownPoint = Unipointer.getPointerPoint( pointer );
+
+ var canPreventDefault = this.canPreventDefaultOnPointerDown( event, pointer );
+ if ( canPreventDefault ) {
+ event.preventDefault();
+ }
+};
+
+// overwriteable method so Flickity can prevent for scrolling
+proto.canPreventDefaultOnPointerDown = function( event ) {
+ // prevent default, unless touchstart or <select>
+ return event.target.nodeName != 'SELECT';
+};
+
+// ----- move event ----- //
+
+/**
+ * drag move
+ * @param {Event} event
+ * @param {Event or Touch} pointer
+ */
+proto.pointerMove = function( event, pointer ) {
+ var moveVector = this._dragPointerMove( event, pointer );
+ this.emitEvent( 'pointerMove', [ event, pointer, moveVector ] );
+ this._dragMove( event, pointer, moveVector );
+};
+
+// base pointer move logic
+proto._dragPointerMove = function( event, pointer ) {
+ var movePoint = Unipointer.getPointerPoint( pointer );
+ var moveVector = {
+ x: movePoint.x - this.pointerDownPoint.x,
+ y: movePoint.y - this.pointerDownPoint.y
+ };
+ // start drag if pointer has moved far enough to start drag
+ if ( !this.isDragging && this.hasDragStarted( moveVector ) ) {
+ this._dragStart( event, pointer );
+ }
+ return moveVector;
+};
+
+// condition if pointer has moved far enough to start drag
+proto.hasDragStarted = function( moveVector ) {
+ return Math.abs( moveVector.x ) > 3 || Math.abs( moveVector.y ) > 3;
+};
+
+
+// ----- end event ----- //
+
+/**
+ * pointer up
+ * @param {Event} event
+ * @param {Event or Touch} pointer
+ */
+proto.pointerUp = function( event, pointer ) {
+ this.emitEvent( 'pointerUp', [ event, pointer ] );
+ this._dragPointerUp( event, pointer );
+};
+
+proto._dragPointerUp = function( event, pointer ) {
+ if ( this.isDragging ) {
+ this._dragEnd( event, pointer );
+ } else {
+ // pointer didn't move enough for drag to start
+ this._staticClick( event, pointer );
+ }
+};
+
+// -------------------------- drag -------------------------- //
+
+// dragStart
+proto._dragStart = function( event, pointer ) {
+ this.isDragging = true;
+ this.dragStartPoint = Unipointer.getPointerPoint( pointer );
+ // prevent clicks
+ this.isPreventingClicks = true;
+
+ this.dragStart( event, pointer );
+};
+
+proto.dragStart = function( event, pointer ) {
+ this.emitEvent( 'dragStart', [ event, pointer ] );
+};
+
+// dragMove
+proto._dragMove = function( event, pointer, moveVector ) {
+ // do not drag if not dragging yet
+ if ( !this.isDragging ) {
+ return;
+ }
+
+ this.dragMove( event, pointer, moveVector );
+};
+
+proto.dragMove = function( event, pointer, moveVector ) {
+ event.preventDefault();
+ this.emitEvent( 'dragMove', [ event, pointer, moveVector ] );
+};
+
+// dragEnd
+proto._dragEnd = function( event, pointer ) {
+ // set flags
+ this.isDragging = false;
+ // re-enable clicking async
+ setTimeout( function() {
+ delete this.isPreventingClicks;
+ }.bind( this ) );
+
+ this.dragEnd( event, pointer );
+};
+
+proto.dragEnd = function( event, pointer ) {
+ this.emitEvent( 'dragEnd', [ event, pointer ] );
+};
+
+// ----- onclick ----- //
+
+// handle all clicks and prevent clicks when dragging
+proto.onclick = function( event ) {
+ if ( this.isPreventingClicks ) {
+ event.preventDefault();
+ }
+};
+
+// ----- staticClick ----- //
+
+// triggered after pointer down & up with no/tiny movement
+proto._staticClick = function( event, pointer ) {
+ // ignore emulated mouse up clicks
+ if ( this.isIgnoringMouseUp && event.type == 'mouseup' ) {
+ return;
+ }
+
+ // allow click in <input>s and <textarea>s
+ var nodeName = event.target.nodeName;
+ if ( nodeName == 'INPUT' || nodeName == 'TEXTAREA' ) {
+ event.target.focus();
+ }
+ this.staticClick( event, pointer );
+
+ // set flag for emulated clicks 300ms after touchend
+ if ( event.type != 'mouseup' ) {
+ this.isIgnoringMouseUp = true;
+ // reset flag after 300ms
+ setTimeout( function() {
+ delete this.isIgnoringMouseUp;
+ }.bind( this ), 400 );
+ }
+};
+
+proto.staticClick = function( event, pointer ) {
+ this.emitEvent( 'staticClick', [ event, pointer ] );
+};
+
+// ----- utils ----- //
+
+Unidragger.getPointerPoint = Unipointer.getPointerPoint;
+
+// ----- ----- //
+
+return Unidragger;
+
+}));
+
+/*!
+ * Draggabilly v2.1.0
+ * Make that shiz draggable
+ * http://draggabilly.desandro.com
+ * MIT license
+ */
+
+/*jshint browser: true, strict: true, undef: true, unused: true */
+
+( function( window, factory ) {
+ // universal module definition
+ /* jshint strict: false */ /*globals define, module, require */
+ if ( typeof define == 'function' && define.amd ) {
+ // AMD
+ define( [
+ 'get-size/get-size',
+ 'unidragger/unidragger'
+ ],
+ function( getSize, Unidragger ) {
+ return factory( window, getSize, Unidragger );
+ });
+ } else if ( typeof module == 'object' && module.exports ) {
+ // CommonJS
+ module.exports = factory(
+ window,
+ require('get-size'),
+ require('unidragger')
+ );
+ } else {
+ // browser global
+ window.Draggabilly = factory(
+ window,
+ window.getSize,
+ window.Unidragger
+ );
+ }
+
+}( window, function factory( window, getSize, Unidragger ) {
+
+
+
+// vars
+var document = window.document;
+
+function noop() {}
+
+// -------------------------- helpers -------------------------- //
+
+// extend objects
+function extend( a, b ) {
+ for ( var prop in b ) {
+ a[ prop ] = b[ prop ];
+ }
+ return a;
+}
+
+function isElement( obj ) {
+ return obj instanceof HTMLElement;
+}
+
+// -------------------------- requestAnimationFrame -------------------------- //
+
+// get rAF, prefixed, if present
+var requestAnimationFrame = window.requestAnimationFrame ||
+ window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame;
+
+// fallback to setTimeout
+var lastTime = 0;
+if ( !requestAnimationFrame ) {
+ requestAnimationFrame = function( callback ) {
+ var currTime = new Date().getTime();
+ var timeToCall = Math.max( 0, 16 - ( currTime - lastTime ) );
+ var id = setTimeout( callback, timeToCall );
+ lastTime = currTime + timeToCall;
+ return id;
+ };
+}
+
+// -------------------------- support -------------------------- //
+
+var docElem = document.documentElement;
+var transformProperty = typeof docElem.style.transform == 'string' ?
+ 'transform' : 'WebkitTransform';
+
+var jQuery = window.jQuery;
+
+// -------------------------- -------------------------- //
+
+function Draggabilly( element, options ) {
+ // querySelector if string
+ this.element = typeof element == 'string' ?
+ document.querySelector( element ) : element;
+
+ if ( jQuery ) {
+ this.$element = jQuery( this.element );
+ }
+
+ // options
+ this.options = extend( {}, this.constructor.defaults );
+ this.option( options );
+
+ this._create();
+}
+
+// inherit Unidragger methods
+var proto = Draggabilly.prototype = Object.create( Unidragger.prototype );
+
+Draggabilly.defaults = {
+};
+
+/**
+ * set options
+ * @param {Object} opts
+ */
+proto.option = function( opts ) {
+ extend( this.options, opts );
+};
+
+proto._create = function() {
+
+ // properties
+ this.position = {};
+ this._getPosition();
+
+ this.startPoint = { x: 0, y: 0 };
+ this.dragPoint = { x: 0, y: 0 };
+
+ this.startPosition = extend( {}, this.position );
+
+ // set relative positioning
+ var style = getComputedStyle( this.element );
+ if ( style.position != 'relative' && style.position != 'absolute' ) {
+ this.element.style.position = 'relative';
+ }
+
+ this.enable();
+ this.setHandles();
+
+};
+
+/**
+ * set this.handles and bind start events to 'em
+ */
+proto.setHandles = function() {
+ this.handles = this.options.handle ?
+ this.element.querySelectorAll( this.options.handle ) : [ this.element ];
+
+ this.bindHandles();
+};
+
+/**
+ * emits events via EvEmitter and jQuery events
+ * @param {String} type - name of event
+ * @param {Event} event - original event
+ * @param {Array} args - extra arguments
+ */
+proto.dispatchEvent = function( type, event, args ) {
+ var emitArgs = [ event ].concat( args );
+ this.emitEvent( type, emitArgs );
+ var jQuery = window.jQuery;
+ // trigger jQuery event
+ if ( jQuery && this.$element ) {
+ if ( event ) {
+ // create jQuery event
+ var $event = jQuery.Event( event );
+ $event.type = type;
+ this.$element.trigger( $event, args );
+ } else {
+ // just trigger with type if no event available
+ this.$element.trigger( type, args );
+ }
+ }
+};
+
+// -------------------------- position -------------------------- //
+
+// get x/y position from style
+Draggabilly.prototype._getPosition = function() {
+ var style = getComputedStyle( this.element );
+ var x = this._getPositionCoord( style.left, 'width' );
+ var y = this._getPositionCoord( style.top, 'height' );
+ // clean up 'auto' or other non-integer values
+ this.position.x = isNaN( x ) ? 0 : x;
+ this.position.y = isNaN( y ) ? 0 : y;
+
+ this._addTransformPosition( style );
+};
+
+Draggabilly.prototype._getPositionCoord = function( styleSide, measure ) {
+ if ( styleSide.indexOf('%') != -1 ) {
+ // convert percent into pixel for Safari, #75
+ var parentSize = getSize( this.element.parentNode );
+ return ( parseFloat( styleSide ) / 100 ) * parentSize[ measure ];
+ }
+
+ return parseInt( styleSide, 10 );
+};
+
+// add transform: translate( x, y ) to position
+proto._addTransformPosition = function( style ) {
+ var transform = style[ transformProperty ];
+ // bail out if value is 'none'
+ if ( transform.indexOf('matrix') !== 0 ) {
+ return;
+ }
+ // split matrix(1, 0, 0, 1, x, y)
+ var matrixValues = transform.split(',');
+ // translate X value is in 12th or 4th position
+ var xIndex = transform.indexOf('matrix3d') === 0 ? 12 : 4;
+ var translateX = parseInt( matrixValues[ xIndex ], 10 );
+ // translate Y value is in 13th or 5th position
+ var translateY = parseInt( matrixValues[ xIndex + 1 ], 10 );
+ this.position.x += translateX;
+ this.position.y += translateY;
+};
+
+// -------------------------- events -------------------------- //
+
+/**
+ * pointer start
+ * @param {Event} event
+ * @param {Event or Touch} pointer
+ */
+proto.pointerDown = function( event, pointer ) {
+ this._dragPointerDown( event, pointer );
+ // kludge to blur focused inputs in dragger
+ var focused = document.activeElement;
+ // do not blur body for IE10, metafizzy/flickity#117
+ if ( focused && focused.blur && focused != document.body ) {
+ focused.blur();
+ }
+ // bind move and end events
+ this._bindPostStartEvents( event );
+ this.element.classList.add('is-pointer-down');
+ this.dispatchEvent( 'pointerDown', event, [ pointer ] );
+};
+
+/**
+ * drag move
+ * @param {Event} event
+ * @param {Event or Touch} pointer
+ */
+proto.pointerMove = function( event, pointer ) {
+ var moveVector = this._dragPointerMove( event, pointer );
+ this.dispatchEvent( 'pointerMove', event, [ pointer, moveVector ] );
+ this._dragMove( event, pointer, moveVector );
+};
+
+/**
+ * drag start
+ * @param {Event} event
+ * @param {Event or Touch} pointer
+ */
+proto.dragStart = function( event, pointer ) {
+ if ( !this.isEnabled ) {
+ return;
+ }
+ this._getPosition();
+ this.measureContainment();
+ // position _when_ drag began
+ this.startPosition.x = this.position.x;
+ this.startPosition.y = this.position.y;
+ // reset left/top style
+ this.setLeftTop();
+
+ this.dragPoint.x = 0;
+ this.dragPoint.y = 0;
+
+ this.element.classList.add('is-dragging');
+ this.dispatchEvent( 'dragStart', event, [ pointer ] );
+ // start animation
+ this.animate();
+};
+
+proto.measureContainment = function() {
+ var containment = this.options.containment;
+ if ( !containment ) {
+ return;
+ }
+
+ // use element if element
+ var container = isElement( containment ) ? containment :
+ // fallback to querySelector if string
+ typeof containment == 'string' ? document.querySelector( containment ) :
+ // otherwise just `true`, use the parent
+ this.element.parentNode;
+
+ var elemSize = getSize( this.element );
+ var containerSize = getSize( container );
+ var elemRect = this.element.getBoundingClientRect();
+ var containerRect = container.getBoundingClientRect();
+
+ var borderSizeX = containerSize.borderLeftWidth + containerSize.borderRightWidth;
+ var borderSizeY = containerSize.borderTopWidth + containerSize.borderBottomWidth;
+
+ var position = this.relativeStartPosition = {
+ x: elemRect.left - ( containerRect.left + containerSize.borderLeftWidth ),
+ y: elemRect.top - ( containerRect.top + containerSize.borderTopWidth )
+ };
+
+ this.containSize = {
+ width: ( containerSize.width - borderSizeX ) - position.x - elemSize.width,
+ height: ( containerSize.height - borderSizeY ) - position.y - elemSize.height
+ };
+};
+
+// ----- move event ----- //
+
+/**
+ * drag move
+ * @param {Event} event
+ * @param {Event or Touch} pointer
+ */
+proto.dragMove = function( event, pointer, moveVector ) {
+ if ( !this.isEnabled ) {
+ return;
+ }
+ var dragX = moveVector.x;
+ var dragY = moveVector.y;
+
+ var grid = this.options.grid;
+ var gridX = grid && grid[0];
+ var gridY = grid && grid[1];
+
+ dragX = applyGrid( dragX, gridX );
+ dragY = applyGrid( dragY, gridY );
+
+ dragX = this.containDrag( 'x', dragX, gridX );
+ dragY = this.containDrag( 'y', dragY, gridY );
+
+ // constrain to axis
+ dragX = this.options.axis == 'y' ? 0 : dragX;
+ dragY = this.options.axis == 'x' ? 0 : dragY;
+
+ this.position.x = this.startPosition.x + dragX;
+ this.position.y = this.startPosition.y + dragY;
+ // set dragPoint properties
+ this.dragPoint.x = dragX;
+ this.dragPoint.y = dragY;
+
+ this.dispatchEvent( 'dragMove', event, [ pointer, moveVector ] );
+};
+
+function applyGrid( value, grid, method ) {
+ method = method || 'round';
+ return grid ? Math[ method ]( value / grid ) * grid : value;
+}
+
+proto.containDrag = function( axis, drag, grid ) {
+ if ( !this.options.containment ) {
+ return drag;
+ }
+ var measure = axis == 'x' ? 'width' : 'height';
+
+ var rel = this.relativeStartPosition[ axis ];
+ var min = applyGrid( -rel, grid, 'ceil' );
+ var max = this.containSize[ measure ];
+ max = applyGrid( max, grid, 'floor' );
+ return Math.min( max, Math.max( min, drag ) );
+};
+
+// ----- end event ----- //
+
+/**
+ * pointer up
+ * @param {Event} event
+ * @param {Event or Touch} pointer
+ */
+proto.pointerUp = function( event, pointer ) {
+ this.element.classList.remove('is-pointer-down');
+ this.dispatchEvent( 'pointerUp', event, [ pointer ] );
+ this._dragPointerUp( event, pointer );
+};
+
+/**
+ * drag end
+ * @param {Event} event
+ * @param {Event or Touch} pointer
+ */
+proto.dragEnd = function( event, pointer ) {
+ if ( !this.isEnabled ) {
+ return;
+ }
+ // use top left position when complete
+ if ( transformProperty ) {
+ this.element.style[ transformProperty ] = '';
+ this.setLeftTop();
+ }
+ this.element.classList.remove('is-dragging');
+ this.dispatchEvent( 'dragEnd', event, [ pointer ] );
+};
+
+// -------------------------- animation -------------------------- //
+
+proto.animate = function() {
+ // only render and animate if dragging
+ if ( !this.isDragging ) {
+ return;
+ }
+
+ this.positionDrag();
+
+ var _this = this;
+ requestAnimationFrame( function animateFrame() {
+ _this.animate();
+ });
+
+};
+
+// left/top positioning
+proto.setLeftTop = function() {
+ this.element.style.left = this.position.x + 'px';
+ this.element.style.top = this.position.y + 'px';
+};
+
+proto.positionDrag = function() {
+ this.element.style[ transformProperty ] = 'translate3d( ' + this.dragPoint.x +
+ 'px, ' + this.dragPoint.y + 'px, 0)';
+};
+
+// ----- staticClick ----- //
+
+proto.staticClick = function( event, pointer ) {
+ this.dispatchEvent( 'staticClick', event, [ pointer ] );
+};
+
+// ----- methods ----- //
+
+proto.enable = function() {
+ this.isEnabled = true;
+};
+
+proto.disable = function() {
+ this.isEnabled = false;
+ if ( this.isDragging ) {
+ this.dragEnd();
+ }
+};
+
+proto.destroy = function() {
+ this.disable();
+ // reset styles
+ this.element.style[ transformProperty ] = '';
+ this.element.style.left = '';
+ this.element.style.top = '';
+ this.element.style.position = '';
+ // unbind handles
+ this.unbindHandles();
+ // remove jQuery data
+ if ( this.$element ) {
+ this.$element.removeData('draggabilly');
+ }
+};
+
+// ----- jQuery bridget ----- //
+
+// required for jQuery bridget
+proto._init = noop;
+
+if ( jQuery && jQuery.bridget ) {
+ jQuery.bridget( 'draggabilly', Draggabilly );
+}
+
+// ----- ----- //
+
+return Draggabilly;
+
+}));
+
diff --git a/etc/js/filelogger.js b/etc/js/filelogger.js
new file mode 100644
index 00000000..725b9dc1
--- /dev/null
+++ b/etc/js/filelogger.js
@@ -0,0 +1,363 @@
+/*!
+ * fileLogger
+ * Copyright 2016 Peter Bakondy https://github.com/pbakondy
+ * See LICENSE in this repository for license information
+ */
+(function(){
+/* global angular, console, cordova */
+/* eslint no-console:0 */
+
+// install : cordova plugin add cordova-plugin-file
+// date format: https://docs.angularjs.org/api/ng/filter/date
+
+angular.module('fileLogger', ['ngCordova.plugins.file'])
+
+ .factory('$fileLogger', ['$q', '$window', '$cordovaFile', '$timeout', '$filter',
+ function ($q, $window, $cordovaFile, $timeout, $filter) {
+
+ 'use strict';
+
+
+ var queue = [];
+ var ongoing = false;
+ var levels = ['DEBUG', 'INFO', 'WARN', 'ERROR'];
+
+ var storageFilename = 'messages.log';
+
+ var dateFormat;
+ var dateTimezone;
+
+ // detecting Ripple Emulator
+ // https://gist.github.com/triceam/4658021
+ function isRipple() {
+ return $window.parent && $window.parent.ripple;
+ }
+
+ function isBrowser() {
+ return (!$window.cordova && !$window.PhoneGap && !$window.phonegap) || isRipple();
+ }
+
+
+ function log(level) {
+ if (angular.isString(level)) {
+ level = level.toUpperCase();
+
+ if (levels.indexOf(level) === -1) {
+ level = 'INFO';
+ }
+ } else {
+ level = 'INFO';
+ }
+
+ var now = new Date();
+ var timestamp = dateFormat ?
+ $filter('date')(now, dateFormat, dateTimezone) : now.toJSON();
+
+ var messages = Array.prototype.slice.call(arguments, 1);
+ var message = [ timestamp, level ];
+ var text;
+
+ for (var i = 0; i < messages.length; i++ ) {
+ if (angular.isArray(messages[i])) {
+ text = '[Array]';
+ try {
+ // avoid "TypeError: Converting circular structure to JSON"
+ text = JSON.stringify(messages[i]);
+ } catch(e) {
+ // do nothing
+ }
+ message.push(text);
+ }
+ else if (angular.isObject(messages[i])) {
+ text = '[Object]';
+ try {
+ // avoid "TypeError: Converting circular structure to JSON"
+ text = JSON.stringify(messages[i]);
+ } catch(e) {
+ // do nothing
+ }
+ message.push(text);
+ }
+ else {
+ message.push(messages[i]);
+ }
+ }
+
+ if (isBrowser()) {
+ // log to browser console
+
+ messages.unshift(timestamp);
+
+ if (angular.isObject(console) && angular.isFunction(console.log)) {
+ switch (level) {
+ case 'DEBUG':
+ if (angular.isFunction(console.debug)) {
+ console.debug.apply(console, messages);
+ } else {
+ console.log.apply(console, messages);
+ }
+ break;
+ case 'INFO':
+ if (angular.isFunction(console.debug)) {
+ console.info.apply(console, messages);
+ } else {
+ console.log.apply(console, messages);
+ }
+ break;
+ case 'WARN':
+ if (angular.isFunction(console.debug)) {
+ console.warn.apply(console, messages);
+ } else {
+ console.log.apply(console, messages);
+ }
+ break;
+ case 'ERROR':
+ if (angular.isFunction(console.debug)) {
+ console.error.apply(console, messages);
+ } else {
+ console.log.apply(console, messages);
+ }
+ break;
+ default:
+ console.log.apply(console, messages);
+ }
+ }
+
+ } else {
+ // log to logcat
+ console.log(message.join(' '));
+ }
+
+ queue.push({ message: message.join(' ') + '\n' });
+
+ if (!ongoing) {
+ process();
+ }
+ }
+
+
+ function process() {
+
+ if (!queue.length) {
+ ongoing = false;
+ return;
+ }
+
+ ongoing = true;
+ var m = queue.shift();
+
+ writeLog(m.message).then(
+ function() {
+ $timeout(function() {
+ process();
+ });
+ },
+ function() {
+ $timeout(function() {
+ process();
+ });
+ }
+ );
+
+ }
+
+
+ function writeLog(message) {
+ var q = $q.defer();
+
+ if (isBrowser()) {
+ // running in browser with 'ionic serve'
+
+ if (!$window.localStorage[storageFilename]) {
+ $window.localStorage[storageFilename] = '';
+ }
+
+ $window.localStorage[storageFilename] += message;
+ q.resolve();
+
+ } else {
+
+ if (!$window.cordova || !$window.cordova.file || !$window.cordova.file.dataDirectory) {
+ q.reject('cordova.file.dataDirectory is not available');
+ return q.promise;
+ }
+
+ $cordovaFile.checkFile(cordova.file.dataDirectory, storageFilename).then(
+ function() {
+ // writeExistingFile(path, fileName, text)
+ $cordovaFile.writeExistingFile(cordova.file.dataDirectory, storageFilename, message).then(
+ function() {
+ q.resolve();
+ },
+ function(error) {
+ q.reject(error);
+ }
+ );
+ },
+ function() {
+ // writeFile(path, fileName, text, replaceBool)
+ $cordovaFile.writeFile(cordova.file.dataDirectory, storageFilename, message, true).then(
+ function() {
+ q.resolve();
+ },
+ function(error) {
+ q.reject(error);
+ }
+ );
+ }
+ );
+
+ }
+
+ return q.promise;
+ }
+
+
+ function getLogfile() {
+ var q = $q.defer();
+
+ if (isBrowser()) {
+ q.resolve($window.localStorage[storageFilename]);
+ } else {
+
+ if (!$window.cordova || !$window.cordova.file || !$window.cordova.file.dataDirectory) {
+ q.reject('cordova.file.dataDirectory is not available');
+ return q.promise;
+ }
+
+ $cordovaFile.readAsText(cordova.file.dataDirectory, storageFilename).then(
+ function(result) {
+ q.resolve(result);
+ },
+ function(error) {
+ q.reject(error);
+ }
+ );
+ }
+
+ return q.promise;
+ }
+
+
+ function deleteLogfile() {
+ var q = $q.defer();
+
+ if (isBrowser()) {
+ $window.localStorage.removeItem(storageFilename);
+ q.resolve();
+ } else {
+
+ if (!$window.cordova || !$window.cordova.file || !$window.cordova.file.dataDirectory) {
+ q.reject('cordova.file.dataDirectory is not available');
+ return q.promise;
+ }
+
+ $cordovaFile.removeFile(cordova.file.dataDirectory, storageFilename).then(
+ function(result) {
+ q.resolve(result);
+ },
+ function(error) {
+ q.reject(error);
+ }
+ );
+ }
+
+ return q.promise;
+ }
+
+
+ function setStorageFilename(filename) {
+ if (angular.isString(filename) && filename.length > 0) {
+ storageFilename = filename;
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+
+ function setTimestampFormat(format, timezone) {
+ if (!(angular.isUndefined(format) || angular.isString(format))) {
+ throw new TypeError('format parameter must be a string or undefined');
+ }
+ if (!(angular.isUndefined(timezone) || angular.isString(timezone))) {
+ throw new TypeError('timezone parameter must be a string or undefined');
+ }
+
+ dateFormat = format;
+ dateTimezone = timezone;
+ }
+
+
+ function checkFile() {
+ var q = $q.defer();
+
+ if (isBrowser()) {
+
+ q.resolve({
+ 'name': storageFilename,
+ 'localURL': 'localStorage://localhost/' + storageFilename,
+ 'type': 'text/plain',
+ 'size': ($window.localStorage[storageFilename] ? $window.localStorage[storageFilename].length : 0)
+ });
+
+ } else {
+
+ if (!$window.cordova || !$window.cordova.file || !$window.cordova.file.dataDirectory) {
+ q.reject('cordova.file.dataDirectory is not available');
+ return q.promise;
+ }
+
+ $cordovaFile.checkFile(cordova.file.dataDirectory, storageFilename).then(function(fileEntry) {
+ fileEntry.file(q.resolve, q.reject);
+ }, q.reject);
+
+ }
+
+ return q.promise;
+ }
+
+ function debug() {
+ var args = Array.prototype.slice.call(arguments, 0);
+ args.unshift('DEBUG');
+ log.apply(undefined, args);
+ }
+
+
+ function info() {
+ var args = Array.prototype.slice.call(arguments, 0);
+ args.unshift('INFO');
+ log.apply(undefined, args);
+ }
+
+
+ function warn() {
+ var args = Array.prototype.slice.call(arguments, 0);
+ args.unshift('WARN');
+ log.apply(undefined, args);
+ }
+
+
+ function error() {
+ var args = Array.prototype.slice.call(arguments, 0);
+ args.unshift('ERROR');
+ log.apply(undefined, args);
+ }
+
+
+ return {
+ log: log,
+ getLogfile: getLogfile,
+ deleteLogfile: deleteLogfile,
+ setStorageFilename: setStorageFilename,
+ setTimestampFormat: setTimestampFormat,
+ checkFile: checkFile,
+ debug: debug,
+ info: info,
+ warn: warn,
+ error: error
+ };
+
+ }]);
+
+})(); \ No newline at end of file
diff --git a/etc/js/holder.js b/etc/js/holder.js
new file mode 100644
index 00000000..c116cdb4
--- /dev/null
+++ b/etc/js/holder.js
@@ -0,0 +1,3070 @@
+/*!
+
+Holder - client side image placeholders
+Version 2.9.6+fblyy
+© 2018 Ivan Malopinsky - http://imsky.co
+
+Site: http://holderjs.com
+Issues: https://github.com/imsky/holder/issues
+License: MIT
+
+*/
+(function (window) {
+ if (!window.document) return;
+ var document = window.document;
+
+ //https://github.com/inexorabletash/polyfill/blob/master/web.js
+ if (!document.querySelectorAll) {
+ document.querySelectorAll = function (selectors) {
+ var style = document.createElement('style'), elements = [], element;
+ document.documentElement.firstChild.appendChild(style);
+ document._qsa = [];
+
+ style.styleSheet.cssText = selectors + '{x-qsa:expression(document._qsa && document._qsa.push(this))}';
+ window.scrollBy(0, 0);
+ style.parentNode.removeChild(style);
+
+ while (document._qsa.length) {
+ element = document._qsa.shift();
+ element.style.removeAttribute('x-qsa');
+ elements.push(element);
+ }
+ document._qsa = null;
+ return elements;
+ };
+ }
+
+ if (!document.querySelector) {
+ document.querySelector = function (selectors) {
+ var elements = document.querySelectorAll(selectors);
+ return (elements.length) ? elements[0] : null;
+ };
+ }
+
+ if (!document.getElementsByClassName) {
+ document.getElementsByClassName = function (classNames) {
+ classNames = String(classNames).replace(/^|\s+/g, '.');
+ return document.querySelectorAll(classNames);
+ };
+ }
+
+ //https://github.com/inexorabletash/polyfill
+ // ES5 15.2.3.14 Object.keys ( O )
+ // https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/keys
+ if (!Object.keys) {
+ Object.keys = function (o) {
+ if (o !== Object(o)) { throw TypeError('Object.keys called on non-object'); }
+ var ret = [], p;
+ for (p in o) {
+ if (Object.prototype.hasOwnProperty.call(o, p)) {
+ ret.push(p);
+ }
+ }
+ return ret;
+ };
+ }
+
+ // ES5 15.4.4.18 Array.prototype.forEach ( callbackfn [ , thisArg ] )
+ // From https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/forEach
+ if (!Array.prototype.forEach) {
+ Array.prototype.forEach = function (fun /*, thisp */) {
+ if (this === void 0 || this === null) { throw TypeError(); }
+
+ var t = Object(this);
+ var len = t.length >>> 0;
+ if (typeof fun !== "function") { throw TypeError(); }
+
+ var thisp = arguments[1], i;
+ for (i = 0; i < len; i++) {
+ if (i in t) {
+ fun.call(thisp, t[i], i, t);
+ }
+ }
+ };
+ }
+
+ //https://github.com/inexorabletash/polyfill/blob/master/web.js
+ (function (global) {
+ var B64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
+ global.atob = global.atob || function (input) {
+ input = String(input);
+ var position = 0,
+ output = [],
+ buffer = 0, bits = 0, n;
+
+ input = input.replace(/\s/g, '');
+ if ((input.length % 4) === 0) { input = input.replace(/=+$/, ''); }
+ if ((input.length % 4) === 1) { throw Error('InvalidCharacterError'); }
+ if (/[^+/0-9A-Za-z]/.test(input)) { throw Error('InvalidCharacterError'); }
+
+ while (position < input.length) {
+ n = B64_ALPHABET.indexOf(input.charAt(position));
+ buffer = (buffer << 6) | n;
+ bits += 6;
+
+ if (bits === 24) {
+ output.push(String.fromCharCode((buffer >> 16) & 0xFF));
+ output.push(String.fromCharCode((buffer >> 8) & 0xFF));
+ output.push(String.fromCharCode(buffer & 0xFF));
+ bits = 0;
+ buffer = 0;
+ }
+ position += 1;
+ }
+
+ if (bits === 12) {
+ buffer = buffer >> 4;
+ output.push(String.fromCharCode(buffer & 0xFF));
+ } else if (bits === 18) {
+ buffer = buffer >> 2;
+ output.push(String.fromCharCode((buffer >> 8) & 0xFF));
+ output.push(String.fromCharCode(buffer & 0xFF));
+ }
+
+ return output.join('');
+ };
+
+ global.btoa = global.btoa || function (input) {
+ input = String(input);
+ var position = 0,
+ out = [],
+ o1, o2, o3,
+ e1, e2, e3, e4;
+
+ if (/[^\x00-\xFF]/.test(input)) { throw Error('InvalidCharacterError'); }
+
+ while (position < input.length) {
+ o1 = input.charCodeAt(position++);
+ o2 = input.charCodeAt(position++);
+ o3 = input.charCodeAt(position++);
+
+ // 111111 112222 222233 333333
+ e1 = o1 >> 2;
+ e2 = ((o1 & 0x3) << 4) | (o2 >> 4);
+ e3 = ((o2 & 0xf) << 2) | (o3 >> 6);
+ e4 = o3 & 0x3f;
+
+ if (position === input.length + 2) {
+ e3 = 64; e4 = 64;
+ }
+ else if (position === input.length + 1) {
+ e4 = 64;
+ }
+
+ out.push(B64_ALPHABET.charAt(e1),
+ B64_ALPHABET.charAt(e2),
+ B64_ALPHABET.charAt(e3),
+ B64_ALPHABET.charAt(e4));
+ }
+
+ return out.join('');
+ };
+ }(window));
+
+ //https://gist.github.com/jimeh/332357
+ if (!Object.prototype.hasOwnProperty){
+ /*jshint -W001, -W103 */
+ Object.prototype.hasOwnProperty = function(prop) {
+ var proto = this.__proto__ || this.constructor.prototype;
+ return (prop in this) && (!(prop in proto) || proto[prop] !== this[prop]);
+ };
+ /*jshint +W001, +W103 */
+ }
+
+ // @license http://opensource.org/licenses/MIT
+ // copyright Paul Irish 2015
+
+
+ // Date.now() is supported everywhere except IE8. For IE8 we use the Date.now polyfill
+ // github.com/Financial-Times/polyfill-service/blob/master/polyfills/Date.now/polyfill.js
+ // as Safari 6 doesn't have support for NavigationTiming, we use a Date.now() timestamp for relative values
+
+ // if you want values similar to what you'd get with real perf.now, place this towards the head of the page
+ // but in reality, you're just getting the delta between now() calls, so it's not terribly important where it's placed
+
+
+ (function(){
+
+ if ('performance' in window === false) {
+ window.performance = {};
+ }
+
+ Date.now = (Date.now || function () { // thanks IE8
+ return new Date().getTime();
+ });
+
+ if ('now' in window.performance === false){
+
+ var nowOffset = Date.now();
+
+ if (performance.timing && performance.timing.navigationStart){
+ nowOffset = performance.timing.navigationStart;
+ }
+
+ window.performance.now = function now(){
+ return Date.now() - nowOffset;
+ };
+ }
+
+ })();
+
+ //requestAnimationFrame polyfill for older Firefox/Chrome versions
+ if (!window.requestAnimationFrame) {
+ if (window.webkitRequestAnimationFrame && window.webkitCancelAnimationFrame) {
+ //https://github.com/Financial-Times/polyfill-service/blob/master/polyfills/requestAnimationFrame/polyfill-webkit.js
+ (function (global) {
+ global.requestAnimationFrame = function (callback) {
+ return webkitRequestAnimationFrame(function () {
+ callback(global.performance.now());
+ });
+ };
+
+ global.cancelAnimationFrame = global.webkitCancelAnimationFrame;
+ }(window));
+ } else if (window.mozRequestAnimationFrame && window.mozCancelAnimationFrame) {
+ //https://github.com/Financial-Times/polyfill-service/blob/master/polyfills/requestAnimationFrame/polyfill-moz.js
+ (function (global) {
+ global.requestAnimationFrame = function (callback) {
+ return mozRequestAnimationFrame(function () {
+ callback(global.performance.now());
+ });
+ };
+
+ global.cancelAnimationFrame = global.mozCancelAnimationFrame;
+ }(window));
+ } else {
+ (function (global) {
+ global.requestAnimationFrame = function (callback) {
+ return global.setTimeout(callback, 1000 / 60);
+ };
+
+ global.cancelAnimationFrame = global.clearTimeout;
+ })(window);
+ }
+ }
+})(this);
+
+(function webpackUniversalModuleDefinition(root, factory) {
+ if(typeof exports === 'object' && typeof module === 'object')
+ module.exports = factory();
+ else if(typeof define === 'function' && define.amd)
+ define([], factory);
+ else if(typeof exports === 'object')
+ exports["Holder"] = factory();
+ else
+ root["Holder"] = factory();
+})(this, function() {
+return /******/ (function(modules) { // webpackBootstrap
+/******/ // The module cache
+/******/ var installedModules = {};
+
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+
+/******/ // Check if module is in cache
+/******/ if(installedModules[moduleId])
+/******/ return installedModules[moduleId].exports;
+
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = installedModules[moduleId] = {
+/******/ exports: {},
+/******/ id: moduleId,
+/******/ loaded: false
+/******/ };
+
+/******/ // Execute the module function
+/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+
+/******/ // Flag the module as loaded
+/******/ module.loaded = true;
+
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+
+
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __webpack_require__.m = modules;
+
+/******/ // expose the module cache
+/******/ __webpack_require__.c = installedModules;
+
+/******/ // __webpack_public_path__
+/******/ __webpack_require__.p = "";
+
+/******/ // Load entry module and return exports
+/******/ return __webpack_require__(0);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ (function(module, exports, __webpack_require__) {
+
+ /*
+ Holder.js - client side image placeholders
+ (c) 2012-2015 Ivan Malopinsky - http://imsky.co
+ */
+
+ module.exports = __webpack_require__(1);
+
+
+/***/ }),
+/* 1 */
+/***/ (function(module, exports, __webpack_require__) {
+
+ /* WEBPACK VAR INJECTION */(function(global) {/*
+ Holder.js - client side image placeholders
+ (c) 2012-2016 Ivan Malopinsky - http://imsky.co
+ */
+
+ //Libraries and functions
+ var onDomReady = __webpack_require__(2);
+ var querystring = __webpack_require__(3);
+
+ var SceneGraph = __webpack_require__(6);
+ var utils = __webpack_require__(7);
+ var SVG = __webpack_require__(8);
+ var DOM = __webpack_require__(9);
+ var Color = __webpack_require__(10);
+ var constants = __webpack_require__(11);
+
+ var svgRenderer = __webpack_require__(12);
+ var sgCanvasRenderer = __webpack_require__(15);
+
+ var extend = utils.extend;
+ var dimensionCheck = utils.dimensionCheck;
+
+ //Constants and definitions
+ var SVG_NS = constants.svg_ns;
+
+ var Holder = {
+ version: constants.version,
+
+ /**
+ * Adds a theme to default settings
+ *
+ * @param {string} name Theme name
+ * @param {Object} theme Theme object, with foreground, background, size, font, and fontweight properties.
+ */
+ addTheme: function(name, theme) {
+ name != null && theme != null && (App.settings.themes[name] = theme);
+ delete App.vars.cache.themeKeys;
+ return this;
+ },
+
+ /**
+ * Appends a placeholder to an element
+ *
+ * @param {string} src Placeholder URL string
+ * @param el A selector or a reference to a DOM node
+ */
+ addImage: function(src, el) {
+ //todo: use jquery fallback if available for all QSA references
+ var nodes = DOM.getNodeArray(el);
+ nodes.forEach(function (node) {
+ var img = DOM.newEl('img');
+ var domProps = {};
+ domProps[App.setup.dataAttr] = src;
+ DOM.setAttr(img, domProps);
+ node.appendChild(img);
+ });
+ return this;
+ },
+
+ /**
+ * Sets whether or not an image is updated on resize.
+ * If an image is set to be updated, it is immediately rendered.
+ *
+ * @param {Object} el Image DOM element
+ * @param {Boolean} value Resizable update flag value
+ */
+ setResizeUpdate: function(el, value) {
+ if (el.holderData) {
+ el.holderData.resizeUpdate = !!value;
+ if (el.holderData.resizeUpdate) {
+ updateResizableElements(el);
+ }
+ }
+ },
+
+ /**
+ * Runs Holder with options. By default runs Holder on all images with "holder.js" in their source attributes.
+ *
+ * @param {Object} userOptions Options object, can contain domain, themes, images, and bgnodes properties
+ */
+ run: function(userOptions) {
+ //todo: split processing into separate queues
+ userOptions = userOptions || {};
+ var engineSettings = {};
+ var options = extend(App.settings, userOptions);
+
+ App.vars.preempted = true;
+ App.vars.dataAttr = options.dataAttr || App.setup.dataAttr;
+
+ engineSettings.renderer = options.renderer ? options.renderer : App.setup.renderer;
+ if (App.setup.renderers.join(',').indexOf(engineSettings.renderer) === -1) {
+ engineSettings.renderer = App.setup.supportsSVG ? 'svg' : (App.setup.supportsCanvas ? 'canvas' : 'html');
+ }
+
+ var images = DOM.getNodeArray(options.images);
+ var bgnodes = DOM.getNodeArray(options.bgnodes);
+ var stylenodes = DOM.getNodeArray(options.stylenodes);
+ var objects = DOM.getNodeArray(options.objects);
+
+ engineSettings.stylesheets = [];
+ engineSettings.svgXMLStylesheet = true;
+ engineSettings.noFontFallback = !!options.noFontFallback;
+ engineSettings.noBackgroundSize = !!options.noBackgroundSize;
+
+ stylenodes.forEach(function (styleNode) {
+ if (styleNode.attributes.rel && styleNode.attributes.href && styleNode.attributes.rel.value == 'stylesheet') {
+ var href = styleNode.attributes.href.value;
+ //todo: write isomorphic relative-to-absolute URL function
+ var proxyLink = DOM.newEl('a');
+ proxyLink.href = href;
+ var stylesheetURL = proxyLink.protocol + '//' + proxyLink.host + proxyLink.pathname + proxyLink.search;
+ engineSettings.stylesheets.push(stylesheetURL);
+ }
+ });
+
+ bgnodes.forEach(function (bgNode) {
+ //Skip processing background nodes if getComputedStyle is unavailable, since only modern browsers would be able to use canvas or SVG to render to background
+ if (!global.getComputedStyle) return;
+ var backgroundImage = global.getComputedStyle(bgNode, null).getPropertyValue('background-image');
+ var dataBackgroundImage = bgNode.getAttribute('data-background-src');
+ var rawURL = dataBackgroundImage || backgroundImage;
+
+ var holderURL = null;
+ var holderString = options.domain + '/';
+ var holderStringIndex = rawURL.indexOf(holderString);
+
+ if (holderStringIndex === 0) {
+ holderURL = rawURL;
+ } else if (holderStringIndex === 1 && rawURL[0] === '?') {
+ holderURL = rawURL.slice(1);
+ } else {
+ var fragment = rawURL.substr(holderStringIndex).match(/([^\"]*)"?\)/);
+ if (fragment !== null) {
+ holderURL = fragment[1];
+ } else if (rawURL.indexOf('url(') === 0) {
+ throw 'Holder: unable to parse background URL: ' + rawURL;
+ }
+ }
+
+ if (holderURL) {
+ var holderFlags = parseURL(holderURL, options);
+ if (holderFlags) {
+ prepareDOMElement({
+ mode: 'background',
+ el: bgNode,
+ flags: holderFlags,
+ engineSettings: engineSettings
+ });
+ }
+ }
+ });
+
+ objects.forEach(function (object) {
+ var objectAttr = {};
+
+ try {
+ objectAttr.data = object.getAttribute('data');
+ objectAttr.dataSrc = object.getAttribute(App.vars.dataAttr);
+ } catch (e) {}
+
+ var objectHasSrcURL = objectAttr.data != null && objectAttr.data.indexOf(options.domain) === 0;
+ var objectHasDataSrcURL = objectAttr.dataSrc != null && objectAttr.dataSrc.indexOf(options.domain) === 0;
+
+ if (objectHasSrcURL) {
+ prepareImageElement(options, engineSettings, objectAttr.data, object);
+ } else if (objectHasDataSrcURL) {
+ prepareImageElement(options, engineSettings, objectAttr.dataSrc, object);
+ }
+ });
+
+ images.forEach(function (image) {
+ var imageAttr = {};
+
+ try {
+ imageAttr.src = image.getAttribute('src');
+ imageAttr.dataSrc = image.getAttribute(App.vars.dataAttr);
+ imageAttr.rendered = image.getAttribute('data-holder-rendered');
+ } catch (e) {}
+
+ var imageHasSrc = imageAttr.src != null;
+ var imageHasDataSrcURL = imageAttr.dataSrc != null && imageAttr.dataSrc.indexOf(options.domain) === 0;
+ var imageRendered = imageAttr.rendered != null && imageAttr.rendered == 'true';
+
+ if (imageHasSrc) {
+ if (imageAttr.src.indexOf(options.domain) === 0) {
+ prepareImageElement(options, engineSettings, imageAttr.src, image);
+ } else if (imageHasDataSrcURL) {
+ //Image has a valid data-src and an invalid src
+ if (imageRendered) {
+ //If the placeholder has already been render, re-render it
+ prepareImageElement(options, engineSettings, imageAttr.dataSrc, image);
+ } else {
+ //If the placeholder has not been rendered, check if the image exists and render a fallback if it doesn't
+ (function(src, options, engineSettings, dataSrc, image) {
+ utils.imageExists(src, function(exists) {
+ if (!exists) {
+ prepareImageElement(options, engineSettings, dataSrc, image);
+ }
+ });
+ })(imageAttr.src, options, engineSettings, imageAttr.dataSrc, image);
+ }
+ }
+ } else if (imageHasDataSrcURL) {
+ prepareImageElement(options, engineSettings, imageAttr.dataSrc, image);
+ }
+ });
+
+ return this;
+ }
+ };
+
+ var App = {
+ settings: {
+ domain: 'holder.js',
+ images: 'img',
+ objects: 'object',
+ bgnodes: 'body .holderjs',
+ stylenodes: 'head link.holderjs',
+ themes: {
+ 'gray': {
+ bg: '#EEEEEE',
+ fg: '#AAAAAA'
+ },
+ 'social': {
+ bg: '#3a5a97',
+ fg: '#FFFFFF'
+ },
+ 'industrial': {
+ bg: '#434A52',
+ fg: '#C2F200'
+ },
+ 'sky': {
+ bg: '#0D8FDB',
+ fg: '#FFFFFF'
+ },
+ 'vine': {
+ bg: '#39DBAC',
+ fg: '#1E292C'
+ },
+ 'lava': {
+ bg: '#F8591A',
+ fg: '#1C2846'
+ }
+ }
+ },
+ defaults: {
+ size: 10,
+ units: 'pt',
+ scale: 1 / 16
+ }
+ };
+
+ /**
+ * Processes provided source attribute and sets up the appropriate rendering workflow
+ *
+ * @private
+ * @param options Instance options from Holder.run
+ * @param renderSettings Instance configuration
+ * @param src Image URL
+ * @param el Image DOM element
+ */
+ function prepareImageElement(options, engineSettings, src, el) {
+ var holderFlags = parseURL(src.substr(src.lastIndexOf(options.domain)), options);
+ if (holderFlags) {
+ prepareDOMElement({
+ mode: null,
+ el: el,
+ flags: holderFlags,
+ engineSettings: engineSettings
+ });
+ }
+ }
+
+ /**
+ * Processes a Holder URL and extracts configuration from query string
+ *
+ * @private
+ * @param url URL
+ * @param instanceOptions Instance options from Holder.run
+ */
+ function parseURL(url, instanceOptions) {
+ var holder = {
+ theme: extend(App.settings.themes.gray, null),
+ stylesheets: instanceOptions.stylesheets,
+ instanceOptions: instanceOptions
+ };
+
+ var firstQuestionMark = url.indexOf('?');
+ var parts = [url];
+
+ if (firstQuestionMark !== -1) {
+ parts = [url.slice(0, firstQuestionMark), url.slice(firstQuestionMark + 1)];
+ }
+
+ var basics = parts[0].split('/');
+
+ holder.holderURL = url;
+
+ var dimensions = basics[1];
+ var dimensionData = dimensions.match(/([\d]+p?)x([\d]+p?)/);
+
+ if (!dimensionData) return false;
+
+ holder.fluid = dimensions.indexOf('p') !== -1;
+
+ holder.dimensions = {
+ width: dimensionData[1].replace('p', '%'),
+ height: dimensionData[2].replace('p', '%')
+ };
+
+ if (parts.length === 2) {
+ var options = querystring.parse(parts[1]);
+
+ // Dimensions
+
+ if (utils.truthy(options.ratio)) {
+ holder.fluid = true;
+ var ratioWidth = parseFloat(holder.dimensions.width.replace('%', ''));
+ var ratioHeight = parseFloat(holder.dimensions.height.replace('%', ''));
+
+ ratioHeight = Math.floor(100 * (ratioHeight / ratioWidth));
+ ratioWidth = 100;
+
+ holder.dimensions.width = ratioWidth + '%';
+ holder.dimensions.height = ratioHeight + '%';
+ }
+
+ holder.auto = utils.truthy(options.auto);
+
+ // Colors
+
+ if (options.bg) {
+ holder.theme.bg = utils.parseColor(options.bg);
+ }
+
+ if (options.fg) {
+ holder.theme.fg = utils.parseColor(options.fg);
+ }
+
+ //todo: add automatic foreground to themes without foreground
+ if (options.bg && !options.fg) {
+ holder.autoFg = true;
+ }
+
+ if (options.theme && holder.instanceOptions.themes.hasOwnProperty(options.theme)) {
+ holder.theme = extend(holder.instanceOptions.themes[options.theme], null);
+ }
+
+ // Text
+
+ if (options.text) {
+ holder.text = options.text;
+ }
+
+ if (options.textmode) {
+ holder.textmode = options.textmode;
+ }
+
+ if (options.size && parseFloat(options.size)) {
+ holder.size = parseFloat(options.size);
+ }
+
+ if (options.font) {
+ holder.font = options.font;
+ }
+
+ if (options.align) {
+ holder.align = options.align;
+ }
+
+ if (options.lineWrap) {
+ holder.lineWrap = options.lineWrap;
+ }
+
+ holder.nowrap = utils.truthy(options.nowrap);
+
+ // Miscellaneous
+
+ holder.outline = utils.truthy(options.outline);
+
+ if (utils.truthy(options.random)) {
+ App.vars.cache.themeKeys = App.vars.cache.themeKeys || Object.keys(holder.instanceOptions.themes);
+ var _theme = App.vars.cache.themeKeys[0 | Math.random() * App.vars.cache.themeKeys.length];
+ holder.theme = extend(holder.instanceOptions.themes[_theme], null);
+ }
+ }
+
+ return holder;
+ }
+
+ /**
+ * Modifies the DOM to fit placeholders and sets up resizable image callbacks (for fluid and automatically sized placeholders)
+ *
+ * @private
+ * @param settings DOM prep settings
+ */
+ function prepareDOMElement(prepSettings) {
+ var mode = prepSettings.mode;
+ var el = prepSettings.el;
+ var flags = prepSettings.flags;
+ var _engineSettings = prepSettings.engineSettings;
+ var dimensions = flags.dimensions,
+ theme = flags.theme;
+ var dimensionsCaption = dimensions.width + 'x' + dimensions.height;
+ mode = mode == null ? (flags.fluid ? 'fluid' : 'image') : mode;
+ var holderTemplateRe = /holder_([a-z]+)/g;
+ var dimensionsInText = false;
+
+ if (flags.text != null) {
+ theme.text = flags.text;
+
+ //<object> SVG embedding doesn't parse Unicode properly
+ if (el.nodeName.toLowerCase() === 'object') {
+ var textLines = theme.text.split('\\n');
+ for (var k = 0; k < textLines.length; k++) {
+ textLines[k] = utils.encodeHtmlEntity(textLines[k]);
+ }
+ theme.text = textLines.join('\\n');
+ }
+ }
+
+ if (theme.text) {
+ var holderTemplateMatches = theme.text.match(holderTemplateRe);
+
+ if (holderTemplateMatches !== null) {
+ //todo: optimize template replacement
+ holderTemplateMatches.forEach(function (match) {
+ if (match === 'holder_dimensions') {
+ theme.text = theme.text.replace(match, dimensionsCaption);
+ }
+ });
+ }
+ }
+
+ var holderURL = flags.holderURL;
+ var engineSettings = extend(_engineSettings, null);
+
+ if (flags.font) {
+ /*
+ If external fonts are used in a <img> placeholder rendered with SVG, Holder falls back to canvas.
+
+ This is done because Firefox and Chrome disallow embedded SVGs from referencing external assets.
+ The workaround is either to change the placeholder tag from <img> to <object> or to use the canvas renderer.
+ */
+ theme.font = flags.font;
+ if (!engineSettings.noFontFallback && el.nodeName.toLowerCase() === 'img' && App.setup.supportsCanvas && engineSettings.renderer === 'svg') {
+ engineSettings = extend(engineSettings, {
+ renderer: 'canvas'
+ });
+ }
+ }
+
+ //Chrome and Opera require a quick 10ms re-render if web fonts are used with canvas
+ if (flags.font && engineSettings.renderer == 'canvas') {
+ engineSettings.reRender = true;
+ }
+
+ if (mode == 'background') {
+ if (el.getAttribute('data-background-src') == null) {
+ DOM.setAttr(el, {
+ 'data-background-src': holderURL
+ });
+ }
+ } else {
+ var domProps = {};
+ domProps[App.vars.dataAttr] = holderURL;
+ DOM.setAttr(el, domProps);
+ }
+
+ flags.theme = theme;
+
+ //todo consider using all renderSettings in holderData
+ el.holderData = {
+ flags: flags,
+ engineSettings: engineSettings
+ };
+
+ if (mode == 'image' || mode == 'fluid') {
+ DOM.setAttr(el, {
+ 'alt': theme.text ? (dimensionsInText ? theme.text : theme.text + ' [' + dimensionsCaption + ']') : dimensionsCaption
+ });
+ }
+
+ var renderSettings = {
+ mode: mode,
+ el: el,
+ holderSettings: {
+ dimensions: dimensions,
+ theme: theme,
+ flags: flags
+ },
+ engineSettings: engineSettings
+ };
+
+ if (mode == 'image') {
+ if (!flags.auto) {
+ el.style.width = dimensions.width + 'px';
+ el.style.height = dimensions.height + 'px';
+ }
+
+ if (engineSettings.renderer == 'html') {
+ el.style.backgroundColor = theme.bg;
+ } else {
+ render(renderSettings);
+
+ if (flags.textmode == 'exact') {
+ el.holderData.resizeUpdate = true;
+ App.vars.resizableImages.push(el);
+ updateResizableElements(el);
+ }
+ }
+ } else if (mode == 'background' && engineSettings.renderer != 'html') {
+ render(renderSettings);
+ } else if (mode == 'fluid') {
+ el.holderData.resizeUpdate = true;
+
+ if (dimensions.height.slice(-1) == '%') {
+ el.style.height = dimensions.height;
+ } else if (flags.auto == null || !flags.auto) {
+ el.style.height = dimensions.height + 'px';
+ }
+ if (dimensions.width.slice(-1) == '%') {
+ el.style.width = dimensions.width;
+ } else if (flags.auto == null || !flags.auto) {
+ el.style.width = dimensions.width + 'px';
+ }
+ if (el.style.display == 'inline' || el.style.display === '' || el.style.display == 'none') {
+ el.style.display = 'block';
+ }
+
+ setInitialDimensions(el);
+
+ if (engineSettings.renderer == 'html') {
+ el.style.backgroundColor = theme.bg;
+ } else {
+ App.vars.resizableImages.push(el);
+ updateResizableElements(el);
+ }
+ }
+ }
+
+ /**
+ * Core function that takes output from renderers and sets it as the source or background-image of the target element
+ *
+ * @private
+ * @param renderSettings Renderer settings
+ */
+ function render(renderSettings) {
+ var image = null;
+ var mode = renderSettings.mode;
+ var el = renderSettings.el;
+ var holderSettings = renderSettings.holderSettings;
+ var engineSettings = renderSettings.engineSettings;
+
+ switch (engineSettings.renderer) {
+ case 'svg':
+ if (!App.setup.supportsSVG) return;
+ break;
+ case 'canvas':
+ if (!App.setup.supportsCanvas) return;
+ break;
+ default:
+ return;
+ }
+
+ //todo: move generation of scene up to flag generation to reduce extra object creation
+ var scene = {
+ width: holderSettings.dimensions.width,
+ height: holderSettings.dimensions.height,
+ theme: holderSettings.theme,
+ flags: holderSettings.flags
+ };
+
+ var sceneGraph = buildSceneGraph(scene);
+
+ function getRenderedImage() {
+ var image = null;
+ switch (engineSettings.renderer) {
+ case 'canvas':
+ image = sgCanvasRenderer(sceneGraph, renderSettings);
+ break;
+ case 'svg':
+ image = svgRenderer(sceneGraph, renderSettings);
+ break;
+ default:
+ throw 'Holder: invalid renderer: ' + engineSettings.renderer;
+ }
+
+ return image;
+ }
+
+ image = getRenderedImage();
+
+ if (image == null) {
+ throw 'Holder: couldn\'t render placeholder';
+ }
+
+ //todo: add <object> canvas rendering
+ if (mode == 'background') {
+ el.style.backgroundImage = 'url(' + image + ')';
+
+ if (!engineSettings.noBackgroundSize) {
+ el.style.backgroundSize = scene.width + 'px ' + scene.height + 'px';
+ }
+ } else {
+ if (el.nodeName.toLowerCase() === 'img') {
+ DOM.setAttr(el, {
+ 'src': image
+ });
+ } else if (el.nodeName.toLowerCase() === 'object') {
+ DOM.setAttr(el, {
+ 'data': image,
+ 'type': 'image/svg+xml'
+ });
+ }
+ if (engineSettings.reRender) {
+ global.setTimeout(function () {
+ var image = getRenderedImage();
+ if (image == null) {
+ throw 'Holder: couldn\'t render placeholder';
+ }
+ //todo: refactor this code into a function
+ if (el.nodeName.toLowerCase() === 'img') {
+ DOM.setAttr(el, {
+ 'src': image
+ });
+ } else if (el.nodeName.toLowerCase() === 'object') {
+ DOM.setAttr(el, {
+ 'data': image,
+ 'type': 'image/svg+xml'
+ });
+ }
+ }, 150);
+ }
+ }
+ //todo: account for re-rendering
+ DOM.setAttr(el, {
+ 'data-holder-rendered': true
+ });
+ }
+
+ /**
+ * Core function that takes a Holder scene description and builds a scene graph
+ *
+ * @private
+ * @param scene Holder scene object
+ */
+ //todo: make this function reusable
+ //todo: merge app defaults and setup properties into the scene argument
+ function buildSceneGraph(scene) {
+ var fontSize = App.defaults.size;
+ if (parseFloat(scene.theme.size)) {
+ fontSize = scene.theme.size;
+ } else if (parseFloat(scene.flags.size)) {
+ fontSize = scene.flags.size;
+ }
+
+ scene.font = {
+ family: scene.theme.font ? scene.theme.font : 'Arial, Helvetica, Open Sans, sans-serif',
+ size: textSize(scene.width, scene.height, fontSize, App.defaults.scale),
+ units: scene.theme.units ? scene.theme.units : App.defaults.units,
+ weight: scene.theme.fontweight ? scene.theme.fontweight : 'bold'
+ };
+
+ scene.text = scene.theme.text || Math.floor(scene.width) + 'x' + Math.floor(scene.height);
+
+ scene.noWrap = scene.theme.nowrap || scene.flags.nowrap;
+
+ scene.align = scene.theme.align || scene.flags.align || 'center';
+
+ switch (scene.flags.textmode) {
+ case 'literal':
+ scene.text = scene.flags.dimensions.width + 'x' + scene.flags.dimensions.height;
+ break;
+ case 'exact':
+ if (!scene.flags.exactDimensions) break;
+ scene.text = Math.floor(scene.flags.exactDimensions.width) + 'x' + Math.floor(scene.flags.exactDimensions.height);
+ break;
+ }
+
+ var lineWrap = scene.flags.lineWrap || App.setup.lineWrapRatio;
+ var sceneMargin = scene.width * lineWrap;
+ var maxLineWidth = sceneMargin;
+
+ var sceneGraph = new SceneGraph({
+ width: scene.width,
+ height: scene.height
+ });
+
+ var Shape = sceneGraph.Shape;
+
+ var holderBg = new Shape.Rect('holderBg', {
+ fill: scene.theme.bg
+ });
+
+ holderBg.resize(scene.width, scene.height);
+ sceneGraph.root.add(holderBg);
+
+ if (scene.flags.outline) {
+ var outlineColor = new Color(holderBg.properties.fill);
+ outlineColor = outlineColor.lighten(outlineColor.lighterThan('7f7f7f') ? -0.1 : 0.1);
+ holderBg.properties.outline = {
+ fill: outlineColor.toHex(true),
+ width: 2
+ };
+ }
+
+ var holderTextColor = scene.theme.fg;
+
+ if (scene.flags.autoFg) {
+ var holderBgColor = new Color(holderBg.properties.fill);
+ var lightColor = new Color('fff');
+ var darkColor = new Color('000', {
+ 'alpha': 0.285714
+ });
+
+ holderTextColor = holderBgColor.blendAlpha(holderBgColor.lighterThan('7f7f7f') ? darkColor : lightColor).toHex(true);
+ }
+
+ var holderTextGroup = new Shape.Group('holderTextGroup', {
+ text: scene.text,
+ align: scene.align,
+ font: scene.font,
+ fill: holderTextColor
+ });
+
+ holderTextGroup.moveTo(null, null, 1);
+ sceneGraph.root.add(holderTextGroup);
+
+ var tpdata = holderTextGroup.textPositionData = stagingRenderer(sceneGraph);
+ if (!tpdata) {
+ throw 'Holder: staging fallback not supported yet.';
+ }
+ holderTextGroup.properties.leading = tpdata.boundingBox.height;
+
+ var textNode = null;
+ var line = null;
+
+ function finalizeLine(parent, line, width, height) {
+ line.width = width;
+ line.height = height;
+ parent.width = Math.max(parent.width, line.width);
+ parent.height += line.height;
+ }
+
+ if (tpdata.lineCount > 1) {
+ var offsetX = 0;
+ var offsetY = 0;
+ var lineIndex = 0;
+ var lineKey;
+ line = new Shape.Group('line' + lineIndex);
+
+ //Double margin so that left/right-aligned next is not flush with edge of image
+ if (scene.align === 'left' || scene.align === 'right') {
+ maxLineWidth = scene.width * (1 - (1 - lineWrap) * 2);
+ }
+
+ for (var i = 0; i < tpdata.words.length; i++) {
+ var word = tpdata.words[i];
+ textNode = new Shape.Text(word.text);
+ var newline = word.text == '\\n';
+ if (!scene.noWrap && (offsetX + word.width >= maxLineWidth || newline === true)) {
+ finalizeLine(holderTextGroup, line, offsetX, holderTextGroup.properties.leading);
+ holderTextGroup.add(line);
+ offsetX = 0;
+ offsetY += holderTextGroup.properties.leading;
+ lineIndex += 1;
+ line = new Shape.Group('line' + lineIndex);
+ line.y = offsetY;
+ }
+ if (newline === true) {
+ continue;
+ }
+ textNode.moveTo(offsetX, 0);
+ offsetX += tpdata.spaceWidth + word.width;
+ line.add(textNode);
+ }
+
+ finalizeLine(holderTextGroup, line, offsetX, holderTextGroup.properties.leading);
+ holderTextGroup.add(line);
+
+ if (scene.align === 'left') {
+ holderTextGroup.moveTo(scene.width - sceneMargin, null, null);
+ } else if (scene.align === 'right') {
+ for (lineKey in holderTextGroup.children) {
+ line = holderTextGroup.children[lineKey];
+ line.moveTo(scene.width - line.width, null, null);
+ }
+
+ holderTextGroup.moveTo(0 - (scene.width - sceneMargin), null, null);
+ } else {
+ for (lineKey in holderTextGroup.children) {
+ line = holderTextGroup.children[lineKey];
+ line.moveTo((holderTextGroup.width - line.width) / 2, null, null);
+ }
+
+ holderTextGroup.moveTo((scene.width - holderTextGroup.width) / 2, null, null);
+ }
+
+ holderTextGroup.moveTo(null, (scene.height - holderTextGroup.height) / 2, null);
+
+ //If the text exceeds vertical space, move it down so the first line is visible
+ if ((scene.height - holderTextGroup.height) / 2 < 0) {
+ holderTextGroup.moveTo(null, 0, null);
+ }
+ } else {
+ textNode = new Shape.Text(scene.text);
+ line = new Shape.Group('line0');
+ line.add(textNode);
+ holderTextGroup.add(line);
+
+ if (scene.align === 'left') {
+ holderTextGroup.moveTo(scene.width - sceneMargin, null, null);
+ } else if (scene.align === 'right') {
+ holderTextGroup.moveTo(0 - (scene.width - sceneMargin), null, null);
+ } else {
+ holderTextGroup.moveTo((scene.width - tpdata.boundingBox.width) / 2, null, null);
+ }
+
+ holderTextGroup.moveTo(null, (scene.height - tpdata.boundingBox.height) / 2, null);
+ }
+
+ //todo: renderlist
+ return sceneGraph;
+ }
+
+ /**
+ * Adaptive text sizing function
+ *
+ * @private
+ * @param width Parent width
+ * @param height Parent height
+ * @param fontSize Requested text size
+ * @param scale Proportional scale of text
+ */
+ function textSize(width, height, fontSize, scale) {
+ var stageWidth = parseInt(width, 10);
+ var stageHeight = parseInt(height, 10);
+
+ var bigSide = Math.max(stageWidth, stageHeight);
+ var smallSide = Math.min(stageWidth, stageHeight);
+
+ var newHeight = 0.8 * Math.min(smallSide, bigSide * scale);
+ return Math.round(Math.max(fontSize, newHeight));
+ }
+
+ /**
+ * Iterates over resizable (fluid or auto) placeholders and renders them
+ *
+ * @private
+ * @param element Optional element selector, specified only if a specific element needs to be re-rendered
+ */
+ function updateResizableElements(element) {
+ var images;
+ if (element == null || element.nodeType == null) {
+ images = App.vars.resizableImages;
+ } else {
+ images = [element];
+ }
+ for (var i = 0, l = images.length; i < l; i++) {
+ var el = images[i];
+ if (el.holderData) {
+ var flags = el.holderData.flags;
+ var dimensions = dimensionCheck(el);
+ if (dimensions) {
+ if (!el.holderData.resizeUpdate) {
+ continue;
+ }
+
+ if (flags.fluid && flags.auto) {
+ var fluidConfig = el.holderData.fluidConfig;
+ switch (fluidConfig.mode) {
+ case 'width':
+ dimensions.height = dimensions.width / fluidConfig.ratio;
+ break;
+ case 'height':
+ dimensions.width = dimensions.height * fluidConfig.ratio;
+ break;
+ }
+ }
+
+ var settings = {
+ mode: 'image',
+ holderSettings: {
+ dimensions: dimensions,
+ theme: flags.theme,
+ flags: flags
+ },
+ el: el,
+ engineSettings: el.holderData.engineSettings
+ };
+
+ if (flags.textmode == 'exact') {
+ flags.exactDimensions = dimensions;
+ settings.holderSettings.dimensions = flags.dimensions;
+ }
+
+ render(settings);
+ } else {
+ setInvisible(el);
+ }
+ }
+ }
+ }
+
+ /**
+ * Sets up aspect ratio metadata for fluid placeholders, in order to preserve proportions when resizing
+ *
+ * @private
+ * @param el Image DOM element
+ */
+ function setInitialDimensions(el) {
+ if (el.holderData) {
+ var dimensions = dimensionCheck(el);
+ if (dimensions) {
+ var flags = el.holderData.flags;
+
+ var fluidConfig = {
+ fluidHeight: flags.dimensions.height.slice(-1) == '%',
+ fluidWidth: flags.dimensions.width.slice(-1) == '%',
+ mode: null,
+ initialDimensions: dimensions
+ };
+
+ if (fluidConfig.fluidWidth && !fluidConfig.fluidHeight) {
+ fluidConfig.mode = 'width';
+ fluidConfig.ratio = fluidConfig.initialDimensions.width / parseFloat(flags.dimensions.height);
+ } else if (!fluidConfig.fluidWidth && fluidConfig.fluidHeight) {
+ fluidConfig.mode = 'height';
+ fluidConfig.ratio = parseFloat(flags.dimensions.width) / fluidConfig.initialDimensions.height;
+ }
+
+ el.holderData.fluidConfig = fluidConfig;
+ } else {
+ setInvisible(el);
+ }
+ }
+ }
+
+ /**
+ * Iterates through all current invisible images, and if they're visible, renders them and removes them from further checks. Runs every animation frame.
+ *
+ * @private
+ */
+ function visibilityCheck() {
+ var renderableImages = [];
+ var keys = Object.keys(App.vars.invisibleImages);
+ var el;
+
+ keys.forEach(function (key) {
+ el = App.vars.invisibleImages[key];
+ if (dimensionCheck(el) && el.nodeName.toLowerCase() == 'img') {
+ renderableImages.push(el);
+ delete App.vars.invisibleImages[key];
+ }
+ });
+
+ if (renderableImages.length) {
+ Holder.run({
+ images: renderableImages
+ });
+ }
+
+ // Done to prevent 100% CPU usage via aggressive calling of requestAnimationFrame
+ setTimeout(function () {
+ global.requestAnimationFrame(visibilityCheck);
+ }, 10);
+ }
+
+ /**
+ * Starts checking for invisible placeholders if not doing so yet. Does nothing otherwise.
+ *
+ * @private
+ */
+ function startVisibilityCheck() {
+ if (!App.vars.visibilityCheckStarted) {
+ global.requestAnimationFrame(visibilityCheck);
+ App.vars.visibilityCheckStarted = true;
+ }
+ }
+
+ /**
+ * Sets a unique ID for an image detected to be invisible and adds it to the map of invisible images checked by visibilityCheck
+ *
+ * @private
+ * @param el Invisible DOM element
+ */
+ function setInvisible(el) {
+ if (!el.holderData.invisibleId) {
+ App.vars.invisibleId += 1;
+ App.vars.invisibleImages['i' + App.vars.invisibleId] = el;
+ el.holderData.invisibleId = App.vars.invisibleId;
+ }
+ }
+
+ //todo: see if possible to convert stagingRenderer to use HTML only
+ var stagingRenderer = (function() {
+ var svg = null,
+ stagingText = null,
+ stagingTextNode = null;
+ return function(graph) {
+ var rootNode = graph.root;
+ if (App.setup.supportsSVG) {
+ var firstTimeSetup = false;
+ var tnode = function(text) {
+ return document.createTextNode(text);
+ };
+ if (svg == null || svg.parentNode !== document.body) {
+ firstTimeSetup = true;
+ }
+
+ svg = SVG.initSVG(svg, rootNode.properties.width, rootNode.properties.height);
+ //Show staging element before staging
+ svg.style.display = 'block';
+
+ if (firstTimeSetup) {
+ stagingText = DOM.newEl('text', SVG_NS);
+ stagingTextNode = tnode(null);
+ DOM.setAttr(stagingText, {
+ x: 0
+ });
+ stagingText.appendChild(stagingTextNode);
+ svg.appendChild(stagingText);
+ document.body.appendChild(svg);
+ svg.style.visibility = 'hidden';
+ svg.style.position = 'absolute';
+ svg.style.top = '-100%';
+ svg.style.left = '-100%';
+ //todo: workaround for zero-dimension <svg> tag in Opera 12
+ //svg.setAttribute('width', 0);
+ //svg.setAttribute('height', 0);
+ }
+
+ var holderTextGroup = rootNode.children.holderTextGroup;
+ var htgProps = holderTextGroup.properties;
+ DOM.setAttr(stagingText, {
+ 'y': htgProps.font.size,
+ 'style': utils.cssProps({
+ 'font-weight': htgProps.font.weight,
+ 'font-size': htgProps.font.size + htgProps.font.units,
+ 'font-family': htgProps.font.family
+ })
+ });
+
+ //Unescape HTML entities to get approximately the right width
+ var txt = DOM.newEl('textarea');
+ txt.innerHTML = htgProps.text;
+ stagingTextNode.nodeValue = txt.value;
+
+ //Get bounding box for the whole string (total width and height)
+ var stagingTextBBox = stagingText.getBBox();
+
+ //Get line count and split the string into words
+ var lineCount = Math.ceil(stagingTextBBox.width / rootNode.properties.width);
+ var words = htgProps.text.split(' ');
+ var newlines = htgProps.text.match(/\\n/g);
+ lineCount += newlines == null ? 0 : newlines.length;
+
+ //Get bounding box for the string with spaces removed
+ stagingTextNode.nodeValue = htgProps.text.replace(/[ ]+/g, '');
+ var computedNoSpaceLength = stagingText.getComputedTextLength();
+
+ //Compute average space width
+ var diffLength = stagingTextBBox.width - computedNoSpaceLength;
+ var spaceWidth = Math.round(diffLength / Math.max(1, words.length - 1));
+
+ //Get widths for every word with space only if there is more than one line
+ var wordWidths = [];
+ if (lineCount > 1) {
+ stagingTextNode.nodeValue = '';
+ for (var i = 0; i < words.length; i++) {
+ if (words[i].length === 0) continue;
+ stagingTextNode.nodeValue = utils.decodeHtmlEntity(words[i]);
+ var bbox = stagingText.getBBox();
+ wordWidths.push({
+ text: words[i],
+ width: bbox.width
+ });
+ }
+ }
+
+ //Hide staging element after staging
+ svg.style.display = 'none';
+
+ return {
+ spaceWidth: spaceWidth,
+ lineCount: lineCount,
+ boundingBox: stagingTextBBox,
+ words: wordWidths
+ };
+ } else {
+ //todo: canvas fallback for measuring text on android 2.3
+ return false;
+ }
+ };
+ })();
+
+ //Helpers
+
+ /**
+ * Prevents a function from being called too often, waits until a timer elapses to call it again
+ *
+ * @param fn Function to call
+ */
+ function debounce(fn) {
+ if (!App.vars.debounceTimer) fn.call(this);
+ if (App.vars.debounceTimer) global.clearTimeout(App.vars.debounceTimer);
+ App.vars.debounceTimer = global.setTimeout(function() {
+ App.vars.debounceTimer = null;
+ fn.call(this);
+ }, App.setup.debounce);
+ }
+
+ /**
+ * Holder-specific resize/orientation change callback, debounced to prevent excessive execution
+ */
+ function resizeEvent() {
+ debounce(function() {
+ updateResizableElements(null);
+ });
+ }
+
+ //Set up flags
+
+ for (var flag in App.flags) {
+ if (!App.flags.hasOwnProperty(flag)) continue;
+ App.flags[flag].match = function(val) {
+ return val.match(this.regex);
+ };
+ }
+
+ //Properties set once on setup
+
+ App.setup = {
+ renderer: 'html',
+ debounce: 100,
+ ratio: 1,
+ supportsCanvas: false,
+ supportsSVG: false,
+ lineWrapRatio: 0.9,
+ dataAttr: 'data-src',
+ renderers: ['html', 'canvas', 'svg']
+ };
+
+ //Properties modified during runtime
+
+ App.vars = {
+ preempted: false,
+ resizableImages: [],
+ invisibleImages: {},
+ invisibleId: 0,
+ visibilityCheckStarted: false,
+ debounceTimer: null,
+ cache: {}
+ };
+
+ //Pre-flight
+
+ (function() {
+ var canvas = DOM.newEl('canvas');
+
+ if (canvas.getContext) {
+ if (canvas.toDataURL('image/png').indexOf('data:image/png') != -1) {
+ App.setup.renderer = 'canvas';
+ App.setup.supportsCanvas = true;
+ }
+ }
+
+ if (!!document.createElementNS && !!document.createElementNS(SVG_NS, 'svg').createSVGRect) {
+ App.setup.renderer = 'svg';
+ App.setup.supportsSVG = true;
+ }
+ })();
+
+ //Starts checking for invisible placeholders
+ startVisibilityCheck();
+
+ if (onDomReady) {
+ onDomReady(function() {
+ if (!App.vars.preempted) {
+ Holder.run();
+ }
+ if (global.addEventListener) {
+ global.addEventListener('resize', resizeEvent, false);
+ global.addEventListener('orientationchange', resizeEvent, false);
+ } else {
+ global.attachEvent('onresize', resizeEvent);
+ }
+
+ if (typeof global.Turbolinks == 'object') {
+ global.document.addEventListener('page:change', function() {
+ Holder.run();
+ });
+ }
+ });
+ }
+
+ module.exports = Holder;
+
+ /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }())))
+
+/***/ }),
+/* 2 */
+/***/ (function(module, exports) {
+
+ /*!
+ * onDomReady.js 1.4.0 (c) 2013 Tubal Martin - MIT license
+ *
+ * Specially modified to work with Holder.js
+ */
+
+ function _onDomReady(win) {
+ //Lazy loading fix for Firefox < 3.6
+ //http://webreflection.blogspot.com/2009/11/195-chars-to-help-lazy-loading.html
+ if (document.readyState == null && document.addEventListener) {
+ document.addEventListener("DOMContentLoaded", function DOMContentLoaded() {
+ document.removeEventListener("DOMContentLoaded", DOMContentLoaded, false);
+ document.readyState = "complete";
+ }, false);
+ document.readyState = "loading";
+ }
+
+ var doc = win.document,
+ docElem = doc.documentElement,
+
+ LOAD = "load",
+ FALSE = false,
+ ONLOAD = "on"+LOAD,
+ COMPLETE = "complete",
+ READYSTATE = "readyState",
+ ATTACHEVENT = "attachEvent",
+ DETACHEVENT = "detachEvent",
+ ADDEVENTLISTENER = "addEventListener",
+ DOMCONTENTLOADED = "DOMContentLoaded",
+ ONREADYSTATECHANGE = "onreadystatechange",
+ REMOVEEVENTLISTENER = "removeEventListener",
+
+ // W3C Event model
+ w3c = ADDEVENTLISTENER in doc,
+ _top = FALSE,
+
+ // isReady: Is the DOM ready to be used? Set to true once it occurs.
+ isReady = FALSE,
+
+ // Callbacks pending execution until DOM is ready
+ callbacks = [];
+
+ // Handle when the DOM is ready
+ function ready( fn ) {
+ if ( !isReady ) {
+
+ // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
+ if ( !doc.body ) {
+ return defer( ready );
+ }
+
+ // Remember that the DOM is ready
+ isReady = true;
+
+ // Execute all callbacks
+ while ( fn = callbacks.shift() ) {
+ defer( fn );
+ }
+ }
+ }
+
+ // The ready event handler
+ function completed( event ) {
+ // readyState === "complete" is good enough for us to call the dom ready in oldIE
+ if ( w3c || event.type === LOAD || doc[READYSTATE] === COMPLETE ) {
+ detach();
+ ready();
+ }
+ }
+
+ // Clean-up method for dom ready events
+ function detach() {
+ if ( w3c ) {
+ doc[REMOVEEVENTLISTENER]( DOMCONTENTLOADED, completed, FALSE );
+ win[REMOVEEVENTLISTENER]( LOAD, completed, FALSE );
+ } else {
+ doc[DETACHEVENT]( ONREADYSTATECHANGE, completed );
+ win[DETACHEVENT]( ONLOAD, completed );
+ }
+ }
+
+ // Defers a function, scheduling it to run after the current call stack has cleared.
+ function defer( fn, wait ) {
+ // Allow 0 to be passed
+ setTimeout( fn, +wait >= 0 ? wait : 1 );
+ }
+
+ // Attach the listeners:
+
+ // Catch cases where onDomReady is called after the browser event has already occurred.
+ // we once tried to use readyState "interactive" here, but it caused issues like the one
+ // discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15
+ if ( doc[READYSTATE] === COMPLETE ) {
+ // Handle it asynchronously to allow scripts the opportunity to delay ready
+ defer( ready );
+
+ // Standards-based browsers support DOMContentLoaded
+ } else if ( w3c ) {
+ // Use the handy event callback
+ doc[ADDEVENTLISTENER]( DOMCONTENTLOADED, completed, FALSE );
+
+ // A fallback to window.onload, that will always work
+ win[ADDEVENTLISTENER]( LOAD, completed, FALSE );
+
+ // If IE event model is used
+ } else {
+ // Ensure firing before onload, maybe late but safe also for iframes
+ doc[ATTACHEVENT]( ONREADYSTATECHANGE, completed );
+
+ // A fallback to window.onload, that will always work
+ win[ATTACHEVENT]( ONLOAD, completed );
+
+ // If IE and not a frame
+ // continually check to see if the document is ready
+ try {
+ _top = win.frameElement == null && docElem;
+ } catch(e) {}
+
+ if ( _top && _top.doScroll ) {
+ (function doScrollCheck() {
+ if ( !isReady ) {
+ try {
+ // Use the trick by Diego Perini
+ // http://javascript.nwbox.com/IEContentLoaded/
+ _top.doScroll("left");
+ } catch(e) {
+ return defer( doScrollCheck, 50 );
+ }
+
+ // detach all dom ready events
+ detach();
+
+ // and execute any waiting functions
+ ready();
+ }
+ })();
+ }
+ }
+
+ function onDomReady( fn ) {
+ // If DOM is ready, execute the function (async), otherwise wait
+ isReady ? defer( fn ) : callbacks.push( fn );
+ }
+
+ // Add version
+ onDomReady.version = "1.4.0";
+ // Add method to check if DOM is ready
+ onDomReady.isReady = function(){
+ return isReady;
+ };
+
+ return onDomReady;
+ }
+
+ module.exports = typeof window !== "undefined" && _onDomReady(window);
+
+/***/ }),
+/* 3 */
+/***/ (function(module, exports, __webpack_require__) {
+
+ //Modified version of component/querystring
+ //Changes: updated dependencies, dot notation parsing, JSHint fixes
+ //Fork at https://github.com/imsky/querystring
+
+ /**
+ * Module dependencies.
+ */
+
+ var encode = encodeURIComponent;
+ var decode = decodeURIComponent;
+ var trim = __webpack_require__(4);
+ var type = __webpack_require__(5);
+
+ var arrayRegex = /(\w+)\[(\d+)\]/;
+ var objectRegex = /\w+\.\w+/;
+
+ /**
+ * Parse the given query `str`.
+ *
+ * @param {String} str
+ * @return {Object}
+ * @api public
+ */
+
+ exports.parse = function(str){
+ if ('string' !== typeof str) return {};
+
+ str = trim(str);
+ if ('' === str) return {};
+ if ('?' === str.charAt(0)) str = str.slice(1);
+
+ var obj = {};
+ var pairs = str.split('&');
+ for (var i = 0; i < pairs.length; i++) {
+ var parts = pairs[i].split('=');
+ var key = decode(parts[0]);
+ var m, ctx, prop;
+
+ if (m = arrayRegex.exec(key)) {
+ obj[m[1]] = obj[m[1]] || [];
+ obj[m[1]][m[2]] = decode(parts[1]);
+ continue;
+ }
+
+ if (m = objectRegex.test(key)) {
+ m = key.split('.');
+ ctx = obj;
+
+ while (m.length) {
+ prop = m.shift();
+
+ if (!prop.length) continue;
+
+ if (!ctx[prop]) {
+ ctx[prop] = {};
+ } else if (ctx[prop] && typeof ctx[prop] !== 'object') {
+ break;
+ }
+
+ if (!m.length) {
+ ctx[prop] = decode(parts[1]);
+ }
+
+ ctx = ctx[prop];
+ }
+
+ continue;
+ }
+
+ obj[parts[0]] = null == parts[1] ? '' : decode(parts[1]);
+ }
+
+ return obj;
+ };
+
+ /**
+ * Stringify the given `obj`.
+ *
+ * @param {Object} obj
+ * @return {String}
+ * @api public
+ */
+
+ exports.stringify = function(obj){
+ if (!obj) return '';
+ var pairs = [];
+
+ for (var key in obj) {
+ var value = obj[key];
+
+ if ('array' == type(value)) {
+ for (var i = 0; i < value.length; ++i) {
+ pairs.push(encode(key + '[' + i + ']') + '=' + encode(value[i]));
+ }
+ continue;
+ }
+
+ pairs.push(encode(key) + '=' + encode(obj[key]));
+ }
+
+ return pairs.join('&');
+ };
+
+
+/***/ }),
+/* 4 */
+/***/ (function(module, exports) {
+
+
+ exports = module.exports = trim;
+
+ function trim(str){
+ return str.replace(/^\s*|\s*$/g, '');
+ }
+
+ exports.left = function(str){
+ return str.replace(/^\s*/, '');
+ };
+
+ exports.right = function(str){
+ return str.replace(/\s*$/, '');
+ };
+
+
+/***/ }),
+/* 5 */
+/***/ (function(module, exports) {
+
+ /**
+ * toString ref.
+ */
+
+ var toString = Object.prototype.toString;
+
+ /**
+ * Return the type of `val`.
+ *
+ * @param {Mixed} val
+ * @return {String}
+ * @api public
+ */
+
+ module.exports = function(val){
+ switch (toString.call(val)) {
+ case '[object Date]': return 'date';
+ case '[object RegExp]': return 'regexp';
+ case '[object Arguments]': return 'arguments';
+ case '[object Array]': return 'array';
+ case '[object Error]': return 'error';
+ }
+
+ if (val === null) return 'null';
+ if (val === undefined) return 'undefined';
+ if (val !== val) return 'nan';
+ if (val && val.nodeType === 1) return 'element';
+
+ if (isBuffer(val)) return 'buffer';
+
+ val = val.valueOf
+ ? val.valueOf()
+ : Object.prototype.valueOf.apply(val);
+
+ return typeof val;
+ };
+
+ // code borrowed from https://github.com/feross/is-buffer/blob/master/index.js
+ function isBuffer(obj) {
+ return !!(obj != null &&
+ (obj._isBuffer || // For Safari 5-7 (missing Object.prototype.constructor)
+ (obj.constructor &&
+ typeof obj.constructor.isBuffer === 'function' &&
+ obj.constructor.isBuffer(obj))
+ ))
+ }
+
+
+/***/ }),
+/* 6 */
+/***/ (function(module, exports) {
+
+ var SceneGraph = function(sceneProperties) {
+ var nodeCount = 1;
+
+ //todo: move merge to helpers section
+ function merge(parent, child) {
+ for (var prop in child) {
+ parent[prop] = child[prop];
+ }
+ return parent;
+ }
+
+ var SceneNode = function(name) {
+ nodeCount++;
+ this.parent = null;
+ this.children = {};
+ this.id = nodeCount;
+ this.name = 'n' + nodeCount;
+ if (typeof name !== 'undefined') {
+ this.name = name;
+ }
+ this.x = this.y = this.z = 0;
+ this.width = this.height = 0;
+ };
+
+ SceneNode.prototype.resize = function(width, height) {
+ if (width != null) {
+ this.width = width;
+ }
+ if (height != null) {
+ this.height = height;
+ }
+ };
+
+ SceneNode.prototype.moveTo = function(x, y, z) {
+ this.x = x != null ? x : this.x;
+ this.y = y != null ? y : this.y;
+ this.z = z != null ? z : this.z;
+ };
+
+ SceneNode.prototype.add = function(child) {
+ var name = child.name;
+ if (typeof this.children[name] === 'undefined') {
+ this.children[name] = child;
+ child.parent = this;
+ } else {
+ throw 'SceneGraph: child already exists: ' + name;
+ }
+ };
+
+ var RootNode = function() {
+ SceneNode.call(this, 'root');
+ this.properties = sceneProperties;
+ };
+
+ RootNode.prototype = new SceneNode();
+
+ var Shape = function(name, props) {
+ SceneNode.call(this, name);
+ this.properties = {
+ 'fill': '#000000'
+ };
+ if (typeof props !== 'undefined') {
+ merge(this.properties, props);
+ } else if (typeof name !== 'undefined' && typeof name !== 'string') {
+ throw 'SceneGraph: invalid node name';
+ }
+ };
+
+ Shape.prototype = new SceneNode();
+
+ var Group = function() {
+ Shape.apply(this, arguments);
+ this.type = 'group';
+ };
+
+ Group.prototype = new Shape();
+
+ var Rect = function() {
+ Shape.apply(this, arguments);
+ this.type = 'rect';
+ };
+
+ Rect.prototype = new Shape();
+
+ var Text = function(text) {
+ Shape.call(this);
+ this.type = 'text';
+ this.properties.text = text;
+ };
+
+ Text.prototype = new Shape();
+
+ var root = new RootNode();
+
+ this.Shape = {
+ 'Rect': Rect,
+ 'Text': Text,
+ 'Group': Group
+ };
+
+ this.root = root;
+ return this;
+ };
+
+ module.exports = SceneGraph;
+
+
+/***/ }),
+/* 7 */
+/***/ (function(module, exports) {
+
+ /* WEBPACK VAR INJECTION */(function(global) {/**
+ * Shallow object clone and merge
+ *
+ * @param a Object A
+ * @param b Object B
+ * @returns {Object} New object with all of A's properties, and all of B's properties, overwriting A's properties
+ */
+ exports.extend = function(a, b) {
+ var c = {};
+ for (var x in a) {
+ if (a.hasOwnProperty(x)) {
+ c[x] = a[x];
+ }
+ }
+ if (b != null) {
+ for (var y in b) {
+ if (b.hasOwnProperty(y)) {
+ c[y] = b[y];
+ }
+ }
+ }
+ return c;
+ };
+
+ /**
+ * Takes a k/v list of CSS properties and returns a rule
+ *
+ * @param props CSS properties object
+ */
+ exports.cssProps = function(props) {
+ var ret = [];
+ for (var p in props) {
+ if (props.hasOwnProperty(p)) {
+ ret.push(p + ':' + props[p]);
+ }
+ }
+ return ret.join(';');
+ };
+
+ /**
+ * Encodes HTML entities in a string
+ *
+ * @param str Input string
+ */
+ exports.encodeHtmlEntity = function(str) {
+ var buf = [];
+ var charCode = 0;
+ for (var i = str.length - 1; i >= 0; i--) {
+ charCode = str.charCodeAt(i);
+ if (charCode > 128) {
+ buf.unshift(['&#', charCode, ';'].join(''));
+ } else {
+ buf.unshift(str[i]);
+ }
+ }
+ return buf.join('');
+ };
+
+ /**
+ * Checks if an image exists
+ *
+ * @param src URL of image
+ * @param callback Callback to call once image status has been found
+ */
+ exports.imageExists = function(src, callback) {
+ var image = new Image();
+ image.onerror = function() {
+ callback.call(this, false);
+ };
+ image.onload = function() {
+ callback.call(this, true);
+ };
+ image.src = src;
+ };
+
+ /**
+ * Decodes HTML entities in a string
+ *
+ * @param str Input string
+ */
+ exports.decodeHtmlEntity = function(str) {
+ return str.replace(/&#(\d+);/g, function(match, dec) {
+ return String.fromCharCode(dec);
+ });
+ };
+
+
+ /**
+ * Returns an element's dimensions if it's visible, `false` otherwise.
+ *
+ * @param el DOM element
+ */
+ exports.dimensionCheck = function(el) {
+ var dimensions = {
+ height: el.clientHeight,
+ width: el.clientWidth
+ };
+
+ if (dimensions.height && dimensions.width) {
+ return dimensions;
+ } else {
+ return false;
+ }
+ };
+
+
+ /**
+ * Returns true if value is truthy or if it is "semantically truthy"
+ * @param val
+ */
+ exports.truthy = function(val) {
+ if (typeof val === 'string') {
+ return val === 'true' || val === 'yes' || val === '1' || val === 'on' || val === '✓';
+ }
+ return !!val;
+ };
+
+ /**
+ * Parses input into a well-formed CSS color
+ * @param val
+ */
+ exports.parseColor = function(val) {
+ var hexre = /(^(?:#?)[0-9a-f]{6}$)|(^(?:#?)[0-9a-f]{3}$)/i;
+ var rgbre = /^rgb\((\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/;
+ var rgbare = /^rgba\((\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(0\.\d{1,}|1)\)$/;
+
+ var match = val.match(hexre);
+ var retval;
+
+ if (match !== null) {
+ retval = match[1] || match[2];
+ if (retval[0] !== '#') {
+ return '#' + retval;
+ } else {
+ return retval;
+ }
+ }
+
+ match = val.match(rgbre);
+
+ if (match !== null) {
+ retval = 'rgb(' + match.slice(1).join(',') + ')';
+ return retval;
+ }
+
+ match = val.match(rgbare);
+
+ if (match !== null) {
+ retval = 'rgba(' + match.slice(1).join(',') + ')';
+ return retval;
+ }
+
+ return null;
+ };
+
+ /**
+ * Provides the correct scaling ratio for canvas drawing operations on HiDPI screens (e.g. Retina displays)
+ */
+ exports.canvasRatio = function () {
+ var devicePixelRatio = 1;
+ var backingStoreRatio = 1;
+
+ if (global.document) {
+ var canvas = global.document.createElement('canvas');
+ if (canvas.getContext) {
+ var ctx = canvas.getContext('2d');
+ devicePixelRatio = global.devicePixelRatio || 1;
+ backingStoreRatio = ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1;
+ }
+ }
+
+ return devicePixelRatio / backingStoreRatio;
+ };
+ /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }())))
+
+/***/ }),
+/* 8 */
+/***/ (function(module, exports, __webpack_require__) {
+
+ /* WEBPACK VAR INJECTION */(function(global) {var DOM = __webpack_require__(9);
+
+ var SVG_NS = 'http://www.w3.org/2000/svg';
+ var NODE_TYPE_COMMENT = 8;
+
+ /**
+ * Generic SVG element creation function
+ *
+ * @param svg SVG context, set to null if new
+ * @param width Document width
+ * @param height Document height
+ */
+ exports.initSVG = function(svg, width, height) {
+ var defs, style, initialize = false;
+
+ if (svg && svg.querySelector) {
+ style = svg.querySelector('style');
+ if (style === null) {
+ initialize = true;
+ }
+ } else {
+ svg = DOM.newEl('svg', SVG_NS);
+ initialize = true;
+ }
+
+ if (initialize) {
+ defs = DOM.newEl('defs', SVG_NS);
+ style = DOM.newEl('style', SVG_NS);
+ DOM.setAttr(style, {
+ 'type': 'text/css'
+ });
+ defs.appendChild(style);
+ svg.appendChild(defs);
+ }
+
+ //IE throws an exception if this is set and Chrome requires it to be set
+ if (svg.webkitMatchesSelector) {
+ svg.setAttribute('xmlns', SVG_NS);
+ }
+
+ //Remove comment nodes
+ for (var i = 0; i < svg.childNodes.length; i++) {
+ if (svg.childNodes[i].nodeType === NODE_TYPE_COMMENT) {
+ svg.removeChild(svg.childNodes[i]);
+ }
+ }
+
+ //Remove CSS
+ while (style.childNodes.length) {
+ style.removeChild(style.childNodes[0]);
+ }
+
+ DOM.setAttr(svg, {
+ 'width': width,
+ 'height': height,
+ 'viewBox': '0 0 ' + width + ' ' + height,
+ 'preserveAspectRatio': 'none'
+ });
+
+ return svg;
+ };
+
+ /**
+ * Converts serialized SVG to a string suitable for data URI use
+ * @param svgString Serialized SVG string
+ * @param [base64] Use base64 encoding for data URI
+ */
+ exports.svgStringToDataURI = function() {
+ var rawPrefix = 'data:image/svg+xml;charset=UTF-8,';
+ var base64Prefix = 'data:image/svg+xml;charset=UTF-8;base64,';
+
+ return function(svgString, base64) {
+ if (base64) {
+ return base64Prefix + btoa(global.unescape(encodeURIComponent(svgString)));
+ } else {
+ return rawPrefix + encodeURIComponent(svgString);
+ }
+ };
+ }();
+
+ /**
+ * Returns serialized SVG with XML processing instructions
+ *
+ * @param svg SVG context
+ * @param stylesheets CSS stylesheets to include
+ */
+ exports.serializeSVG = function(svg, engineSettings) {
+ if (!global.XMLSerializer) return;
+ var serializer = new XMLSerializer();
+ var svgCSS = '';
+ var stylesheets = engineSettings.stylesheets;
+
+ //External stylesheets: Processing Instruction method
+ if (engineSettings.svgXMLStylesheet) {
+ var xml = DOM.createXML();
+ //Add <?xml-stylesheet ?> directives
+ for (var i = stylesheets.length - 1; i >= 0; i--) {
+ var csspi = xml.createProcessingInstruction('xml-stylesheet', 'href="' + stylesheets[i] + '" rel="stylesheet"');
+ xml.insertBefore(csspi, xml.firstChild);
+ }
+
+ xml.removeChild(xml.documentElement);
+ svgCSS = serializer.serializeToString(xml);
+ }
+
+ var svgText = serializer.serializeToString(svg);
+ svgText = svgText.replace(/\&amp;(\#[0-9]{2,}\;)/g, '&$1');
+ return svgCSS + svgText;
+ };
+
+ /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }())))
+
+/***/ }),
+/* 9 */
+/***/ (function(module, exports) {
+
+ /* WEBPACK VAR INJECTION */(function(global) {/**
+ * Generic new DOM element function
+ *
+ * @param tag Tag to create
+ * @param namespace Optional namespace value
+ */
+ exports.newEl = function(tag, namespace) {
+ if (!global.document) return;
+
+ if (namespace == null) {
+ return global.document.createElement(tag);
+ } else {
+ return global.document.createElementNS(namespace, tag);
+ }
+ };
+
+ /**
+ * Generic setAttribute function
+ *
+ * @param el Reference to DOM element
+ * @param attrs Object with attribute keys and values
+ */
+ exports.setAttr = function (el, attrs) {
+ for (var a in attrs) {
+ el.setAttribute(a, attrs[a]);
+ }
+ };
+
+ /**
+ * Creates a XML document
+ * @private
+ */
+ exports.createXML = function() {
+ if (!global.DOMParser) return;
+ return new DOMParser().parseFromString('<xml />', 'application/xml');
+ };
+
+ /**
+ * Converts a value into an array of DOM nodes
+ *
+ * @param val A string, a NodeList, a Node, or an HTMLCollection
+ */
+ exports.getNodeArray = function(val) {
+ var retval = null;
+ if (typeof(val) == 'string') {
+ retval = document.querySelectorAll(val);
+ } else if (global.NodeList && val instanceof global.NodeList) {
+ retval = val;
+ } else if (global.Node && val instanceof global.Node) {
+ retval = [val];
+ } else if (global.HTMLCollection && val instanceof global.HTMLCollection) {
+ retval = val;
+ } else if (val instanceof Array) {
+ retval = val;
+ } else if (val === null) {
+ retval = [];
+ }
+
+ retval = Array.prototype.slice.call(retval);
+
+ return retval;
+ };
+
+ /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }())))
+
+/***/ }),
+/* 10 */
+/***/ (function(module, exports) {
+
+ var Color = function(color, options) {
+ //todo: support rgba, hsla, and rrggbbaa notation
+ //todo: use CIELAB internally
+ //todo: add clamp function (with sign)
+ if (typeof color !== 'string') return;
+
+ this.original = color;
+
+ if (color.charAt(0) === '#') {
+ color = color.slice(1);
+ }
+
+ if (/[^a-f0-9]+/i.test(color)) return;
+
+ if (color.length === 3) {
+ color = color.replace(/./g, '$&$&');
+ }
+
+ if (color.length !== 6) return;
+
+ this.alpha = 1;
+
+ if (options && options.alpha) {
+ this.alpha = options.alpha;
+ }
+
+ this.set(parseInt(color, 16));
+ };
+
+ //todo: jsdocs
+ Color.rgb2hex = function(r, g, b) {
+ function format (decimal) {
+ var hex = (decimal | 0).toString(16);
+ if (decimal < 16) {
+ hex = '0' + hex;
+ }
+ return hex;
+ }
+
+ return [r, g, b].map(format).join('');
+ };
+
+ //todo: jsdocs
+ Color.hsl2rgb = function (h, s, l) {
+ var H = h / 60;
+ var C = (1 - Math.abs(2 * l - 1)) * s;
+ var X = C * (1 - Math.abs(parseInt(H) % 2 - 1));
+ var m = l - (C / 2);
+
+ var r = 0, g = 0, b = 0;
+
+ if (H >= 0 && H < 1) {
+ r = C;
+ g = X;
+ } else if (H >= 1 && H < 2) {
+ r = X;
+ g = C;
+ } else if (H >= 2 && H < 3) {
+ g = C;
+ b = X;
+ } else if (H >= 3 && H < 4) {
+ g = X;
+ b = C;
+ } else if (H >= 4 && H < 5) {
+ r = X;
+ b = C;
+ } else if (H >= 5 && H < 6) {
+ r = C;
+ b = X;
+ }
+
+ r += m;
+ g += m;
+ b += m;
+
+ r = parseInt(r * 255);
+ g = parseInt(g * 255);
+ b = parseInt(b * 255);
+
+ return [r, g, b];
+ };
+
+ /**
+ * Sets the color from a raw RGB888 integer
+ * @param raw RGB888 representation of color
+ */
+ //todo: refactor into a static method
+ //todo: factor out individual color spaces
+ //todo: add HSL, CIELAB, and CIELUV
+ Color.prototype.set = function (val) {
+ this.raw = val;
+
+ var r = (this.raw & 0xFF0000) >> 16;
+ var g = (this.raw & 0x00FF00) >> 8;
+ var b = (this.raw & 0x0000FF);
+
+ // BT.709
+ var y = 0.2126 * r + 0.7152 * g + 0.0722 * b;
+ var u = -0.09991 * r - 0.33609 * g + 0.436 * b;
+ var v = 0.615 * r - 0.55861 * g - 0.05639 * b;
+
+ this.rgb = {
+ r: r,
+ g: g,
+ b: b
+ };
+
+ this.yuv = {
+ y: y,
+ u: u,
+ v: v
+ };
+
+ return this;
+ };
+
+ /**
+ * Lighten or darken a color
+ * @param multiplier Amount to lighten or darken (-1 to 1)
+ */
+ Color.prototype.lighten = function(multiplier) {
+ var cm = Math.min(1, Math.max(0, Math.abs(multiplier))) * (multiplier < 0 ? -1 : 1);
+ var bm = (255 * cm) | 0;
+ var cr = Math.min(255, Math.max(0, this.rgb.r + bm));
+ var cg = Math.min(255, Math.max(0, this.rgb.g + bm));
+ var cb = Math.min(255, Math.max(0, this.rgb.b + bm));
+ var hex = Color.rgb2hex(cr, cg, cb);
+ return new Color(hex);
+ };
+
+ /**
+ * Output color in hex format
+ * @param addHash Add a hash character to the beginning of the output
+ */
+ Color.prototype.toHex = function(addHash) {
+ return (addHash ? '#' : '') + this.raw.toString(16);
+ };
+
+ /**
+ * Returns whether or not current color is lighter than another color
+ * @param color Color to compare against
+ */
+ Color.prototype.lighterThan = function(color) {
+ if (!(color instanceof Color)) {
+ color = new Color(color);
+ }
+
+ return this.yuv.y > color.yuv.y;
+ };
+
+ /**
+ * Returns the result of mixing current color with another color
+ * @param color Color to mix with
+ * @param multiplier How much to mix with the other color
+ */
+ /*
+ Color.prototype.mix = function (color, multiplier) {
+ if (!(color instanceof Color)) {
+ color = new Color(color);
+ }
+
+ var r = this.rgb.r;
+ var g = this.rgb.g;
+ var b = this.rgb.b;
+ var a = this.alpha;
+
+ var m = typeof multiplier !== 'undefined' ? multiplier : 0.5;
+
+ //todo: write a lerp function
+ r = r + m * (color.rgb.r - r);
+ g = g + m * (color.rgb.g - g);
+ b = b + m * (color.rgb.b - b);
+ a = a + m * (color.alpha - a);
+
+ return new Color(Color.rgbToHex(r, g, b), {
+ 'alpha': a
+ });
+ };
+ */
+
+ /**
+ * Returns the result of blending another color on top of current color with alpha
+ * @param color Color to blend on top of current color, i.e. "Ca"
+ */
+ //todo: see if .blendAlpha can be merged into .mix
+ Color.prototype.blendAlpha = function(color) {
+ if (!(color instanceof Color)) {
+ color = new Color(color);
+ }
+
+ var Ca = color;
+ var Cb = this;
+
+ //todo: write alpha blending function
+ var r = Ca.alpha * Ca.rgb.r + (1 - Ca.alpha) * Cb.rgb.r;
+ var g = Ca.alpha * Ca.rgb.g + (1 - Ca.alpha) * Cb.rgb.g;
+ var b = Ca.alpha * Ca.rgb.b + (1 - Ca.alpha) * Cb.rgb.b;
+
+ return new Color(Color.rgb2hex(r, g, b));
+ };
+
+ module.exports = Color;
+
+
+/***/ }),
+/* 11 */
+/***/ (function(module, exports) {
+
+ module.exports = {
+ 'version': '2.9.6',
+ 'svg_ns': 'http://www.w3.org/2000/svg'
+ };
+
+/***/ }),
+/* 12 */
+/***/ (function(module, exports, __webpack_require__) {
+
+ var shaven = __webpack_require__(13);
+
+ var SVG = __webpack_require__(8);
+ var constants = __webpack_require__(11);
+ var utils = __webpack_require__(7);
+
+ var SVG_NS = constants.svg_ns;
+
+ var templates = {
+ 'element': function (options) {
+ var tag = options.tag;
+ var content = options.content || '';
+ delete options.tag;
+ delete options.content;
+ return [tag, content, options];
+ }
+ };
+
+ //todo: deprecate tag arg, infer tag from shape object
+ function convertShape (shape, tag) {
+ return templates.element({
+ 'tag': tag,
+ 'width': shape.width,
+ 'height': shape.height,
+ 'fill': shape.properties.fill
+ });
+ }
+
+ function textCss (properties) {
+ return utils.cssProps({
+ 'fill': properties.fill,
+ 'font-weight': properties.font.weight,
+ 'font-family': properties.font.family + ', monospace',
+ 'font-size': properties.font.size + properties.font.units
+ });
+ }
+
+ function outlinePath (bgWidth, bgHeight, outlineWidth) {
+ var outlineOffsetWidth = outlineWidth / 2;
+
+ return [
+ 'M', outlineOffsetWidth, outlineOffsetWidth,
+ 'H', bgWidth - outlineOffsetWidth,
+ 'V', bgHeight - outlineOffsetWidth,
+ 'H', outlineOffsetWidth,
+ 'V', 0,
+ 'M', 0, outlineOffsetWidth,
+ 'L', bgWidth, bgHeight - outlineOffsetWidth,
+ 'M', 0, bgHeight - outlineOffsetWidth,
+ 'L', bgWidth, outlineOffsetWidth
+ ].join(' ');
+ }
+
+ module.exports = function (sceneGraph, renderSettings) {
+ var engineSettings = renderSettings.engineSettings;
+ var stylesheets = engineSettings.stylesheets;
+ var stylesheetXml = stylesheets.map(function (stylesheet) {
+ return '<?xml-stylesheet rel="stylesheet" href="' + stylesheet + '"?>';
+ }).join('\n');
+
+ var holderId = 'holder_' + Number(new Date()).toString(16);
+
+ var root = sceneGraph.root;
+ var textGroup = root.children.holderTextGroup;
+
+ var css = '#' + holderId + ' text { ' + textCss(textGroup.properties) + ' } ';
+
+ // push text down to be equally vertically aligned with canvas renderer
+ textGroup.y += textGroup.textPositionData.boundingBox.height * 0.8;
+
+ var wordTags = [];
+
+ Object.keys(textGroup.children).forEach(function (lineKey) {
+ var line = textGroup.children[lineKey];
+
+ Object.keys(line.children).forEach(function (wordKey) {
+ var word = line.children[wordKey];
+ var x = textGroup.x + line.x + word.x;
+ var y = textGroup.y + line.y + word.y;
+ var wordTag = templates.element({
+ 'tag': 'text',
+ 'content': word.properties.text,
+ 'x': x,
+ 'y': y
+ });
+
+ wordTags.push(wordTag);
+ });
+ });
+
+ var text = templates.element({
+ 'tag': 'g',
+ 'content': wordTags
+ });
+
+ var outline = null;
+
+ if (root.children.holderBg.properties.outline) {
+ var outlineProperties = root.children.holderBg.properties.outline;
+ outline = templates.element({
+ 'tag': 'path',
+ 'd': outlinePath(root.children.holderBg.width, root.children.holderBg.height, outlineProperties.width),
+ 'stroke-width': outlineProperties.width,
+ 'stroke': outlineProperties.fill,
+ 'fill': 'none'
+ });
+ }
+
+ var bg = convertShape(root.children.holderBg, 'rect');
+
+ var sceneContent = [];
+
+ sceneContent.push(bg);
+ if (outlineProperties) {
+ sceneContent.push(outline);
+ }
+ sceneContent.push(text);
+
+ var scene = templates.element({
+ 'tag': 'g',
+ 'id': holderId,
+ 'content': sceneContent
+ });
+
+ var style = templates.element({
+ 'tag': 'style',
+ //todo: figure out how to add CDATA directive
+ 'content': css,
+ 'type': 'text/css'
+ });
+
+ var defs = templates.element({
+ 'tag': 'defs',
+ 'content': style
+ });
+
+ var svg = templates.element({
+ 'tag': 'svg',
+ 'content': [defs, scene],
+ 'width': root.properties.width,
+ 'height': root.properties.height,
+ 'xmlns': SVG_NS,
+ 'viewBox': [0, 0, root.properties.width, root.properties.height].join(' '),
+ 'preserveAspectRatio': 'none'
+ });
+
+ var output = shaven(svg);
+
+ if (/\&amp;(x)?#[0-9A-Fa-f]/.test(output[0])) {
+ output[0] = output[0].replace(/&amp;#/gm, '&#');
+ }
+
+ output = stylesheetXml + output[0];
+
+ var svgString = SVG.svgStringToDataURI(output, renderSettings.mode === 'background');
+ return svgString;
+ };
+
+/***/ }),
+/* 13 */
+/***/ (function(module, exports, __webpack_require__) {
+
+ var escape = __webpack_require__(14)
+
+ // TODO: remove namespace
+
+ module.exports = function shaven (array, namespace, returnObject) {
+
+ 'use strict'
+
+ var i = 1
+ var doesEscape = true
+ var HTMLString
+ var attributeKey
+ var callback
+ var key
+
+
+ returnObject = returnObject || {}
+
+
+ function createElement (sugarString) {
+
+ var tags = sugarString.match(/^[\w-]+/)
+ var element = {
+ tag: tags ? tags[0] : 'div',
+ attr: {},
+ children: []
+ }
+ var id = sugarString.match(/#([\w-]+)/)
+ var reference = sugarString.match(/\$([\w-]+)/)
+ var classNames = sugarString.match(/\.[\w-]+/g)
+
+
+ // Assign id if is set
+ if (id) {
+ element.attr.id = id[1]
+
+ // Add element to the return object
+ returnObject[id[1]] = element
+ }
+
+ if (reference)
+ returnObject[reference[1]] = element
+
+ if (classNames)
+ element.attr.class = classNames.join(' ').replace(/\./g, '')
+
+ if (sugarString.match(/&$/g))
+ doesEscape = false
+
+ return element
+ }
+
+ function replacer (key, value) {
+
+ if (value === null || value === false || value === undefined)
+ return
+
+ if (typeof value !== 'string' && typeof value !== 'object')
+ return String(value)
+
+ return value
+ }
+
+ function escapeAttribute (string) {
+ return (string || string === 0) ?
+ String(string)
+ .replace(/&/g, '&amp;')
+ .replace(/"/g, '&quot;') :
+ ''
+ }
+
+ function escapeHTML (string) {
+ return String(string)
+ .replace(/&/g, '&amp;')
+ .replace(/"/g, '&quot;')
+ .replace(/'/g, '&apos;')
+ .replace(/</g, '&lt;')
+ .replace(/>/g, '&gt;')
+ }
+
+
+ if (typeof array[0] === 'string')
+ array[0] = createElement(array[0])
+
+ else if (Array.isArray(array[0]))
+ i = 0
+
+ else
+ throw new Error(
+ 'First element of array must be a string, ' +
+ 'or an array and not ' + JSON.stringify(array[0])
+ )
+
+
+ for (; i < array.length; i++) {
+
+ // Don't render element if value is false or null
+ if (array[i] === false || array[i] === null) {
+ array[0] = false
+ break
+ }
+
+ // Continue with next array value if current value is undefined or true
+ else if (array[i] === undefined || array[i] === true) {
+ continue
+ }
+
+ else if (typeof array[i] === 'string') {
+ if (doesEscape)
+ array[i] = escapeHTML(array[i])
+
+ array[0].children.push(array[i])
+ }
+
+ else if (typeof array[i] === 'number') {
+
+ array[0].children.push(array[i])
+ }
+
+ else if (Array.isArray(array[i])) {
+
+ if (Array.isArray(array[i][0])) {
+ array[i].reverse().forEach(function (subArray) {
+ array.splice(i + 1, 0, subArray)
+ })
+
+ if (i !== 0)
+ continue
+ i++
+ }
+
+ shaven(array[i], namespace, returnObject)
+
+ if (array[i][0])
+ array[0].children.push(array[i][0])
+ }
+
+ else if (typeof array[i] === 'function')
+ callback = array[i]
+
+
+ else if (typeof array[i] === 'object') {
+ for (attributeKey in array[i])
+ if (array[i].hasOwnProperty(attributeKey))
+ if (array[i][attributeKey] !== null &&
+ array[i][attributeKey] !== false)
+ if (attributeKey === 'style' &&
+ typeof array[i][attributeKey] === 'object')
+ array[0].attr[attributeKey] = JSON
+ .stringify(array[i][attributeKey], replacer)
+ .slice(2, -2)
+ .replace(/","/g, ';')
+ .replace(/":"/g, ':')
+ .replace(/\\"/g, '\'')
+
+ else
+ array[0].attr[attributeKey] = array[i][attributeKey]
+ }
+
+ else
+ throw new TypeError('"' + array[i] + '" is not allowed as a value.')
+ }
+
+
+ if (array[0] !== false) {
+
+ HTMLString = '<' + array[0].tag
+
+ for (key in array[0].attr)
+ if (array[0].attr.hasOwnProperty(key))
+ HTMLString += ' ' + key + '="' +
+ escapeAttribute(array[0].attr[key]) + '"'
+
+ HTMLString += '>'
+
+ array[0].children.forEach(function (child) {
+ HTMLString += child
+ })
+
+ HTMLString += '</' + array[0].tag + '>'
+
+ array[0] = HTMLString
+ }
+
+ // Return root element on index 0
+ returnObject[0] = array[0]
+
+ if (callback)
+ callback(array[0])
+
+ // returns object containing all elements with an id and the root element
+ return returnObject
+ }
+
+
+/***/ }),
+/* 14 */
+/***/ (function(module, exports) {
+
+ /*!
+ * escape-html
+ * Copyright(c) 2012-2013 TJ Holowaychuk
+ * Copyright(c) 2015 Andreas Lubbe
+ * Copyright(c) 2015 Tiancheng "Timothy" Gu
+ * MIT Licensed
+ */
+
+ 'use strict';
+
+ /**
+ * Module variables.
+ * @private
+ */
+
+ var matchHtmlRegExp = /["'&<>]/;
+
+ /**
+ * Module exports.
+ * @public
+ */
+
+ module.exports = escapeHtml;
+
+ /**
+ * Escape special characters in the given string of html.
+ *
+ * @param {string} string The string to escape for inserting into HTML
+ * @return {string}
+ * @public
+ */
+
+ function escapeHtml(string) {
+ var str = '' + string;
+ var match = matchHtmlRegExp.exec(str);
+
+ if (!match) {
+ return str;
+ }
+
+ var escape;
+ var html = '';
+ var index = 0;
+ var lastIndex = 0;
+
+ for (index = match.index; index < str.length; index++) {
+ switch (str.charCodeAt(index)) {
+ case 34: // "
+ escape = '&quot;';
+ break;
+ case 38: // &
+ escape = '&amp;';
+ break;
+ case 39: // '
+ escape = '&#39;';
+ break;
+ case 60: // <
+ escape = '&lt;';
+ break;
+ case 62: // >
+ escape = '&gt;';
+ break;
+ default:
+ continue;
+ }
+
+ if (lastIndex !== index) {
+ html += str.substring(lastIndex, index);
+ }
+
+ lastIndex = index + 1;
+ html += escape;
+ }
+
+ return lastIndex !== index
+ ? html + str.substring(lastIndex, index)
+ : html;
+ }
+
+
+/***/ }),
+/* 15 */
+/***/ (function(module, exports, __webpack_require__) {
+
+ var DOM = __webpack_require__(9);
+ var utils = __webpack_require__(7);
+
+ module.exports = (function() {
+ var canvas = DOM.newEl('canvas');
+ var ctx = null;
+
+ return function(sceneGraph) {
+ if (ctx == null) {
+ ctx = canvas.getContext('2d');
+ }
+
+ var dpr = utils.canvasRatio();
+ var root = sceneGraph.root;
+ canvas.width = dpr * root.properties.width;
+ canvas.height = dpr * root.properties.height ;
+ ctx.textBaseline = 'middle';
+
+ var bg = root.children.holderBg;
+ var bgWidth = dpr * bg.width;
+ var bgHeight = dpr * bg.height;
+ //todo: parametrize outline width (e.g. in scene object)
+ var outlineWidth = 2;
+ var outlineOffsetWidth = outlineWidth / 2;
+
+ ctx.fillStyle = bg.properties.fill;
+ ctx.fillRect(0, 0, bgWidth, bgHeight);
+
+ if (bg.properties.outline) {
+ //todo: abstract this into a method
+ ctx.strokeStyle = bg.properties.outline.fill;
+ ctx.lineWidth = bg.properties.outline.width;
+ ctx.moveTo(outlineOffsetWidth, outlineOffsetWidth);
+ // TL, TR, BR, BL
+ ctx.lineTo(bgWidth - outlineOffsetWidth, outlineOffsetWidth);
+ ctx.lineTo(bgWidth - outlineOffsetWidth, bgHeight - outlineOffsetWidth);
+ ctx.lineTo(outlineOffsetWidth, bgHeight - outlineOffsetWidth);
+ ctx.lineTo(outlineOffsetWidth, outlineOffsetWidth);
+ // Diagonals
+ ctx.moveTo(0, outlineOffsetWidth);
+ ctx.lineTo(bgWidth, bgHeight - outlineOffsetWidth);
+ ctx.moveTo(0, bgHeight - outlineOffsetWidth);
+ ctx.lineTo(bgWidth, outlineOffsetWidth);
+ ctx.stroke();
+ }
+
+ var textGroup = root.children.holderTextGroup;
+ ctx.font = textGroup.properties.font.weight + ' ' + (dpr * textGroup.properties.font.size) + textGroup.properties.font.units + ' ' + textGroup.properties.font.family + ', monospace';
+ ctx.fillStyle = textGroup.properties.fill;
+
+ for (var lineKey in textGroup.children) {
+ var line = textGroup.children[lineKey];
+ for (var wordKey in line.children) {
+ var word = line.children[wordKey];
+ var x = dpr * (textGroup.x + line.x + word.x);
+ var y = dpr * (textGroup.y + line.y + word.y + (textGroup.properties.leading / 2));
+
+ ctx.fillText(word.properties.text, x, y);
+ }
+ }
+
+ return canvas.toDataURL('image/png');
+ };
+ })();
+
+/***/ })
+/******/ ])
+});
+;
+(function(ctx, isMeteorPackage) {
+ if (isMeteorPackage) {
+ Holder = ctx.Holder;
+ }
+})(this, typeof Meteor !== 'undefined' && typeof Package !== 'undefined');
diff --git a/etc/js/imagesloaded.pkgd.js b/etc/js/imagesloaded.pkgd.js
new file mode 100644
index 00000000..ef23971b
--- /dev/null
+++ b/etc/js/imagesloaded.pkgd.js
@@ -0,0 +1,487 @@
+/*!
+ * imagesLoaded PACKAGED v4.1.0
+ * JavaScript is all like "You images are done yet or what?"
+ * MIT License
+ */
+
+/**
+ * EvEmitter v1.0.1
+ * Lil' event emitter
+ * MIT License
+ */
+
+/* jshint unused: true, undef: true, strict: true */
+
+( function( global, factory ) {
+ // universal module definition
+ /* jshint strict: false */ /* globals define, module */
+ if ( typeof define == 'function' && define.amd ) {
+ // AMD - RequireJS
+ define( 'ev-emitter/ev-emitter',factory );
+ } else if ( typeof module == 'object' && module.exports ) {
+ // CommonJS - Browserify, Webpack
+ module.exports = factory();
+ } else {
+ // Browser globals
+ global.EvEmitter = factory();
+ }
+
+}( this, function() {
+
+
+
+function EvEmitter() {}
+
+var proto = EvEmitter.prototype;
+
+proto.on = function( eventName, listener ) {
+ if ( !eventName || !listener ) {
+ return;
+ }
+ // set events hash
+ var events = this._events = this._events || {};
+ // set listeners array
+ var listeners = events[ eventName ] = events[ eventName ] || [];
+ // only add once
+ if ( listeners.indexOf( listener ) == -1 ) {
+ listeners.push( listener );
+ }
+
+ return this;
+};
+
+proto.once = function( eventName, listener ) {
+ if ( !eventName || !listener ) {
+ return;
+ }
+ // add event
+ this.on( eventName, listener );
+ // set once flag
+ // set onceEvents hash
+ var onceEvents = this._onceEvents = this._onceEvents || {};
+ // set onceListeners array
+ var onceListeners = onceEvents[ eventName ] = onceEvents[ eventName ] || [];
+ // set flag
+ onceListeners[ listener ] = true;
+
+ return this;
+};
+
+proto.off = function( eventName, listener ) {
+ var listeners = this._events && this._events[ eventName ];
+ if ( !listeners || !listeners.length ) {
+ return;
+ }
+ var index = listeners.indexOf( listener );
+ if ( index != -1 ) {
+ listeners.splice( index, 1 );
+ }
+
+ return this;
+};
+
+proto.emitEvent = function( eventName, args ) {
+ var listeners = this._events && this._events[ eventName ];
+ if ( !listeners || !listeners.length ) {
+ return;
+ }
+ var i = 0;
+ var listener = listeners[i];
+ args = args || [];
+ // once stuff
+ var onceListeners = this._onceEvents && this._onceEvents[ eventName ];
+
+ while ( listener ) {
+ var isOnce = onceListeners && onceListeners[ listener ];
+ if ( isOnce ) {
+ // remove listener
+ // remove before trigger to prevent recursion
+ this.off( eventName, listener );
+ // unset once flag
+ delete onceListeners[ listener ];
+ }
+ // trigger listener
+ listener.apply( this, args );
+ // get next listener
+ i += isOnce ? 0 : 1;
+ listener = listeners[i];
+ }
+
+ return this;
+};
+
+return EvEmitter;
+
+}));
+
+/*!
+ * imagesLoaded v4.1.0
+ * JavaScript is all like "You images are done yet or what?"
+ * MIT License
+ */
+
+( function( window, factory ) { 'use strict';
+ // universal module definition
+
+ /*global define: false, module: false, require: false */
+
+ if ( typeof define == 'function' && define.amd ) {
+ // AMD
+ define( [
+ 'ev-emitter/ev-emitter'
+ ], function( EvEmitter ) {
+ return factory( window, EvEmitter );
+ });
+ } else if ( typeof module == 'object' && module.exports ) {
+ // CommonJS
+ module.exports = factory(
+ window,
+ require('ev-emitter')
+ );
+ } else {
+ // browser global
+ window.imagesLoaded = factory(
+ window,
+ window.EvEmitter
+ );
+ }
+
+})( window,
+
+// -------------------------- factory -------------------------- //
+
+function factory( window, EvEmitter ) {
+
+
+
+var $ = window.jQuery;
+var console = window.console;
+
+// -------------------------- helpers -------------------------- //
+
+// extend objects
+function extend( a, b ) {
+ for ( var prop in b ) {
+ a[ prop ] = b[ prop ];
+ }
+ return a;
+}
+
+// turn element or nodeList into an array
+function makeArray( obj ) {
+ var ary = [];
+ if ( Array.isArray( obj ) ) {
+ // use object if already an array
+ ary = obj;
+ } else if ( typeof obj.length == 'number' ) {
+ // convert nodeList to array
+ for ( var i=0; i < obj.length; i++ ) {
+ ary.push( obj[i] );
+ }
+ } else {
+ // array of single index
+ ary.push( obj );
+ }
+ return ary;
+}
+
+// -------------------------- imagesLoaded -------------------------- //
+
+/**
+ * @param {Array, Element, NodeList, String} elem
+ * @param {Object or Function} options - if function, use as callback
+ * @param {Function} onAlways - callback function
+ */
+function ImagesLoaded( elem, options, onAlways ) {
+ // coerce ImagesLoaded() without new, to be new ImagesLoaded()
+ if ( !( this instanceof ImagesLoaded ) ) {
+ return new ImagesLoaded( elem, options, onAlways );
+ }
+ // use elem as selector string
+ if ( typeof elem == 'string' ) {
+ elem = document.querySelectorAll( elem );
+ }
+
+ this.elements = makeArray( elem );
+ this.options = extend( {}, this.options );
+
+ if ( typeof options == 'function' ) {
+ onAlways = options;
+ } else {
+ extend( this.options, options );
+ }
+
+ if ( onAlways ) {
+ this.on( 'always', onAlways );
+ }
+
+ this.getImages();
+
+ if ( $ ) {
+ // add jQuery Deferred object
+ this.jqDeferred = new $.Deferred();
+ }
+
+ // HACK check async to allow time to bind listeners
+ setTimeout( function() {
+ this.check();
+ }.bind( this ));
+}
+
+ImagesLoaded.prototype = Object.create( EvEmitter.prototype );
+
+ImagesLoaded.prototype.options = {};
+
+ImagesLoaded.prototype.getImages = function() {
+ this.images = [];
+
+ // filter & find items if we have an item selector
+ this.elements.forEach( this.addElementImages, this );
+};
+
+/**
+ * @param {Node} element
+ */
+ImagesLoaded.prototype.addElementImages = function( elem ) {
+ // filter siblings
+ if ( elem.nodeName == 'IMG' ) {
+ this.addImage( elem );
+ }
+ // get background image on element
+ if ( this.options.background === true ) {
+ this.addElementBackgroundImages( elem );
+ }
+
+ // find children
+ // no non-element nodes, #143
+ var nodeType = elem.nodeType;
+ if ( !nodeType || !elementNodeTypes[ nodeType ] ) {
+ return;
+ }
+ var childImgs = elem.querySelectorAll('img');
+ // concat childElems to filterFound array
+ for ( var i=0; i < childImgs.length; i++ ) {
+ var img = childImgs[i];
+ this.addImage( img );
+ }
+
+ // get child background images
+ if ( typeof this.options.background == 'string' ) {
+ var children = elem.querySelectorAll( this.options.background );
+ for ( i=0; i < children.length; i++ ) {
+ var child = children[i];
+ this.addElementBackgroundImages( child );
+ }
+ }
+};
+
+var elementNodeTypes = {
+ 1: true,
+ 9: true,
+ 11: true
+};
+
+ImagesLoaded.prototype.addElementBackgroundImages = function( elem ) {
+ var style = getComputedStyle( elem );
+ if ( !style ) {
+ // Firefox returns null if in a hidden iframe https://bugzil.la/548397
+ return;
+ }
+ // get url inside url("...")
+ var reURL = /url\((['"])?(.*?)\1\)/gi;
+ var matches = reURL.exec( style.backgroundImage );
+ while ( matches !== null ) {
+ var url = matches && matches[2];
+ if ( url ) {
+ this.addBackground( url, elem );
+ }
+ matches = reURL.exec( style.backgroundImage );
+ }
+};
+
+/**
+ * @param {Image} img
+ */
+ImagesLoaded.prototype.addImage = function( img ) {
+ var loadingImage = new LoadingImage( img );
+ this.images.push( loadingImage );
+};
+
+ImagesLoaded.prototype.addBackground = function( url, elem ) {
+ var background = new Background( url, elem );
+ this.images.push( background );
+};
+
+ImagesLoaded.prototype.check = function() {
+ var _this = this;
+ this.progressedCount = 0;
+ this.hasAnyBroken = false;
+ // complete if no images
+ if ( !this.images.length ) {
+ this.complete();
+ return;
+ }
+
+ function onProgress( image, elem, message ) {
+ // HACK - Chrome triggers event before object properties have changed. #83
+ setTimeout( function() {
+ _this.progress( image, elem, message );
+ });
+ }
+
+ this.images.forEach( function( loadingImage ) {
+ loadingImage.once( 'progress', onProgress );
+ loadingImage.check();
+ });
+};
+
+ImagesLoaded.prototype.progress = function( image, elem, message ) {
+ this.progressedCount++;
+ this.hasAnyBroken = this.hasAnyBroken || !image.isLoaded;
+ // progress event
+ this.emitEvent( 'progress', [ this, image, elem ] );
+ if ( this.jqDeferred && this.jqDeferred.notify ) {
+ this.jqDeferred.notify( this, image );
+ }
+ // check if completed
+ if ( this.progressedCount == this.images.length ) {
+ this.complete();
+ }
+
+ if ( this.options.debug && console ) {
+ console.log( 'progress: ' + message, image, elem );
+ }
+};
+
+ImagesLoaded.prototype.complete = function() {
+ var eventName = this.hasAnyBroken ? 'fail' : 'done';
+ this.isComplete = true;
+ this.emitEvent( eventName, [ this ] );
+ this.emitEvent( 'always', [ this ] );
+ if ( this.jqDeferred ) {
+ var jqMethod = this.hasAnyBroken ? 'reject' : 'resolve';
+ this.jqDeferred[ jqMethod ]( this );
+ }
+};
+
+// -------------------------- -------------------------- //
+
+function LoadingImage( img ) {
+ this.img = img;
+}
+
+LoadingImage.prototype = Object.create( EvEmitter.prototype );
+
+LoadingImage.prototype.check = function() {
+ // If complete is true and browser supports natural sizes,
+ // try to check for image status manually.
+ var isComplete = this.getIsImageComplete();
+ if ( isComplete ) {
+ // report based on naturalWidth
+ this.confirm( this.img.naturalWidth !== 0, 'naturalWidth' );
+ return;
+ }
+
+ // If none of the checks above matched, simulate loading on detached element.
+ this.proxyImage = new Image();
+ this.proxyImage.addEventListener( 'load', this );
+ this.proxyImage.addEventListener( 'error', this );
+ // bind to image as well for Firefox. #191
+ this.img.addEventListener( 'load', this );
+ this.img.addEventListener( 'error', this );
+ this.proxyImage.src = this.img.src;
+};
+
+LoadingImage.prototype.getIsImageComplete = function() {
+ return this.img.complete && this.img.naturalWidth !== undefined;
+};
+
+LoadingImage.prototype.confirm = function( isLoaded, message ) {
+ this.isLoaded = isLoaded;
+ this.emitEvent( 'progress', [ this, this.img, message ] );
+};
+
+// ----- events ----- //
+
+// trigger specified handler for event type
+LoadingImage.prototype.handleEvent = function( event ) {
+ var method = 'on' + event.type;
+ if ( this[ method ] ) {
+ this[ method ]( event );
+ }
+};
+
+LoadingImage.prototype.onload = function() {
+ this.confirm( true, 'onload' );
+ this.unbindEvents();
+};
+
+LoadingImage.prototype.onerror = function() {
+ this.confirm( false, 'onerror' );
+ this.unbindEvents();
+};
+
+LoadingImage.prototype.unbindEvents = function() {
+ this.proxyImage.removeEventListener( 'load', this );
+ this.proxyImage.removeEventListener( 'error', this );
+ this.img.removeEventListener( 'load', this );
+ this.img.removeEventListener( 'error', this );
+};
+
+// -------------------------- Background -------------------------- //
+
+function Background( url, element ) {
+ this.url = url;
+ this.element = element;
+ this.img = new Image();
+}
+
+// inherit LoadingImage prototype
+Background.prototype = Object.create( LoadingImage.prototype );
+
+Background.prototype.check = function() {
+ this.img.addEventListener( 'load', this );
+ this.img.addEventListener( 'error', this );
+ this.img.src = this.url;
+ // check if image is already complete
+ var isComplete = this.getIsImageComplete();
+ if ( isComplete ) {
+ this.confirm( this.img.naturalWidth !== 0, 'naturalWidth' );
+ this.unbindEvents();
+ }
+};
+
+Background.prototype.unbindEvents = function() {
+ this.img.removeEventListener( 'load', this );
+ this.img.removeEventListener( 'error', this );
+};
+
+Background.prototype.confirm = function( isLoaded, message ) {
+ this.isLoaded = isLoaded;
+ this.emitEvent( 'progress', [ this, this.element, message ] );
+};
+
+// -------------------------- jQuery -------------------------- //
+
+ImagesLoaded.makeJQueryPlugin = function( jQuery ) {
+ jQuery = jQuery || window.jQuery;
+ if ( !jQuery ) {
+ return;
+ }
+ // set local variable
+ $ = jQuery;
+ // $().imagesLoaded()
+ $.fn.imagesLoaded = function( options, callback ) {
+ var instance = new ImagesLoaded( this, options, callback );
+ return instance.jqDeferred.promise( $(this) );
+ };
+};
+// try making plugin
+ImagesLoaded.makeJQueryPlugin();
+
+// -------------------------- -------------------------- //
+
+return ImagesLoaded;
+
+});
+
diff --git a/etc/js/ion-pullup.js b/etc/js/ion-pullup.js
new file mode 100644
index 00000000..9c0b2470
--- /dev/null
+++ b/etc/js/ion-pullup.js
@@ -0,0 +1,363 @@
+/*
+ionic-pullup v1.1.0
+
+Copyright 2016 Ariel Faur (https://github.com/arielfaur)
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+angular.module('ionic-pullup', [])
+ .constant('ionPullUpFooterState', {
+ COLLAPSED: 'COLLAPSED',
+ MINIMIZED: 'MINIMIZED',
+ EXPANDED: 'EXPANDED'
+ })
+ .constant('ionPullUpFooterBehavior', {
+ HIDE: 'HIDE',
+ EXPAND: 'EXPAND'
+ })
+ .directive('ionPullUpFooter', [function () {
+ return {
+ restrict: 'AE',
+ scope: {
+ state: '=?',
+ onExpand: '&',
+ onCollapse: '&',
+ onMinimize: '&',
+ allowMidRange: '='
+ },
+ controller: ['$scope', '$element', '$attrs', '$timeout', '$rootScope', '$window', '$ionicPlatform', 'ionPullUpFooterState', 'ionPullUpFooterBehavior', function ($scope, $element, $attrs, $timeout, $rootScope, $window, $ionicPlatform, FooterState, FooterBehavior) {
+ var
+ tabs, hasBottomTabs, header, tabsHeight, headerHeight, handleHeight = 0,
+ footer = {
+ height: 0,
+ posY: 0,
+ lastPosY: 0,
+ defaultHeight: $element[0].offsetHeight,
+ maxHeight: parseInt($attrs.maxHeight, 10) || 0,
+ initialState: $attrs.initialState ? $attrs.initialState.toUpperCase() : FooterState.COLLAPSED,
+ defaultBehavior: $attrs.defaultBehavior ? $attrs.defaultBehavior.toUpperCase() : FooterBehavior.EXPAND
+ };
+
+ this.$onInit = function () {
+ $timeout(function () {
+ computeDefaultHeights();
+ $element.css({ 'transition': '300ms ease-in-out', 'padding': 0 });
+ if (tabs && hasBottomTabs) {
+ $element.css('bottom', tabsHeight + 'px');
+ }
+ });
+ updateUI();
+ }
+
+ function computeDefaultHeights() {
+ var el = $element[0];
+ tabs = el.closest('ion-tabs');
+ hasBottomTabs = tabs && tabs.classList.contains('tabs-bottom');
+ header = document.querySelector('ion-nav-bar .nav-bar-block[nav-bar=entering] > .bar-header');
+ tabsHeight = tabs ? tabs.querySelector('.tabs').offsetHeight : 0;
+ headerHeight = header ? header.offsetHeight : 0;
+ }
+
+ //PP
+ function recomputeAllHeights() {
+ computeDefaultHeights();
+ footer.height = footer.maxHeight > 0 ? footer.maxHeight : $window.innerHeight - headerHeight - handleHeight - tabsHeight;
+
+ // PP
+ footer.height -=50;
+
+ if ($rootScope.platformOS == 'ios') footer.height -=30;
+ //if ($rootScope.platformOS == 'android') footer.height -=10;
+ }
+
+ function computeHeights() {
+ footer.height = footer.maxHeight > 0 ? footer.maxHeight : $window.innerHeight - headerHeight - handleHeight - tabsHeight;
+
+ // PP
+
+ // PP
+ footer.height -=50;
+
+ if ($rootScope.platformOS == 'ios') footer.height -=30;
+ //if ($rootScope.platformOS == 'android') footer.height -=10;
+
+ $element.css({ 'height': footer.height + 'px' });
+
+ if (footer.initialState == FooterState.MINIMIZED) {
+
+ minimize();
+ } else {
+ collapse();
+ }
+ }
+
+ function updateUI() {
+ $timeout(function () {
+ computeHeights();
+ }, 300);
+ $element.css({ 'transition': 'none', 'padding': 0 });
+ }
+
+ function expand() {
+ recomputeAllHeights();
+ footer.lastPosY = 0;
+ // adjust CSS values with new heights in case they changed
+ // PP added height
+ $element.css({ 'height':footer.height + 'px', '-webkit-transform': 'translate3d(0, 0, 0)', 'transform': 'translate3d(0, 0, 0)' });
+ $element.css({ 'transition': '300ms ease-in-out', 'padding': 0 });
+ $scope.onExpand();
+ $scope.state = FooterState.EXPANDED;
+ }
+
+ function collapse() {
+ footer.lastPosY = tabs ? footer.height - tabsHeight : footer.height - footer.defaultHeight;
+ $element.css({ '-webkit-transform': 'translate3d(0, ' + footer.lastPosY + 'px, 0)', 'transform': 'translate3d(0, ' + footer.lastPosY + 'px, 0)' });
+ $scope.onCollapse();
+ $scope.state = FooterState.COLLAPSED;
+ }
+
+ function minimize() {
+ footer.lastPosY = footer.height;
+ $element.css({ '-webkit-transform': 'translate3d(0, ' + footer.lastPosY + 'px, 0)', 'transform': 'translate3d(0, ' + footer.lastPosY + 'px, 0)' });
+ $scope.onMinimize();
+ $scope.state = FooterState.MINIMIZED;
+ }
+
+
+ this.setHandleHeight = function (height) {
+ handleHeight = height;
+ };
+
+ this.getHeight = function () {
+ return $element[0].offsetHeight;
+ };
+
+ this.getBackground = function () {
+ return $window.getComputedStyle($element[0]).background;
+ };
+
+ this.getInitialState = function () {
+ return footer.initialState;
+ };
+
+ this.getDefaultBehavior = function () {
+ return footer.defaultBehavior;
+ };
+
+ this.onTap = function (e) {
+ e.gesture.srcEvent.preventDefault();
+ e.gesture.preventDefault();
+
+ $timeout(function () {
+ if ($scope.state == FooterState.COLLAPSED) {
+ if (footer.defaultBehavior == FooterBehavior.HIDE) {
+ $scope.state = FooterState.MINIMIZED;
+ } else {
+ $scope.state = FooterState.EXPANDED;
+ }
+ } else {
+ if ($scope.state == FooterState.MINIMIZED) {
+ if (footer.defaultBehavior == FooterBehavior.HIDE)
+ $scope.state = FooterState.COLLAPSED;
+ else
+ $scope.state = FooterState.EXPANDED;
+ } else {
+ // footer is expanded
+ $scope.state = footer.initialState == FooterState.MINIMIZED ? FooterState.MINIMIZED : FooterState.COLLAPSED;
+ }
+ }
+ });
+ };
+
+ this.onDrag = function (e) {
+ e.gesture.srcEvent.preventDefault();
+ e.gesture.preventDefault();
+
+ switch (e.type) {
+ case 'dragstart':
+ $element.css('transition', 'none');
+ break;
+ case 'drag':
+ footer.posY = Math.round(e.gesture.deltaY) + footer.lastPosY;
+ if (footer.posY < 0 || footer.posY > footer.height) return;
+ $element.css({ '-webkit-transform': 'translate3d(0, ' + footer.posY + 'px, 0)', 'transform': 'translate3d(0, ' + footer.posY + 'px, 0)' });
+ break;
+ case 'dragend':
+ $element.css({ 'transition': '300ms ease-in-out' });
+ if (!$scope.allowMidRange) {
+ $timeout(function () {
+ if (footer.lastPosY > footer.posY) {
+ $scope.state = FooterState.EXPANDED;
+ }
+ else if (footer.lastPosY < footer.posY) {
+ $scope.state = (footer.initialState == FooterState.MINIMIZED) ? FooterState.MINIMIZED : FooterState.COLLAPSED;
+ }
+ });
+ }
+ else {
+ footer.lastPosY = footer.posY;
+ }
+ break;
+ }
+ };
+
+ var deregisterWatch = $scope.$watch('state', function (newState, oldState) {
+ if (oldState === undefined || newState == oldState) return;
+ switch (newState) {
+ case FooterState.COLLAPSED:
+ collapse();
+ break;
+ case FooterState.EXPANDED:
+ expand();
+ break;
+ case FooterState.MINIMIZED:
+ minimize();
+ break;
+ }
+ $rootScope.$broadcast('ionPullUp:tap', $scope.state, footer.defaultBehavior);
+ });
+
+ $scope.$on('$destroy', deregisterWatch);
+
+ $ionicPlatform.ready(function () {
+ $window.addEventListener('orientationchange', updateUI);
+ $ionicPlatform.on("resume", updateUI);
+ });
+
+ }],
+ compile: function(element, attrs) {
+ attrs.defaultHeight && element.css('height', parseInt(attrs.defaultHeight, 10) + 'px');
+ element.addClass('bar bar-footer');
+ }
+ }
+ }])
+ .component('ionPullUpContent', {
+ require: {
+ FooterController: '^ionPullUpFooter'
+ },
+ controller: ['$element', '$attrs', function ($element, $attrs) {
+ this.$onInit = function () {
+ var controller = this.FooterController,
+ footerHeight = controller.getHeight();
+ $element.css({ 'display': 'block', 'margin-top': footerHeight + 'px', width: '100%' });
+ // add scrolling if needed
+ if ($attrs.scroll && $attrs.scroll.toUpperCase() == 'TRUE') {
+ $element.css({ 'overflow-y': 'scroll', 'overflow-x': 'hidden' });
+ }
+ }
+ }]
+ })
+ .component('ionPullUpBar', {
+ require: {
+ FooterController: '^ionPullUpFooter'
+ },
+ controller: ['$element', function ($element) {
+ this.$onInit = function () {
+ var controller = this.FooterController,
+ footerHeight = controller.getHeight();
+ $element.css({ 'display': 'flex', 'height': footerHeight + 'px', position: 'absolute', right: '0', left: '0' });
+ }
+ }]
+ })
+ .directive('ionPullUpTrigger', ['$ionicGesture', function ($ionicGesture) {
+ return {
+ restrict: 'AE',
+ require: '^ionPullUpFooter',
+ link: function (scope, element, attrs, controller) {
+ // add gesture
+ $ionicGesture.on('tap', controller.onTap, element);
+ $ionicGesture.on('drag dragstart dragend', controller.onDrag, element);
+ }
+ }
+ }])
+ .component('ionPullUpHandle', {
+ require: {
+ FooterController: '^ionPullUpFooter'
+ },
+ controller: ['$scope', '$element', '$attrs', 'ionPullUpFooterState', 'ionPullUpFooterBehavior', '$ionicGesture', '$ionicPlatform', '$timeout', '$window', function ($scope, $element, $attrs, FooterState, FooterBehavior, $ionicGesture, $ionicPlatform, $timeout, $window) {
+ var height = parseInt($attrs.height, 10) || 25, width = parseInt($attrs.width, 10) || 100,
+ iconExpand = $attrs.iconExpand,
+ iconCollapse = $attrs.iconCollapse;
+
+ $element
+ .css({
+ display: 'block',
+ 'background-color': 'inherit',
+ position: 'absolute',
+ top: 1 - height + 'px',
+ left: (($window.innerWidth - width) / 2) + 'px',
+ height: height + 'px',
+ width: width + 'px',
+ 'text-align': 'center'
+ })
+ .append('<i>');
+
+ this.$onInit = function () {
+ var controller = this.FooterController;
+
+ // add gesture
+ $ionicGesture.on('tap', controller.onTap, $element);
+ $ionicGesture.on('drag dragstart dragend', controller.onDrag, $element);
+
+ controller.setHandleHeight(height);
+
+ var state = controller.getInitialState(),
+ behavior = controller.getDefaultBehavior();
+
+ toggleIcons(state, behavior);
+
+ updateUI();
+ }
+
+ $scope.$on('ionPullUp:tap', function (e, state, behavior) {
+ toggleIcons(state, behavior);
+ });
+
+ function toggleIcons(state, behavior) {
+ if (!iconExpand || !iconCollapse) return;
+
+ //remove any icons
+ $element.find('i').removeClass([iconExpand, iconCollapse].join(' '));
+
+ if (state == FooterState.COLLAPSED) {
+ if (behavior == FooterBehavior.HIDE) {
+ $element.find('i').addClass(iconCollapse);
+ } else {
+ $element.find('i').addClass(iconExpand);
+ }
+ } else {
+ if (state == FooterState.MINIMIZED) {
+ if (behavior == FooterBehavior.HIDE)
+ $element.find('i').addClass(iconExpand);
+ else
+ $element.find('i').addClass(iconExpand);
+ } else {
+ // footer is expanded
+ $element.find('i').addClass(iconCollapse);
+ }
+ }
+ }
+
+ function updateUI() {
+ $timeout(function () {
+ $element.css('left', (($window.innerWidth - width) / 2) + 'px');
+ }, 300);
+ }
+
+ $ionicPlatform.ready(function () {
+ $window.addEventListener('orientationchange', updateUI);
+ $ionicPlatform.on("resume", updateUI);
+ });
+ }]
+ });
diff --git a/etc/js/ionRadio.js b/etc/js/ionRadio.js
new file mode 100644
index 00000000..cb240b76
--- /dev/null
+++ b/etc/js/ionRadio.js
@@ -0,0 +1,55 @@
+/**
+ * ionRadioFix - fixes a bug in iOS 9 UIWebView that breaks the tilde selector in CSS. To
+ * use this fix, include it after your Ionic bundle JS.
+ *
+ * Note: due to Angular directive override limitations, you'll need to change any reference
+ * to <ion-radio> to <ion-radio-fix> to apply this patched radio button.
+ *
+ * Also, make sure to add the new CSS from the second part of this gist.
+ */
+angular.module('ionic').directive('ionRadioFix', function() {
+ return {
+ restrict: 'E',
+ replace: true,
+ require: '?ngModel',
+ transclude: true,
+ template:
+ '<label class="item item-radio">' +
+ '<input type="radio" name="radio-group">' +
+ '<div class="radio-content">' +
+ '<div class="item-content disable-pointer-events" ng-transclude></div>' +
+ '<i class="radio-icon disable-pointer-events icon ion-checkmark"></i>' +
+ '</div>' +
+ '</label>',
+
+ compile: function(element, attr) {
+ if (attr.icon) {
+ var iconElm = element.find('i');
+ iconElm.removeClass('ion-checkmark').addClass(attr.icon);
+ }
+
+ var input = element.find('input');
+ angular.forEach({
+ 'name': attr.name,
+ 'value': attr.value,
+ 'disabled': attr.disabled,
+ 'ng-value': attr.ngValue,
+ 'ng-model': attr.ngModel,
+ 'ng-disabled': attr.ngDisabled,
+ 'ng-change': attr.ngChange,
+ 'ng-required': attr.ngRequired,
+ 'required': attr.required
+ }, function(value, name) {
+ if (angular.isDefined(value)) {
+ input.attr(name, value);
+ }
+ });
+
+ return function(scope, element, attr) {
+ scope.getValue = function() {
+ return scope.ngValue || attr.value;
+ };
+ };
+ }
+ };
+}); \ No newline at end of file
diff --git a/etc/js/ionic.content.banner.js b/etc/js/ionic.content.banner.js
new file mode 100644
index 00000000..900b96a6
--- /dev/null
+++ b/etc/js/ionic.content.banner.js
@@ -0,0 +1,190 @@
+angular.module('jett.ionic.content.banner', ['ionic']);
+/* global angular */
+(function (angular) {
+ 'use strict';
+
+ angular.module('jett.ionic.content.banner')
+ .directive('ionContentBanner', [
+ '$interval',
+ function ($interval) {
+ return {
+ restrict: 'E',
+ scope: true,
+ link: function ($scope, $element) {
+ var stopInterval;
+
+ $scope.currentIndex = 0;
+
+ if ($scope.text.length > 1) {
+ stopInterval = $interval(function () {
+ $scope.currentIndex = ($scope.currentIndex < $scope.text.length - 1) ? $scope.currentIndex + 1 : 0;
+ }, $scope.interval);
+ }
+
+ $scope.$on('$destroy', function() {
+ $element.remove();
+ if (stopInterval) {
+ $interval.cancel(stopInterval);
+ }
+ });
+ },
+ template:
+ '<div class="content-banner-text-wrapper">' +
+ '<div ng-repeat="item in text track by $index" ng-class="{active: $index === currentIndex}" class="content-banner-text" ng-bind="item"></div>' +
+ '</div>' +
+ '<button class="content-banner-close button button-icon icon {{::icon}}" ng-click="close()"></button>'
+ };
+ }]);
+
+})(angular);
+
+/* global angular,ionic */
+/**
+ * @ngdoc service
+ * @name $ionicContentBanner
+ * @module ionic
+ * @description The Content Banner is an animated banner that will show specific information to a user.
+ */
+(function (angular, ionic) {
+ 'use strict';
+
+ angular.module('jett.ionic.content.banner')
+ .factory('$ionicContentBanner', [
+ '$document',
+ '$rootScope',
+ '$compile',
+ '$timeout',
+ '$ionicPlatform',
+ function ($document, $rootScope, $compile, $timeout, $ionicPlatform) {
+
+ function isActiveView (node) {
+ // walk up the child-parent node chain until we get to the root or the BODY
+ while (node !== null && node.nodeName !== 'BODY') {
+ var navView = node.getAttribute("nav-view");
+
+ // as soon as we encounter a cached (parent) view then we know the view can't be active
+ if (navView !== null && navView === 'cached') {
+ return false;
+ }
+ node = node.parentNode;
+ }
+ // no cached parent seen, the view must be really active
+ return true;
+ }
+
+ function getActiveView (body) {
+ // check if there is an active modal
+ var modal = body.querySelector('ion-modal-view[class*="ng-enter-active"]');
+ if (modal != null) {
+ // check if modal is not leaving
+ if (modal.getAttribute('class').indexOf('ng-leave') == -1) {
+ return modal;
+ }
+ }
+ // get the candidate active views
+ var views = body.querySelectorAll('ion-view[nav-view="active"]');
+
+ // only one candidate, so we just take it
+ if (views.length === 1) {
+ return views[0];
+ }
+
+ // convert the NodeList to an array, filter it using 'isActiveView' and return the first element
+ return Array.prototype.slice.call(views).filter(function (view) {
+ return isActiveView(view);
+ })[0];
+ }
+
+ /**
+ * @ngdoc method
+ * @name $ionicContentBanner#show
+ * @description
+ * Load and show a new content banner.
+ */
+ function contentBanner (opts) {
+ var scope = $rootScope.$new(true);
+
+ angular.extend(scope, {
+ icon: 'ion-ios-close-empty',
+ transition: 'vertical',
+ interval: 7000,
+ type: 'info',
+ $deregisterBackButton: angular.noop,
+ closeOnStateChange: true,
+ autoClose: null
+ }, opts);
+
+ // Compile the template
+ var classes = 'content-banner ' + scope.type + ' content-banner-transition-' + scope.transition;
+ var element = scope.element = $compile('<ion-content-banner class="' + classes + '"></ion-content-banner>')(scope);
+ var body = $document[0].body;
+
+ var stateChangeListenDone = scope.closeOnStateChange ?
+ $rootScope.$on('$stateChangeSuccess', function() { scope.close(); }) :
+ angular.noop;
+
+ scope.$deregisterBackButton = $ionicPlatform.registerBackButtonAction(
+ function() {
+ $timeout(scope.close);
+ }, 300
+ );
+
+ scope.close = function() {
+ if (scope.removed) {
+ return;
+ }
+ scope.removed = true;
+
+ ionic.requestAnimationFrame(function () {
+ element.removeClass('content-banner-in');
+
+ $timeout(function () {
+ scope.$destroy();
+ element.remove();
+ body = stateChangeListenDone = null;
+ }, 400);
+ });
+
+ scope.$deregisterBackButton();
+ stateChangeListenDone();
+ };
+
+ scope.show = function() {
+ if (scope.removed) {
+ return;
+ }
+ // PP: get rid of querySelector
+ if (getActiveView(body) !== undefined)
+ getActiveView(body).querySelector('.scroll-content').appendChild(element[0]);
+
+ ionic.requestAnimationFrame(function () {
+ $timeout(function () {
+ element.addClass('content-banner-in');
+ //automatically close if autoClose is configured
+ if (scope.autoClose) {
+ $timeout(function () {
+ scope.close();
+ }, scope.autoClose, false);
+ }
+ }, 20, false);
+ });
+ };
+
+ //set small timeout to let ionic set the active/cached view
+ $timeout(function () {
+ scope.show();
+ }, 10, false);
+
+ // Expose the scope on $ionContentBanner's return value for the sake of testing it.
+ scope.close.$scope = scope;
+
+ return scope.close;
+ }
+
+ return {
+ show: contentBanner
+ };
+ }]);
+
+
+})(angular, ionic);
diff --git a/etc/js/localforage-cordovasqlitedriver.js b/etc/js/localforage-cordovasqlitedriver.js
new file mode 100644
index 00000000..635e6e66
--- /dev/null
+++ b/etc/js/localforage-cordovasqlitedriver.js
@@ -0,0 +1,156 @@
+(function (global, factory) {
+ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+ typeof define === 'function' && define.amd ? define(factory) :
+ (global.cordovaSQLiteDriver = factory());
+}(this, function () { 'use strict';
+
+ function getSerializerPromise(localForageInstance) {
+ if (getSerializerPromise.result) {
+ return getSerializerPromise.result;
+ }
+ if (!localForageInstance || typeof localForageInstance.getSerializer !== 'function') {
+ return Promise.reject(new Error('localforage.getSerializer() was not available! ' + 'localforage v1.4+ is required!'));
+ }
+ getSerializerPromise.result = localForageInstance.getSerializer();
+ return getSerializerPromise.result;
+ }
+
+ function getDriverPromise(localForageInstance, driverName) {
+ getDriverPromise.result = getDriverPromise.result || {};
+ if (getDriverPromise.result[driverName]) {
+ return getDriverPromise.result[driverName];
+ }
+ if (!localForageInstance || typeof localForageInstance.getDriver !== 'function') {
+ return Promise.reject(new Error('localforage.getDriver() was not available! ' + 'localforage v1.4+ is required!'));
+ }
+ getDriverPromise.result[driverName] = localForageInstance.getDriver(driverName);
+ return getDriverPromise.result[driverName];
+ }
+
+ function getWebSqlDriverPromise(localForageInstance) {
+ return getDriverPromise(localForageInstance, localForageInstance.WEBSQL);
+ }
+
+ /* global document, sqlitePlugin */
+ // we can't import this, since it gets defined later
+ // import sqlitePlugin from 'sqlitePlugin';
+
+ var deviceReady = new Promise(function (resolve, reject) {
+ if (typeof sqlitePlugin !== 'undefined') {
+ resolve();
+ } else if (typeof cordova === 'undefined') {
+ reject(new Error('cordova is not defined.'));
+ } else {
+ // Wait for Cordova to load
+ document.addEventListener("deviceready", function () {
+ return resolve();
+ }, false);
+ }
+ });
+
+ var deviceReadyDone = deviceReady.catch(function () {
+ return Promise.resolve();
+ });
+
+ function getOpenDatabasePromise() {
+ return deviceReadyDone.then(function () {
+ if (typeof sqlitePlugin !== 'undefined' && typeof sqlitePlugin.openDatabase === 'function') {
+ return sqlitePlugin.openDatabase;
+ } else {
+ throw new Error('SQLite plugin is not present.');
+ }
+ });
+ }
+
+ // // If cordova is not present, we can stop now.
+ // if (!globalObject.cordova) {
+ // return;
+ // }
+
+ // Open the cordova sqlite plugin database (automatically creates one if one didn't
+ // previously exist), using any options set in the config.
+ function _initStorage(options) {
+ var self = this;
+ var dbInfo = {
+ db: null
+ };
+
+ if (options) {
+ for (var i in options) {
+ dbInfo[i] = typeof options[i] !== 'string' ? options[i].toString() : options[i];
+ }
+ }
+
+ var dbInfoPromise = getOpenDatabasePromise().then(function (openDatabase) {
+ return new Promise(function (resolve, reject) {
+ // Open the database; the openDatabase API will automatically
+ // create it for us if it doesn't exist.
+ try {
+ dbInfo.location = dbInfo.location || 'default';
+ dbInfo.db = openDatabase({
+ name: dbInfo.name,
+ version: String(dbInfo.version),
+ description: dbInfo.description,
+ size: dbInfo.size,
+ location: dbInfo.location
+ });
+ } catch (e) {
+ reject(e);
+ }
+
+ // Create our key/value table if it doesn't exist.
+ dbInfo.db.transaction(function (t) {
+ t.executeSql('CREATE TABLE IF NOT EXISTS ' + dbInfo.storeName + ' (id INTEGER PRIMARY KEY, key unique, value)', [], function () {
+ self._dbInfo = dbInfo;
+ resolve();
+ }, function (t, error) {
+ reject(error);
+ });
+ });
+ });
+ });
+
+ var serializerPromise = getSerializerPromise(self);
+ var webSqlDriverPromise = getWebSqlDriverPromise(self);
+
+ return Promise.all([serializerPromise, webSqlDriverPromise, dbInfoPromise]).then(function (results) {
+ dbInfo.serializer = results[0];
+ return dbInfoPromise;
+ });
+ }
+
+ var cordovaSQLiteDriver = {
+ _driver: 'cordovaSQLiteDriver',
+ _initStorage: _initStorage,
+ _support: function _support() {
+ return getOpenDatabasePromise().then(function (openDatabase) {
+ return !!openDatabase;
+ }).catch(function () {
+ return false;
+ });
+ }
+ };
+
+ function wireUpDriverMethods(driver) {
+ var LibraryMethods = ['clear', 'getItem', 'iterate', 'key', 'keys', 'length', 'removeItem', 'setItem'];
+
+ function wireUpDriverMethod(driver, methodName) {
+ driver[methodName] = function () {
+ var localForageInstance = this;
+ var args = arguments;
+ return getWebSqlDriverPromise(localForageInstance).then(function (webSqlDriver) {
+ return webSqlDriver[methodName].apply(localForageInstance, args);
+ });
+ };
+ }
+
+ for (var i = 0, len = LibraryMethods.length; i < len; i++) {
+ wireUpDriverMethod(driver, LibraryMethods[i]);
+ }
+ }
+
+ wireUpDriverMethods(cordovaSQLiteDriver);
+
+ return cordovaSQLiteDriver;
+
+})); \ No newline at end of file
diff --git a/etc/js/localforage.js b/etc/js/localforage.js
new file mode 100644
index 00000000..e7df8c27
--- /dev/null
+++ b/etc/js/localforage.js
@@ -0,0 +1,2797 @@
+/*!
+ localForage -- Offline Storage, Improved
+ Version 1.7.3
+ https://localforage.github.io/localForage
+ (c) 2013-2017 Mozilla, Apache License 2.0
+*/
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.localforage = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw (f.code="MODULE_NOT_FOUND", f)}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(_dereq_,module,exports){
+(function (global){
+'use strict';
+var Mutation = global.MutationObserver || global.WebKitMutationObserver;
+
+var scheduleDrain;
+
+{
+ if (Mutation) {
+ var called = 0;
+ var observer = new Mutation(nextTick);
+ var element = global.document.createTextNode('');
+ observer.observe(element, {
+ characterData: true
+ });
+ scheduleDrain = function () {
+ element.data = (called = ++called % 2);
+ };
+ } else if (!global.setImmediate && typeof global.MessageChannel !== 'undefined') {
+ var channel = new global.MessageChannel();
+ channel.port1.onmessage = nextTick;
+ scheduleDrain = function () {
+ channel.port2.postMessage(0);
+ };
+ } else if ('document' in global && 'onreadystatechange' in global.document.createElement('script')) {
+ scheduleDrain = function () {
+
+ // Create a <script> element; its readystatechange event will be fired asynchronously once it is inserted
+ // into the document. Do so, thus queuing up the task. Remember to clean up once it's been called.
+ var scriptEl = global.document.createElement('script');
+ scriptEl.onreadystatechange = function () {
+ nextTick();
+
+ scriptEl.onreadystatechange = null;
+ scriptEl.parentNode.removeChild(scriptEl);
+ scriptEl = null;
+ };
+ global.document.documentElement.appendChild(scriptEl);
+ };
+ } else {
+ scheduleDrain = function () {
+ setTimeout(nextTick, 0);
+ };
+ }
+}
+
+var draining;
+var queue = [];
+//named nextTick for less confusing stack traces
+function nextTick() {
+ draining = true;
+ var i, oldQueue;
+ var len = queue.length;
+ while (len) {
+ oldQueue = queue;
+ queue = [];
+ i = -1;
+ while (++i < len) {
+ oldQueue[i]();
+ }
+ len = queue.length;
+ }
+ draining = false;
+}
+
+module.exports = immediate;
+function immediate(task) {
+ if (queue.push(task) === 1 && !draining) {
+ scheduleDrain();
+ }
+}
+
+}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+},{}],2:[function(_dereq_,module,exports){
+'use strict';
+var immediate = _dereq_(1);
+
+/* istanbul ignore next */
+function INTERNAL() {}
+
+var handlers = {};
+
+var REJECTED = ['REJECTED'];
+var FULFILLED = ['FULFILLED'];
+var PENDING = ['PENDING'];
+
+module.exports = Promise;
+
+function Promise(resolver) {
+ if (typeof resolver !== 'function') {
+ throw new TypeError('resolver must be a function');
+ }
+ this.state = PENDING;
+ this.queue = [];
+ this.outcome = void 0;
+ if (resolver !== INTERNAL) {
+ safelyResolveThenable(this, resolver);
+ }
+}
+
+Promise.prototype["catch"] = function (onRejected) {
+ return this.then(null, onRejected);
+};
+Promise.prototype.then = function (onFulfilled, onRejected) {
+ if (typeof onFulfilled !== 'function' && this.state === FULFILLED ||
+ typeof onRejected !== 'function' && this.state === REJECTED) {
+ return this;
+ }
+ var promise = new this.constructor(INTERNAL);
+ if (this.state !== PENDING) {
+ var resolver = this.state === FULFILLED ? onFulfilled : onRejected;
+ unwrap(promise, resolver, this.outcome);
+ } else {
+ this.queue.push(new QueueItem(promise, onFulfilled, onRejected));
+ }
+
+ return promise;
+};
+function QueueItem(promise, onFulfilled, onRejected) {
+ this.promise = promise;
+ if (typeof onFulfilled === 'function') {
+ this.onFulfilled = onFulfilled;
+ this.callFulfilled = this.otherCallFulfilled;
+ }
+ if (typeof onRejected === 'function') {
+ this.onRejected = onRejected;
+ this.callRejected = this.otherCallRejected;
+ }
+}
+QueueItem.prototype.callFulfilled = function (value) {
+ handlers.resolve(this.promise, value);
+};
+QueueItem.prototype.otherCallFulfilled = function (value) {
+ unwrap(this.promise, this.onFulfilled, value);
+};
+QueueItem.prototype.callRejected = function (value) {
+ handlers.reject(this.promise, value);
+};
+QueueItem.prototype.otherCallRejected = function (value) {
+ unwrap(this.promise, this.onRejected, value);
+};
+
+function unwrap(promise, func, value) {
+ immediate(function () {
+ var returnValue;
+ try {
+ returnValue = func(value);
+ } catch (e) {
+ return handlers.reject(promise, e);
+ }
+ if (returnValue === promise) {
+ handlers.reject(promise, new TypeError('Cannot resolve promise with itself'));
+ } else {
+ handlers.resolve(promise, returnValue);
+ }
+ });
+}
+
+handlers.resolve = function (self, value) {
+ var result = tryCatch(getThen, value);
+ if (result.status === 'error') {
+ return handlers.reject(self, result.value);
+ }
+ var thenable = result.value;
+
+ if (thenable) {
+ safelyResolveThenable(self, thenable);
+ } else {
+ self.state = FULFILLED;
+ self.outcome = value;
+ var i = -1;
+ var len = self.queue.length;
+ while (++i < len) {
+ self.queue[i].callFulfilled(value);
+ }
+ }
+ return self;
+};
+handlers.reject = function (self, error) {
+ self.state = REJECTED;
+ self.outcome = error;
+ var i = -1;
+ var len = self.queue.length;
+ while (++i < len) {
+ self.queue[i].callRejected(error);
+ }
+ return self;
+};
+
+function getThen(obj) {
+ // Make sure we only access the accessor once as required by the spec
+ var then = obj && obj.then;
+ if (obj && (typeof obj === 'object' || typeof obj === 'function') && typeof then === 'function') {
+ return function appyThen() {
+ then.apply(obj, arguments);
+ };
+ }
+}
+
+function safelyResolveThenable(self, thenable) {
+ // Either fulfill, reject or reject with error
+ var called = false;
+ function onError(value) {
+ if (called) {
+ return;
+ }
+ called = true;
+ handlers.reject(self, value);
+ }
+
+ function onSuccess(value) {
+ if (called) {
+ return;
+ }
+ called = true;
+ handlers.resolve(self, value);
+ }
+
+ function tryToUnwrap() {
+ thenable(onSuccess, onError);
+ }
+
+ var result = tryCatch(tryToUnwrap);
+ if (result.status === 'error') {
+ onError(result.value);
+ }
+}
+
+function tryCatch(func, value) {
+ var out = {};
+ try {
+ out.value = func(value);
+ out.status = 'success';
+ } catch (e) {
+ out.status = 'error';
+ out.value = e;
+ }
+ return out;
+}
+
+Promise.resolve = resolve;
+function resolve(value) {
+ if (value instanceof this) {
+ return value;
+ }
+ return handlers.resolve(new this(INTERNAL), value);
+}
+
+Promise.reject = reject;
+function reject(reason) {
+ var promise = new this(INTERNAL);
+ return handlers.reject(promise, reason);
+}
+
+Promise.all = all;
+function all(iterable) {
+ var self = this;
+ if (Object.prototype.toString.call(iterable) !== '[object Array]') {
+ return this.reject(new TypeError('must be an array'));
+ }
+
+ var len = iterable.length;
+ var called = false;
+ if (!len) {
+ return this.resolve([]);
+ }
+
+ var values = new Array(len);
+ var resolved = 0;
+ var i = -1;
+ var promise = new this(INTERNAL);
+
+ while (++i < len) {
+ allResolver(iterable[i], i);
+ }
+ return promise;
+ function allResolver(value, i) {
+ self.resolve(value).then(resolveFromAll, function (error) {
+ if (!called) {
+ called = true;
+ handlers.reject(promise, error);
+ }
+ });
+ function resolveFromAll(outValue) {
+ values[i] = outValue;
+ if (++resolved === len && !called) {
+ called = true;
+ handlers.resolve(promise, values);
+ }
+ }
+ }
+}
+
+Promise.race = race;
+function race(iterable) {
+ var self = this;
+ if (Object.prototype.toString.call(iterable) !== '[object Array]') {
+ return this.reject(new TypeError('must be an array'));
+ }
+
+ var len = iterable.length;
+ var called = false;
+ if (!len) {
+ return this.resolve([]);
+ }
+
+ var i = -1;
+ var promise = new this(INTERNAL);
+
+ while (++i < len) {
+ resolver(iterable[i]);
+ }
+ return promise;
+ function resolver(value) {
+ self.resolve(value).then(function (response) {
+ if (!called) {
+ called = true;
+ handlers.resolve(promise, response);
+ }
+ }, function (error) {
+ if (!called) {
+ called = true;
+ handlers.reject(promise, error);
+ }
+ });
+ }
+}
+
+},{"1":1}],3:[function(_dereq_,module,exports){
+(function (global){
+'use strict';
+if (typeof global.Promise !== 'function') {
+ global.Promise = _dereq_(2);
+}
+
+}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+},{"2":2}],4:[function(_dereq_,module,exports){
+'use strict';
+
+var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+function getIDB() {
+ /* global indexedDB,webkitIndexedDB,mozIndexedDB,OIndexedDB,msIndexedDB */
+ try {
+ if (typeof indexedDB !== 'undefined') {
+ return indexedDB;
+ }
+ if (typeof webkitIndexedDB !== 'undefined') {
+ return webkitIndexedDB;
+ }
+ if (typeof mozIndexedDB !== 'undefined') {
+ return mozIndexedDB;
+ }
+ if (typeof OIndexedDB !== 'undefined') {
+ return OIndexedDB;
+ }
+ if (typeof msIndexedDB !== 'undefined') {
+ return msIndexedDB;
+ }
+ } catch (e) {
+ return;
+ }
+}
+
+var idb = getIDB();
+
+function isIndexedDBValid() {
+ try {
+ // Initialize IndexedDB; fall back to vendor-prefixed versions
+ // if needed.
+ if (!idb) {
+ return false;
+ }
+ // We mimic PouchDB here;
+ //
+ // We test for openDatabase because IE Mobile identifies itself
+ // as Safari. Oh the lulz...
+ var isSafari = typeof openDatabase !== 'undefined' && /(Safari|iPhone|iPad|iPod)/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent) && !/BlackBerry/.test(navigator.platform);
+
+ var hasFetch = typeof fetch === 'function' && fetch.toString().indexOf('[native code') !== -1;
+
+ // Safari <10.1 does not meet our requirements for IDB support (#5572)
+ // since Safari 10.1 shipped with fetch, we can use that to detect it
+ return (!isSafari || hasFetch) && typeof indexedDB !== 'undefined' &&
+ // some outdated implementations of IDB that appear on Samsung
+ // and HTC Android devices <4.4 are missing IDBKeyRange
+ // See: https://github.com/mozilla/localForage/issues/128
+ // See: https://github.com/mozilla/localForage/issues/272
+ typeof IDBKeyRange !== 'undefined';
+ } catch (e) {
+ return false;
+ }
+}
+
+// Abstracts constructing a Blob object, so it also works in older
+// browsers that don't support the native Blob constructor. (i.e.
+// old QtWebKit versions, at least).
+// Abstracts constructing a Blob object, so it also works in older
+// browsers that don't support the native Blob constructor. (i.e.
+// old QtWebKit versions, at least).
+function createBlob(parts, properties) {
+ /* global BlobBuilder,MSBlobBuilder,MozBlobBuilder,WebKitBlobBuilder */
+ parts = parts || [];
+ properties = properties || {};
+ try {
+ return new Blob(parts, properties);
+ } catch (e) {
+ if (e.name !== 'TypeError') {
+ throw e;
+ }
+ var Builder = typeof BlobBuilder !== 'undefined' ? BlobBuilder : typeof MSBlobBuilder !== 'undefined' ? MSBlobBuilder : typeof MozBlobBuilder !== 'undefined' ? MozBlobBuilder : WebKitBlobBuilder;
+ var builder = new Builder();
+ for (var i = 0; i < parts.length; i += 1) {
+ builder.append(parts[i]);
+ }
+ return builder.getBlob(properties.type);
+ }
+}
+
+// This is CommonJS because lie is an external dependency, so Rollup
+// can just ignore it.
+if (typeof Promise === 'undefined') {
+ // In the "nopromises" build this will just throw if you don't have
+ // a global promise object, but it would throw anyway later.
+ _dereq_(3);
+}
+var Promise$1 = Promise;
+
+function executeCallback(promise, callback) {
+ if (callback) {
+ promise.then(function (result) {
+ callback(null, result);
+ }, function (error) {
+ callback(error);
+ });
+ }
+}
+
+function executeTwoCallbacks(promise, callback, errorCallback) {
+ if (typeof callback === 'function') {
+ promise.then(callback);
+ }
+
+ if (typeof errorCallback === 'function') {
+ promise["catch"](errorCallback);
+ }
+}
+
+function normalizeKey(key) {
+ // Cast the key to a string, as that's all we can set as a key.
+ if (typeof key !== 'string') {
+ console.warn(key + ' used as a key, but it is not a string.');
+ key = String(key);
+ }
+
+ return key;
+}
+
+function getCallback() {
+ if (arguments.length && typeof arguments[arguments.length - 1] === 'function') {
+ return arguments[arguments.length - 1];
+ }
+}
+
+// Some code originally from async_storage.js in
+// [Gaia](https://github.com/mozilla-b2g/gaia).
+
+var DETECT_BLOB_SUPPORT_STORE = 'local-forage-detect-blob-support';
+var supportsBlobs = void 0;
+var dbContexts = {};
+var toString = Object.prototype.toString;
+
+// Transaction Modes
+var READ_ONLY = 'readonly';
+var READ_WRITE = 'readwrite';
+
+// Transform a binary string to an array buffer, because otherwise
+// weird stuff happens when you try to work with the binary string directly.
+// It is known.
+// From http://stackoverflow.com/questions/14967647/ (continues on next line)
+// encode-decode-image-with-base64-breaks-image (2013-04-21)
+function _binStringToArrayBuffer(bin) {
+ var length = bin.length;
+ var buf = new ArrayBuffer(length);
+ var arr = new Uint8Array(buf);
+ for (var i = 0; i < length; i++) {
+ arr[i] = bin.charCodeAt(i);
+ }
+ return buf;
+}
+
+//
+// Blobs are not supported in all versions of IndexedDB, notably
+// Chrome <37 and Android <5. In those versions, storing a blob will throw.
+//
+// Various other blob bugs exist in Chrome v37-42 (inclusive).
+// Detecting them is expensive and confusing to users, and Chrome 37-42
+// is at very low usage worldwide, so we do a hacky userAgent check instead.
+//
+// content-type bug: https://code.google.com/p/chromium/issues/detail?id=408120
+// 404 bug: https://code.google.com/p/chromium/issues/detail?id=447916
+// FileReader bug: https://code.google.com/p/chromium/issues/detail?id=447836
+//
+// Code borrowed from PouchDB. See:
+// https://github.com/pouchdb/pouchdb/blob/master/packages/node_modules/pouchdb-adapter-idb/src/blobSupport.js
+//
+function _checkBlobSupportWithoutCaching(idb) {
+ return new Promise$1(function (resolve) {
+ var txn = idb.transaction(DETECT_BLOB_SUPPORT_STORE, READ_WRITE);
+ var blob = createBlob(['']);
+ txn.objectStore(DETECT_BLOB_SUPPORT_STORE).put(blob, 'key');
+
+ txn.onabort = function (e) {
+ // If the transaction aborts now its due to not being able to
+ // write to the database, likely due to the disk being full
+ e.preventDefault();
+ e.stopPropagation();
+ resolve(false);
+ };
+
+ txn.oncomplete = function () {
+ var matchedChrome = navigator.userAgent.match(/Chrome\/(\d+)/);
+ var matchedEdge = navigator.userAgent.match(/Edge\//);
+ // MS Edge pretends to be Chrome 42:
+ // https://msdn.microsoft.com/en-us/library/hh869301%28v=vs.85%29.aspx
+ resolve(matchedEdge || !matchedChrome || parseInt(matchedChrome[1], 10) >= 43);
+ };
+ })["catch"](function () {
+ return false; // error, so assume unsupported
+ });
+}
+
+function _checkBlobSupport(idb) {
+ if (typeof supportsBlobs === 'boolean') {
+ return Promise$1.resolve(supportsBlobs);
+ }
+ return _checkBlobSupportWithoutCaching(idb).then(function (value) {
+ supportsBlobs = value;
+ return supportsBlobs;
+ });
+}
+
+function _deferReadiness(dbInfo) {
+ var dbContext = dbContexts[dbInfo.name];
+
+ // Create a deferred object representing the current database operation.
+ var deferredOperation = {};
+
+ deferredOperation.promise = new Promise$1(function (resolve, reject) {
+ deferredOperation.resolve = resolve;
+ deferredOperation.reject = reject;
+ });
+
+ // Enqueue the deferred operation.
+ dbContext.deferredOperations.push(deferredOperation);
+
+ // Chain its promise to the database readiness.
+ if (!dbContext.dbReady) {
+ dbContext.dbReady = deferredOperation.promise;
+ } else {
+ dbContext.dbReady = dbContext.dbReady.then(function () {
+ return deferredOperation.promise;
+ });
+ }
+}
+
+function _advanceReadiness(dbInfo) {
+ var dbContext = dbContexts[dbInfo.name];
+
+ // Dequeue a deferred operation.
+ var deferredOperation = dbContext.deferredOperations.pop();
+
+ // Resolve its promise (which is part of the database readiness
+ // chain of promises).
+ if (deferredOperation) {
+ deferredOperation.resolve();
+ return deferredOperation.promise;
+ }
+}
+
+function _rejectReadiness(dbInfo, err) {
+ var dbContext = dbContexts[dbInfo.name];
+
+ // Dequeue a deferred operation.
+ var deferredOperation = dbContext.deferredOperations.pop();
+
+ // Reject its promise (which is part of the database readiness
+ // chain of promises).
+ if (deferredOperation) {
+ deferredOperation.reject(err);
+ return deferredOperation.promise;
+ }
+}
+
+function _getConnection(dbInfo, upgradeNeeded) {
+ return new Promise$1(function (resolve, reject) {
+ dbContexts[dbInfo.name] = dbContexts[dbInfo.name] || createDbContext();
+
+ if (dbInfo.db) {
+ if (upgradeNeeded) {
+ _deferReadiness(dbInfo);
+ dbInfo.db.close();
+ } else {
+ return resolve(dbInfo.db);
+ }
+ }
+
+ var dbArgs = [dbInfo.name];
+
+ if (upgradeNeeded) {
+ dbArgs.push(dbInfo.version);
+ }
+
+ var openreq = idb.open.apply(idb, dbArgs);
+
+ if (upgradeNeeded) {
+ openreq.onupgradeneeded = function (e) {
+ var db = openreq.result;
+ try {
+ db.createObjectStore(dbInfo.storeName);
+ if (e.oldVersion <= 1) {
+ // Added when support for blob shims was added
+ db.createObjectStore(DETECT_BLOB_SUPPORT_STORE);
+ }
+ } catch (ex) {
+ if (ex.name === 'ConstraintError') {
+ console.warn('The database "' + dbInfo.name + '"' + ' has been upgraded from version ' + e.oldVersion + ' to version ' + e.newVersion + ', but the storage "' + dbInfo.storeName + '" already exists.');
+ } else {
+ throw ex;
+ }
+ }
+ };
+ }
+
+ openreq.onerror = function (e) {
+ e.preventDefault();
+ reject(openreq.error);
+ };
+
+ openreq.onsuccess = function () {
+ resolve(openreq.result);
+ _advanceReadiness(dbInfo);
+ };
+ });
+}
+
+function _getOriginalConnection(dbInfo) {
+ return _getConnection(dbInfo, false);
+}
+
+function _getUpgradedConnection(dbInfo) {
+ return _getConnection(dbInfo, true);
+}
+
+function _isUpgradeNeeded(dbInfo, defaultVersion) {
+ if (!dbInfo.db) {
+ return true;
+ }
+
+ var isNewStore = !dbInfo.db.objectStoreNames.contains(dbInfo.storeName);
+ var isDowngrade = dbInfo.version < dbInfo.db.version;
+ var isUpgrade = dbInfo.version > dbInfo.db.version;
+
+ if (isDowngrade) {
+ // If the version is not the default one
+ // then warn for impossible downgrade.
+ if (dbInfo.version !== defaultVersion) {
+ console.warn('The database "' + dbInfo.name + '"' + " can't be downgraded from version " + dbInfo.db.version + ' to version ' + dbInfo.version + '.');
+ }
+ // Align the versions to prevent errors.
+ dbInfo.version = dbInfo.db.version;
+ }
+
+ if (isUpgrade || isNewStore) {
+ // If the store is new then increment the version (if needed).
+ // This will trigger an "upgradeneeded" event which is required
+ // for creating a store.
+ if (isNewStore) {
+ var incVersion = dbInfo.db.version + 1;
+ if (incVersion > dbInfo.version) {
+ dbInfo.version = incVersion;
+ }
+ }
+
+ return true;
+ }
+
+ return false;
+}
+
+// encode a blob for indexeddb engines that don't support blobs
+function _encodeBlob(blob) {
+ return new Promise$1(function (resolve, reject) {
+ var reader = new FileReader();
+ reader.onerror = reject;
+ reader.onloadend = function (e) {
+ var base64 = btoa(e.target.result || '');
+ resolve({
+ __local_forage_encoded_blob: true,
+ data: base64,
+ type: blob.type
+ });
+ };
+ reader.readAsBinaryString(blob);
+ });
+}
+
+// decode an encoded blob
+function _decodeBlob(encodedBlob) {
+ var arrayBuff = _binStringToArrayBuffer(atob(encodedBlob.data));
+ return createBlob([arrayBuff], { type: encodedBlob.type });
+}
+
+// is this one of our fancy encoded blobs?
+function _isEncodedBlob(value) {
+ return value && value.__local_forage_encoded_blob;
+}
+
+// Specialize the default `ready()` function by making it dependent
+// on the current database operations. Thus, the driver will be actually
+// ready when it's been initialized (default) *and* there are no pending
+// operations on the database (initiated by some other instances).
+function _fullyReady(callback) {
+ var self = this;
+
+ var promise = self._initReady().then(function () {
+ var dbContext = dbContexts[self._dbInfo.name];
+
+ if (dbContext && dbContext.dbReady) {
+ return dbContext.dbReady;
+ }
+ });
+
+ executeTwoCallbacks(promise, callback, callback);
+ return promise;
+}
+
+// Try to establish a new db connection to replace the
+// current one which is broken (i.e. experiencing
+// InvalidStateError while creating a transaction).
+function _tryReconnect(dbInfo) {
+ _deferReadiness(dbInfo);
+
+ var dbContext = dbContexts[dbInfo.name];
+ var forages = dbContext.forages;
+
+ for (var i = 0; i < forages.length; i++) {
+ var forage = forages[i];
+ if (forage._dbInfo.db) {
+ forage._dbInfo.db.close();
+ forage._dbInfo.db = null;
+ }
+ }
+ dbInfo.db = null;
+
+ return _getOriginalConnection(dbInfo).then(function (db) {
+ dbInfo.db = db;
+ if (_isUpgradeNeeded(dbInfo)) {
+ // Reopen the database for upgrading.
+ return _getUpgradedConnection(dbInfo);
+ }
+ return db;
+ }).then(function (db) {
+ // store the latest db reference
+ // in case the db was upgraded
+ dbInfo.db = dbContext.db = db;
+ for (var i = 0; i < forages.length; i++) {
+ forages[i]._dbInfo.db = db;
+ }
+ })["catch"](function (err) {
+ _rejectReadiness(dbInfo, err);
+ throw err;
+ });
+}
+
+// FF doesn't like Promises (micro-tasks) and IDDB store operations,
+// so we have to do it with callbacks
+function createTransaction(dbInfo, mode, callback, retries) {
+ if (retries === undefined) {
+ retries = 1;
+ }
+
+ try {
+ var tx = dbInfo.db.transaction(dbInfo.storeName, mode);
+ callback(null, tx);
+ } catch (err) {
+ if (retries > 0 && (!dbInfo.db || err.name === 'InvalidStateError' || err.name === 'NotFoundError')) {
+ return Promise$1.resolve().then(function () {
+ if (!dbInfo.db || err.name === 'NotFoundError' && !dbInfo.db.objectStoreNames.contains(dbInfo.storeName) && dbInfo.version <= dbInfo.db.version) {
+ // increase the db version, to create the new ObjectStore
+ if (dbInfo.db) {
+ dbInfo.version = dbInfo.db.version + 1;
+ }
+ // Reopen the database for upgrading.
+ return _getUpgradedConnection(dbInfo);
+ }
+ }).then(function () {
+ return _tryReconnect(dbInfo).then(function () {
+ createTransaction(dbInfo, mode, callback, retries - 1);
+ });
+ })["catch"](callback);
+ }
+
+ callback(err);
+ }
+}
+
+function createDbContext() {
+ return {
+ // Running localForages sharing a database.
+ forages: [],
+ // Shared database.
+ db: null,
+ // Database readiness (promise).
+ dbReady: null,
+ // Deferred operations on the database.
+ deferredOperations: []
+ };
+}
+
+// Open the IndexedDB database (automatically creates one if one didn't
+// previously exist), using any options set in the config.
+function _initStorage(options) {
+ var self = this;
+ var dbInfo = {
+ db: null
+ };
+
+ if (options) {
+ for (var i in options) {
+ dbInfo[i] = options[i];
+ }
+ }
+
+ // Get the current context of the database;
+ var dbContext = dbContexts[dbInfo.name];
+
+ // ...or create a new context.
+ if (!dbContext) {
+ dbContext = createDbContext();
+ // Register the new context in the global container.
+ dbContexts[dbInfo.name] = dbContext;
+ }
+
+ // Register itself as a running localForage in the current context.
+ dbContext.forages.push(self);
+
+ // Replace the default `ready()` function with the specialized one.
+ if (!self._initReady) {
+ self._initReady = self.ready;
+ self.ready = _fullyReady;
+ }
+
+ // Create an array of initialization states of the related localForages.
+ var initPromises = [];
+
+ function ignoreErrors() {
+ // Don't handle errors here,
+ // just makes sure related localForages aren't pending.
+ return Promise$1.resolve();
+ }
+
+ for (var j = 0; j < dbContext.forages.length; j++) {
+ var forage = dbContext.forages[j];
+ if (forage !== self) {
+ // Don't wait for itself...
+ initPromises.push(forage._initReady()["catch"](ignoreErrors));
+ }
+ }
+
+ // Take a snapshot of the related localForages.
+ var forages = dbContext.forages.slice(0);
+
+ // Initialize the connection process only when
+ // all the related localForages aren't pending.
+ return Promise$1.all(initPromises).then(function () {
+ dbInfo.db = dbContext.db;
+ // Get the connection or open a new one without upgrade.
+ return _getOriginalConnection(dbInfo);
+ }).then(function (db) {
+ dbInfo.db = db;
+ if (_isUpgradeNeeded(dbInfo, self._defaultConfig.version)) {
+ // Reopen the database for upgrading.
+ return _getUpgradedConnection(dbInfo);
+ }
+ return db;
+ }).then(function (db) {
+ dbInfo.db = dbContext.db = db;
+ self._dbInfo = dbInfo;
+ // Share the final connection amongst related localForages.
+ for (var k = 0; k < forages.length; k++) {
+ var forage = forages[k];
+ if (forage !== self) {
+ // Self is already up-to-date.
+ forage._dbInfo.db = dbInfo.db;
+ forage._dbInfo.version = dbInfo.version;
+ }
+ }
+ });
+}
+
+function getItem(key, callback) {
+ var self = this;
+
+ key = normalizeKey(key);
+
+ var promise = new Promise$1(function (resolve, reject) {
+ self.ready().then(function () {
+ createTransaction(self._dbInfo, READ_ONLY, function (err, transaction) {
+ if (err) {
+ return reject(err);
+ }
+
+ try {
+ var store = transaction.objectStore(self._dbInfo.storeName);
+ var req = store.get(key);
+
+ req.onsuccess = function () {
+ var value = req.result;
+ if (value === undefined) {
+ value = null;
+ }
+ if (_isEncodedBlob(value)) {
+ value = _decodeBlob(value);
+ }
+ resolve(value);
+ };
+
+ req.onerror = function () {
+ reject(req.error);
+ };
+ } catch (e) {
+ reject(e);
+ }
+ });
+ })["catch"](reject);
+ });
+
+ executeCallback(promise, callback);
+ return promise;
+}
+
+// Iterate over all items stored in database.
+function iterate(iterator, callback) {
+ var self = this;
+
+ var promise = new Promise$1(function (resolve, reject) {
+ self.ready().then(function () {
+ createTransaction(self._dbInfo, READ_ONLY, function (err, transaction) {
+ if (err) {
+ return reject(err);
+ }
+
+ try {
+ var store = transaction.objectStore(self._dbInfo.storeName);
+ var req = store.openCursor();
+ var iterationNumber = 1;
+
+ req.onsuccess = function () {
+ var cursor = req.result;
+
+ if (cursor) {
+ var value = cursor.value;
+ if (_isEncodedBlob(value)) {
+ value = _decodeBlob(value);
+ }
+ var result = iterator(value, cursor.key, iterationNumber++);
+
+ // when the iterator callback retuns any
+ // (non-`undefined`) value, then we stop
+ // the iteration immediately
+ if (result !== void 0) {
+ resolve(result);
+ } else {
+ cursor["continue"]();
+ }
+ } else {
+ resolve();
+ }
+ };
+
+ req.onerror = function () {
+ reject(req.error);
+ };
+ } catch (e) {
+ reject(e);
+ }
+ });
+ })["catch"](reject);
+ });
+
+ executeCallback(promise, callback);
+
+ return promise;
+}
+
+function setItem(key, value, callback) {
+ var self = this;
+
+ key = normalizeKey(key);
+
+ var promise = new Promise$1(function (resolve, reject) {
+ var dbInfo;
+ self.ready().then(function () {
+ dbInfo = self._dbInfo;
+ if (toString.call(value) === '[object Blob]') {
+ return _checkBlobSupport(dbInfo.db).then(function (blobSupport) {
+ if (blobSupport) {
+ return value;
+ }
+ return _encodeBlob(value);
+ });
+ }
+ return value;
+ }).then(function (value) {
+ createTransaction(self._dbInfo, READ_WRITE, function (err, transaction) {
+ if (err) {
+ return reject(err);
+ }
+
+ try {
+ var store = transaction.objectStore(self._dbInfo.storeName);
+
+ // The reason we don't _save_ null is because IE 10 does
+ // not support saving the `null` type in IndexedDB. How
+ // ironic, given the bug below!
+ // See: https://github.com/mozilla/localForage/issues/161
+ if (value === null) {
+ value = undefined;
+ }
+
+ var req = store.put(value, key);
+
+ transaction.oncomplete = function () {
+ // Cast to undefined so the value passed to
+ // callback/promise is the same as what one would get out
+ // of `getItem()` later. This leads to some weirdness
+ // (setItem('foo', undefined) will return `null`), but
+ // it's not my fault localStorage is our baseline and that
+ // it's weird.
+ if (value === undefined) {
+ value = null;
+ }
+
+ resolve(value);
+ };
+ transaction.onabort = transaction.onerror = function () {
+ var err = req.error ? req.error : req.transaction.error;
+ reject(err);
+ };
+ } catch (e) {
+ reject(e);
+ }
+ });
+ })["catch"](reject);
+ });
+
+ executeCallback(promise, callback);
+ return promise;
+}
+
+function removeItem(key, callback) {
+ var self = this;
+
+ key = normalizeKey(key);
+
+ var promise = new Promise$1(function (resolve, reject) {
+ self.ready().then(function () {
+ createTransaction(self._dbInfo, READ_WRITE, function (err, transaction) {
+ if (err) {
+ return reject(err);
+ }
+
+ try {
+ var store = transaction.objectStore(self._dbInfo.storeName);
+ // We use a Grunt task to make this safe for IE and some
+ // versions of Android (including those used by Cordova).
+ // Normally IE won't like `.delete()` and will insist on
+ // using `['delete']()`, but we have a build step that
+ // fixes this for us now.
+ var req = store["delete"](key);
+ transaction.oncomplete = function () {
+ resolve();
+ };
+
+ transaction.onerror = function () {
+ reject(req.error);
+ };
+
+ // The request will be also be aborted if we've exceeded our storage
+ // space.
+ transaction.onabort = function () {
+ var err = req.error ? req.error : req.transaction.error;
+ reject(err);
+ };
+ } catch (e) {
+ reject(e);
+ }
+ });
+ })["catch"](reject);
+ });
+
+ executeCallback(promise, callback);
+ return promise;
+}
+
+function clear(callback) {
+ var self = this;
+
+ var promise = new Promise$1(function (resolve, reject) {
+ self.ready().then(function () {
+ createTransaction(self._dbInfo, READ_WRITE, function (err, transaction) {
+ if (err) {
+ return reject(err);
+ }
+
+ try {
+ var store = transaction.objectStore(self._dbInfo.storeName);
+ var req = store.clear();
+
+ transaction.oncomplete = function () {
+ resolve();
+ };
+
+ transaction.onabort = transaction.onerror = function () {
+ var err = req.error ? req.error : req.transaction.error;
+ reject(err);
+ };
+ } catch (e) {
+ reject(e);
+ }
+ });
+ })["catch"](reject);
+ });
+
+ executeCallback(promise, callback);
+ return promise;
+}
+
+function length(callback) {
+ var self = this;
+
+ var promise = new Promise$1(function (resolve, reject) {
+ self.ready().then(function () {
+ createTransaction(self._dbInfo, READ_ONLY, function (err, transaction) {
+ if (err) {
+ return reject(err);
+ }
+
+ try {
+ var store = transaction.objectStore(self._dbInfo.storeName);
+ var req = store.count();
+
+ req.onsuccess = function () {
+ resolve(req.result);
+ };
+
+ req.onerror = function () {
+ reject(req.error);
+ };
+ } catch (e) {
+ reject(e);
+ }
+ });
+ })["catch"](reject);
+ });
+
+ executeCallback(promise, callback);
+ return promise;
+}
+
+function key(n, callback) {
+ var self = this;
+
+ var promise = new Promise$1(function (resolve, reject) {
+ if (n < 0) {
+ resolve(null);
+
+ return;
+ }
+
+ self.ready().then(function () {
+ createTransaction(self._dbInfo, READ_ONLY, function (err, transaction) {
+ if (err) {
+ return reject(err);
+ }
+
+ try {
+ var store = transaction.objectStore(self._dbInfo.storeName);
+ var advanced = false;
+ var req = store.openCursor();
+
+ req.onsuccess = function () {
+ var cursor = req.result;
+ if (!cursor) {
+ // this means there weren't enough keys
+ resolve(null);
+
+ return;
+ }
+
+ if (n === 0) {
+ // We have the first key, return it if that's what they
+ // wanted.
+ resolve(cursor.key);
+ } else {
+ if (!advanced) {
+ // Otherwise, ask the cursor to skip ahead n
+ // records.
+ advanced = true;
+ cursor.advance(n);
+ } else {
+ // When we get here, we've got the nth key.
+ resolve(cursor.key);
+ }
+ }
+ };
+
+ req.onerror = function () {
+ reject(req.error);
+ };
+ } catch (e) {
+ reject(e);
+ }
+ });
+ })["catch"](reject);
+ });
+
+ executeCallback(promise, callback);
+ return promise;
+}
+
+function keys(callback) {
+ var self = this;
+
+ var promise = new Promise$1(function (resolve, reject) {
+ self.ready().then(function () {
+ createTransaction(self._dbInfo, READ_ONLY, function (err, transaction) {
+ if (err) {
+ return reject(err);
+ }
+
+ try {
+ var store = transaction.objectStore(self._dbInfo.storeName);
+ var req = store.openCursor();
+ var keys = [];
+
+ req.onsuccess = function () {
+ var cursor = req.result;
+
+ if (!cursor) {
+ resolve(keys);
+ return;
+ }
+
+ keys.push(cursor.key);
+ cursor["continue"]();
+ };
+
+ req.onerror = function () {
+ reject(req.error);
+ };
+ } catch (e) {
+ reject(e);
+ }
+ });
+ })["catch"](reject);
+ });
+
+ executeCallback(promise, callback);
+ return promise;
+}
+
+function dropInstance(options, callback) {
+ callback = getCallback.apply(this, arguments);
+
+ var currentConfig = this.config();
+ options = typeof options !== 'function' && options || {};
+ if (!options.name) {
+ options.name = options.name || currentConfig.name;
+ options.storeName = options.storeName || currentConfig.storeName;
+ }
+
+ var self = this;
+ var promise;
+ if (!options.name) {
+ promise = Promise$1.reject('Invalid arguments');
+ } else {
+ var isCurrentDb = options.name === currentConfig.name && self._dbInfo.db;
+
+ var dbPromise = isCurrentDb ? Promise$1.resolve(self._dbInfo.db) : _getOriginalConnection(options).then(function (db) {
+ var dbContext = dbContexts[options.name];
+ var forages = dbContext.forages;
+ dbContext.db = db;
+ for (var i = 0; i < forages.length; i++) {
+ forages[i]._dbInfo.db = db;
+ }
+ return db;
+ });
+
+ if (!options.storeName) {
+ promise = dbPromise.then(function (db) {
+ _deferReadiness(options);
+
+ var dbContext = dbContexts[options.name];
+ var forages = dbContext.forages;
+
+ db.close();
+ for (var i = 0; i < forages.length; i++) {
+ var forage = forages[i];
+ forage._dbInfo.db = null;
+ }
+
+ var dropDBPromise = new Promise$1(function (resolve, reject) {
+ var req = idb.deleteDatabase(options.name);
+
+ req.onerror = req.onblocked = function (err) {
+ var db = req.result;
+ if (db) {
+ db.close();
+ }
+ reject(err);
+ };
+
+ req.onsuccess = function () {
+ var db = req.result;
+ if (db) {
+ db.close();
+ }
+ resolve(db);
+ };
+ });
+
+ return dropDBPromise.then(function (db) {
+ dbContext.db = db;
+ for (var i = 0; i < forages.length; i++) {
+ var _forage = forages[i];
+ _advanceReadiness(_forage._dbInfo);
+ }
+ })["catch"](function (err) {
+ (_rejectReadiness(options, err) || Promise$1.resolve())["catch"](function () {});
+ throw err;
+ });
+ });
+ } else {
+ promise = dbPromise.then(function (db) {
+ if (!db.objectStoreNames.contains(options.storeName)) {
+ return;
+ }
+
+ var newVersion = db.version + 1;
+
+ _deferReadiness(options);
+
+ var dbContext = dbContexts[options.name];
+ var forages = dbContext.forages;
+
+ db.close();
+ for (var i = 0; i < forages.length; i++) {
+ var forage = forages[i];
+ forage._dbInfo.db = null;
+ forage._dbInfo.version = newVersion;
+ }
+
+ var dropObjectPromise = new Promise$1(function (resolve, reject) {
+ var req = idb.open(options.name, newVersion);
+
+ req.onerror = function (err) {
+ var db = req.result;
+ db.close();
+ reject(err);
+ };
+
+ req.onupgradeneeded = function () {
+ var db = req.result;
+ db.deleteObjectStore(options.storeName);
+ };
+
+ req.onsuccess = function () {
+ var db = req.result;
+ db.close();
+ resolve(db);
+ };
+ });
+
+ return dropObjectPromise.then(function (db) {
+ dbContext.db = db;
+ for (var j = 0; j < forages.length; j++) {
+ var _forage2 = forages[j];
+ _forage2._dbInfo.db = db;
+ _advanceReadiness(_forage2._dbInfo);
+ }
+ })["catch"](function (err) {
+ (_rejectReadiness(options, err) || Promise$1.resolve())["catch"](function () {});
+ throw err;
+ });
+ });
+ }
+ }
+
+ executeCallback(promise, callback);
+ return promise;
+}
+
+var asyncStorage = {
+ _driver: 'asyncStorage',
+ _initStorage: _initStorage,
+ _support: isIndexedDBValid(),
+ iterate: iterate,
+ getItem: getItem,
+ setItem: setItem,
+ removeItem: removeItem,
+ clear: clear,
+ length: length,
+ key: key,
+ keys: keys,
+ dropInstance: dropInstance
+};
+
+function isWebSQLValid() {
+ return typeof openDatabase === 'function';
+}
+
+// Sadly, the best way to save binary data in WebSQL/localStorage is serializing
+// it to Base64, so this is how we store it to prevent very strange errors with less
+// verbose ways of binary <-> string data storage.
+var BASE_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
+
+var BLOB_TYPE_PREFIX = '~~local_forage_type~';
+var BLOB_TYPE_PREFIX_REGEX = /^~~local_forage_type~([^~]+)~/;
+
+var SERIALIZED_MARKER = '__lfsc__:';
+var SERIALIZED_MARKER_LENGTH = SERIALIZED_MARKER.length;
+
+// OMG the serializations!
+var TYPE_ARRAYBUFFER = 'arbf';
+var TYPE_BLOB = 'blob';
+var TYPE_INT8ARRAY = 'si08';
+var TYPE_UINT8ARRAY = 'ui08';
+var TYPE_UINT8CLAMPEDARRAY = 'uic8';
+var TYPE_INT16ARRAY = 'si16';
+var TYPE_INT32ARRAY = 'si32';
+var TYPE_UINT16ARRAY = 'ur16';
+var TYPE_UINT32ARRAY = 'ui32';
+var TYPE_FLOAT32ARRAY = 'fl32';
+var TYPE_FLOAT64ARRAY = 'fl64';
+var TYPE_SERIALIZED_MARKER_LENGTH = SERIALIZED_MARKER_LENGTH + TYPE_ARRAYBUFFER.length;
+
+var toString$1 = Object.prototype.toString;
+
+function stringToBuffer(serializedString) {
+ // Fill the string into a ArrayBuffer.
+ var bufferLength = serializedString.length * 0.75;
+ var len = serializedString.length;
+ var i;
+ var p = 0;
+ var encoded1, encoded2, encoded3, encoded4;
+
+ if (serializedString[serializedString.length - 1] === '=') {
+ bufferLength--;
+ if (serializedString[serializedString.length - 2] === '=') {
+ bufferLength--;
+ }
+ }
+
+ var buffer = new ArrayBuffer(bufferLength);
+ var bytes = new Uint8Array(buffer);
+
+ for (i = 0; i < len; i += 4) {
+ encoded1 = BASE_CHARS.indexOf(serializedString[i]);
+ encoded2 = BASE_CHARS.indexOf(serializedString[i + 1]);
+ encoded3 = BASE_CHARS.indexOf(serializedString[i + 2]);
+ encoded4 = BASE_CHARS.indexOf(serializedString[i + 3]);
+
+ /*jslint bitwise: true */
+ bytes[p++] = encoded1 << 2 | encoded2 >> 4;
+ bytes[p++] = (encoded2 & 15) << 4 | encoded3 >> 2;
+ bytes[p++] = (encoded3 & 3) << 6 | encoded4 & 63;
+ }
+ return buffer;
+}
+
+// Converts a buffer to a string to store, serialized, in the backend
+// storage library.
+function bufferToString(buffer) {
+ // base64-arraybuffer
+ var bytes = new Uint8Array(buffer);
+ var base64String = '';
+ var i;
+
+ for (i = 0; i < bytes.length; i += 3) {
+ /*jslint bitwise: true */
+ base64String += BASE_CHARS[bytes[i] >> 2];
+ base64String += BASE_CHARS[(bytes[i] & 3) << 4 | bytes[i + 1] >> 4];
+ base64String += BASE_CHARS[(bytes[i + 1] & 15) << 2 | bytes[i + 2] >> 6];
+ base64String += BASE_CHARS[bytes[i + 2] & 63];
+ }
+
+ if (bytes.length % 3 === 2) {
+ base64String = base64String.substring(0, base64String.length - 1) + '=';
+ } else if (bytes.length % 3 === 1) {
+ base64String = base64String.substring(0, base64String.length - 2) + '==';
+ }
+
+ return base64String;
+}
+
+// Serialize a value, afterwards executing a callback (which usually
+// instructs the `setItem()` callback/promise to be executed). This is how
+// we store binary data with localStorage.
+function serialize(value, callback) {
+ var valueType = '';
+ if (value) {
+ valueType = toString$1.call(value);
+ }
+
+ // Cannot use `value instanceof ArrayBuffer` or such here, as these
+ // checks fail when running the tests using casper.js...
+ //
+ // TODO: See why those tests fail and use a better solution.
+ if (value && (valueType === '[object ArrayBuffer]' || value.buffer && toString$1.call(value.buffer) === '[object ArrayBuffer]')) {
+ // Convert binary arrays to a string and prefix the string with
+ // a special marker.
+ var buffer;
+ var marker = SERIALIZED_MARKER;
+
+ if (value instanceof ArrayBuffer) {
+ buffer = value;
+ marker += TYPE_ARRAYBUFFER;
+ } else {
+ buffer = value.buffer;
+
+ if (valueType === '[object Int8Array]') {
+ marker += TYPE_INT8ARRAY;
+ } else if (valueType === '[object Uint8Array]') {
+ marker += TYPE_UINT8ARRAY;
+ } else if (valueType === '[object Uint8ClampedArray]') {
+ marker += TYPE_UINT8CLAMPEDARRAY;
+ } else if (valueType === '[object Int16Array]') {
+ marker += TYPE_INT16ARRAY;
+ } else if (valueType === '[object Uint16Array]') {
+ marker += TYPE_UINT16ARRAY;
+ } else if (valueType === '[object Int32Array]') {
+ marker += TYPE_INT32ARRAY;
+ } else if (valueType === '[object Uint32Array]') {
+ marker += TYPE_UINT32ARRAY;
+ } else if (valueType === '[object Float32Array]') {
+ marker += TYPE_FLOAT32ARRAY;
+ } else if (valueType === '[object Float64Array]') {
+ marker += TYPE_FLOAT64ARRAY;
+ } else {
+ callback(new Error('Failed to get type for BinaryArray'));
+ }
+ }
+
+ callback(marker + bufferToString(buffer));
+ } else if (valueType === '[object Blob]') {
+ // Conver the blob to a binaryArray and then to a string.
+ var fileReader = new FileReader();
+
+ fileReader.onload = function () {
+ // Backwards-compatible prefix for the blob type.
+ var str = BLOB_TYPE_PREFIX + value.type + '~' + bufferToString(this.result);
+
+ callback(SERIALIZED_MARKER + TYPE_BLOB + str);
+ };
+
+ fileReader.readAsArrayBuffer(value);
+ } else {
+ try {
+ callback(JSON.stringify(value));
+ } catch (e) {
+ console.error("Couldn't convert value into a JSON string: ", value);
+
+ callback(null, e);
+ }
+ }
+}
+
+// Deserialize data we've inserted into a value column/field. We place
+// special markers into our strings to mark them as encoded; this isn't
+// as nice as a meta field, but it's the only sane thing we can do whilst
+// keeping localStorage support intact.
+//
+// Oftentimes this will just deserialize JSON content, but if we have a
+// special marker (SERIALIZED_MARKER, defined above), we will extract
+// some kind of arraybuffer/binary data/typed array out of the string.
+function deserialize(value) {
+ // If we haven't marked this string as being specially serialized (i.e.
+ // something other than serialized JSON), we can just return it and be
+ // done with it.
+ if (value.substring(0, SERIALIZED_MARKER_LENGTH) !== SERIALIZED_MARKER) {
+ return JSON.parse(value);
+ }
+
+ // The following code deals with deserializing some kind of Blob or
+ // TypedArray. First we separate out the type of data we're dealing
+ // with from the data itself.
+ var serializedString = value.substring(TYPE_SERIALIZED_MARKER_LENGTH);
+ var type = value.substring(SERIALIZED_MARKER_LENGTH, TYPE_SERIALIZED_MARKER_LENGTH);
+
+ var blobType;
+ // Backwards-compatible blob type serialization strategy.
+ // DBs created with older versions of localForage will simply not have the blob type.
+ if (type === TYPE_BLOB && BLOB_TYPE_PREFIX_REGEX.test(serializedString)) {
+ var matcher = serializedString.match(BLOB_TYPE_PREFIX_REGEX);
+ blobType = matcher[1];
+ serializedString = serializedString.substring(matcher[0].length);
+ }
+ var buffer = stringToBuffer(serializedString);
+
+ // Return the right type based on the code/type set during
+ // serialization.
+ switch (type) {
+ case TYPE_ARRAYBUFFER:
+ return buffer;
+ case TYPE_BLOB:
+ return createBlob([buffer], { type: blobType });
+ case TYPE_INT8ARRAY:
+ return new Int8Array(buffer);
+ case TYPE_UINT8ARRAY:
+ return new Uint8Array(buffer);
+ case TYPE_UINT8CLAMPEDARRAY:
+ return new Uint8ClampedArray(buffer);
+ case TYPE_INT16ARRAY:
+ return new Int16Array(buffer);
+ case TYPE_UINT16ARRAY:
+ return new Uint16Array(buffer);
+ case TYPE_INT32ARRAY:
+ return new Int32Array(buffer);
+ case TYPE_UINT32ARRAY:
+ return new Uint32Array(buffer);
+ case TYPE_FLOAT32ARRAY:
+ return new Float32Array(buffer);
+ case TYPE_FLOAT64ARRAY:
+ return new Float64Array(buffer);
+ default:
+ throw new Error('Unkown type: ' + type);
+ }
+}
+
+var localforageSerializer = {
+ serialize: serialize,
+ deserialize: deserialize,
+ stringToBuffer: stringToBuffer,
+ bufferToString: bufferToString
+};
+
+/*
+ * Includes code from:
+ *
+ * base64-arraybuffer
+ * https://github.com/niklasvh/base64-arraybuffer
+ *
+ * Copyright (c) 2012 Niklas von Hertzen
+ * Licensed under the MIT license.
+ */
+
+function createDbTable(t, dbInfo, callback, errorCallback) {
+ t.executeSql('CREATE TABLE IF NOT EXISTS ' + dbInfo.storeName + ' ' + '(id INTEGER PRIMARY KEY, key unique, value)', [], callback, errorCallback);
+}
+
+// Open the WebSQL database (automatically creates one if one didn't
+// previously exist), using any options set in the config.
+function _initStorage$1(options) {
+ var self = this;
+ var dbInfo = {
+ db: null
+ };
+
+ if (options) {
+ for (var i in options) {
+ dbInfo[i] = typeof options[i] !== 'string' ? options[i].toString() : options[i];
+ }
+ }
+
+ var dbInfoPromise = new Promise$1(function (resolve, reject) {
+ // Open the database; the openDatabase API will automatically
+ // create it for us if it doesn't exist.
+ try {
+ dbInfo.db = openDatabase(dbInfo.name, String(dbInfo.version), dbInfo.description, dbInfo.size);
+ } catch (e) {
+ return reject(e);
+ }
+
+ // Create our key/value table if it doesn't exist.
+ dbInfo.db.transaction(function (t) {
+ createDbTable(t, dbInfo, function () {
+ self._dbInfo = dbInfo;
+ resolve();
+ }, function (t, error) {
+ reject(error);
+ });
+ }, reject);
+ });
+
+ dbInfo.serializer = localforageSerializer;
+ return dbInfoPromise;
+}
+
+function tryExecuteSql(t, dbInfo, sqlStatement, args, callback, errorCallback) {
+ t.executeSql(sqlStatement, args, callback, function (t, error) {
+ if (error.code === error.SYNTAX_ERR) {
+ t.executeSql('SELECT name FROM sqlite_master ' + "WHERE type='table' AND name = ?", [dbInfo.storeName], function (t, results) {
+ if (!results.rows.length) {
+ // if the table is missing (was deleted)
+ // re-create it table and retry
+ createDbTable(t, dbInfo, function () {
+ t.executeSql(sqlStatement, args, callback, errorCallback);
+ }, errorCallback);
+ } else {
+ errorCallback(t, error);
+ }
+ }, errorCallback);
+ } else {
+ errorCallback(t, error);
+ }
+ }, errorCallback);
+}
+
+function getItem$1(key, callback) {
+ var self = this;
+
+ key = normalizeKey(key);
+
+ var promise = new Promise$1(function (resolve, reject) {
+ self.ready().then(function () {
+ var dbInfo = self._dbInfo;
+ dbInfo.db.transaction(function (t) {
+ tryExecuteSql(t, dbInfo, 'SELECT * FROM ' + dbInfo.storeName + ' WHERE key = ? LIMIT 1', [key], function (t, results) {
+ var result = results.rows.length ? results.rows.item(0).value : null;
+
+ // Check to see if this is serialized content we need to
+ // unpack.
+ if (result) {
+ result = dbInfo.serializer.deserialize(result);
+ }
+
+ resolve(result);
+ }, function (t, error) {
+ reject(error);
+ });
+ });
+ })["catch"](reject);
+ });
+
+ executeCallback(promise, callback);
+ return promise;
+}
+
+function iterate$1(iterator, callback) {
+ var self = this;
+
+ var promise = new Promise$1(function (resolve, reject) {
+ self.ready().then(function () {
+ var dbInfo = self._dbInfo;
+
+ dbInfo.db.transaction(function (t) {
+ tryExecuteSql(t, dbInfo, 'SELECT * FROM ' + dbInfo.storeName, [], function (t, results) {
+ var rows = results.rows;
+ var length = rows.length;
+
+ for (var i = 0; i < length; i++) {
+ var item = rows.item(i);
+ var result = item.value;
+
+ // Check to see if this is serialized content
+ // we need to unpack.
+ if (result) {
+ result = dbInfo.serializer.deserialize(result);
+ }
+
+ result = iterator(result, item.key, i + 1);
+
+ // void(0) prevents problems with redefinition
+ // of `undefined`.
+ if (result !== void 0) {
+ resolve(result);
+ return;
+ }
+ }
+
+ resolve();
+ }, function (t, error) {
+ reject(error);
+ });
+ });
+ })["catch"](reject);
+ });
+
+ executeCallback(promise, callback);
+ return promise;
+}
+
+function _setItem(key, value, callback, retriesLeft) {
+ var self = this;
+
+ key = normalizeKey(key);
+
+ var promise = new Promise$1(function (resolve, reject) {
+ self.ready().then(function () {
+ // The localStorage API doesn't return undefined values in an
+ // "expected" way, so undefined is always cast to null in all
+ // drivers. See: https://github.com/mozilla/localForage/pull/42
+ if (value === undefined) {
+ value = null;
+ }
+
+ // Save the original value to pass to the callback.
+ var originalValue = value;
+
+ var dbInfo = self._dbInfo;
+ dbInfo.serializer.serialize(value, function (value, error) {
+ if (error) {
+ reject(error);
+ } else {
+ dbInfo.db.transaction(function (t) {
+ tryExecuteSql(t, dbInfo, 'INSERT OR REPLACE INTO ' + dbInfo.storeName + ' ' + '(key, value) VALUES (?, ?)', [key, value], function () {
+ resolve(originalValue);
+ }, function (t, error) {
+ reject(error);
+ });
+ }, function (sqlError) {
+ // The transaction failed; check
+ // to see if it's a quota error.
+ if (sqlError.code === sqlError.QUOTA_ERR) {
+ // We reject the callback outright for now, but
+ // it's worth trying to re-run the transaction.
+ // Even if the user accepts the prompt to use
+ // more storage on Safari, this error will
+ // be called.
+ //
+ // Try to re-run the transaction.
+ if (retriesLeft > 0) {
+ resolve(_setItem.apply(self, [key, originalValue, callback, retriesLeft - 1]));
+ return;
+ }
+ reject(sqlError);
+ }
+ });
+ }
+ });
+ })["catch"](reject);
+ });
+
+ executeCallback(promise, callback);
+ return promise;
+}
+
+function setItem$1(key, value, callback) {
+ return _setItem.apply(this, [key, value, callback, 1]);
+}
+
+function removeItem$1(key, callback) {
+ var self = this;
+
+ key = normalizeKey(key);
+
+ var promise = new Promise$1(function (resolve, reject) {
+ self.ready().then(function () {
+ var dbInfo = self._dbInfo;
+ dbInfo.db.transaction(function (t) {
+ tryExecuteSql(t, dbInfo, 'DELETE FROM ' + dbInfo.storeName + ' WHERE key = ?', [key], function () {
+ resolve();
+ }, function (t, error) {
+ reject(error);
+ });
+ });
+ })["catch"](reject);
+ });
+
+ executeCallback(promise, callback);
+ return promise;
+}
+
+// Deletes every item in the table.
+// TODO: Find out if this resets the AUTO_INCREMENT number.
+function clear$1(callback) {
+ var self = this;
+
+ var promise = new Promise$1(function (resolve, reject) {
+ self.ready().then(function () {
+ var dbInfo = self._dbInfo;
+ dbInfo.db.transaction(function (t) {
+ tryExecuteSql(t, dbInfo, 'DELETE FROM ' + dbInfo.storeName, [], function () {
+ resolve();
+ }, function (t, error) {
+ reject(error);
+ });
+ });
+ })["catch"](reject);
+ });
+
+ executeCallback(promise, callback);
+ return promise;
+}
+
+// Does a simple `COUNT(key)` to get the number of items stored in
+// localForage.
+function length$1(callback) {
+ var self = this;
+
+ var promise = new Promise$1(function (resolve, reject) {
+ self.ready().then(function () {
+ var dbInfo = self._dbInfo;
+ dbInfo.db.transaction(function (t) {
+ // Ahhh, SQL makes this one soooooo easy.
+ tryExecuteSql(t, dbInfo, 'SELECT COUNT(key) as c FROM ' + dbInfo.storeName, [], function (t, results) {
+ var result = results.rows.item(0).c;
+ resolve(result);
+ }, function (t, error) {
+ reject(error);
+ });
+ });
+ })["catch"](reject);
+ });
+
+ executeCallback(promise, callback);
+ return promise;
+}
+
+// Return the key located at key index X; essentially gets the key from a
+// `WHERE id = ?`. This is the most efficient way I can think to implement
+// this rarely-used (in my experience) part of the API, but it can seem
+// inconsistent, because we do `INSERT OR REPLACE INTO` on `setItem()`, so
+// the ID of each key will change every time it's updated. Perhaps a stored
+// procedure for the `setItem()` SQL would solve this problem?
+// TODO: Don't change ID on `setItem()`.
+function key$1(n, callback) {
+ var self = this;
+
+ var promise = new Promise$1(function (resolve, reject) {
+ self.ready().then(function () {
+ var dbInfo = self._dbInfo;
+ dbInfo.db.transaction(function (t) {
+ tryExecuteSql(t, dbInfo, 'SELECT key FROM ' + dbInfo.storeName + ' WHERE id = ? LIMIT 1', [n + 1], function (t, results) {
+ var result = results.rows.length ? results.rows.item(0).key : null;
+ resolve(result);
+ }, function (t, error) {
+ reject(error);
+ });
+ });
+ })["catch"](reject);
+ });
+
+ executeCallback(promise, callback);
+ return promise;
+}
+
+function keys$1(callback) {
+ var self = this;
+
+ var promise = new Promise$1(function (resolve, reject) {
+ self.ready().then(function () {
+ var dbInfo = self._dbInfo;
+ dbInfo.db.transaction(function (t) {
+ tryExecuteSql(t, dbInfo, 'SELECT key FROM ' + dbInfo.storeName, [], function (t, results) {
+ var keys = [];
+
+ for (var i = 0; i < results.rows.length; i++) {
+ keys.push(results.rows.item(i).key);
+ }
+
+ resolve(keys);
+ }, function (t, error) {
+ reject(error);
+ });
+ });
+ })["catch"](reject);
+ });
+
+ executeCallback(promise, callback);
+ return promise;
+}
+
+// https://www.w3.org/TR/webdatabase/#databases
+// > There is no way to enumerate or delete the databases available for an origin from this API.
+function getAllStoreNames(db) {
+ return new Promise$1(function (resolve, reject) {
+ db.transaction(function (t) {
+ t.executeSql('SELECT name FROM sqlite_master ' + "WHERE type='table' AND name <> '__WebKitDatabaseInfoTable__'", [], function (t, results) {
+ var storeNames = [];
+
+ for (var i = 0; i < results.rows.length; i++) {
+ storeNames.push(results.rows.item(i).name);
+ }
+
+ resolve({
+ db: db,
+ storeNames: storeNames
+ });
+ }, function (t, error) {
+ reject(error);
+ });
+ }, function (sqlError) {
+ reject(sqlError);
+ });
+ });
+}
+
+function dropInstance$1(options, callback) {
+ callback = getCallback.apply(this, arguments);
+
+ var currentConfig = this.config();
+ options = typeof options !== 'function' && options || {};
+ if (!options.name) {
+ options.name = options.name || currentConfig.name;
+ options.storeName = options.storeName || currentConfig.storeName;
+ }
+
+ var self = this;
+ var promise;
+ if (!options.name) {
+ promise = Promise$1.reject('Invalid arguments');
+ } else {
+ promise = new Promise$1(function (resolve) {
+ var db;
+ if (options.name === currentConfig.name) {
+ // use the db reference of the current instance
+ db = self._dbInfo.db;
+ } else {
+ db = openDatabase(options.name, '', '', 0);
+ }
+
+ if (!options.storeName) {
+ // drop all database tables
+ resolve(getAllStoreNames(db));
+ } else {
+ resolve({
+ db: db,
+ storeNames: [options.storeName]
+ });
+ }
+ }).then(function (operationInfo) {
+ return new Promise$1(function (resolve, reject) {
+ operationInfo.db.transaction(function (t) {
+ function dropTable(storeName) {
+ return new Promise$1(function (resolve, reject) {
+ t.executeSql('DROP TABLE IF EXISTS ' + storeName, [], function () {
+ resolve();
+ }, function (t, error) {
+ reject(error);
+ });
+ });
+ }
+
+ var operations = [];
+ for (var i = 0, len = operationInfo.storeNames.length; i < len; i++) {
+ operations.push(dropTable(operationInfo.storeNames[i]));
+ }
+
+ Promise$1.all(operations).then(function () {
+ resolve();
+ })["catch"](function (e) {
+ reject(e);
+ });
+ }, function (sqlError) {
+ reject(sqlError);
+ });
+ });
+ });
+ }
+
+ executeCallback(promise, callback);
+ return promise;
+}
+
+var webSQLStorage = {
+ _driver: 'webSQLStorage',
+ _initStorage: _initStorage$1,
+ _support: isWebSQLValid(),
+ iterate: iterate$1,
+ getItem: getItem$1,
+ setItem: setItem$1,
+ removeItem: removeItem$1,
+ clear: clear$1,
+ length: length$1,
+ key: key$1,
+ keys: keys$1,
+ dropInstance: dropInstance$1
+};
+
+function isLocalStorageValid() {
+ try {
+ return typeof localStorage !== 'undefined' && 'setItem' in localStorage &&
+ // in IE8 typeof localStorage.setItem === 'object'
+ !!localStorage.setItem;
+ } catch (e) {
+ return false;
+ }
+}
+
+function _getKeyPrefix(options, defaultConfig) {
+ var keyPrefix = options.name + '/';
+
+ if (options.storeName !== defaultConfig.storeName) {
+ keyPrefix += options.storeName + '/';
+ }
+ return keyPrefix;
+}
+
+// Check if localStorage throws when saving an item
+function checkIfLocalStorageThrows() {
+ var localStorageTestKey = '_localforage_support_test';
+
+ try {
+ localStorage.setItem(localStorageTestKey, true);
+ localStorage.removeItem(localStorageTestKey);
+
+ return false;
+ } catch (e) {
+ return true;
+ }
+}
+
+// Check if localStorage is usable and allows to save an item
+// This method checks if localStorage is usable in Safari Private Browsing
+// mode, or in any other case where the available quota for localStorage
+// is 0 and there wasn't any saved items yet.
+function _isLocalStorageUsable() {
+ return !checkIfLocalStorageThrows() || localStorage.length > 0;
+}
+
+// Config the localStorage backend, using options set in the config.
+function _initStorage$2(options) {
+ var self = this;
+ var dbInfo = {};
+ if (options) {
+ for (var i in options) {
+ dbInfo[i] = options[i];
+ }
+ }
+
+ dbInfo.keyPrefix = _getKeyPrefix(options, self._defaultConfig);
+
+ if (!_isLocalStorageUsable()) {
+ return Promise$1.reject();
+ }
+
+ self._dbInfo = dbInfo;
+ dbInfo.serializer = localforageSerializer;
+
+ return Promise$1.resolve();
+}
+
+// Remove all keys from the datastore, effectively destroying all data in
+// the app's key/value store!
+function clear$2(callback) {
+ var self = this;
+ var promise = self.ready().then(function () {
+ var keyPrefix = self._dbInfo.keyPrefix;
+
+ for (var i = localStorage.length - 1; i >= 0; i--) {
+ var key = localStorage.key(i);
+
+ if (key.indexOf(keyPrefix) === 0) {
+ localStorage.removeItem(key);
+ }
+ }
+ });
+
+ executeCallback(promise, callback);
+ return promise;
+}
+
+// Retrieve an item from the store. Unlike the original async_storage
+// library in Gaia, we don't modify return values at all. If a key's value
+// is `undefined`, we pass that value to the callback function.
+function getItem$2(key, callback) {
+ var self = this;
+
+ key = normalizeKey(key);
+
+ var promise = self.ready().then(function () {
+ var dbInfo = self._dbInfo;
+ var result = localStorage.getItem(dbInfo.keyPrefix + key);
+
+ // If a result was found, parse it from the serialized
+ // string into a JS object. If result isn't truthy, the key
+ // is likely undefined and we'll pass it straight to the
+ // callback.
+ if (result) {
+ result = dbInfo.serializer.deserialize(result);
+ }
+
+ return result;
+ });
+
+ executeCallback(promise, callback);
+ return promise;
+}
+
+// Iterate over all items in the store.
+function iterate$2(iterator, callback) {
+ var self = this;
+
+ var promise = self.ready().then(function () {
+ var dbInfo = self._dbInfo;
+ var keyPrefix = dbInfo.keyPrefix;
+ var keyPrefixLength = keyPrefix.length;
+ var length = localStorage.length;
+
+ // We use a dedicated iterator instead of the `i` variable below
+ // so other keys we fetch in localStorage aren't counted in
+ // the `iterationNumber` argument passed to the `iterate()`
+ // callback.
+ //
+ // See: github.com/mozilla/localForage/pull/435#discussion_r38061530
+ var iterationNumber = 1;
+
+ for (var i = 0; i < length; i++) {
+ var key = localStorage.key(i);
+ if (key.indexOf(keyPrefix) !== 0) {
+ continue;
+ }
+ var value = localStorage.getItem(key);
+
+ // If a result was found, parse it from the serialized
+ // string into a JS object. If result isn't truthy, the
+ // key is likely undefined and we'll pass it straight
+ // to the iterator.
+ if (value) {
+ value = dbInfo.serializer.deserialize(value);
+ }
+
+ value = iterator(value, key.substring(keyPrefixLength), iterationNumber++);
+
+ if (value !== void 0) {
+ return value;
+ }
+ }
+ });
+
+ executeCallback(promise, callback);
+ return promise;
+}
+
+// Same as localStorage's key() method, except takes a callback.
+function key$2(n, callback) {
+ var self = this;
+ var promise = self.ready().then(function () {
+ var dbInfo = self._dbInfo;
+ var result;
+ try {
+ result = localStorage.key(n);
+ } catch (error) {
+ result = null;
+ }
+
+ // Remove the prefix from the key, if a key is found.
+ if (result) {
+ result = result.substring(dbInfo.keyPrefix.length);
+ }
+
+ return result;
+ });
+
+ executeCallback(promise, callback);
+ return promise;
+}
+
+function keys$2(callback) {
+ var self = this;
+ var promise = self.ready().then(function () {
+ var dbInfo = self._dbInfo;
+ var length = localStorage.length;
+ var keys = [];
+
+ for (var i = 0; i < length; i++) {
+ var itemKey = localStorage.key(i);
+ if (itemKey.indexOf(dbInfo.keyPrefix) === 0) {
+ keys.push(itemKey.substring(dbInfo.keyPrefix.length));
+ }
+ }
+
+ return keys;
+ });
+
+ executeCallback(promise, callback);
+ return promise;
+}
+
+// Supply the number of keys in the datastore to the callback function.
+function length$2(callback) {
+ var self = this;
+ var promise = self.keys().then(function (keys) {
+ return keys.length;
+ });
+
+ executeCallback(promise, callback);
+ return promise;
+}
+
+// Remove an item from the store, nice and simple.
+function removeItem$2(key, callback) {
+ var self = this;
+
+ key = normalizeKey(key);
+
+ var promise = self.ready().then(function () {
+ var dbInfo = self._dbInfo;
+ localStorage.removeItem(dbInfo.keyPrefix + key);
+ });
+
+ executeCallback(promise, callback);
+ return promise;
+}
+
+// Set a key's value and run an optional callback once the value is set.
+// Unlike Gaia's implementation, the callback function is passed the value,
+// in case you want to operate on that value only after you're sure it
+// saved, or something like that.
+function setItem$2(key, value, callback) {
+ var self = this;
+
+ key = normalizeKey(key);
+
+ var promise = self.ready().then(function () {
+ // Convert undefined values to null.
+ // https://github.com/mozilla/localForage/pull/42
+ if (value === undefined) {
+ value = null;
+ }
+
+ // Save the original value to pass to the callback.
+ var originalValue = value;
+
+ return new Promise$1(function (resolve, reject) {
+ var dbInfo = self._dbInfo;
+ dbInfo.serializer.serialize(value, function (value, error) {
+ if (error) {
+ reject(error);
+ } else {
+ try {
+ localStorage.setItem(dbInfo.keyPrefix + key, value);
+ resolve(originalValue);
+ } catch (e) {
+ // localStorage capacity exceeded.
+ // TODO: Make this a specific error/event.
+ if (e.name === 'QuotaExceededError' || e.name === 'NS_ERROR_DOM_QUOTA_REACHED') {
+ reject(e);
+ }
+ reject(e);
+ }
+ }
+ });
+ });
+ });
+
+ executeCallback(promise, callback);
+ return promise;
+}
+
+function dropInstance$2(options, callback) {
+ callback = getCallback.apply(this, arguments);
+
+ options = typeof options !== 'function' && options || {};
+ if (!options.name) {
+ var currentConfig = this.config();
+ options.name = options.name || currentConfig.name;
+ options.storeName = options.storeName || currentConfig.storeName;
+ }
+
+ var self = this;
+ var promise;
+ if (!options.name) {
+ promise = Promise$1.reject('Invalid arguments');
+ } else {
+ promise = new Promise$1(function (resolve) {
+ if (!options.storeName) {
+ resolve(options.name + '/');
+ } else {
+ resolve(_getKeyPrefix(options, self._defaultConfig));
+ }
+ }).then(function (keyPrefix) {
+ for (var i = localStorage.length - 1; i >= 0; i--) {
+ var key = localStorage.key(i);
+
+ if (key.indexOf(keyPrefix) === 0) {
+ localStorage.removeItem(key);
+ }
+ }
+ });
+ }
+
+ executeCallback(promise, callback);
+ return promise;
+}
+
+var localStorageWrapper = {
+ _driver: 'localStorageWrapper',
+ _initStorage: _initStorage$2,
+ _support: isLocalStorageValid(),
+ iterate: iterate$2,
+ getItem: getItem$2,
+ setItem: setItem$2,
+ removeItem: removeItem$2,
+ clear: clear$2,
+ length: length$2,
+ key: key$2,
+ keys: keys$2,
+ dropInstance: dropInstance$2
+};
+
+var sameValue = function sameValue(x, y) {
+ return x === y || typeof x === 'number' && typeof y === 'number' && isNaN(x) && isNaN(y);
+};
+
+var includes = function includes(array, searchElement) {
+ var len = array.length;
+ var i = 0;
+ while (i < len) {
+ if (sameValue(array[i], searchElement)) {
+ return true;
+ }
+ i++;
+ }
+
+ return false;
+};
+
+var isArray = Array.isArray || function (arg) {
+ return Object.prototype.toString.call(arg) === '[object Array]';
+};
+
+// Drivers are stored here when `defineDriver()` is called.
+// They are shared across all instances of localForage.
+var DefinedDrivers = {};
+
+var DriverSupport = {};
+
+var DefaultDrivers = {
+ INDEXEDDB: asyncStorage,
+ WEBSQL: webSQLStorage,
+ LOCALSTORAGE: localStorageWrapper
+};
+
+var DefaultDriverOrder = [DefaultDrivers.INDEXEDDB._driver, DefaultDrivers.WEBSQL._driver, DefaultDrivers.LOCALSTORAGE._driver];
+
+var OptionalDriverMethods = ['dropInstance'];
+
+var LibraryMethods = ['clear', 'getItem', 'iterate', 'key', 'keys', 'length', 'removeItem', 'setItem'].concat(OptionalDriverMethods);
+
+var DefaultConfig = {
+ description: '',
+ driver: DefaultDriverOrder.slice(),
+ name: 'localforage',
+ // Default DB size is _JUST UNDER_ 5MB, as it's the highest size
+ // we can use without a prompt.
+ size: 4980736,
+ storeName: 'keyvaluepairs',
+ version: 1.0
+};
+
+function callWhenReady(localForageInstance, libraryMethod) {
+ localForageInstance[libraryMethod] = function () {
+ var _args = arguments;
+ return localForageInstance.ready().then(function () {
+ return localForageInstance[libraryMethod].apply(localForageInstance, _args);
+ });
+ };
+}
+
+function extend() {
+ for (var i = 1; i < arguments.length; i++) {
+ var arg = arguments[i];
+
+ if (arg) {
+ for (var _key in arg) {
+ if (arg.hasOwnProperty(_key)) {
+ if (isArray(arg[_key])) {
+ arguments[0][_key] = arg[_key].slice();
+ } else {
+ arguments[0][_key] = arg[_key];
+ }
+ }
+ }
+ }
+ }
+
+ return arguments[0];
+}
+
+var LocalForage = function () {
+ function LocalForage(options) {
+ _classCallCheck(this, LocalForage);
+
+ for (var driverTypeKey in DefaultDrivers) {
+ if (DefaultDrivers.hasOwnProperty(driverTypeKey)) {
+ var driver = DefaultDrivers[driverTypeKey];
+ var driverName = driver._driver;
+ this[driverTypeKey] = driverName;
+
+ if (!DefinedDrivers[driverName]) {
+ // we don't need to wait for the promise,
+ // since the default drivers can be defined
+ // in a blocking manner
+ this.defineDriver(driver);
+ }
+ }
+ }
+
+ this._defaultConfig = extend({}, DefaultConfig);
+ this._config = extend({}, this._defaultConfig, options);
+ this._driverSet = null;
+ this._initDriver = null;
+ this._ready = false;
+ this._dbInfo = null;
+
+ this._wrapLibraryMethodsWithReady();
+ this.setDriver(this._config.driver)["catch"](function () {});
+ }
+
+ // Set any config values for localForage; can be called anytime before
+ // the first API call (e.g. `getItem`, `setItem`).
+ // We loop through options so we don't overwrite existing config
+ // values.
+
+
+ LocalForage.prototype.config = function config(options) {
+ // If the options argument is an object, we use it to set values.
+ // Otherwise, we return either a specified config value or all
+ // config values.
+ if ((typeof options === 'undefined' ? 'undefined' : _typeof(options)) === 'object') {
+ // If localforage is ready and fully initialized, we can't set
+ // any new configuration values. Instead, we return an error.
+ if (this._ready) {
+ return new Error("Can't call config() after localforage " + 'has been used.');
+ }
+
+ for (var i in options) {
+ if (i === 'storeName') {
+ options[i] = options[i].replace(/\W/g, '_');
+ }
+
+ if (i === 'version' && typeof options[i] !== 'number') {
+ return new Error('Database version must be a number.');
+ }
+
+ this._config[i] = options[i];
+ }
+
+ // after all config options are set and
+ // the driver option is used, try setting it
+ if ('driver' in options && options.driver) {
+ return this.setDriver(this._config.driver);
+ }
+
+ return true;
+ } else if (typeof options === 'string') {
+ return this._config[options];
+ } else {
+ return this._config;
+ }
+ };
+
+ // Used to define a custom driver, shared across all instances of
+ // localForage.
+
+
+ LocalForage.prototype.defineDriver = function defineDriver(driverObject, callback, errorCallback) {
+ var promise = new Promise$1(function (resolve, reject) {
+ try {
+ var driverName = driverObject._driver;
+ var complianceError = new Error('Custom driver not compliant; see ' + 'https://mozilla.github.io/localForage/#definedriver');
+
+ // A driver name should be defined and not overlap with the
+ // library-defined, default drivers.
+ if (!driverObject._driver) {
+ reject(complianceError);
+ return;
+ }
+
+ var driverMethods = LibraryMethods.concat('_initStorage');
+ for (var i = 0, len = driverMethods.length; i < len; i++) {
+ var driverMethodName = driverMethods[i];
+
+ // when the property is there,
+ // it should be a method even when optional
+ var isRequired = !includes(OptionalDriverMethods, driverMethodName);
+ if ((isRequired || driverObject[driverMethodName]) && typeof driverObject[driverMethodName] !== 'function') {
+ reject(complianceError);
+ return;
+ }
+ }
+
+ var configureMissingMethods = function configureMissingMethods() {
+ var methodNotImplementedFactory = function methodNotImplementedFactory(methodName) {
+ return function () {
+ var error = new Error('Method ' + methodName + ' is not implemented by the current driver');
+ var promise = Promise$1.reject(error);
+ executeCallback(promise, arguments[arguments.length - 1]);
+ return promise;
+ };
+ };
+
+ for (var _i = 0, _len = OptionalDriverMethods.length; _i < _len; _i++) {
+ var optionalDriverMethod = OptionalDriverMethods[_i];
+ if (!driverObject[optionalDriverMethod]) {
+ driverObject[optionalDriverMethod] = methodNotImplementedFactory(optionalDriverMethod);
+ }
+ }
+ };
+
+ configureMissingMethods();
+
+ var setDriverSupport = function setDriverSupport(support) {
+ if (DefinedDrivers[driverName]) {
+ console.info('Redefining LocalForage driver: ' + driverName);
+ }
+ DefinedDrivers[driverName] = driverObject;
+ DriverSupport[driverName] = support;
+ // don't use a then, so that we can define
+ // drivers that have simple _support methods
+ // in a blocking manner
+ resolve();
+ };
+
+ if ('_support' in driverObject) {
+ if (driverObject._support && typeof driverObject._support === 'function') {
+ driverObject._support().then(setDriverSupport, reject);
+ } else {
+ setDriverSupport(!!driverObject._support);
+ }
+ } else {
+ setDriverSupport(true);
+ }
+ } catch (e) {
+ reject(e);
+ }
+ });
+
+ executeTwoCallbacks(promise, callback, errorCallback);
+ return promise;
+ };
+
+ LocalForage.prototype.driver = function driver() {
+ return this._driver || null;
+ };
+
+ LocalForage.prototype.getDriver = function getDriver(driverName, callback, errorCallback) {
+ var getDriverPromise = DefinedDrivers[driverName] ? Promise$1.resolve(DefinedDrivers[driverName]) : Promise$1.reject(new Error('Driver not found.'));
+
+ executeTwoCallbacks(getDriverPromise, callback, errorCallback);
+ return getDriverPromise;
+ };
+
+ LocalForage.prototype.getSerializer = function getSerializer(callback) {
+ var serializerPromise = Promise$1.resolve(localforageSerializer);
+ executeTwoCallbacks(serializerPromise, callback);
+ return serializerPromise;
+ };
+
+ LocalForage.prototype.ready = function ready(callback) {
+ var self = this;
+
+ var promise = self._driverSet.then(function () {
+ if (self._ready === null) {
+ self._ready = self._initDriver();
+ }
+
+ return self._ready;
+ });
+
+ executeTwoCallbacks(promise, callback, callback);
+ return promise;
+ };
+
+ LocalForage.prototype.setDriver = function setDriver(drivers, callback, errorCallback) {
+ var self = this;
+
+ if (!isArray(drivers)) {
+ drivers = [drivers];
+ }
+
+ var supportedDrivers = this._getSupportedDrivers(drivers);
+
+ function setDriverToConfig() {
+ self._config.driver = self.driver();
+ }
+
+ function extendSelfWithDriver(driver) {
+ self._extend(driver);
+ setDriverToConfig();
+
+ self._ready = self._initStorage(self._config);
+ return self._ready;
+ }
+
+ function initDriver(supportedDrivers) {
+ return function () {
+ var currentDriverIndex = 0;
+
+ function driverPromiseLoop() {
+ while (currentDriverIndex < supportedDrivers.length) {
+ var driverName = supportedDrivers[currentDriverIndex];
+ currentDriverIndex++;
+
+ self._dbInfo = null;
+ self._ready = null;
+
+ return self.getDriver(driverName).then(extendSelfWithDriver)["catch"](driverPromiseLoop);
+ }
+
+ setDriverToConfig();
+ var error = new Error('No available storage method found.');
+ self._driverSet = Promise$1.reject(error);
+ return self._driverSet;
+ }
+
+ return driverPromiseLoop();
+ };
+ }
+
+ // There might be a driver initialization in progress
+ // so wait for it to finish in order to avoid a possible
+ // race condition to set _dbInfo
+ var oldDriverSetDone = this._driverSet !== null ? this._driverSet["catch"](function () {
+ return Promise$1.resolve();
+ }) : Promise$1.resolve();
+
+ this._driverSet = oldDriverSetDone.then(function () {
+ var driverName = supportedDrivers[0];
+ self._dbInfo = null;
+ self._ready = null;
+
+ return self.getDriver(driverName).then(function (driver) {
+ self._driver = driver._driver;
+ setDriverToConfig();
+ self._wrapLibraryMethodsWithReady();
+ self._initDriver = initDriver(supportedDrivers);
+ });
+ })["catch"](function () {
+ setDriverToConfig();
+ var error = new Error('No available storage method found.');
+ self._driverSet = Promise$1.reject(error);
+ return self._driverSet;
+ });
+
+ executeTwoCallbacks(this._driverSet, callback, errorCallback);
+ return this._driverSet;
+ };
+
+ LocalForage.prototype.supports = function supports(driverName) {
+ return !!DriverSupport[driverName];
+ };
+
+ LocalForage.prototype._extend = function _extend(libraryMethodsAndProperties) {
+ extend(this, libraryMethodsAndProperties);
+ };
+
+ LocalForage.prototype._getSupportedDrivers = function _getSupportedDrivers(drivers) {
+ var supportedDrivers = [];
+ for (var i = 0, len = drivers.length; i < len; i++) {
+ var driverName = drivers[i];
+ if (this.supports(driverName)) {
+ supportedDrivers.push(driverName);
+ }
+ }
+ return supportedDrivers;
+ };
+
+ LocalForage.prototype._wrapLibraryMethodsWithReady = function _wrapLibraryMethodsWithReady() {
+ // Add a stub for each driver API method that delays the call to the
+ // corresponding driver method until localForage is ready. These stubs
+ // will be replaced by the driver methods as soon as the driver is
+ // loaded, so there is no performance impact.
+ for (var i = 0, len = LibraryMethods.length; i < len; i++) {
+ callWhenReady(this, LibraryMethods[i]);
+ }
+ };
+
+ LocalForage.prototype.createInstance = function createInstance(options) {
+ return new LocalForage(options);
+ };
+
+ return LocalForage;
+}();
+
+// The actual localForage object that we expose as a module or via a
+// global. It's extended by pulling in one of our other libraries.
+
+
+var localforage_js = new LocalForage();
+
+module.exports = localforage_js;
+
+},{"3":3}]},{},[4])(4)
+});
diff --git a/etc/js/mfb-directive.js b/etc/js/mfb-directive.js
new file mode 100644
index 00000000..68a3c350
--- /dev/null
+++ b/etc/js/mfb-directive.js
@@ -0,0 +1,175 @@
++(function(window, angular, undefined){
+
+ 'use strict';
+
+ var mfb = angular.module('ng-mfb', []);
+
+ mfb.run(['$templateCache', function($templateCache) {
+ $templateCache.put('ng-mfb-menu-default.tpl.html',
+ '<ul class="mfb-component--{{position}} mfb-{{effect}}"' +
+ ' data-mfb-toggle="{{togglingMethod}}" data-mfb-state="{{menuState}}">' +
+ ' <li class="mfb-component__wrap">' +
+ ' <a ng-click="clicked()" ng-mouseenter="hovered()" ng-mouseleave="hovered()"' +
+ ' ng-attr-data-mfb-label="{{label}}" class="mfb-component__button--main">' +
+ ' <i class="mfb-component__main-icon--resting {{resting}}"></i>' +
+ ' <i class="mfb-component__main-icon--active {{active}}"></i>' +
+ ' </a>' +
+ ' <ul class="mfb-component__list" ng-transclude>' +
+ ' </ul>' +
+ '</li>' +
+ '</ul>'
+ );
+
+ $templateCache.put('ng-mfb-menu-md.tpl.html',
+ '<ul class="mfb-component--{{position}} mfb-{{effect}}"' +
+ ' data-mfb-toggle="{{togglingMethod}}" data-mfb-state="{{menuState}}">' +
+ ' <li class="mfb-component__wrap">' +
+ ' <a ng-click="clicked()" ng-mouseenter="hovered()" ng-mouseleave="hovered()"' +
+ ' style="background: transparent; box-shadow: none;"' +
+ ' ng-attr-data-mfb-label="{{label}}" class="mfb-component__button--main">' +
+ ' <md-button class="md-fab md-primary" aria-label={{label}} style="position:relative;">' +
+ ' <md-icon style="left: 0;" md-svg-icon="{{resting}}"' +
+ ' class="mfb-component__main-icon--resting"></md-icon>' +
+ ' <md-icon style="position:initial;" md-svg-icon="{{active}}"' +
+ ' class="mfb-component__main-icon--active"></md-icon>' +
+ ' </md-button>' +
+ ' </a>' +
+ ' <ul class="mfb-component__list" ng-transclude>' +
+ ' </ul>' +
+ '</li>' +
+ '</ul>'
+ );
+
+ $templateCache.put('ng-mfb-button-default.tpl.html',
+ '<li>' +
+ ' <a data-mfb-label="{{label}}" class="mfb-component__button--child">' +
+ ' <i class="mfb-component__child-icon {{icon}}">' +
+ ' </i>' +
+ ' </a>' +
+ '</li>'
+ );
+
+ $templateCache.put('ng-mfb-button-md.tpl.html',
+ '<li>' +
+ ' <a href="" data-mfb-label="{{label}}" class="mfb-component__button--child" ' +
+ ' style="background: transparent; box-shadow: none;">' +
+ ' <md-button class="md-fab md-primary" aria-label={{label}}>' +
+ //' <md-icon md-svg-src="img/icons/android.svg"></md-icon>' +
+ ' <md-icon md-svg-icon="{{icon}}"></md-icon>' +
+ ' </md-button>' +
+ ' </a>' +
+ '</li>'
+ );
+ }]);
+
+ mfb.directive('mfbMenu', ['$timeout',function($timeout){
+ return {
+ restrict: 'EA',
+ transclude: true,
+ replace: true,
+ scope: {
+ position: '@',
+ effect: '@',
+ label: '@',
+ resting: '@restingIcon',
+ active: '@activeIcon',
+ mainAction: '&',
+ menuState: '=?',
+ togglingMethod: '@'
+ },
+ templateUrl: function(elem, attrs) {
+ return attrs.templateUrl || 'ng-mfb-menu-default.tpl.html';
+ },
+ link: function(scope, elem, attrs) {
+
+ var openState = 'open',
+ closedState = 'closed';
+
+ /**
+ * Check if we're on a touch-enabled device.
+ * Requires Modernizr to run, otherwise simply returns false
+ */
+ function _isTouchDevice(){
+ return window.Modernizr && Modernizr.touch;
+ }
+
+ function _isHoverActive(){
+ return scope.togglingMethod === 'hover';
+ }
+
+ /**
+ * Convert the toggling method to 'click'.
+ * This is used when 'hover' is selected by the user
+ * but a touch device is enabled.
+ */
+ function useClick(){
+ scope.$apply(function(){
+ scope.togglingMethod = 'click';
+ });
+ }
+ /**
+ * Invert the current state of the menu.
+ */
+ function flipState() {
+ scope.menuState = scope.menuState === openState ? closedState : openState;
+ }
+
+ /**
+ * Set the state to user-defined value. Fallback to closed if no
+ * value is passed from the outside.
+ */
+ //scope.menuState = attrs.menuState || closedState;
+ if(!scope.menuState){
+ scope.menuState = closedState;
+ }
+
+ scope.clicked = function() {
+ // If there is a main action, let's fire it
+ if (scope.mainAction) {
+ scope.mainAction();
+ }
+
+ if(!_isHoverActive()){
+ flipState();
+ }
+ };
+ scope.hovered = function() {
+ if(_isHoverActive()){
+ //flipState();
+ }
+ };
+
+ /**
+ * If on touch device AND 'hover' method is selected:
+ * wait for the digest to perform and then change hover to click.
+ */
+ if ( _isTouchDevice() && _isHoverActive() ){
+ $timeout(useClick);
+ }
+
+ attrs.$observe('menuState', function(){
+ scope.currentState = scope.menuState;
+ });
+
+ }
+ };
+ }]);
+
+
+ mfb.directive('mfbButton', [function(){
+ return {
+ require: '^mfbMenu',
+ restrict: 'EA',
+ transclude: true,
+ replace: true,
+ scope: {
+ icon: '@',
+ label: '@'
+ },
+ templateUrl: function(elem, attrs) {
+ return attrs.templateUrl || 'ng-mfb-button-default.tpl.html';
+ }
+ };
+ }]);
+
+})(window, angular);
diff --git a/etc/js/ng-cordova.js b/etc/js/ng-cordova.js
new file mode 100644
index 00000000..b8c1c53d
--- /dev/null
+++ b/etc/js/ng-cordova.js
@@ -0,0 +1,7361 @@
+/*!
+ * ngCordova
+ * v0.1.27-alpha
+ * Copyright 2015 Drifty Co. http://drifty.com/
+ * See LICENSE in this repository for license information
+ */
+(function(){
+
+angular.module('ngCordova', [
+ 'ngCordova.plugins'
+]);
+
+// install : cordova plugin add https://github.com/EddyVerbruggen/cordova-plugin-3dtouch.git
+// link : https://github.com/EddyVerbruggen/cordova-plugin-3dtouch
+
+angular.module('ngCordova.plugins.3dtouch', [])
+
+ .factory('$cordova3DTouch', ['$q', function($q) {
+ var quickActions = [];
+ var quickActionHandler = {};
+
+ var createQuickActionHandler = function(quickActionHandler) {
+ return function (payload) {
+ for (var key in quickActionHandler) {
+ if (payload.type === key) {
+ quickActionHandler[key]();
+ }
+ }
+ };
+ };
+
+ return {
+ /*
+ * Checks if Cordova 3D touch is present and loaded
+ *
+ * @return promise
+ */
+ isAvailable: function () {
+ var deferred = $q.defer();
+ if (!window.cordova) {
+ deferred.reject('Not supported in browser');
+ } else {
+ if (!window.ThreeDeeTouch) {
+ deferred.reject('Could not find 3D touch plugin');
+ } else {
+ window.ThreeDeeTouch.isAvailable(function (value) {
+ deferred.resolve(value);
+ }, function (err) {
+ deferred.reject(err);
+ });
+ }
+ }
+
+ return deferred.promise;
+ },
+
+ /*
+ * Add a quick action to menu
+ *
+ * @param string type
+ * @param string title
+ * @param string iconType (optional)
+ * @param string subtitle (optional)
+ * @param function callback (optional)
+ * @return promise
+ */
+ addQuickAction: function(type, title, iconType, iconTemplate, subtitle, callback) {
+ var deferred = $q.defer();
+
+ var quickAction = {
+ type: type,
+ title: title,
+ subtitle: subtitle
+ };
+
+ if (iconType) {
+ quickAction.iconType = iconType;
+ }
+
+ if (iconTemplate) {
+ quickAction.iconTemplate = iconTemplate;
+ }
+
+ this.isAvailable().then(function() {
+ quickActions.push(quickAction);
+ quickActionHandler[type] = callback;
+ window.ThreeDeeTouch.configureQuickActions(quickActions);
+ window.ThreeDeeTouch.onHomeIconPressed = createQuickActionHandler(quickActionHandler);
+ deferred.resolve(quickActions);
+ },
+ function(err) {
+ deferred.reject(err);
+ });
+
+ return deferred.promise;
+ },
+
+ /*
+ * Add a quick action handler. Used for static quick actions
+ *
+ * @param string type
+ * @param function callback
+ * @return promise
+ */
+ addQuickActionHandler: function(type, callback) {
+ var deferred = $q.defer();
+
+ this.isAvailable().then(function() {
+ quickActionHandler[type] = callback;
+ window.ThreeDeeTouch.onHomeIconPressed = createQuickActionHandler(quickActionHandler);
+ deferred.resolve(true);
+ },
+ function(err) {
+ deferred.reject(err);
+ });
+
+ return deferred.promise;
+ },
+
+ /*
+ * Enable link preview popup when force touch is appled to link elements
+ *
+ * @return bool
+ */
+ enableLinkPreview: function() {
+ var deferred = $q.defer();
+
+ this.isAvailable().then(function() {
+ window.ThreeDeeTouch.enableLinkPreview();
+ deferred.resolve(true);
+ },
+ function(err) {
+ deferred.reject(err);
+ });
+
+ return deferred.promise;
+ },
+
+ /*
+ * Add a hanlder function for force touch events,
+ *
+ * @param function callback
+ * @return promise
+ */
+ addForceTouchHandler: function(callback) {
+ var deferred = $q.defer();
+
+ this.isAvailable().then(function() {
+ window.ThreeDeeTouch.watchForceTouches(callback);
+ deferred.resolve(true);
+ },
+ function(err) {
+ deferred.reject(err);
+ });
+
+ return deferred.promise;
+ }
+ };
+ }]);
+
+// install : cordova plugin add https://github.com/EddyVerbruggen/cordova-plugin-actionsheet.git
+// link : https://github.com/EddyVerbruggen/cordova-plugin-actionsheet
+
+angular.module('ngCordova.plugins.actionSheet', [])
+
+ .factory('$cordovaActionSheet', ['$q', '$window', function ($q, $window) {
+
+ return {
+ show: function (options) {
+ var q = $q.defer();
+
+ $window.plugins.actionsheet.show(options, function (result) {
+ q.resolve(result);
+ });
+
+ return q.promise;
+ },
+
+ hide: function () {
+ return $window.plugins.actionsheet.hide();
+ }
+ };
+ }]);
+
+// install : cordova plugin add https://github.com/floatinghotpot/cordova-plugin-admob.git
+// link : https://github.com/floatinghotpot/cordova-plugin-admob
+
+angular.module('ngCordova.plugins.adMob', [])
+
+ .factory('$cordovaAdMob', ['$q', '$window', function ($q, $window) {
+
+ return {
+ createBannerView: function (options) {
+ var d = $q.defer();
+
+ $window.plugins.AdMob.createBannerView(options, function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ createInterstitialView: function (options) {
+ var d = $q.defer();
+
+ $window.plugins.AdMob.createInterstitialView(options, function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ requestAd: function (options) {
+ var d = $q.defer();
+
+ $window.plugins.AdMob.requestAd(options, function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ showAd: function (options) {
+ var d = $q.defer();
+
+ $window.plugins.AdMob.showAd(options, function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ requestInterstitialAd: function (options) {
+ var d = $q.defer();
+
+ $window.plugins.AdMob.requestInterstitialAd(options, function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ }
+ };
+ }]);
+
+// install : cordova plugin add https://github.com/ohh2ahh/AppAvailability.git
+// link : https://github.com/ohh2ahh/AppAvailability
+
+/* globals appAvailability: true */
+angular.module('ngCordova.plugins.appAvailability', [])
+
+ .factory('$cordovaAppAvailability', ['$q', function ($q) {
+
+ return {
+ check: function (urlScheme) {
+ var q = $q.defer();
+
+ appAvailability.check(urlScheme, function (result) {
+ q.resolve(result);
+ }, function (err) {
+ q.reject(err);
+ });
+
+ return q.promise;
+ }
+ };
+ }]);
+
+// install : cordova plugin add https://github.com/pushandplay/cordova-plugin-apprate.git
+// link : https://github.com/pushandplay/cordova-plugin-apprate
+
+/* globals AppRate: true */
+angular.module('ngCordova.plugins.appRate', [])
+
+ .provider('$cordovaAppRate', [function () {
+
+ /**
+ * Set defaults settings to AppRate
+ *
+ * @param {Object} defaults - AppRate default settings
+ * @param {string} defaults.language
+ * @param {string} defaults.appName
+ * @param {boolean} defaults.promptForNewVersion
+ * @param {boolean} defaults.openStoreInApp
+ * @param {number} defaults.usesUntilPrompt
+ * @param {boolean} defaults.useCustomRateDialog
+ * @param {string} defaults.iosURL
+ * @param {string} defaults.androidURL
+ * @param {string} defaults.blackberryURL
+ * @param {string} defaults.windowsURL
+ */
+ this.setPreferences = function (defaults) {
+ if (!defaults || !angular.isObject(defaults)) {
+ return;
+ }
+
+ AppRate.preferences.useLanguage = defaults.language || null;
+ AppRate.preferences.displayAppName = defaults.appName || '';
+ AppRate.preferences.promptAgainForEachNewVersion = defaults.promptForNewVersion || true;
+ AppRate.preferences.openStoreInApp = defaults.openStoreInApp || false;
+ AppRate.preferences.usesUntilPrompt = defaults.usesUntilPrompt || 3;
+ AppRate.preferences.useCustomRateDialog = defaults.useCustomRateDialog || false;
+ AppRate.preferences.storeAppURL.ios = defaults.iosURL || null;
+ AppRate.preferences.storeAppURL.android = defaults.androidURL || null;
+ AppRate.preferences.storeAppURL.blackberry = defaults.blackberryURL || null;
+ AppRate.preferences.storeAppURL.windows8 = defaults.windowsURL || null;
+ };
+
+ /**
+ * Set custom locale
+ *
+ * @param {Object} customObj
+ * @param {string} customObj.title
+ * @param {string} customObj.message
+ * @param {string} customObj.cancelButtonLabel
+ * @param {string} customObj.laterButtonLabel
+ * @param {string} customObj.rateButtonLabel
+ */
+ this.setCustomLocale = function (customObj) {
+ var strings = {
+ title: 'Rate %@',
+ message: 'If you enjoy using %@, would you mind taking a moment to rate it? It won’t take more than a minute. Thanks for your support!',
+ cancelButtonLabel: 'No, Thanks',
+ laterButtonLabel: 'Remind Me Later',
+ rateButtonLabel: 'Rate It Now'
+ };
+
+ strings = angular.extend(strings, customObj);
+
+ AppRate.preferences.customLocale = strings;
+ };
+
+ this.$get = ['$q', function ($q) {
+ return {
+ promptForRating: function (immediate) {
+ var q = $q.defer();
+ var prompt = AppRate.promptForRating(immediate);
+ q.resolve(prompt);
+
+ return q.promise;
+ },
+
+ navigateToAppStore: function () {
+ var q = $q.defer();
+ var navigate = AppRate.navigateToAppStore();
+ q.resolve(navigate);
+
+ return q.promise;
+ },
+
+ onButtonClicked: function (cb) {
+ AppRate.preferences.callbacks.onButtonClicked = cb.bind(this);
+ },
+
+ onRateDialogShow: function (cb) {
+ AppRate.preferences.callbacks.onRateDialogShow = cb.bind(this);
+ }
+ };
+ }];
+ }]);
+
+// install : cordova plugin add https://github.com/whiteoctober/cordova-plugin-app-version.git
+// link : https://github.com/whiteoctober/cordova-plugin-app-version
+
+angular.module('ngCordova.plugins.appVersion', [])
+
+ .factory('$cordovaAppVersion', ['$q', function ($q) {
+
+ return {
+ getAppName: function () {
+ var q = $q.defer();
+ cordova.getAppVersion.getAppName(function (name) {
+ q.resolve(name);
+ });
+
+ return q.promise;
+ },
+
+ getPackageName: function () {
+ var q = $q.defer();
+ cordova.getAppVersion.getPackageName(function (pack) {
+ q.resolve(pack);
+ });
+
+ return q.promise;
+ },
+
+ getVersionNumber: function () {
+ var q = $q.defer();
+ cordova.getAppVersion.getVersionNumber(function (version) {
+ q.resolve(version);
+ });
+
+ return q.promise;
+ },
+
+ getVersionCode: function () {
+ var q = $q.defer();
+ cordova.getAppVersion.getVersionCode(function (code) {
+ q.resolve(code);
+ });
+
+ return q.promise;
+ }
+ };
+ }]);
+
+// install : cordova plugin add https://github.com/christocracy/cordova-plugin-background-geolocation.git
+// link : https://github.com/christocracy/cordova-plugin-background-geolocation
+
+angular.module('ngCordova.plugins.backgroundGeolocation', [])
+
+ .factory('$cordovaBackgroundGeolocation', ['$q', '$window', function ($q, $window) {
+
+ return {
+
+ init: function () {
+ $window.navigator.geolocation.getCurrentPosition(function (location) {
+ return location;
+ });
+ },
+
+ configure: function (options) {
+
+ this.init();
+ var q = $q.defer();
+
+ $window.plugins.backgroundGeoLocation.configure(
+ function (result) {
+ q.notify(result);
+ $window.plugins.backgroundGeoLocation.finish();
+ },
+ function (err) {
+ q.reject(err);
+ }, options);
+
+ this.start();
+
+ return q.promise;
+ },
+
+ start: function () {
+ var q = $q.defer();
+
+ $window.plugins.backgroundGeoLocation.start(
+ function (result) {
+ q.resolve(result);
+ },
+ function (err) {
+ q.reject(err);
+ });
+
+ return q.promise;
+ },
+
+ stop: function () {
+ var q = $q.defer();
+
+ $window.plugins.backgroundGeoLocation.stop(
+ function (result) {
+ q.resolve(result);
+ },
+ function (err) {
+ q.reject(err);
+ });
+
+ return q.promise;
+ }
+ };
+ }
+
+ ]);
+
+// install : cordova plugin add https://github.com/katzer/cordova-plugin-badge.git
+// link : https://github.com/katzer/cordova-plugin-badge
+
+angular.module('ngCordova.plugins.badge', [])
+
+ .factory('$cordovaBadge', ['$q', function ($q) {
+
+ return {
+ hasPermission: function () {
+ var q = $q.defer();
+ cordova.plugins.notification.badge.hasPermission(function (permission) {
+ if (permission) {
+ q.resolve(true);
+ } else {
+ q.reject('You do not have permission');
+ }
+ });
+
+ return q.promise;
+ },
+
+ promptForPermission: function () {
+ return cordova.plugins.notification.badge.promptForPermission();
+ },
+
+ set: function (badge, callback, scope) {
+ var q = $q.defer();
+
+ cordova.plugins.notification.badge.hasPermission(function (permission) {
+ if (permission) {
+ q.resolve(
+ cordova.plugins.notification.badge.set(badge, callback, scope)
+ );
+ } else {
+ q.reject('You do not have permission to set Badge');
+ }
+ });
+ return q.promise;
+ },
+
+ get: function () {
+ var q = $q.defer();
+ cordova.plugins.notification.badge.hasPermission(function (permission) {
+ if (permission) {
+ cordova.plugins.notification.badge.get(function (badge) {
+ q.resolve(badge);
+ });
+ } else {
+ q.reject('You do not have permission to get Badge');
+ }
+ });
+
+ return q.promise;
+ },
+
+ clear: function (callback, scope) {
+ var q = $q.defer();
+
+ cordova.plugins.notification.badge.hasPermission(function (permission) {
+ if (permission) {
+ q.resolve(cordova.plugins.notification.badge.clear(callback, scope));
+ } else {
+ q.reject('You do not have permission to clear Badge');
+ }
+ });
+ return q.promise;
+ },
+
+ increase: function (count, callback, scope) {
+ var q = $q.defer();
+
+ this.hasPermission().then(function (){
+ q.resolve(
+ cordova.plugins.notification.badge.increase(count, callback, scope)
+ );
+ }, function (){
+ q.reject('You do not have permission to increase Badge');
+ }) ;
+
+ return q.promise;
+ },
+
+ decrease: function (count, callback, scope) {
+ var q = $q.defer();
+
+ this.hasPermission().then(function (){
+ q.resolve(
+ cordova.plugins.notification.badge.decrease(count, callback, scope)
+ );
+ }, function (){
+ q.reject('You do not have permission to decrease Badge');
+ }) ;
+
+ return q.promise;
+ },
+
+ configure: function (config) {
+ return cordova.plugins.notification.badge.configure(config);
+ }
+ };
+ }]);
+
+// install : cordova plugin add https://github.com/phonegap/phonegap-plugin-barcodescanner.git
+// link : https://github.com/phonegap/phonegap-plugin-barcodescanner
+
+angular.module('ngCordova.plugins.barcodeScanner', [])
+
+ .factory('$cordovaBarcodeScanner', ['$q', function ($q) {
+
+ return {
+ scan: function (config) {
+ var q = $q.defer();
+
+ cordova.plugins.barcodeScanner.scan(function (result) {
+ q.resolve(result);
+ }, function (err) {
+ q.reject(err);
+ }, config);
+
+ return q.promise;
+ },
+
+ encode: function (type, data) {
+ var q = $q.defer();
+ type = type || 'TEXT_TYPE';
+
+ cordova.plugins.barcodeScanner.encode(type, data, function (result) {
+ q.resolve(result);
+ }, function (err) {
+ q.reject(err);
+ });
+
+ return q.promise;
+ }
+ };
+ }]);
+
+// install : cordova plugin add cordova-plugin-battery-status
+// link : https://github.com/apache/cordova-plugin-battery-status
+
+angular.module('ngCordova.plugins.batteryStatus', [])
+
+ .factory('$cordovaBatteryStatus', ['$rootScope', '$window', '$timeout', function ($rootScope, $window, $timeout) {
+
+ /**
+ * @param {string} status
+ */
+ var batteryStatus = function (status) {
+ $timeout(function () {
+ $rootScope.$broadcast('$cordovaBatteryStatus:status', status);
+ });
+ };
+
+ /**
+ * @param {string} status
+ */
+ var batteryCritical = function (status) {
+ $timeout(function () {
+ $rootScope.$broadcast('$cordovaBatteryStatus:critical', status);
+ });
+ };
+
+ /**
+ * @param {string} status
+ */
+ var batteryLow = function (status) {
+ $timeout(function () {
+ $rootScope.$broadcast('$cordovaBatteryStatus:low', status);
+ });
+ };
+
+ document.addEventListener('deviceready', function () {
+ if (navigator.battery) {
+ $window.addEventListener('batterystatus', batteryStatus, false);
+ $window.addEventListener('batterycritical', batteryCritical, false);
+ $window.addEventListener('batterylow', batteryLow, false);
+
+ }
+ }, false);
+ return true;
+ }])
+ .run(['$injector', function ($injector) {
+ $injector.get('$cordovaBatteryStatus'); //ensure the factory and subsequent event listeners get initialised
+ }]);
+
+// install : cordova plugin add https://github.com/petermetz/cordova-plugin-ibeacon.git
+// link : https://github.com/petermetz/cordova-plugin-ibeacon
+
+angular.module('ngCordova.plugins.beacon', [])
+
+ .factory('$cordovaBeacon', ['$window', '$rootScope', '$timeout', '$q', function ($window, $rootScope, $timeout, $q) {
+ var callbackDidDetermineStateForRegion = null;
+ var callbackDidStartMonitoringForRegion = null;
+ var callbackDidExitRegion = null;
+ var callbackDidEnterRegion = null;
+ var callbackDidRangeBeaconsInRegion = null;
+ var callbackPeripheralManagerDidStartAdvertising = null;
+ var callbackPeripheralManagerDidUpdateState = null;
+ var callbackDidChangeAuthorizationStatus = null;
+
+ document.addEventListener('deviceready', function () {
+ if ($window.cordova &&
+ $window.cordova.plugins &&
+ $window.cordova.plugins.locationManager) {
+ var delegate = new $window.cordova.plugins.locationManager.Delegate();
+
+ delegate.didDetermineStateForRegion = function (pluginResult) {
+ $timeout(function () {
+ $rootScope.$broadcast('$cordovaBeacon:didDetermineStateForRegion', pluginResult);
+ });
+
+ if (callbackDidDetermineStateForRegion) {
+ callbackDidDetermineStateForRegion(pluginResult);
+ }
+ };
+
+ delegate.didStartMonitoringForRegion = function (pluginResult) {
+ $timeout(function () {
+ $rootScope.$broadcast('$cordovaBeacon:didStartMonitoringForRegion', pluginResult);
+ });
+
+ if (callbackDidStartMonitoringForRegion) {
+ callbackDidStartMonitoringForRegion(pluginResult);
+ }
+ };
+
+ delegate.didExitRegion = function (pluginResult) {
+ $timeout(function () {
+ $rootScope.$broadcast('$cordovaBeacon:didExitRegion', pluginResult);
+ });
+
+ if (callbackDidExitRegion) {
+ callbackDidExitRegion(pluginResult);
+ }
+ };
+
+ delegate.didEnterRegion = function (pluginResult) {
+ $timeout(function () {
+ $rootScope.$broadcast('$cordovaBeacon:didEnterRegion', pluginResult);
+ });
+
+ if (callbackDidEnterRegion) {
+ callbackDidEnterRegion(pluginResult);
+ }
+ };
+
+ delegate.didRangeBeaconsInRegion = function (pluginResult) {
+ $timeout(function () {
+ $rootScope.$broadcast('$cordovaBeacon:didRangeBeaconsInRegion', pluginResult);
+ });
+
+ if (callbackDidRangeBeaconsInRegion) {
+ callbackDidRangeBeaconsInRegion(pluginResult);
+ }
+ };
+
+ delegate.peripheralManagerDidStartAdvertising = function (pluginResult) {
+ $timeout(function () {
+ $rootScope.$broadcast('$cordovaBeacon:peripheralManagerDidStartAdvertising', pluginResult);
+ });
+
+ if (callbackPeripheralManagerDidStartAdvertising) {
+ callbackPeripheralManagerDidStartAdvertising(pluginResult);
+ }
+ };
+
+ delegate.peripheralManagerDidUpdateState = function (pluginResult) {
+ $timeout(function () {
+ $rootScope.$broadcast('$cordovaBeacon:peripheralManagerDidUpdateState', pluginResult);
+ });
+
+ if (callbackPeripheralManagerDidUpdateState) {
+ callbackPeripheralManagerDidUpdateState(pluginResult);
+ }
+ };
+
+ delegate.didChangeAuthorizationStatus = function (status) {
+ $timeout(function () {
+ $rootScope.$broadcast('$cordovaBeacon:didChangeAuthorizationStatus', status);
+ });
+
+ if (callbackDidChangeAuthorizationStatus) {
+ callbackDidChangeAuthorizationStatus(status);
+ }
+ };
+
+ $window.cordova.plugins.locationManager.setDelegate(delegate);
+ }
+ }, false);
+
+ return {
+ setCallbackDidDetermineStateForRegion: function (callback) {
+ callbackDidDetermineStateForRegion = callback;
+ },
+ setCallbackDidStartMonitoringForRegion: function (callback) {
+ callbackDidStartMonitoringForRegion = callback;
+ },
+ setCallbackDidExitRegion: function (callback) {
+ callbackDidExitRegion = callback;
+ },
+ setCallbackDidEnterRegion: function (callback) {
+ callbackDidEnterRegion = callback;
+ },
+ setCallbackDidRangeBeaconsInRegion: function (callback) {
+ callbackDidRangeBeaconsInRegion = callback;
+ },
+ setCallbackPeripheralManagerDidStartAdvertising: function (callback) {
+ callbackPeripheralManagerDidStartAdvertising = callback;
+ },
+ setCallbackPeripheralManagerDidUpdateState: function (callback) {
+ callbackPeripheralManagerDidUpdateState = callback;
+ },
+ setCallbackDidChangeAuthorizationStatus: function (callback) {
+ callbackDidChangeAuthorizationStatus = callback;
+ },
+ createBeaconRegion: function (identifier, uuid, major, minor, notifyEntryStateOnDisplay) {
+ major = major || undefined;
+ minor = minor || undefined;
+
+ return new $window.cordova.plugins.locationManager.BeaconRegion(
+ identifier,
+ uuid,
+ major,
+ minor,
+ notifyEntryStateOnDisplay
+ );
+ },
+ isBluetoothEnabled: function () {
+ return $q.when($window.cordova.plugins.locationManager.isBluetoothEnabled());
+ },
+ enableBluetooth: function () {
+ return $q.when($window.cordova.plugins.locationManager.enableBluetooth());
+ },
+ disableBluetooth: function () {
+ return $q.when($window.cordova.plugins.locationManager.disableBluetooth());
+ },
+ startMonitoringForRegion: function (region) {
+ return $q.when($window.cordova.plugins.locationManager.startMonitoringForRegion(region));
+ },
+ stopMonitoringForRegion: function (region) {
+ return $q.when($window.cordova.plugins.locationManager.stopMonitoringForRegion(region));
+ },
+ requestStateForRegion: function (region) {
+ return $q.when($window.cordova.plugins.locationManager.requestStateForRegion(region));
+ },
+ startRangingBeaconsInRegion: function (region) {
+ return $q.when($window.cordova.plugins.locationManager.startRangingBeaconsInRegion(region));
+ },
+ stopRangingBeaconsInRegion: function (region) {
+ return $q.when($window.cordova.plugins.locationManager.stopRangingBeaconsInRegion(region));
+ },
+ getAuthorizationStatus: function () {
+ return $q.when($window.cordova.plugins.locationManager.getAuthorizationStatus());
+ },
+ requestWhenInUseAuthorization: function () {
+ return $q.when($window.cordova.plugins.locationManager.requestWhenInUseAuthorization());
+ },
+ requestAlwaysAuthorization: function () {
+ return $q.when($window.cordova.plugins.locationManager.requestAlwaysAuthorization());
+ },
+ getMonitoredRegions: function () {
+ return $q.when($window.cordova.plugins.locationManager.getMonitoredRegions());
+ },
+ getRangedRegions: function () {
+ return $q.when($window.cordova.plugins.locationManager.getRangedRegions());
+ },
+ isRangingAvailable: function () {
+ return $q.when($window.cordova.plugins.locationManager.isRangingAvailable());
+ },
+ isMonitoringAvailableForClass: function (region) {
+ return $q.when($window.cordova.plugins.locationManager.isMonitoringAvailableForClass(region));
+ },
+ startAdvertising: function (region, measuredPower) {
+ return $q.when($window.cordova.plugins.locationManager.startAdvertising(region, measuredPower));
+ },
+ stopAdvertising: function () {
+ return $q.when($window.cordova.plugins.locationManager.stopAdvertising());
+ },
+ isAdvertisingAvailable: function () {
+ return $q.when($window.cordova.plugins.locationManager.isAdvertisingAvailable());
+ },
+ isAdvertising: function () {
+ return $q.when($window.cordova.plugins.locationManager.isAdvertising());
+ },
+ disableDebugLogs: function () {
+ return $q.when($window.cordova.plugins.locationManager.disableDebugLogs());
+ },
+ enableDebugNotifications: function () {
+ return $q.when($window.cordova.plugins.locationManager.enableDebugNotifications());
+ },
+ disableDebugNotifications: function () {
+ return $q.when($window.cordova.plugins.locationManager.disableDebugNotifications());
+ },
+ enableDebugLogs: function () {
+ return $q.when($window.cordova.plugins.locationManager.enableDebugLogs());
+ },
+ appendToDeviceLog: function (message) {
+ return $q.when($window.cordova.plugins.locationManager.appendToDeviceLog(message));
+ }
+ };
+ }]);
+
+// install : cordova plugin add https://github.com/don/cordova-plugin-ble-central.git
+// link : https://github.com/don/cordova-plugin-ble-central
+
+/* globals ble: true */
+angular.module('ngCordova.plugins.ble', [])
+
+ .factory('$cordovaBLE', ['$q', '$timeout', '$log', function ($q, $timeout, $log) {
+
+ return {
+ scan: function (services, seconds) {
+ var q = $q.defer();
+
+ ble.startScan(services, function (result) {
+ q.notify(result);
+ }, function (error) {
+ q.reject(error);
+ });
+
+ $timeout(function () {
+ ble.stopScan(function () {
+ q.resolve();
+ }, function (error) {
+ q.reject(error);
+ });
+ }, seconds*1000);
+
+ return q.promise;
+ },
+
+ startScan: function (services, callback, errorCallback) {
+ return ble.startScan(services, callback, errorCallback);
+ },
+
+ stopScan: function () {
+ var q = $q.defer();
+ ble.stopScan(function () {
+ q.resolve();
+ }, function (error) {
+ q.reject(error);
+ });
+ return q.promise;
+ },
+
+ connect: function (deviceID) {
+ var q = $q.defer();
+ ble.connect(deviceID, function (result) {
+ q.resolve(result);
+ }, function (error) {
+ q.reject(error);
+ });
+ return q.promise;
+ },
+
+ disconnect: function (deviceID) {
+ var q = $q.defer();
+ ble.disconnect(deviceID, function (result) {
+ q.resolve(result);
+ }, function (error) {
+ q.reject(error);
+ });
+ return q.promise;
+ },
+
+ read: function (deviceID, serviceUUID, characteristicUUID) {
+ var q = $q.defer();
+ ble.read(deviceID, serviceUUID, characteristicUUID, function (result) {
+ q.resolve(result);
+ }, function (error) {
+ q.reject(error);
+ });
+ return q.promise;
+ },
+
+ write: function (deviceID, serviceUUID, characteristicUUID, data) {
+ var q = $q.defer();
+ ble.write(deviceID, serviceUUID, characteristicUUID, data, function (result) {
+ q.resolve(result);
+ }, function (error) {
+ q.reject(error);
+ });
+ return q.promise;
+ },
+
+ writeWithoutResponse: function (deviceID, serviceUUID, characteristicUUID, data) {
+ var q = $q.defer();
+ ble.writeWithoutResponse(deviceID, serviceUUID, characteristicUUID, data, function (result) {
+ q.resolve(result);
+ }, function (error) {
+ q.reject(error);
+ });
+ return q.promise;
+ },
+
+ writeCommand: function (deviceID, serviceUUID, characteristicUUID, data) {
+ $log.warning('writeCommand is deprecated, use writeWithoutResponse');
+ return this.writeWithoutResponse(deviceID, serviceUUID, characteristicUUID, data);
+ },
+
+ startNotification: function (deviceID, serviceUUID, characteristicUUID, callback, errorCallback) {
+ return ble.startNotification(deviceID, serviceUUID, characteristicUUID, callback, errorCallback);
+ },
+
+ stopNotification: function (deviceID, serviceUUID, characteristicUUID) {
+ var q = $q.defer();
+ ble.stopNotification(deviceID, serviceUUID, characteristicUUID, function (result) {
+ q.resolve(result);
+ }, function (error) {
+ q.reject(error);
+ });
+ return q.promise;
+ },
+
+ isConnected: function (deviceID) {
+ var q = $q.defer();
+ ble.isConnected(deviceID, function (result) {
+ q.resolve(result);
+ }, function (error) {
+ q.reject(error);
+ });
+ return q.promise;
+ },
+
+ enable: function () {
+ var q = $q.defer();
+ ble.enable(function (result) {
+ q.resolve(result);
+ }, function (error) {
+ q.reject(error);
+ });
+ return q.promise;
+ },
+
+ isEnabled: function () {
+ var q = $q.defer();
+ ble.isEnabled(function (result) {
+ q.resolve(result);
+ }, function (error) {
+ q.reject(error);
+ });
+ return q.promise;
+ }
+ };
+ }]);
+
+// install : cordova plugin add https://github.com/don/BluetoothSerial.git
+// link : https://github.com/don/BluetoothSerial
+
+angular.module('ngCordova.plugins.bluetoothSerial', [])
+
+ .factory('$cordovaBluetoothSerial', ['$q', '$window', function ($q, $window) {
+
+ return {
+ connect: function (address) {
+ var q = $q.defer();
+ var disconnectionPromise = $q.defer();
+ var isConnected = false;
+ $window.bluetoothSerial.connect(address, function () {
+ isConnected = true;
+ q.resolve(disconnectionPromise);
+ }, function (error) {
+ if(isConnected === false) {
+ disconnectionPromise.reject(error);
+ }
+ q.reject(error);
+ });
+ return q.promise;
+ },
+
+ // not supported on iOS
+ connectInsecure: function (address) {
+ var q = $q.defer();
+ $window.bluetoothSerial.connectInsecure(address, function () {
+ q.resolve();
+ }, function (error) {
+ q.reject(error);
+ });
+ return q.promise;
+ },
+
+ disconnect: function () {
+ var q = $q.defer();
+ $window.bluetoothSerial.disconnect(function () {
+ q.resolve();
+ }, function (error) {
+ q.reject(error);
+ });
+ return q.promise;
+ },
+
+ list: function () {
+ var q = $q.defer();
+ $window.bluetoothSerial.list(function (data) {
+ q.resolve(data);
+ }, function (error) {
+ q.reject(error);
+ });
+ return q.promise;
+ },
+
+ discoverUnpaired: function () {
+ var q = $q.defer();
+ $window.bluetoothSerial.discoverUnpaired(function (data) {
+ q.resolve(data);
+ }, function (error) {
+ q.reject(error);
+ });
+ return q.promise;
+ },
+
+ setDeviceDiscoveredListener: function () {
+ var q = $q.defer();
+ $window.bluetoothSerial.setDeviceDiscoveredListener(function (data) {
+ q.notify(data);
+ });
+ return q.promise;
+ },
+
+ clearDeviceDiscoveredListener: function () {
+ $window.bluetoothSerial.clearDeviceDiscoveredListener();
+ },
+
+ showBluetoothSettings: function () {
+ var q = $q.defer();
+ $window.bluetoothSerial.showBluetoothSettings(function () {
+ q.resolve();
+ }, function (error) {
+ q.reject(error);
+ });
+ return q.promise;
+ },
+
+ isEnabled: function () {
+ var q = $q.defer();
+ $window.bluetoothSerial.isEnabled(function () {
+ q.resolve();
+ }, function () {
+ q.reject();
+ });
+ return q.promise;
+ },
+
+ enable: function () {
+ var q = $q.defer();
+ $window.bluetoothSerial.enable(function () {
+ q.resolve();
+ }, function () {
+ q.reject();
+ });
+ return q.promise;
+ },
+
+ isConnected: function () {
+ var q = $q.defer();
+ $window.bluetoothSerial.isConnected(function () {
+ q.resolve();
+ }, function () {
+ q.reject();
+ });
+ return q.promise;
+ },
+
+ available: function () {
+ var q = $q.defer();
+ $window.bluetoothSerial.available(function (data) {
+ q.resolve(data);
+ }, function (error) {
+ q.reject(error);
+ });
+ return q.promise;
+ },
+
+ read: function () {
+ var q = $q.defer();
+ $window.bluetoothSerial.read(function (data) {
+ q.resolve(data);
+ }, function (error) {
+ q.reject(error);
+ });
+ return q.promise;
+ },
+
+ readUntil: function (delimiter) {
+ var q = $q.defer();
+ $window.bluetoothSerial.readUntil(delimiter, function (data) {
+ q.resolve(data);
+ }, function (error) {
+ q.reject(error);
+ });
+ return q.promise;
+ },
+
+ write: function (data) {
+ var q = $q.defer();
+ $window.bluetoothSerial.write(data, function () {
+ q.resolve();
+ }, function (error) {
+ q.reject(error);
+ });
+ return q.promise;
+ },
+
+ subscribe: function (delimiter) {
+ var q = $q.defer();
+ $window.bluetoothSerial.subscribe(delimiter, function (data) {
+ q.notify(data);
+ }, function (error) {
+ q.reject(error);
+ });
+ return q.promise;
+ },
+
+ subscribeRawData: function () {
+ var q = $q.defer();
+ $window.bluetoothSerial.subscribeRawData(function (data) {
+ q.notify(data);
+ }, function (error) {
+ q.reject(error);
+ });
+ return q.promise;
+ },
+
+ unsubscribe: function () {
+ var q = $q.defer();
+ $window.bluetoothSerial.unsubscribe(function () {
+ q.resolve();
+ }, function (error) {
+ q.reject(error);
+ });
+ return q.promise;
+ },
+
+ unsubscribeRawData: function () {
+ var q = $q.defer();
+ $window.bluetoothSerial.unsubscribeRawData(function () {
+ q.resolve();
+ }, function (error) {
+ q.reject(error);
+ });
+ return q.promise;
+ },
+
+ clear: function () {
+ var q = $q.defer();
+ $window.bluetoothSerial.clear(function () {
+ q.resolve();
+ }, function (error) {
+ q.reject(error);
+ });
+ return q.promise;
+ },
+
+ readRSSI: function () {
+ var q = $q.defer();
+ $window.bluetoothSerial.readRSSI(function (data) {
+ q.resolve(data);
+ }, function (error) {
+ q.reject(error);
+ });
+ return q.promise;
+ }
+ };
+ }]);
+
+// install : cordova plugin add https://github.com/fiscal-cliff/phonegap-plugin-brightness.git
+// link : https://github.com/fiscal-cliff/phonegap-plugin-brightness
+
+angular.module('ngCordova.plugins.brightness', [])
+
+ .factory('$cordovaBrightness', ['$q', '$window', function ($q, $window) {
+
+ return {
+ get: function () {
+ var q = $q.defer();
+
+ if (!$window.cordova) {
+ q.reject('Not supported without cordova.js');
+ } else {
+ $window.cordova.plugins.brightness.getBrightness(function (result) {
+ q.resolve(result);
+ }, function (err) {
+ q.reject(err);
+ });
+ }
+
+ return q.promise;
+ },
+
+ set: function (data) {
+ var q = $q.defer();
+
+ if (!$window.cordova) {
+ q.reject('Not supported without cordova.js');
+ } else {
+ $window.cordova.plugins.brightness.setBrightness(data, function (result) {
+ q.resolve(result);
+ }, function (err) {
+ q.reject(err);
+ });
+ }
+
+ return q.promise;
+ },
+
+ setKeepScreenOn: function (bool) {
+ var q = $q.defer();
+
+ if (!$window.cordova) {
+ q.reject('Not supported without cordova.js');
+ } else {
+ $window.cordova.plugins.brightness.setKeepScreenOn(bool, function (result) {
+ q.resolve(result);
+ }, function (err) {
+ q.reject(err);
+ });
+ }
+
+ return q.promise;
+ }
+ };
+ }]);
+
+
+// install : cordova plugin add https://github.com/EddyVerbruggen/Calendar-PhoneGap-Plugin.git
+// link : https://github.com/EddyVerbruggen/Calendar-PhoneGap-Plugin
+
+angular.module('ngCordova.plugins.calendar', [])
+
+ .factory('$cordovaCalendar', ['$q', '$window', function ($q, $window) {
+
+ return {
+ createCalendar: function (options) {
+ var d = $q.defer(),
+ createCalOptions = $window.plugins.calendar.getCreateCalendarOptions();
+
+ if (typeof options === 'string') {
+ createCalOptions.calendarName = options;
+ } else {
+ createCalOptions = angular.extend(createCalOptions, options);
+ }
+
+ $window.plugins.calendar.createCalendar(createCalOptions, function (message) {
+ d.resolve(message);
+ }, function (error) {
+ d.reject(error);
+ });
+
+ return d.promise;
+ },
+
+ deleteCalendar: function (calendarName) {
+ var d = $q.defer();
+
+ $window.plugins.calendar.deleteCalendar(calendarName, function (message) {
+ d.resolve(message);
+ }, function (error) {
+ d.reject(error);
+ });
+
+ return d.promise;
+ },
+
+ createEvent: function (options) {
+ var d = $q.defer(),
+ defaultOptions = {
+ title: null,
+ location: null,
+ notes: null,
+ startDate: null,
+ endDate: null
+ };
+
+ defaultOptions = angular.extend(defaultOptions, options);
+
+ $window.plugins.calendar.createEvent(
+ defaultOptions.title,
+ defaultOptions.location,
+ defaultOptions.notes,
+ new Date(defaultOptions.startDate),
+ new Date(defaultOptions.endDate),
+ function (message) {
+ d.resolve(message);
+ }, function (error) {
+ d.reject(error);
+ }
+ );
+
+ return d.promise;
+ },
+
+ createEventWithOptions: function (options) {
+ var d = $q.defer(),
+ defaultOptionKeys = [],
+ calOptions = window.plugins.calendar.getCalendarOptions(),
+ defaultOptions = {
+ title: null,
+ location: null,
+ notes: null,
+ startDate: null,
+ endDate: null
+ };
+
+ defaultOptionKeys = Object.keys(defaultOptions);
+
+ for (var key in options) {
+ if (defaultOptionKeys.indexOf(key) === -1) {
+ calOptions[key] = options[key];
+ } else {
+ defaultOptions[key] = options[key];
+ }
+ }
+
+ $window.plugins.calendar.createEventWithOptions(
+ defaultOptions.title,
+ defaultOptions.location,
+ defaultOptions.notes,
+ new Date(defaultOptions.startDate),
+ new Date(defaultOptions.endDate),
+ calOptions,
+ function (message) {
+ d.resolve(message);
+ }, function (error) {
+ d.reject(error);
+ }
+ );
+
+ return d.promise;
+ },
+
+ createEventInteractively: function (options) {
+ var d = $q.defer(),
+ defaultOptions = {
+ title: null,
+ location: null,
+ notes: null,
+ startDate: null,
+ endDate: null
+ };
+
+ defaultOptions = angular.extend(defaultOptions, options);
+
+ $window.plugins.calendar.createEventInteractively(
+ defaultOptions.title,
+ defaultOptions.location,
+ defaultOptions.notes,
+ new Date(defaultOptions.startDate),
+ new Date(defaultOptions.endDate),
+ function (message) {
+ d.resolve(message);
+ }, function (error) {
+ d.reject(error);
+ }
+ );
+
+ return d.promise;
+ },
+
+ createEventInNamedCalendar: function (options) {
+ var d = $q.defer(),
+ defaultOptions = {
+ title: null,
+ location: null,
+ notes: null,
+ startDate: null,
+ endDate: null,
+ calendarName: null
+ };
+
+ defaultOptions = angular.extend(defaultOptions, options);
+
+ $window.plugins.calendar.createEventInNamedCalendar(
+ defaultOptions.title,
+ defaultOptions.location,
+ defaultOptions.notes,
+ new Date(defaultOptions.startDate),
+ new Date(defaultOptions.endDate),
+ defaultOptions.calendarName,
+ function (message) {
+ d.resolve(message);
+ }, function (error) {
+ d.reject(error);
+ }
+ );
+
+ return d.promise;
+ },
+
+ findEvent: function (options) {
+ var d = $q.defer(),
+ defaultOptions = {
+ title: null,
+ location: null,
+ notes: null,
+ startDate: null,
+ endDate: null
+ };
+
+ defaultOptions = angular.extend(defaultOptions, options);
+
+ $window.plugins.calendar.findEvent(
+ defaultOptions.title,
+ defaultOptions.location,
+ defaultOptions.notes,
+ new Date(defaultOptions.startDate),
+ new Date(defaultOptions.endDate),
+ function (foundEvent) {
+ d.resolve(foundEvent);
+ }, function (error) {
+ d.reject(error);
+ }
+ );
+
+ return d.promise;
+ },
+
+ listEventsInRange: function (startDate, endDate) {
+ var d = $q.defer();
+
+ $window.plugins.calendar.listEventsInRange(startDate, endDate, function (events) {
+ d.resolve(events);
+ }, function (error) {
+ d.reject(error);
+ });
+
+ return d.promise;
+ },
+
+ listCalendars: function () {
+ var d = $q.defer();
+
+ $window.plugins.calendar.listCalendars(function (calendars) {
+ d.resolve(calendars);
+ }, function (error) {
+ d.reject(error);
+ });
+
+ return d.promise;
+ },
+
+ findAllEventsInNamedCalendar: function (calendarName) {
+ var d = $q.defer();
+
+ $window.plugins.calendar.findAllEventsInNamedCalendar(calendarName, function (events) {
+ d.resolve(events);
+ }, function (error) {
+ d.reject(error);
+ });
+
+ return d.promise;
+ },
+
+ modifyEvent: function (options) {
+ var d = $q.defer(),
+ defaultOptions = {
+ title: null,
+ location: null,
+ notes: null,
+ startDate: null,
+ endDate: null,
+ newTitle: null,
+ newLocation: null,
+ newNotes: null,
+ newStartDate: null,
+ newEndDate: null
+ };
+
+ defaultOptions = angular.extend(defaultOptions, options);
+
+ $window.plugins.calendar.modifyEvent(
+ defaultOptions.title,
+ defaultOptions.location,
+ defaultOptions.notes,
+ new Date(defaultOptions.startDate),
+ new Date(defaultOptions.endDate),
+ defaultOptions.newTitle,
+ defaultOptions.newLocation,
+ defaultOptions.newNotes,
+ new Date(defaultOptions.newStartDate),
+ new Date(defaultOptions.newEndDate),
+ function (message) {
+ d.resolve(message);
+ }, function (error) {
+ d.reject(error);
+ }
+ );
+
+ return d.promise;
+ },
+
+ deleteEvent: function (options) {
+ var d = $q.defer(),
+ defaultOptions = {
+ newTitle: null,
+ location: null,
+ notes: null,
+ startDate: null,
+ endDate: null
+ };
+
+ defaultOptions = angular.extend(defaultOptions, options);
+
+ $window.plugins.calendar.deleteEvent(
+ defaultOptions.newTitle,
+ defaultOptions.location,
+ defaultOptions.notes,
+ new Date(defaultOptions.startDate),
+ new Date(defaultOptions.endDate),
+ function (message) {
+ d.resolve(message);
+ }, function (error) {
+ d.reject(error);
+ }
+ );
+
+ return d.promise;
+ }
+ };
+ }]);
+
+// install : cordova plugin add cordova-plugin-camera
+// link : https://github.com/apache/cordova-plugin-camera
+
+angular.module('ngCordova.plugins.camera', [])
+
+ .factory('$cordovaCamera', ['$q', function ($q) {
+
+ return {
+ getPicture: function (options) {
+ var q = $q.defer();
+
+ if (!navigator.camera) {
+ q.resolve(null);
+ return q.promise;
+ }
+
+ navigator.camera.getPicture(function (imageData) {
+ q.resolve(imageData);
+ }, function (err) {
+ q.reject(err);
+ }, options);
+
+ return q.promise;
+ },
+
+ cleanup: function () {
+ var q = $q.defer();
+
+ navigator.camera.cleanup(function () {
+ q.resolve();
+ }, function (err) {
+ q.reject(err);
+ });
+
+ return q.promise;
+ }
+ };
+ }]);
+
+// install : cordova plugin add cordova-plugin-media-capture
+// link : https://github.com/apache/cordova-plugin-media-capture
+
+angular.module('ngCordova.plugins.capture', [])
+
+ .factory('$cordovaCapture', ['$q', function ($q) {
+
+ return {
+ captureAudio: function (options) {
+ var q = $q.defer();
+
+ if (!navigator.device.capture) {
+ q.resolve(null);
+ return q.promise;
+ }
+
+ navigator.device.capture.captureAudio(function (audioData) {
+ q.resolve(audioData);
+ }, function (err) {
+ q.reject(err);
+ }, options);
+
+ return q.promise;
+ },
+ captureImage: function (options) {
+ var q = $q.defer();
+
+ if (!navigator.device.capture) {
+ q.resolve(null);
+ return q.promise;
+ }
+
+ navigator.device.capture.captureImage(function (imageData) {
+ q.resolve(imageData);
+ }, function (err) {
+ q.reject(err);
+ }, options);
+
+ return q.promise;
+ },
+ captureVideo: function (options) {
+ var q = $q.defer();
+
+ if (!navigator.device.capture) {
+ q.resolve(null);
+ return q.promise;
+ }
+
+ navigator.device.capture.captureVideo(function (videoData) {
+ q.resolve(videoData);
+ }, function (err) {
+ q.reject(err);
+ }, options);
+
+ return q.promise;
+ }
+ };
+ }]);
+
+// install : cordova plugin add https://github.com/vkeepe/card.io.git
+// link : https://github.com/vkeepe/card.io.git
+
+/* globals CardIO: true */
+angular.module('ngCordova.plugins.cardIO', [])
+
+ .provider(
+ '$cordovaNgCardIO', [function () {
+
+ /**
+ * Default array of response data from cardIO scan card
+ */
+ var defaultRespFields = [
+ 'card_type',
+ 'redacted_card_number',
+ 'card_number',
+ 'expiry_month',
+ 'expiry_year',
+ 'short_expiry_year',
+ 'cvv',
+ 'zip'
+ ];
+
+ /**
+ * Default config for cardIO scan function
+ */
+ var defaultScanConfig = {
+ 'expiry': true,
+ 'cvv': true,
+ 'zip': false,
+ 'suppressManual': false,
+ 'suppressConfirm': false,
+ 'hideLogo': true
+ };
+
+ /**
+ * Configuring defaultRespFields using $cordovaNgCardIOProvider
+ *
+ */
+ this.setCardIOResponseFields = function (fields) {
+ if (!fields || !angular.isArray(fields)) {
+ return;
+ }
+ defaultRespFields = fields;
+ };
+
+ /**
+ *
+ * Configuring defaultScanConfig using $cordovaNgCardIOProvider
+ */
+ this.setScanerConfig = function (config) {
+ if (!config || !angular.isObject(config)) {
+ return;
+ }
+
+ defaultScanConfig.expiry = config.expiry || true;
+ defaultScanConfig.cvv = config.cvv || true;
+ defaultScanConfig.zip = config.zip || false;
+ defaultScanConfig.suppressManual = config.suppressManual || false;
+ defaultScanConfig.suppressConfirm = config.suppressConfirm || false;
+ defaultScanConfig.hideLogo = config.hideLogo || true;
+ };
+
+ /**
+ * Function scanCard for $cordovaNgCardIO service to make scan of card
+ *
+ */
+ this.$get = ['$q', function ($q) {
+ return {
+ scanCard: function () {
+
+ var deferred = $q.defer();
+ CardIO.scan(
+ defaultScanConfig,
+ function (response) {
+
+ if (response === null) {
+ deferred.reject(null);
+ } else {
+
+ var respData = {};
+ for (
+ var i = 0, len = defaultRespFields.length; i < len; i++) {
+ var field = defaultRespFields[i];
+
+ if (field === 'short_expiry_year') {
+ respData[field] = String(response.expiry_year).substr( // jshint ignore:line
+ 2, 2
+ ) || '';
+ } else {
+ respData[field] = response[field] || '';
+ }
+ }
+ deferred.resolve(respData);
+ }
+ },
+ function () {
+ deferred.reject(null);
+ }
+ );
+ return deferred.promise;
+ }
+ };
+ }];
+ }]
+);
+
+// install : cordova plugin add https://github.com/VersoSolutions/CordovaClipboard.git
+// link : https://github.com/VersoSolutions/CordovaClipboard
+
+angular.module('ngCordova.plugins.clipboard', [])
+
+ .factory('$cordovaClipboard', ['$q', '$window', function ($q, $window) {
+
+ return {
+ copy: function (text) {
+ var q = $q.defer();
+
+ $window.cordova.plugins.clipboard.copy(text,
+ function () {
+ q.resolve();
+ }, function () {
+ q.reject();
+ });
+
+ return q.promise;
+ },
+
+ paste: function () {
+ var q = $q.defer();
+
+ $window.cordova.plugins.clipboard.paste(function (text) {
+ q.resolve(text);
+ }, function () {
+ q.reject();
+ });
+
+ return q.promise;
+ }
+ };
+ }]);
+
+// install : cordova plugin add cordova-plugin-contacts
+// link : https://github.com/apache/cordova-plugin-contacts
+
+angular.module('ngCordova.plugins.contacts', [])
+
+ .factory('$cordovaContacts', ['$q', function ($q) {
+
+ return {
+ save: function (contact) {
+ var q = $q.defer();
+ var deviceContact = navigator.contacts.create(contact);
+
+ deviceContact.save(function (result) {
+ q.resolve(result);
+ }, function (err) {
+ q.reject(err);
+ });
+ return q.promise;
+ },
+
+ remove: function (contact) {
+ var q = $q.defer();
+ var deviceContact = navigator.contacts.create(contact);
+
+ deviceContact.remove(function (result) {
+ q.resolve(result);
+ }, function (err) {
+ q.reject(err);
+ });
+ return q.promise;
+ },
+
+ clone: function (contact) {
+ var deviceContact = navigator.contacts.create(contact);
+ return deviceContact.clone(contact);
+ },
+
+ find: function (options) {
+ var q = $q.defer();
+ var fields = options.fields || ['id', 'displayName'];
+ delete options.fields;
+ if (Object.keys(options).length === 0) {
+ navigator.contacts.find(fields, function (results) {
+ q.resolve(results);
+ },function (err) {
+ q.reject(err);
+ });
+ }
+ else {
+ navigator.contacts.find(fields, function (results) {
+ q.resolve(results);
+ }, function (err) {
+ q.reject(err);
+ }, options);
+ }
+ return q.promise;
+ },
+
+ pickContact: function () {
+ var q = $q.defer();
+
+ navigator.contacts.pickContact(function (contact) {
+ q.resolve(contact);
+ }, function (err) {
+ q.reject(err);
+ });
+
+ return q.promise;
+ }
+
+ // TODO: method to set / get ContactAddress
+ // TODO: method to set / get ContactError
+ // TODO: method to set / get ContactField
+ // TODO: method to set / get ContactName
+ // TODO: method to set / get ContactOrganization
+ };
+ }]);
+
+// install : cordova plugin add https://github.com/VitaliiBlagodir/cordova-plugin-datepicker.git
+// link : https://github.com/VitaliiBlagodir/cordova-plugin-datepicker
+
+angular.module('ngCordova.plugins.datePicker', [])
+
+ .factory('$cordovaDatePicker', ['$window', '$q', function ($window, $q) {
+
+ return {
+ show: function (options) {
+ var q = $q.defer();
+ options = options || {date: new Date(), mode: 'date'};
+ $window.datePicker.show(options, function (date) {
+ q.resolve(date);
+ }, function (error){
+ q.reject(error);
+ });
+ return q.promise;
+ }
+ };
+ }]);
+// install : cordova plugin add cordova-plugin-device
+// link : https://github.com/apache/cordova-plugin-device
+
+/* globals device: true */
+angular.module('ngCordova.plugins.device', [])
+
+ .factory('$cordovaDevice', [function () {
+
+ return {
+ /**
+ * Returns the whole device object.
+ * @see https://github.com/apache/cordova-plugin-device
+ * @returns {Object} The device object.
+ */
+ getDevice: function () {
+ return device;
+ },
+
+ /**
+ * Returns the Cordova version.
+ * @see https://github.com/apache/cordova-plugin-device#devicecordova
+ * @returns {String} The Cordova version.
+ */
+ getCordova: function () {
+ return device.cordova;
+ },
+
+ /**
+ * Returns the name of the device's model or product.
+ * @see https://github.com/apache/cordova-plugin-device#devicemodel
+ * @returns {String} The name of the device's model or product.
+ */
+ getModel: function () {
+ return device.model;
+ },
+
+ /**
+ * @deprecated device.name is deprecated as of version 2.3.0. Use device.model instead.
+ * @returns {String}
+ */
+ getName: function () {
+ return device.name;
+ },
+
+ /**
+ * Returns the device's operating system name.
+ * @see https://github.com/apache/cordova-plugin-device#deviceplatform
+ * @returns {String} The device's operating system name.
+ */
+ getPlatform: function () {
+ return device.platform;
+ },
+
+ /**
+ * Returns the device's Universally Unique Identifier.
+ * @see https://github.com/apache/cordova-plugin-device#deviceuuid
+ * @returns {String} The device's Universally Unique Identifier
+ */
+ getUUID: function () {
+ return device.uuid;
+ },
+
+ /**
+ * Returns the operating system version.
+ * @see https://github.com/apache/cordova-plugin-device#deviceversion
+ * @returns {String}
+ */
+ getVersion: function () {
+ return device.version;
+ },
+
+ /**
+ * Returns the device manufacturer.
+ * @returns {String}
+ */
+ getManufacturer: function () {
+ return device.manufacturer;
+ }
+ };
+ }]);
+
+// install : cordova plugin add cordova-plugin-device-motion
+// link : https://github.com/apache/cordova-plugin-device-motion
+
+angular.module('ngCordova.plugins.deviceMotion', [])
+
+ .factory('$cordovaDeviceMotion', ['$q', function ($q) {
+
+ return {
+ getCurrentAcceleration: function () {
+ var q = $q.defer();
+
+ if (angular.isUndefined(navigator.accelerometer) ||
+ !angular.isFunction(navigator.accelerometer.getCurrentAcceleration)) {
+ q.reject('Device do not support watchAcceleration');
+ return q.promise;
+ }
+
+ navigator.accelerometer.getCurrentAcceleration(function (result) {
+ q.resolve(result);
+ }, function (err) {
+ q.reject(err);
+ });
+
+ return q.promise;
+ },
+
+ watchAcceleration: function (options) {
+ var q = $q.defer();
+
+ if (angular.isUndefined(navigator.accelerometer) ||
+ !angular.isFunction(navigator.accelerometer.watchAcceleration)) {
+ q.reject('Device do not support watchAcceleration');
+ return q.promise;
+ }
+
+ var watchID = navigator.accelerometer.watchAcceleration(function (result) {
+ q.notify(result);
+ }, function (err) {
+ q.reject(err);
+ }, options);
+
+ q.promise.cancel = function () {
+ navigator.accelerometer.clearWatch(watchID);
+ };
+
+ q.promise.clearWatch = function (id) {
+ navigator.accelerometer.clearWatch(id || watchID);
+ };
+
+ q.promise.watchID = watchID;
+
+ return q.promise;
+ },
+
+ clearWatch: function (watchID) {
+ return navigator.accelerometer.clearWatch(watchID);
+ }
+ };
+ }]);
+
+// install : cordova plugin add cordova-plugin-device-orientation
+// link : https://github.com/apache/cordova-plugin-device-orientation
+
+angular.module('ngCordova.plugins.deviceOrientation', [])
+
+ .factory('$cordovaDeviceOrientation', ['$q', function ($q) {
+
+ var defaultOptions = {
+ frequency: 3000 // every 3s
+ };
+
+ return {
+ getCurrentHeading: function () {
+ var q = $q.defer();
+
+ if(!navigator.compass) {
+ q.reject('No compass on Device');
+ return q.promise;
+ }
+
+ navigator.compass.getCurrentHeading(function (result) {
+ q.resolve(result);
+ }, function (err) {
+ q.reject(err);
+ });
+
+ return q.promise;
+ },
+
+ watchHeading: function (options) {
+ var q = $q.defer();
+
+ if(!navigator.compass) {
+ q.reject('No compass on Device');
+ return q.promise;
+ }
+
+ var _options = angular.extend(defaultOptions, options);
+ var watchID = navigator.compass.watchHeading(function (result) {
+ q.notify(result);
+ }, function (err) {
+ q.reject(err);
+ }, _options);
+
+ q.promise.cancel = function () {
+ navigator.compass.clearWatch(watchID);
+ };
+
+ q.promise.clearWatch = function (id) {
+ navigator.compass.clearWatch(id || watchID);
+ };
+
+ q.promise.watchID = watchID;
+
+ return q.promise;
+ },
+
+ clearWatch: function (watchID) {
+ return navigator.compass.clearWatch(watchID);
+ }
+ };
+ }]);
+
+// install : cordova plugin add cordova-plugin-dialogs
+// link : https://github.com/apache/cordova-plugin-dialogs
+
+angular.module('ngCordova.plugins.dialogs', [])
+
+ .factory('$cordovaDialogs', ['$q', '$window', function ($q, $window) {
+
+ return {
+ alert: function (message, title, buttonName) {
+ var q = $q.defer();
+
+ if (!$window.navigator.notification) {
+ $window.alert(message);
+ q.resolve();
+ } else {
+ navigator.notification.alert(message, function () {
+ q.resolve();
+ }, title, buttonName);
+ }
+
+ return q.promise;
+ },
+
+ confirm: function (message, title, buttonLabels) {
+ var q = $q.defer();
+
+ if (!$window.navigator.notification) {
+ if ($window.confirm(message)) {
+ q.resolve(1);
+ } else {
+ q.resolve(2);
+ }
+ } else {
+ navigator.notification.confirm(message, function (buttonIndex) {
+ q.resolve(buttonIndex);
+ }, title, buttonLabels);
+ }
+
+ return q.promise;
+ },
+
+ prompt: function (message, title, buttonLabels, defaultText) {
+ var q = $q.defer();
+
+ if (!$window.navigator.notification) {
+ var res = $window.prompt(message, defaultText);
+ if (res !== null) {
+ q.resolve({input1: res, buttonIndex: 1});
+ } else {
+ q.resolve({input1: res, buttonIndex: 2});
+ }
+ } else {
+ navigator.notification.prompt(message, function (result) {
+ q.resolve(result);
+ }, title, buttonLabels, defaultText);
+ }
+ return q.promise;
+ },
+
+ beep: function (times) {
+ return navigator.notification.beep(times);
+ },
+
+ activityStart: function (message, title) {
+ var q = $q.defer();
+
+ if (cordova.platformId === 'android') {
+ navigator.notification.activityStart(title, message);
+ q.resolve();
+ } else {
+ q.reject(message, title);
+ }
+
+ return q.promise;
+ },
+
+ activityStop: function () {
+ var q = $q.defer();
+
+ if (cordova.platformId === 'android') {
+ navigator.notification.activityStop();
+ q.resolve();
+ } else {
+ q.reject();
+ }
+
+ return q.promise;
+ },
+
+ progressStart: function (message, title) {
+ var q = $q.defer();
+
+ if (cordova.platformId === 'android') {
+ navigator.notification.progressStart(title, message);
+ q.resolve();
+ } else {
+ q.reject(message, title);
+ }
+
+ return q.promise;
+ },
+
+ progressStop: function () {
+ var q = $q.defer();
+
+ if (cordova.platformId === 'android') {
+ navigator.notification.progressStop();
+ q.resolve();
+ } else {
+ q.reject();
+ }
+
+ return q.promise;
+ },
+
+ progressValue: function (value) {
+ var q = $q.defer();
+
+ if (cordova.platformId === 'android') {
+ navigator.notification.progressValue(value);
+ q.resolve();
+ } else {
+ q.reject(value);
+ }
+
+ return q.promise;
+ }
+ };
+ }]);
+
+// install : cordova plugin add https://github.com/katzer/cordova-plugin-email-composer.git
+// link : https://github.com/katzer/cordova-plugin-email-composer
+
+angular.module('ngCordova.plugins.emailComposer', [])
+
+ .factory('$cordovaEmailComposer', ['$q', function ($q) {
+
+ return {
+ isAvailable: function () {
+ var q = $q.defer();
+
+ cordova.plugins.email.isAvailable(function (isAvailable) {
+ if (isAvailable) {
+ q.resolve();
+ } else {
+ q.reject();
+ }
+ });
+
+ return q.promise;
+ },
+
+ open: function (properties) {
+ var q = $q.defer();
+
+ cordova.plugins.email.open(properties, function () {
+ q.reject(); // user closed email composer
+ });
+
+ return q.promise;
+ },
+
+ addAlias: function (app, schema) {
+ cordova.plugins.email.addAlias(app, schema);
+ }
+ };
+ }]);
+
+// install : cordova -d plugin add https://github.com/Wizcorp/phonegap-facebook-plugin.git --variable APP_ID="123456789" --variable APP_NAME="myApplication"
+// link : https://github.com/Wizcorp/phonegap-facebook-plugin
+
+/* globals facebookConnectPlugin: true */
+angular.module('ngCordova.plugins.facebook', [])
+
+ .provider('$cordovaFacebook', [function () {
+
+ /**
+ * Init browser settings for Facebook plugin
+ *
+ * @param {number} id
+ * @param {string} version
+ */
+ this.browserInit = function (id, version) {
+ this.appID = id;
+ this.appVersion = version || 'v2.0';
+ facebookConnectPlugin.browserInit(this.appID, this.appVersion);
+ };
+
+ this.$get = ['$q', function ($q) {
+ return {
+ login: function (permissions) {
+ var q = $q.defer();
+ facebookConnectPlugin.login(permissions, function (res) {
+ q.resolve(res);
+ }, function (res) {
+ q.reject(res);
+ });
+
+ return q.promise;
+ },
+
+ showDialog: function (options) {
+ var q = $q.defer();
+ facebookConnectPlugin.showDialog(options, function (res) {
+ q.resolve(res);
+ }, function (err) {
+ q.reject(err);
+ });
+ return q.promise;
+ },
+
+ api: function (path, permissions) {
+ var q = $q.defer();
+ facebookConnectPlugin.api(path, permissions, function (res) {
+ q.resolve(res);
+ }, function (err) {
+ q.reject(err);
+ });
+ return q.promise;
+ },
+
+ getAccessToken: function () {
+ var q = $q.defer();
+ facebookConnectPlugin.getAccessToken(function (res) {
+ q.resolve(res);
+ }, function (err) {
+ q.reject(err);
+ });
+ return q.promise;
+ },
+
+ getLoginStatus: function () {
+ var q = $q.defer();
+ facebookConnectPlugin.getLoginStatus(function (res) {
+ q.resolve(res);
+ }, function (err) {
+ q.reject(err);
+ });
+ return q.promise;
+ },
+
+ logout: function () {
+ var q = $q.defer();
+ facebookConnectPlugin.logout(function (res) {
+ q.resolve(res);
+ }, function (err) {
+ q.reject(err);
+ });
+ return q.promise;
+ }
+ };
+ }];
+ }]);
+
+// install : cordova plugin add https://github.com/floatinghotpot/cordova-plugin-facebookads.git
+// link : https://github.com/floatinghotpot/cordova-plugin-facebookads
+
+angular.module('ngCordova.plugins.facebookAds', [])
+
+ .factory('$cordovaFacebookAds', ['$q', '$window', function ($q, $window) {
+
+ return {
+ setOptions: function (options) {
+ var d = $q.defer();
+
+ $window.FacebookAds.setOptions(options, function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ createBanner: function (options) {
+ var d = $q.defer();
+
+ $window.FacebookAds.createBanner(options, function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ removeBanner: function () {
+ var d = $q.defer();
+
+ $window.FacebookAds.removeBanner(function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ showBanner: function (position) {
+ var d = $q.defer();
+
+ $window.FacebookAds.showBanner(position, function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ showBannerAtXY: function (x, y) {
+ var d = $q.defer();
+
+ $window.FacebookAds.showBannerAtXY(x, y, function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ hideBanner: function () {
+ var d = $q.defer();
+
+ $window.FacebookAds.hideBanner(function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ prepareInterstitial: function (options) {
+ var d = $q.defer();
+
+ $window.FacebookAds.prepareInterstitial(options, function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ showInterstitial: function () {
+ var d = $q.defer();
+
+ $window.FacebookAds.showInterstitial(function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ }
+ };
+ }]);
+
+// install : cordova plugin add cordova-plugin-file
+// link : https://github.com/apache/cordova-plugin-file
+
+angular.module('ngCordova.plugins.file', [])
+
+ .constant('$cordovaFileError', {
+ 1: 'NOT_FOUND_ERR',
+ 2: 'SECURITY_ERR',
+ 3: 'ABORT_ERR',
+ 4: 'NOT_READABLE_ERR',
+ 5: 'ENCODING_ERR',
+ 6: 'NO_MODIFICATION_ALLOWED_ERR',
+ 7: 'INVALID_STATE_ERR',
+ 8: 'SYNTAX_ERR',
+ 9: 'INVALID_MODIFICATION_ERR',
+ 10: 'QUOTA_EXCEEDED_ERR',
+ 11: 'TYPE_MISMATCH_ERR',
+ 12: 'PATH_EXISTS_ERR'
+ })
+
+ .provider('$cordovaFile', [function () {
+
+ this.$get = ['$q', '$window', '$cordovaFileError', function ($q, $window, $cordovaFileError) {
+
+ return {
+
+ getFreeDiskSpace: function () {
+ var q = $q.defer();
+ cordova.exec(function (result) {
+ q.resolve(result);
+ }, function (error) {
+ q.reject(error);
+ }, 'File', 'getFreeDiskSpace', []);
+ return q.promise;
+ },
+
+ checkDir: function (path, dir) {
+ var q = $q.defer();
+
+ if ((/^\//.test(dir))) {
+ q.reject('directory cannot start with \/');
+ }
+
+ try {
+ var directory = path + dir;
+ $window.resolveLocalFileSystemURL(directory, function (fileSystem) {
+ if (fileSystem.isDirectory === true) {
+ q.resolve(fileSystem);
+ } else {
+ q.reject({code: 13, message: 'input is not a directory'});
+ }
+ }, function (error) {
+ error.message = $cordovaFileError[error.code];
+ q.reject(error);
+ });
+ } catch (err) {
+ err.message = $cordovaFileError[err.code];
+ q.reject(err);
+ }
+
+ return q.promise;
+ },
+
+ checkFile: function (path, file) {
+ var q = $q.defer();
+
+ if ((/^\//.test(file))) {
+ q.reject('directory cannot start with \/');
+ }
+
+ try {
+ var directory = path + file;
+ $window.resolveLocalFileSystemURL(directory, function (fileSystem) {
+ if (fileSystem.isFile === true) {
+ q.resolve(fileSystem);
+ } else {
+ q.reject({code: 13, message: 'input is not a file'});
+ }
+ }, function (error) {
+ error.message = $cordovaFileError[error.code];
+ q.reject(error);
+ });
+ } catch (err) {
+ err.message = $cordovaFileError[err.code];
+ q.reject(err);
+ }
+
+ return q.promise;
+ },
+
+ createDir: function (path, dirName, replaceBool) {
+ var q = $q.defer();
+
+ if ((/^\//.test(dirName))) {
+ q.reject('directory cannot start with \/');
+ }
+
+ replaceBool = replaceBool ? false : true;
+
+ var options = {
+ create: true,
+ exclusive: replaceBool
+ };
+
+ try {
+ $window.resolveLocalFileSystemURL(path, function (fileSystem) {
+ fileSystem.getDirectory(dirName, options, function (result) {
+ q.resolve(result);
+ }, function (error) {
+ error.message = $cordovaFileError[error.code];
+ q.reject(error);
+ });
+ }, function (err) {
+ err.message = $cordovaFileError[err.code];
+ q.reject(err);
+ });
+ } catch (e) {
+ e.message = $cordovaFileError[e.code];
+ q.reject(e);
+ }
+
+ return q.promise;
+ },
+
+ createFile: function (path, fileName, replaceBool) {
+ var q = $q.defer();
+
+ if ((/^\//.test(fileName))) {
+ q.reject('file-name cannot start with \/');
+ }
+
+ replaceBool = replaceBool ? false : true;
+
+ var options = {
+ create: true,
+ exclusive: replaceBool
+ };
+
+ try {
+ $window.resolveLocalFileSystemURL(path, function (fileSystem) {
+ fileSystem.getFile(fileName, options, function (result) {
+ q.resolve(result);
+ }, function (error) {
+ error.message = $cordovaFileError[error.code];
+ q.reject(error);
+ });
+ }, function (err) {
+ err.message = $cordovaFileError[err.code];
+ q.reject(err);
+ });
+ } catch (e) {
+ e.message = $cordovaFileError[e.code];
+ q.reject(e);
+ }
+ return q.promise;
+ },
+
+ removeDir: function (path, dirName) {
+ var q = $q.defer();
+
+ if ((/^\//.test(dirName))) {
+ q.reject('file-name cannot start with \/');
+ }
+
+ try {
+ $window.resolveLocalFileSystemURL(path, function (fileSystem) {
+ fileSystem.getDirectory(dirName, {create: false}, function (dirEntry) {
+ dirEntry.remove(function () {
+ q.resolve({success: true, fileRemoved: dirEntry});
+ }, function (error) {
+ error.message = $cordovaFileError[error.code];
+ q.reject(error);
+ });
+ }, function (err) {
+ err.message = $cordovaFileError[err.code];
+ q.reject(err);
+ });
+ }, function (er) {
+ er.message = $cordovaFileError[er.code];
+ q.reject(er);
+ });
+ } catch (e) {
+ e.message = $cordovaFileError[e.code];
+ q.reject(e);
+ }
+ return q.promise;
+ },
+
+ removeFile: function (path, fileName) {
+ var q = $q.defer();
+
+ if ((/^\//.test(fileName))) {
+ q.reject('file-name cannot start with \/');
+ }
+
+ try {
+ $window.resolveLocalFileSystemURL(path, function (fileSystem) {
+ fileSystem.getFile(fileName, {create: false}, function (fileEntry) {
+ fileEntry.remove(function () {
+ q.resolve({success: true, fileRemoved: fileEntry});
+ }, function (error) {
+ error.message = $cordovaFileError[error.code];
+ q.reject(error);
+ });
+ }, function (err) {
+ err.message = $cordovaFileError[err.code];
+ q.reject(err);
+ });
+ }, function (er) {
+ er.message = $cordovaFileError[er.code];
+ q.reject(er);
+ });
+ } catch (e) {
+ e.message = $cordovaFileError[e.code];
+ q.reject(e);
+ }
+ return q.promise;
+ },
+
+ removeRecursively: function (path, dirName) {
+ var q = $q.defer();
+
+ if ((/^\//.test(dirName))) {
+ q.reject('file-name cannot start with \/');
+ }
+
+ try {
+ $window.resolveLocalFileSystemURL(path, function (fileSystem) {
+ fileSystem.getDirectory(dirName, {create: false}, function (dirEntry) {
+ dirEntry.removeRecursively(function () {
+ q.resolve({success: true, fileRemoved: dirEntry});
+ }, function (error) {
+ error.message = $cordovaFileError[error.code];
+ q.reject(error);
+ });
+ }, function (err) {
+ err.message = $cordovaFileError[err.code];
+ q.reject(err);
+ });
+ }, function (er) {
+ er.message = $cordovaFileError[er.code];
+ q.reject(er);
+ });
+ } catch (e) {
+ e.message = $cordovaFileError[e.code];
+ q.reject(e);
+ }
+ return q.promise;
+ },
+
+ writeFile: function (path, fileName, text, replaceBool) {
+ var q = $q.defer();
+
+ if ((/^\//.test(fileName))) {
+ q.reject('file-name cannot start with \/');
+ }
+
+ replaceBool = replaceBool ? false : true;
+
+ var options = {
+ create: true,
+ exclusive: replaceBool
+ };
+
+ try {
+ $window.resolveLocalFileSystemURL(path, function (fileSystem) {
+ fileSystem.getFile(fileName, options, function (fileEntry) {
+ fileEntry.createWriter(function (writer) {
+ if (options.append === true) {
+ writer.seek(writer.length);
+ }
+
+ if (options.truncate) {
+ writer.truncate(options.truncate);
+ }
+
+ writer.onwriteend = function (evt) {
+ if (this.error) {
+ q.reject(this.error);
+ } else {
+ q.resolve(evt);
+ }
+ };
+
+ writer.write(text);
+
+ q.promise.abort = function () {
+ writer.abort();
+ };
+ });
+ }, function (error) {
+ error.message = $cordovaFileError[error.code];
+ q.reject(error);
+ });
+ }, function (err) {
+ err.message = $cordovaFileError[err.code];
+ q.reject(err);
+ });
+ } catch (e) {
+ e.message = $cordovaFileError[e.code];
+ q.reject(e);
+ }
+
+ return q.promise;
+ },
+
+ writeExistingFile: function (path, fileName, text) {
+ var q = $q.defer();
+
+ if ((/^\//.test(fileName))) {
+ q.reject('file-name cannot start with \/');
+ }
+
+ try {
+ $window.resolveLocalFileSystemURL(path, function (fileSystem) {
+ fileSystem.getFile(fileName, {create: false}, function (fileEntry) {
+ fileEntry.createWriter(function (writer) {
+ writer.seek(writer.length);
+
+ writer.onwriteend = function (evt) {
+ if (this.error) {
+ q.reject(this.error);
+ } else {
+ q.resolve(evt);
+ }
+ };
+
+ writer.write(text);
+
+ q.promise.abort = function () {
+ writer.abort();
+ };
+ });
+ }, function (error) {
+ error.message = $cordovaFileError[error.code];
+ q.reject(error);
+ });
+ }, function (err) {
+ err.message = $cordovaFileError[err.code];
+ q.reject(err);
+ });
+ } catch (e) {
+ e.message = $cordovaFileError[e.code];
+ q.reject(e);
+ }
+
+ return q.promise;
+ },
+
+ readAsText: function (path, file) {
+ var q = $q.defer();
+
+ if ((/^\//.test(file))) {
+ q.reject('file-name cannot start with \/');
+ }
+
+ try {
+ $window.resolveLocalFileSystemURL(path, function (fileSystem) {
+ fileSystem.getFile(file, {create: false}, function (fileEntry) {
+ fileEntry.file(function (fileData) {
+ var reader = new FileReader();
+
+ reader.onloadend = function (evt) {
+ if (evt.target.result !== undefined || evt.target.result !== null) {
+ q.resolve(evt.target.result);
+ } else if (evt.target.error !== undefined || evt.target.error !== null) {
+ q.reject(evt.target.error);
+ } else {
+ q.reject({code: null, message: 'READER_ONLOADEND_ERR'});
+ }
+ };
+
+ reader.readAsText(fileData);
+ });
+ }, function (error) {
+ error.message = $cordovaFileError[error.code];
+ q.reject(error);
+ });
+ }, function (err) {
+ err.message = $cordovaFileError[err.code];
+ q.reject(err);
+ });
+ } catch (e) {
+ e.message = $cordovaFileError[e.code];
+ q.reject(e);
+ }
+
+ return q.promise;
+ },
+
+ readAsDataURL: function (path, file) {
+ var q = $q.defer();
+
+ if ((/^\//.test(file))) {
+ q.reject('file-name cannot start with \/');
+ }
+
+ try {
+ $window.resolveLocalFileSystemURL(path, function (fileSystem) {
+ fileSystem.getFile(file, {create: false}, function (fileEntry) {
+ fileEntry.file(function (fileData) {
+ var reader = new FileReader();
+ reader.onloadend = function (evt) {
+ if (evt.target.result !== undefined || evt.target.result !== null) {
+ q.resolve(evt.target.result);
+ } else if (evt.target.error !== undefined || evt.target.error !== null) {
+ q.reject(evt.target.error);
+ } else {
+ q.reject({code: null, message: 'READER_ONLOADEND_ERR'});
+ }
+ };
+ reader.readAsDataURL(fileData);
+ });
+ }, function (error) {
+ error.message = $cordovaFileError[error.code];
+ q.reject(error);
+ });
+ }, function (err) {
+ err.message = $cordovaFileError[err.code];
+ q.reject(err);
+ });
+ } catch (e) {
+ e.message = $cordovaFileError[e.code];
+ q.reject(e);
+ }
+
+ return q.promise;
+ },
+
+ readAsBinaryString: function (path, file) {
+ var q = $q.defer();
+
+ if ((/^\//.test(file))) {
+ q.reject('file-name cannot start with \/');
+ }
+
+ try {
+ $window.resolveLocalFileSystemURL(path, function (fileSystem) {
+ fileSystem.getFile(file, {create: false}, function (fileEntry) {
+ fileEntry.file(function (fileData) {
+ var reader = new FileReader();
+ reader.onloadend = function (evt) {
+ if (evt.target.result !== undefined || evt.target.result !== null) {
+ q.resolve(evt.target.result);
+ } else if (evt.target.error !== undefined || evt.target.error !== null) {
+ q.reject(evt.target.error);
+ } else {
+ q.reject({code: null, message: 'READER_ONLOADEND_ERR'});
+ }
+ };
+ reader.readAsBinaryString(fileData);
+ });
+ }, function (error) {
+ error.message = $cordovaFileError[error.code];
+ q.reject(error);
+ });
+ }, function (err) {
+ err.message = $cordovaFileError[err.code];
+ q.reject(err);
+ });
+ } catch (e) {
+ e.message = $cordovaFileError[e.code];
+ q.reject(e);
+ }
+
+ return q.promise;
+ },
+
+ readAsArrayBuffer: function (path, file) {
+ var q = $q.defer();
+
+ if ((/^\//.test(file))) {
+ q.reject('file-name cannot start with \/');
+ }
+
+ try {
+ $window.resolveLocalFileSystemURL(path, function (fileSystem) {
+ fileSystem.getFile(file, {create: false}, function (fileEntry) {
+ fileEntry.file(function (fileData) {
+ var reader = new FileReader();
+ reader.onloadend = function (evt) {
+ if (evt.target.result !== undefined || evt.target.result !== null) {
+ q.resolve(evt.target.result);
+ } else if (evt.target.error !== undefined || evt.target.error !== null) {
+ q.reject(evt.target.error);
+ } else {
+ q.reject({code: null, message: 'READER_ONLOADEND_ERR'});
+ }
+ };
+ reader.readAsArrayBuffer(fileData);
+ });
+ }, function (error) {
+ error.message = $cordovaFileError[error.code];
+ q.reject(error);
+ });
+ }, function (err) {
+ err.message = $cordovaFileError[err.code];
+ q.reject(err);
+ });
+ } catch (e) {
+ e.message = $cordovaFileError[e.code];
+ q.reject(e);
+ }
+
+ return q.promise;
+ },
+
+ moveFile: function (path, fileName, newPath, newFileName) {
+ var q = $q.defer();
+
+ newFileName = newFileName || fileName;
+
+ if ((/^\//.test(fileName)) || (/^\//.test(newFileName))) {
+ q.reject('file-name cannot start with \/');
+ }
+
+ try {
+ $window.resolveLocalFileSystemURL(path, function (fileSystem) {
+ fileSystem.getFile(fileName, {create: false}, function (fileEntry) {
+ $window.resolveLocalFileSystemURL(newPath, function (newFileEntry) {
+ fileEntry.moveTo(newFileEntry, newFileName, function (result) {
+ q.resolve(result);
+ }, function (error) {
+ q.reject(error);
+ });
+ }, function (err) {
+ q.reject(err);
+ });
+ }, function (err) {
+ q.reject(err);
+ });
+ }, function (er) {
+ q.reject(er);
+ });
+ } catch (e) {
+ q.reject(e);
+ }
+ return q.promise;
+ },
+
+ moveDir: function (path, dirName, newPath, newDirName) {
+ var q = $q.defer();
+
+ newDirName = newDirName || dirName;
+
+ if (/^\//.test(dirName) || (/^\//.test(newDirName))) {
+ q.reject('file-name cannot start with \/');
+ }
+
+ try {
+ $window.resolveLocalFileSystemURL(path, function (fileSystem) {
+ fileSystem.getDirectory(dirName, {create: false}, function (dirEntry) {
+ $window.resolveLocalFileSystemURL(newPath, function (newDirEntry) {
+ dirEntry.moveTo(newDirEntry, newDirName, function (result) {
+ q.resolve(result);
+ }, function (error) {
+ q.reject(error);
+ });
+ }, function (erro) {
+ q.reject(erro);
+ });
+ }, function (err) {
+ q.reject(err);
+ });
+ }, function (er) {
+ q.reject(er);
+ });
+ } catch (e) {
+ q.reject(e);
+ }
+ return q.promise;
+ },
+
+ copyDir: function (path, dirName, newPath, newDirName) {
+ var q = $q.defer();
+
+ newDirName = newDirName || dirName;
+
+ if (/^\//.test(dirName) || (/^\//.test(newDirName))) {
+ q.reject('file-name cannot start with \/');
+ }
+
+ try {
+ $window.resolveLocalFileSystemURL(path, function (fileSystem) {
+ fileSystem.getDirectory(dirName, {create: false, exclusive: false}, function (dirEntry) {
+
+ $window.resolveLocalFileSystemURL(newPath, function (newDirEntry) {
+ dirEntry.copyTo(newDirEntry, newDirName, function (result) {
+ q.resolve(result);
+ }, function (error) {
+ error.message = $cordovaFileError[error.code];
+ q.reject(error);
+ });
+ }, function (erro) {
+ erro.message = $cordovaFileError[erro.code];
+ q.reject(erro);
+ });
+ }, function (err) {
+ err.message = $cordovaFileError[err.code];
+ q.reject(err);
+ });
+ }, function (er) {
+ er.message = $cordovaFileError[er.code];
+ q.reject(er);
+ });
+ } catch (e) {
+ e.message = $cordovaFileError[e.code];
+ q.reject(e);
+ }
+ return q.promise;
+ },
+
+ copyFile: function (path, fileName, newPath, newFileName) {
+ var q = $q.defer();
+
+ newFileName = newFileName || fileName;
+
+ if ((/^\//.test(fileName))) {
+ q.reject('file-name cannot start with \/');
+ }
+
+ try {
+ $window.resolveLocalFileSystemURL(path, function (fileSystem) {
+ fileSystem.getFile(fileName, {create: false, exclusive: false}, function (fileEntry) {
+
+ $window.resolveLocalFileSystemURL(newPath, function (newFileEntry) {
+ fileEntry.copyTo(newFileEntry, newFileName, function (result) {
+ q.resolve(result);
+ }, function (error) {
+ error.message = $cordovaFileError[error.code];
+ q.reject(error);
+ });
+ }, function (erro) {
+ erro.message = $cordovaFileError[erro.code];
+ q.reject(erro);
+ });
+ }, function (err) {
+ err.message = $cordovaFileError[err.code];
+ q.reject(err);
+ });
+ }, function (er) {
+ er.message = $cordovaFileError[er.code];
+ q.reject(er);
+ });
+ } catch (e) {
+ e.message = $cordovaFileError[e.code];
+ q.reject(e);
+ }
+ return q.promise;
+ },
+
+ readFileMetadata: function (path, file) {
+ var q = $q.defer();
+
+ if ((/^\//.test(file))) {
+ q.reject('directory cannot start with \/');
+ }
+
+ try {
+ var directory = path + file;
+ $window.resolveLocalFileSystemURL(directory, function (fileEntry) {
+ fileEntry.file(function (result) {
+ q.resolve(result);
+ }, function (error) {
+ error.message = $cordovaFileError[error.code];
+ q.reject(error);
+ });
+ }, function (err) {
+ err.message = $cordovaFileError[err.code];
+ q.reject(err);
+ });
+ } catch (e) {
+ e.message = $cordovaFileError[e.code];
+ q.reject(e);
+ }
+
+ return q.promise;
+ }
+
+ /*
+ listFiles: function (path, dir) {
+
+ },
+
+ listDir: function (path, dirName) {
+ var q = $q.defer();
+
+ try {
+ $window.resolveLocalFileSystemURL(path, function (fileSystem) {
+ fileSystem.getDirectory(dirName, options, function (parent) {
+ var reader = parent.createReader();
+ reader.readEntries(function (entries) {
+ q.resolve(entries);
+ }, function () {
+ q.reject('DIR_READ_ERROR : ' + path + dirName);
+ });
+ }, function (error) {
+ error.message = $cordovaFileError[error.code];
+ q.reject(error);
+ });
+ }, function (err) {
+ err.message = $cordovaFileError[err.code];
+ q.reject(err);
+ });
+ } catch (e) {
+ e.message = $cordovaFileError[e.code];
+ q.reject(e);
+ }
+
+ return q.promise;
+ },
+
+ */
+ };
+
+ }];
+ }]);
+
+// install : cordova plugin add https://github.com/pwlin/cordova-plugin-file-opener2.git
+// link : https://github.com/pwlin/cordova-plugin-file-opener2
+
+angular.module('ngCordova.plugins.fileOpener2', [])
+
+ .factory('$cordovaFileOpener2', ['$q', function ($q) {
+
+ return {
+ open: function (file, type) {
+ var q = $q.defer();
+ cordova.plugins.fileOpener2.open(file, type, {
+ error: function (e) {
+ q.reject(e);
+ }, success: function () {
+ q.resolve();
+ }
+ });
+ return q.promise;
+ },
+
+ uninstall: function (pack) {
+ var q = $q.defer();
+ cordova.plugins.fileOpener2.uninstall(pack, {
+ error: function (e) {
+ q.reject(e);
+ }, success: function () {
+ q.resolve();
+ }
+ });
+ return q.promise;
+ },
+
+ appIsInstalled: function (pack) {
+ var q = $q.defer();
+ cordova.plugins.fileOpener2.appIsInstalled(pack, {
+ success: function (res) {
+ q.resolve(res);
+ }
+ });
+ return q.promise;
+ }
+ };
+ }]);
+
+// install : cordova plugin add cordova-plugin-file-transfer
+// link : https://github.com/apache/cordova-plugin-file-transfer
+
+/* globals FileTransfer: true */
+angular.module('ngCordova.plugins.fileTransfer', [])
+
+ .factory('$cordovaFileTransfer', ['$q', '$timeout', function ($q, $timeout) {
+ return {
+ download: function (source, filePath, options, trustAllHosts) {
+ var q = $q.defer();
+ var ft = new FileTransfer();
+ var uri = (options && options.encodeURI === false) ? source : encodeURI(source);
+
+ if (options && options.timeout !== undefined && options.timeout !== null) {
+ $timeout(function () {
+ ft.abort();
+ }, options.timeout);
+ options.timeout = null;
+ }
+
+ ft.onprogress = function (progress) {
+ q.notify(progress);
+ };
+
+ q.promise.abort = function () {
+ ft.abort();
+ };
+
+ ft.download(uri, filePath, q.resolve, q.reject, trustAllHosts, options);
+ return q.promise;
+ },
+
+ upload: function (server, filePath, options, trustAllHosts) {
+ var q = $q.defer();
+ var ft = new FileTransfer();
+ var uri = (options && options.encodeURI === false) ? server : encodeURI(server);
+
+ if (options && options.timeout !== undefined && options.timeout !== null) {
+ $timeout(function () {
+ ft.abort();
+ }, options.timeout);
+ options.timeout = null;
+ }
+
+ ft.onprogress = function (progress) {
+ q.notify(progress);
+ };
+
+ q.promise.abort = function () {
+ ft.abort();
+ };
+
+ ft.upload(filePath, uri, q.resolve, q.reject, options, trustAllHosts);
+ return q.promise;
+ }
+ };
+ }]);
+
+// install : cordova plugin add https://github.com/EddyVerbruggen/Flashlight-PhoneGap-Plugin.git
+// link : https://github.com/EddyVerbruggen/Flashlight-PhoneGap-Plugin
+
+angular.module('ngCordova.plugins.flashlight', [])
+
+ .factory('$cordovaFlashlight', ['$q', '$window', function ($q, $window) {
+
+ return {
+ available: function () {
+ var q = $q.defer();
+ $window.plugins.flashlight.available(function (isAvailable) {
+ q.resolve(isAvailable);
+ });
+ return q.promise;
+ },
+
+ switchOn: function () {
+ var q = $q.defer();
+ $window.plugins.flashlight.switchOn(function (response) {
+ q.resolve(response);
+ }, function (error) {
+ q.reject(error);
+ });
+ return q.promise;
+ },
+
+ switchOff: function () {
+ var q = $q.defer();
+ $window.plugins.flashlight.switchOff(function (response) {
+ q.resolve(response);
+ }, function (error) {
+ q.reject(error);
+ });
+ return q.promise;
+ },
+
+ toggle: function () {
+ var q = $q.defer();
+ $window.plugins.flashlight.toggle(function (response) {
+ q.resolve(response);
+ }, function (error) {
+ q.reject(error);
+ });
+ return q.promise;
+ }
+ };
+ }]);
+
+// install : cordova plugin add https://github.com/floatinghotpot/cordova-plugin-flurry.git
+// link : https://github.com/floatinghotpot/cordova-plugin-flurry
+
+angular.module('ngCordova.plugins.flurryAds', [])
+ .factory('$cordovaFlurryAds', ['$q', '$window', function ($q, $window) {
+
+ return {
+ setOptions: function (options) {
+ var d = $q.defer();
+
+ $window.FlurryAds.setOptions(options, function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ createBanner: function (options) {
+ var d = $q.defer();
+
+ $window.FlurryAds.createBanner(options, function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ removeBanner: function () {
+ var d = $q.defer();
+
+ $window.FlurryAds.removeBanner(function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ showBanner: function (position) {
+ var d = $q.defer();
+
+ $window.FlurryAds.showBanner(position, function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ showBannerAtXY: function (x, y) {
+ var d = $q.defer();
+
+ $window.FlurryAds.showBannerAtXY(x, y, function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ hideBanner: function () {
+ var d = $q.defer();
+
+ $window.FlurryAds.hideBanner(function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ prepareInterstitial: function (options) {
+ var d = $q.defer();
+
+ $window.FlurryAds.prepareInterstitial(options, function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ showInterstitial: function () {
+ var d = $q.defer();
+
+ $window.FlurryAds.showInterstitial(function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ }
+ };
+ }]);
+
+// install : cordova plugin add https://github.com/phonegap-build/GAPlugin.git
+// link : https://github.com/phonegap-build/GAPlugin
+
+angular.module('ngCordova.plugins.ga', [])
+
+ .factory('$cordovaGA', ['$q', '$window', function ($q, $window) {
+
+ return {
+ init: function (id, mingap) {
+ var q = $q.defer();
+ mingap = (mingap >= 0) ? mingap : 10;
+ $window.plugins.gaPlugin.init(function (result) {
+ q.resolve(result);
+ },
+ function (error) {
+ q.reject(error);
+ },
+ id, mingap);
+ return q.promise;
+ },
+
+ trackEvent: function (success, fail, category, eventAction, eventLabel, eventValue) {
+ var q = $q.defer();
+ $window.plugins.gaPlugin.trackEvent(function (result) {
+ q.resolve(result);
+ },
+ function (error) {
+ q.reject(error);
+ },
+ category, eventAction, eventLabel, eventValue);
+ return q.promise;
+ },
+
+ trackPage: function (success, fail, pageURL) {
+ var q = $q.defer();
+ $window.plugins.gaPlugin.trackPage(function (result) {
+ q.resolve(result);
+ },
+ function (error) {
+ q.reject(error);
+ },
+ pageURL);
+ return q.promise;
+ },
+
+ setVariable: function (success, fail, index, value) {
+ var q = $q.defer();
+ $window.plugins.gaPlugin.setVariable(function (result) {
+ q.resolve(result);
+ },
+ function (error) {
+ q.reject(error);
+ },
+ index, value);
+ return q.promise;
+ },
+
+ exit: function () {
+ var q = $q.defer();
+ $window.plugins.gaPlugin.exit(function (result) {
+ q.resolve(result);
+ },
+ function (error) {
+ q.reject(error);
+ });
+ return q.promise;
+ }
+ };
+ }]);
+
+// install : cordova plugin add cordova-plugin-geolocation
+// link : https://github.com/apache/cordova-plugin-geolocation
+
+angular.module('ngCordova.plugins.geolocation', [])
+
+ .factory('$cordovaGeolocation', ['$q', function ($q) {
+
+ return {
+ getCurrentPosition: function (options) {
+ var q = $q.defer();
+
+ navigator.geolocation.getCurrentPosition(function (result) {
+ q.resolve(result);
+ }, function (err) {
+ q.reject(err);
+ }, options);
+
+ return q.promise;
+ },
+
+ watchPosition: function (options) {
+ var q = $q.defer();
+
+ var watchID = navigator.geolocation.watchPosition(function (result) {
+ q.notify(result);
+ }, function (err) {
+ q.reject(err);
+ }, options);
+
+ q.promise.cancel = function () {
+ navigator.geolocation.clearWatch(watchID);
+ };
+
+ q.promise.clearWatch = function (id) {
+ navigator.geolocation.clearWatch(id || watchID);
+ };
+
+ q.promise.watchID = watchID;
+
+ return q.promise;
+ },
+
+ clearWatch: function (watchID) {
+ return navigator.geolocation.clearWatch(watchID);
+ }
+ };
+ }]);
+
+// install : cordova plugin add cordova-plugin-globalization
+// link : https://github.com/apache/cordova-plugin-globalization
+
+angular.module('ngCordova.plugins.globalization', [])
+
+ .factory('$cordovaGlobalization', ['$q', function ($q) {
+
+ return {
+ getPreferredLanguage: function () {
+ var q = $q.defer();
+
+ navigator.globalization.getPreferredLanguage(function (result) {
+ q.resolve(result);
+ },
+ function (err) {
+ q.reject(err);
+ });
+ return q.promise;
+ },
+
+ getLocaleName: function () {
+ var q = $q.defer();
+
+ navigator.globalization.getLocaleName(function (result) {
+ q.resolve(result);
+ },
+ function (err) {
+ q.reject(err);
+ });
+ return q.promise;
+ },
+
+ getFirstDayOfWeek: function () {
+ var q = $q.defer();
+
+ navigator.globalization.getFirstDayOfWeek(function (result) {
+ q.resolve(result);
+ },
+ function (err) {
+ q.reject(err);
+ });
+ return q.promise;
+ },
+
+ // "date" parameter must be a JavaScript Date Object.
+ dateToString: function (date, options) {
+ var q = $q.defer();
+
+ navigator.globalization.dateToString(
+ date,
+ function (result) {
+ q.resolve(result);
+ },
+ function (err) {
+ q.reject(err);
+ },
+ options);
+ return q.promise;
+ },
+
+ stringToDate: function (dateString, options) {
+ var q = $q.defer();
+
+ navigator.globalization.stringToDate(
+ dateString,
+ function (result) {
+ q.resolve(result);
+ },
+ function (err) {
+ q.reject(err);
+ },
+ options);
+ return q.promise;
+ },
+
+ getDatePattern: function (options) {
+ var q = $q.defer();
+
+ navigator.globalization.getDatePattern(
+ function (result) {
+ q.resolve(result);
+ },
+ function (err) {
+ q.reject(err);
+ },
+ options);
+ return q.promise;
+ },
+
+ getDateNames: function (options) {
+ var q = $q.defer();
+
+ navigator.globalization.getDateNames(
+ function (result) {
+ q.resolve(result);
+ },
+ function (err) {
+ q.reject(err);
+ },
+ options);
+ return q.promise;
+ },
+
+ // "date" parameter must be a JavaScript Date Object.
+ isDayLightSavingsTime: function (date) {
+ var q = $q.defer();
+
+ navigator.globalization.isDayLightSavingsTime(
+ date,
+ function (result) {
+ q.resolve(result);
+ },
+ function (err) {
+ q.reject(err);
+ });
+ return q.promise;
+ },
+
+ numberToString: function (number, options) {
+ var q = $q.defer();
+
+ navigator.globalization.numberToString(
+ number,
+ function (result) {
+ q.resolve(result);
+ },
+ function (err) {
+ q.reject(err);
+ },
+ options);
+ return q.promise;
+ },
+
+ stringToNumber: function (numberString, options) {
+ var q = $q.defer();
+
+ navigator.globalization.stringToNumber(
+ numberString,
+ function (result) {
+ q.resolve(result);
+ },
+ function (err) {
+ q.reject(err);
+ },
+ options);
+ return q.promise;
+ },
+
+ getNumberPattern: function (options) {
+ var q = $q.defer();
+
+ navigator.globalization.getNumberPattern(
+ function (result) {
+ q.resolve(result);
+ },
+ function (err) {
+ q.reject(err);
+ },
+ options);
+ return q.promise;
+ },
+
+ getCurrencyPattern: function (currencyCode) {
+ var q = $q.defer();
+
+ navigator.globalization.getCurrencyPattern(
+ currencyCode,
+ function (result) {
+ q.resolve(result);
+ },
+ function (err) {
+ q.reject(err);
+ });
+ return q.promise;
+ }
+
+ };
+ }]);
+
+// install : cordova plugin add https://github.com/floatinghotpot/cordova-admob-pro.git
+// link : https://github.com/floatinghotpot/cordova-admob-pro
+
+angular.module('ngCordova.plugins.googleAds', [])
+
+ .factory('$cordovaGoogleAds', ['$q', '$window', function ($q, $window) {
+
+ return {
+ setOptions: function (options) {
+ var d = $q.defer();
+
+ $window.AdMob.setOptions(options, function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ createBanner: function (options) {
+ var d = $q.defer();
+
+ $window.AdMob.createBanner(options, function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ removeBanner: function () {
+ var d = $q.defer();
+
+ $window.AdMob.removeBanner(function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ showBanner: function (position) {
+ var d = $q.defer();
+
+ $window.AdMob.showBanner(position, function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ showBannerAtXY: function (x, y) {
+ var d = $q.defer();
+
+ $window.AdMob.showBannerAtXY(x, y, function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ hideBanner: function () {
+ var d = $q.defer();
+
+ $window.AdMob.hideBanner(function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ prepareInterstitial: function (options) {
+ var d = $q.defer();
+
+ $window.AdMob.prepareInterstitial(options, function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ showInterstitial: function () {
+ var d = $q.defer();
+
+ $window.AdMob.showInterstitial(function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ }
+ };
+ }]);
+
+// install : cordova plugin add https://github.com/danwilson/google-analytics-plugin.git
+// link : https://github.com/danwilson/google-analytics-plugin
+
+angular.module('ngCordova.plugins.googleAnalytics', [])
+
+ .factory('$cordovaGoogleAnalytics', ['$q', '$window', function ($q, $window) {
+
+ return {
+ startTrackerWithId: function (id) {
+ var d = $q.defer();
+
+ $window.analytics.startTrackerWithId(id, function (response) {
+ d.resolve(response);
+ }, function (error) {
+ d.reject(error);
+ });
+
+ return d.promise;
+ },
+
+ setUserId: function (id) {
+ var d = $q.defer();
+
+ $window.analytics.setUserId(id, function (response) {
+ d.resolve(response);
+ }, function (error) {
+ d.reject(error);
+ });
+
+ return d.promise;
+ },
+
+ debugMode: function () {
+ var d = $q.defer();
+
+ $window.analytics.debugMode(function (response) {
+ d.resolve(response);
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ trackView: function (screenName) {
+ var d = $q.defer();
+
+ $window.analytics.trackView(screenName, function (response) {
+ d.resolve(response);
+ }, function (error) {
+ d.reject(error);
+ });
+
+ return d.promise;
+ },
+
+ addCustomDimension: function (key, value) {
+ var d = $q.defer();
+ var parsedKey = parseInt(key, 10);
+
+ if (isNaN(parsedKey)) {
+ d.reject('Parameter "key" must be an integer.');
+ }
+
+ $window.analytics.addCustomDimension(parsedKey, value, function () {
+ d.resolve();
+ }, function (error) {
+ d.reject(error);
+ });
+
+ return d.promise;
+ },
+
+ trackEvent: function (category, action, label, value) {
+ var d = $q.defer();
+
+ $window.analytics.trackEvent(category, action, label, value, function (response) {
+ d.resolve(response);
+ }, function (error) {
+ d.reject(error);
+ });
+
+ return d.promise;
+ },
+
+ trackException: function (description, fatal) {
+ var d = $q.defer();
+
+ $window.analytics.trackException(description, fatal, function (response) {
+ d.resolve(response);
+ }, function (error) {
+ d.reject(error);
+ });
+
+ return d.promise;
+ },
+
+ trackTiming: function (category, milliseconds, variable, label) {
+ var d = $q.defer();
+
+ $window.analytics.trackTiming(category, milliseconds, variable, label, function (response) {
+ d.resolve(response);
+ }, function (error) {
+ d.reject(error);
+ });
+
+ return d.promise;
+ },
+
+ addTransaction: function (transactionId, affiliation, revenue, tax, shipping, currencyCode) {
+ var d = $q.defer();
+
+ $window.analytics.addTransaction(transactionId, affiliation, revenue, tax, shipping, currencyCode, function (response) {
+ d.resolve(response);
+ }, function (error) {
+ d.reject(error);
+ });
+
+ return d.promise;
+ },
+
+ addTransactionItem: function (transactionId, name, sku, category, price, quantity, currencyCode) {
+ var d = $q.defer();
+
+ $window.analytics.addTransactionItem(transactionId, name, sku, category, price, quantity, currencyCode, function (response) {
+ d.resolve(response);
+ }, function (error) {
+ d.reject(error);
+ });
+
+ return d.promise;
+ }
+ };
+ }]);
+
+// install :
+// link :
+
+// Google Maps needs ALOT of work!
+// Not for production use
+
+angular.module('ngCordova.plugins.googleMap', [])
+
+ .factory('$cordovaGoogleMap', ['$q', '$window', function ($q, $window) {
+
+ var map = null;
+
+ return {
+ getMap: function (options) {
+ var q = $q.defer();
+
+ if (!$window.plugin.google.maps) {
+ q.reject(null);
+ } else {
+ var div = document.getElementById('map_canvas');
+ map = $window.plugin.google.maps.Map.getMap(options);
+ map.setDiv(div);
+ q.resolve(map);
+ }
+ return q.promise;
+ },
+
+ isMapLoaded: function () { // check if an instance of the map exists
+ return !!map;
+ },
+ addMarker: function (markerOptions) { // add a marker to the map with given markerOptions
+ var q = $q.defer();
+ map.addMarker(markerOptions, function (marker) {
+ q.resolve(marker);
+ });
+
+ return q.promise;
+ },
+ getMapTypeIds: function () {
+ return $window.plugin.google.maps.mapTypeId;
+ },
+ setVisible: function (isVisible) {
+ var q = $q.defer();
+ map.setVisible(isVisible);
+ return q.promise;
+ },
+ // I don't know how to deallocate te map and the google map plugin.
+ cleanup: function () {
+ map = null;
+ // delete map;
+ }
+ };
+ }]);
+
+// install : cordova plugin add https://github.com/ptgamr/cordova-google-play-game.git --variable APP_ID=123456789
+// link : https://github.com/ptgamr/cordova-google-play-game
+
+/* globals googleplaygame: true */
+angular.module('ngCordova.plugins.googlePlayGame', [])
+
+ .factory('$cordovaGooglePlayGame', ['$q', function ($q) {
+
+ return {
+ auth: function () {
+ var q = $q.defer();
+
+ googleplaygame.auth(function (success) {
+ return q.resolve(success);
+ }, function (err) {
+ return q.reject(err);
+ });
+
+ return q.promise;
+ },
+ signout: function () {
+ var q = $q.defer();
+
+ googleplaygame.signout(function (success) {
+ return q.resolve(success);
+ }, function (err) {
+ return q.reject(err);
+ });
+
+ return q.promise;
+ },
+ isSignedIn: function () {
+ var q = $q.defer();
+
+ googleplaygame.isSignedIn(function (success) {
+ return q.resolve(success);
+ }, function (err) {
+ return q.reject(err);
+ });
+
+ return q.promise;
+ },
+ showPlayer: function () {
+ var q = $q.defer();
+
+ googleplaygame.showPlayer(function (success) {
+ return q.resolve(success);
+ }, function (err) {
+ return q.reject(err);
+ });
+
+ return q.promise;
+ },
+ submitScore: function (data) {
+ var q = $q.defer();
+
+ googleplaygame.submitScore(data, function (success) {
+ return q.resolve(success);
+ }, function (err) {
+ return q.reject(err);
+ });
+
+ return q.promise;
+ },
+ showAllLeaderboards: function () {
+ var q = $q.defer();
+
+ googleplaygame.showAllLeaderboards(function (success) {
+ return q.resolve(success);
+ }, function (err) {
+ return q.reject(err);
+ });
+
+ return q.promise;
+ },
+ showLeaderboard: function (data) {
+ var q = $q.defer();
+
+ googleplaygame.showLeaderboard(data, function (success) {
+ return q.resolve(success);
+ }, function (err) {
+ return q.reject(err);
+ });
+
+ return q.promise;
+ },
+ unlockAchievement: function (data) {
+ var q = $q.defer();
+
+ googleplaygame.unlockAchievement(data, function (success) {
+ return q.resolve(success);
+ }, function (err) {
+ return q.reject(err);
+ });
+
+ return q.promise;
+ },
+ incrementAchievement: function (data) {
+ var q = $q.defer();
+
+ googleplaygame.incrementAchievement(data, function (success) {
+ return q.resolve(success);
+ }, function (err) {
+ return q.reject(err);
+ });
+
+ return q.promise;
+ },
+ showAchievements: function () {
+ var q = $q.defer();
+
+ googleplaygame.showAchievements(function (success) {
+ return q.resolve(success);
+ }, function (err) {
+ return q.reject(err);
+ });
+
+ return q.promise;
+ }
+ };
+
+ }]);
+
+// install : cordova plugin add https://github.com/EddyVerbruggen/cordova-plugin-googleplus.git
+// link : https://github.com/EddyVerbruggen/cordova-plugin-googleplus
+
+angular.module('ngCordova.plugins.googlePlus', [])
+
+ .factory('$cordovaGooglePlus', ['$q', '$window', function ($q, $window) {
+
+ return {
+ login: function (iosKey) {
+ var q = $q.defer();
+
+ if (iosKey === undefined) {
+ iosKey = {};
+ }
+ $window.plugins.googleplus.login({'iOSApiKey': iosKey}, function (response) {
+ q.resolve(response);
+ }, function (error) {
+ q.reject(error);
+ });
+
+ return q.promise;
+ },
+
+ silentLogin: function (iosKey) {
+ var q = $q.defer();
+
+ if (iosKey === undefined) {
+ iosKey = {};
+ }
+ $window.plugins.googleplus.trySilentLogin({'iOSApiKey': iosKey}, function (response) {
+ q.resolve(response);
+ }, function (error) {
+ q.reject(error);
+ });
+
+ return q.promise;
+ },
+
+ logout: function () {
+ var q = $q.defer();
+ $window.plugins.googleplus.logout(function (response) {
+ q.resolve(response);
+ });
+ },
+
+ disconnect: function () {
+ var q = $q.defer();
+ $window.plugins.googleplus.disconnect(function (response) {
+ q.resolve(response);
+ });
+ },
+
+ isAvailable: function () {
+ var q = $q.defer();
+ $window.plugins.googleplus.isAvailable(function (available) {
+ if (available) {
+ q.resolve(available);
+ } else {
+ q.reject(available);
+ }
+ });
+
+ return q.promise;
+ }
+ };
+
+ }]);
+
+// install : cordova plugin add https://github.com/Telerik-Verified-Plugins/HealthKit.git
+// link : https://github.com/Telerik-Verified-Plugins/HealthKit
+
+angular.module('ngCordova.plugins.healthKit', [])
+
+ .factory('$cordovaHealthKit', ['$q', '$window', function ($q, $window) {
+
+ return {
+ isAvailable: function () {
+ var q = $q.defer();
+
+ $window.plugins.healthkit.available(function (success) {
+ q.resolve(success);
+ }, function (err) {
+ q.reject(err);
+ });
+
+ return q.promise;
+ },
+
+ /**
+ * Check whether or not the user granted your app access to a specific HealthKit type.
+ * Reference for possible types:
+ * https://developer.apple.com/library/ios/documentation/HealthKit/Reference/HealthKit_Constants/
+ */
+ checkAuthStatus: function (type) {
+ var q = $q.defer();
+
+ type = type || 'HKQuantityTypeIdentifierHeight';
+
+ $window.plugins.healthkit.checkAuthStatus({
+ 'type': type
+ }, function (success) {
+ q.resolve(success);
+ }, function (err) {
+ q.reject(err);
+ });
+
+ return q.promise;
+ },
+
+ /**
+ * Request authorization to access HealthKit data. See the full HealthKit constants
+ * reference for possible read and write types:
+ * https://developer.apple.com/library/ios/documentation/HealthKit/Reference/HealthKit_Constants/
+ */
+ requestAuthorization: function (readTypes, writeTypes) {
+ var q = $q.defer();
+
+ readTypes = readTypes || [
+ 'HKCharacteristicTypeIdentifierDateOfBirth', 'HKQuantityTypeIdentifierActiveEnergyBurned', 'HKQuantityTypeIdentifierHeight'
+ ];
+ writeTypes = writeTypes || [
+ 'HKQuantityTypeIdentifierActiveEnergyBurned', 'HKQuantityTypeIdentifierHeight', 'HKQuantityTypeIdentifierDistanceCycling'
+ ];
+
+ $window.plugins.healthkit.requestAuthorization({
+ 'readTypes': readTypes,
+ 'writeTypes': writeTypes
+ }, function (success) {
+ q.resolve(success);
+ }, function (err) {
+ q.reject(err);
+ });
+
+ return q.promise;
+ },
+
+ readDateOfBirth: function () {
+ var q = $q.defer();
+ $window.plugins.healthkit.readDateOfBirth(
+ function (success) {
+ q.resolve(success);
+ },
+ function (err) {
+ q.resolve(err);
+ }
+ );
+
+ return q.promise;
+ },
+
+ readGender: function () {
+ var q = $q.defer();
+ $window.plugins.healthkit.readGender(
+ function (success) {
+ q.resolve(success);
+ },
+ function (err) {
+ q.resolve(err);
+ }
+ );
+
+ return q.promise;
+ },
+
+ saveWeight: function (value, units, date) {
+ var q = $q.defer();
+ $window.plugins.healthkit.saveWeight({
+ 'unit': units || 'lb',
+ 'amount': value,
+ 'date': date || new Date()
+ },
+ function (success) {
+ q.resolve(success);
+ },
+ function (err) {
+ q.resolve(err);
+ }
+ );
+ return q.promise;
+ },
+
+ readWeight: function (units) {
+ var q = $q.defer();
+ $window.plugins.healthkit.readWeight({
+ 'unit': units || 'lb'
+ },
+ function (success) {
+ q.resolve(success);
+ },
+ function (err) {
+ q.resolve(err);
+ }
+ );
+
+ return q.promise;
+ },
+ saveHeight: function (value, units, date) {
+ var q = $q.defer();
+ $window.plugins.healthkit.saveHeight({
+ 'unit': units || 'in',
+ 'amount': value,
+ 'date': date || new Date()
+ },
+ function (success) {
+ q.resolve(success);
+ },
+ function (err) {
+ q.resolve(err);
+ }
+ );
+ return q.promise;
+ },
+ readHeight: function (units) {
+ var q = $q.defer();
+ $window.plugins.healthkit.readHeight({
+ 'unit': units || 'in'
+ },
+ function (success) {
+ q.resolve(success);
+ },
+ function (err) {
+ q.resolve(err);
+ }
+ );
+
+ return q.promise;
+ },
+
+ findWorkouts: function () {
+ var q = $q.defer();
+ $window.plugins.healthkit.findWorkouts({},
+ function (success) {
+ q.resolve(success);
+ },
+ function (err) {
+ q.resolve(err);
+ }
+ );
+ return q.promise;
+ },
+
+ /**
+ * Save a workout.
+ *
+ * Workout param should be of the format:
+ {
+ 'activityType': 'HKWorkoutActivityTypeCycling', // HKWorkoutActivityType constant (https://developer.apple.com/library/ios/documentation/HealthKit/Reference/HKWorkout_Class/#//apple_ref/c/tdef/HKWorkoutActivityType)
+ 'quantityType': 'HKQuantityTypeIdentifierDistanceCycling',
+ 'startDate': new Date(), // mandatory
+ 'endDate': null, // optional, use either this or duration
+ 'duration': 3600, // in seconds, optional, use either this or endDate
+ 'energy': 300, //
+ 'energyUnit': 'kcal', // J|cal|kcal
+ 'distance': 11, // optional
+ 'distanceUnit': 'km' // probably useful with the former param
+ // 'extraData': "", // Not sure how necessary this is
+ },
+ */
+ saveWorkout: function (workout) {
+ var q = $q.defer();
+ $window.plugins.healthkit.saveWorkout(workout,
+ function (success) {
+ q.resolve(success);
+ },
+ function (err) {
+ q.resolve(err);
+ }
+ );
+ return q.promise;
+ },
+
+ /**
+ * Sample any kind of health data through a given date range.
+ * sampleQuery of the format:
+ {
+ 'startDate': yesterday, // mandatory
+ 'endDate': tomorrow, // mandatory
+ 'sampleType': 'HKQuantityTypeIdentifierHeight',
+ 'unit' : 'cm'
+ },
+ */
+ querySampleType: function (sampleQuery) {
+ var q = $q.defer();
+ $window.plugins.healthkit.querySampleType(sampleQuery,
+ function (success) {
+ q.resolve(success);
+ },
+ function (err) {
+ q.resolve(err);
+ }
+ );
+ return q.promise;
+ }
+ };
+ }]);
+
+// install : cordova plugin add https://github.com/floatinghotpot/cordova-httpd.git
+// link : https://github.com/floatinghotpot/cordova-httpd
+
+angular.module('ngCordova.plugins.httpd', [])
+
+ .factory('$cordovaHttpd', ['$q', function ($q) {
+
+ return {
+ startServer: function (options) {
+ var d = $q.defer();
+
+ cordova.plugins.CorHttpd.startServer(options, function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ stopServer: function () {
+ var d = $q.defer();
+
+ cordova.plugins.CorHttpd.stopServer(function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ getURL: function () {
+ var d = $q.defer();
+
+ cordova.plugins.CorHttpd.getURL(function (url) {
+ d.resolve(url);
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ getLocalPath: function () {
+ var d = $q.defer();
+
+ cordova.plugins.CorHttpd.getLocalPath(function (path) {
+ d.resolve(path);
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ }
+
+ };
+ }]);
+
+// install : cordova plugin add https://github.com/floatinghotpot/cordova-plugin-iad.git
+// link : https://github.com/floatinghotpot/cordova-plugin-iad
+
+angular.module('ngCordova.plugins.iAd', [])
+ .factory('$cordovaiAd', ['$q', '$window', function ($q, $window) {
+
+ return {
+ setOptions: function (options) {
+ var d = $q.defer();
+
+ $window.iAd.setOptions(options, function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ createBanner: function (options) {
+ var d = $q.defer();
+
+ $window.iAd.createBanner(options, function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ removeBanner: function () {
+ var d = $q.defer();
+
+ $window.iAd.removeBanner(function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ showBanner: function (position) {
+ var d = $q.defer();
+
+ $window.iAd.showBanner(position, function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ showBannerAtXY: function (x, y) {
+ var d = $q.defer();
+
+ $window.iAd.showBannerAtXY(x, y, function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ hideBanner: function () {
+ var d = $q.defer();
+
+ $window.iAd.hideBanner(function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ prepareInterstitial: function (options) {
+ var d = $q.defer();
+
+ $window.iAd.prepareInterstitial(options, function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ showInterstitial: function () {
+ var d = $q.defer();
+
+ $window.iAd.showInterstitial(function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ }
+ };
+ }]);
+
+// install : cordova plugin add https://github.com/wymsee/cordova-imagePicker.git
+// link : https://github.com/wymsee/cordova-imagePicker
+
+angular.module('ngCordova.plugins.imagePicker', [])
+
+ .factory('$cordovaImagePicker', ['$q', '$window', function ($q, $window) {
+
+ return {
+ getPictures: function (options) {
+ var q = $q.defer();
+
+ $window.imagePicker.getPictures(function (results) {
+ q.resolve(results);
+ }, function (error) {
+ q.reject(error);
+ }, options);
+
+ return q.promise;
+ }
+ };
+ }]);
+
+// install : cordova plugin add cordova-plugin-inappbrowser
+// link : https://github.com/apache/cordova-plugin-inappbrowser
+
+angular.module('ngCordova.plugins.inAppBrowser', [])
+
+ .provider('$cordovaInAppBrowser', [function () {
+
+ var ref;
+ var defaultOptions = this.defaultOptions = {};
+
+ this.setDefaultOptions = function (config) {
+ defaultOptions = angular.extend(defaultOptions, config);
+ };
+
+ this.$get = ['$rootScope', '$q', '$window', '$timeout', function ($rootScope, $q, $window, $timeout) {
+ return {
+ open: function (url, target, requestOptions) {
+ var q = $q.defer();
+
+ if (requestOptions && !angular.isObject(requestOptions)) {
+ q.reject('options must be an object');
+ return q.promise;
+ }
+
+ var options = angular.extend({}, defaultOptions, requestOptions);
+
+ var opt = [];
+ angular.forEach(options, function (value, key) {
+ opt.push(key + '=' + value);
+ });
+ var optionsString = opt.join();
+
+ ref = $window.open(url, target, optionsString);
+
+ ref.addEventListener('loadstart', function (event) {
+ $timeout(function () {
+ $rootScope.$broadcast('$cordovaInAppBrowser:loadstart', event);
+ });
+ }, false);
+
+ ref.addEventListener('loadstop', function (event) {
+ q.resolve(event);
+ $timeout(function () {
+ $rootScope.$broadcast('$cordovaInAppBrowser:loadstop', event);
+ });
+ }, false);
+
+ ref.addEventListener('loaderror', function (event) {
+ q.reject(event);
+ $timeout(function () {
+ $rootScope.$broadcast('$cordovaInAppBrowser:loaderror', event);
+ });
+ }, false);
+
+ ref.addEventListener('exit', function (event) {
+ $timeout(function () {
+ $rootScope.$broadcast('$cordovaInAppBrowser:exit', event);
+ });
+ }, false);
+
+ return q.promise;
+ },
+
+ close: function () {
+ ref.close();
+ ref = null;
+ },
+
+ show: function () {
+ ref.show();
+ },
+
+ executeScript: function (details) {
+ var q = $q.defer();
+
+ ref.executeScript(details, function (result) {
+ q.resolve(result);
+ });
+
+ return q.promise;
+ },
+
+ insertCSS: function (details) {
+ var q = $q.defer();
+
+ ref.insertCSS(details, function (result) {
+ q.resolve(result);
+ });
+
+ return q.promise;
+ }
+ };
+ }];
+ }]);
+
+// install : cordova plugin add https://github.com/EddyVerbruggen/Insomnia-PhoneGap-Plugin.git
+// link : https://github.com/EddyVerbruggen/Insomnia-PhoneGap-Plugin
+angular.module('ngCordova.plugins.insomnia', [])
+
+ .factory('$cordovaInsomnia', ['$window', function ($window) {
+
+ return {
+ keepAwake: function () {
+ return $window.plugins.insomnia.keepAwake();
+ },
+ allowSleepAgain: function () {
+ return $window.plugins.insomnia.allowSleepAgain();
+ }
+ };
+
+ }]);
+
+// install : cordova plugins add https://github.com/vstirbu/InstagramPlugin.git
+// link : https://github.com/vstirbu/InstagramPlugin
+
+/* globals Instagram: true */
+angular.module('ngCordova.plugins.instagram', [])
+
+.factory('$cordovaInstagram', ['$q', function ($q) {
+
+ return {
+ share: function (options) {
+ var q = $q.defer();
+
+ if (!window.Instagram) {
+ console.error('Tried to call Instagram.share but the Instagram plugin isn\'t installed!');
+ q.resolve(null);
+ return q.promise;
+ }
+
+ Instagram.share(options.image, options.caption, function (err) {
+ if(err) {
+ q.reject(err);
+ } else {
+ q.resolve(true);
+ }
+ });
+ return q.promise;
+ },
+ isInstalled: function () {
+ var q = $q.defer();
+
+ if (!window.Instagram) {
+ console.error('Tried to call Instagram.isInstalled but the Instagram plugin isn\'t installed!');
+ q.resolve(null);
+ return q.promise;
+ }
+
+ Instagram.isInstalled(function (err, installed) {
+ if (err) {
+ q.reject(err);
+ } else {
+ q.resolve(installed);
+ }
+ });
+ return q.promise;
+ }
+ };
+}]);
+
+// install : cordova plugin add https://github.com/driftyco/ionic-plugins-keyboard.git
+// link : https://github.com/driftyco/ionic-plugins-keyboard
+
+angular.module('ngCordova.plugins.keyboard', [])
+
+ .factory('$cordovaKeyboard', ['$rootScope', function ($rootScope) {
+
+ var keyboardShowEvent = function () {
+ $rootScope.$evalAsync(function () {
+ $rootScope.$broadcast('$cordovaKeyboard:show');
+ });
+ };
+
+ var keyboardHideEvent = function () {
+ $rootScope.$evalAsync(function () {
+ $rootScope.$broadcast('$cordovaKeyboard:hide');
+ });
+ };
+
+ document.addEventListener('deviceready', function () {
+ if (cordova.plugins.Keyboard) {
+ window.addEventListener('native.keyboardshow', keyboardShowEvent, false);
+ window.addEventListener('native.keyboardhide', keyboardHideEvent, false);
+ }
+ });
+
+ return {
+ hideAccessoryBar: function (bool) {
+ return cordova.plugins.Keyboard.hideKeyboardAccessoryBar(bool);
+ },
+
+ close: function () {
+ return cordova.plugins.Keyboard.close();
+ },
+
+ show: function () {
+ return cordova.plugins.Keyboard.show();
+ },
+
+ disableScroll: function (bool) {
+ return cordova.plugins.Keyboard.disableScroll(bool);
+ },
+
+ isVisible: function () {
+ return cordova.plugins.Keyboard.isVisible;
+ },
+
+ clearShowWatch: function () {
+ document.removeEventListener('native.keyboardshow', keyboardShowEvent);
+ $rootScope.$$listeners['$cordovaKeyboard:show'] = [];
+ },
+
+ clearHideWatch: function () {
+ document.removeEventListener('native.keyboardhide', keyboardHideEvent);
+ $rootScope.$$listeners['$cordovaKeyboard:hide'] = [];
+ }
+ };
+ }]);
+
+// install : cordova plugin add https://github.com/shazron/KeychainPlugin.git
+// link : https://github.com/shazron/KeychainPlugin
+
+/* globals Keychain: true */
+angular.module('ngCordova.plugins.keychain', [])
+
+ .factory('$cordovaKeychain', ['$q', function ($q) {
+
+ return {
+ getForKey: function (key, serviceName) {
+ var defer = $q.defer(),
+ kc = new Keychain();
+
+ kc.getForKey(defer.resolve, defer.reject, key, serviceName);
+
+ return defer.promise;
+ },
+
+ setForKey: function (key, serviceName, value) {
+ var defer = $q.defer(),
+ kc = new Keychain();
+
+ kc.setForKey(defer.resolve, defer.reject, key, serviceName, value);
+
+ return defer.promise;
+ },
+
+ removeForKey: function (key, serviceName) {
+ var defer = $q.defer(),
+ kc = new Keychain();
+
+ kc.removeForKey(defer.resolve, defer.reject, key, serviceName);
+
+ return defer.promise;
+ }
+ };
+ }]);
+
+// install : cordova plugin add uk.co.workingedge.phonegap.plugin.launchnavigator
+// link : https://github.com/dpa99c/phonegap-launch-navigator
+
+/* globals launchnavigator: true */
+angular.module('ngCordova.plugins.launchNavigator', [])
+
+ .factory('$cordovaLaunchNavigator', ['$q', function ($q) {
+
+ return {
+ navigate: function (destination, start, options) {
+ var q = $q.defer();
+ launchnavigator.navigate(
+ destination,
+ start,
+ function (){
+ q.resolve();
+ },
+ function (error){
+ q.reject(error);
+ },
+ options);
+ return q.promise;
+ }
+ };
+
+ }]);
+
+// install : cordova plugin add https://github.com/katzer/cordova-plugin-local-notifications.git
+// link : https://github.com/katzer/cordova-plugin-local-notifications
+
+angular.module('ngCordova.plugins.localNotification', [])
+
+ .factory('$cordovaLocalNotification', ['$q', '$window', '$rootScope', '$timeout', function ($q, $window, $rootScope, $timeout) {
+ document.addEventListener('deviceready', function () {
+ if ($window.cordova &&
+ $window.cordova.plugins &&
+ $window.cordova.plugins.notification &&
+ $window.cordova.plugins.notification.local) {
+ // ----- "Scheduling" events
+
+ // A local notification was scheduled
+ $window.cordova.plugins.notification.local.on('schedule', function (notification, state) {
+ $timeout(function () {
+ $rootScope.$broadcast('$cordovaLocalNotification:schedule', notification, state);
+ });
+ });
+
+ // A local notification was triggered
+ $window.cordova.plugins.notification.local.on('trigger', function (notification, state) {
+ $timeout(function () {
+ $rootScope.$broadcast('$cordovaLocalNotification:trigger', notification, state);
+ });
+ });
+
+ // ----- "Update" events
+
+ // A local notification was updated
+ $window.cordova.plugins.notification.local.on('update', function (notification, state) {
+ $timeout(function () {
+ $rootScope.$broadcast('$cordovaLocalNotification:update', notification, state);
+ });
+ });
+
+ // ----- "Clear" events
+
+ // A local notification was cleared from the notification center
+ $window.cordova.plugins.notification.local.on('clear', function (notification, state) {
+ $timeout(function () {
+ $rootScope.$broadcast('$cordovaLocalNotification:clear', notification, state);
+ });
+ });
+
+ // All local notifications were cleared from the notification center
+ $window.cordova.plugins.notification.local.on('clearall', function (state) {
+ $timeout(function () {
+ $rootScope.$broadcast('$cordovaLocalNotification:clearall', state);
+ });
+ });
+
+ // ----- "Cancel" events
+
+ // A local notification was cancelled
+ $window.cordova.plugins.notification.local.on('cancel', function (notification, state) {
+ $timeout(function () {
+ $rootScope.$broadcast('$cordovaLocalNotification:cancel', notification, state);
+ });
+ });
+
+ // All local notifications were cancelled
+ $window.cordova.plugins.notification.local.on('cancelall', function (state) {
+ $timeout(function () {
+ $rootScope.$broadcast('$cordovaLocalNotification:cancelall', state);
+ });
+ });
+
+ // ----- Other events
+
+ // A local notification was clicked
+ $window.cordova.plugins.notification.local.on('click', function (notification, state) {
+ $timeout(function () {
+ $rootScope.$broadcast('$cordovaLocalNotification:click', notification, state);
+ });
+ });
+ }
+ }, false);
+ return {
+ schedule: function (options, scope) {
+ var q = $q.defer();
+ scope = scope || null;
+
+ $window.cordova.plugins.notification.local.schedule(options, function (result) {
+ q.resolve(result);
+ }, scope);
+
+ return q.promise;
+ },
+
+ add: function (options, scope) {
+ console.warn('Deprecated: use "schedule" instead.');
+
+ var q = $q.defer();
+ scope = scope || null;
+
+ $window.cordova.plugins.notification.local.schedule(options, function (result) {
+ q.resolve(result);
+ }, scope);
+
+ return q.promise;
+ },
+
+ update: function (options, scope) {
+ var q = $q.defer();
+ scope = scope || null;
+
+ $window.cordova.plugins.notification.local.update(options, function (result) {
+ q.resolve(result);
+ }, scope);
+
+ return q.promise;
+ },
+
+ clear: function (ids, scope) {
+ var q = $q.defer();
+ scope = scope || null;
+
+ $window.cordova.plugins.notification.local.clear(ids, function (result) {
+ q.resolve(result);
+ }, scope);
+
+ return q.promise;
+ },
+
+ clearAll: function (scope) {
+ var q = $q.defer();
+ scope = scope || null;
+
+ $window.cordova.plugins.notification.local.clearAll(function (result) {
+ q.resolve(result);
+ }, scope);
+
+ return q.promise;
+ },
+
+ cancel: function (ids, scope) {
+ var q = $q.defer();
+ scope = scope || null;
+
+ $window.cordova.plugins.notification.local.cancel(ids, function (result) {
+ q.resolve(result);
+ }, scope);
+
+ return q.promise;
+ },
+
+ cancelAll: function (scope) {
+ var q = $q.defer();
+ scope = scope || null;
+
+ $window.cordova.plugins.notification.local.cancelAll(function (result) {
+ q.resolve(result);
+ }, scope);
+
+ return q.promise;
+ },
+
+ isPresent: function (id, scope) {
+ var q = $q.defer();
+ scope = scope || null;
+
+ $window.cordova.plugins.notification.local.isPresent(id, function (result) {
+ q.resolve(result);
+ }, scope);
+
+ return q.promise;
+ },
+
+ isScheduled: function (id, scope) {
+ var q = $q.defer();
+ scope = scope || null;
+
+ $window.cordova.plugins.notification.local.isScheduled(id, function (result) {
+ q.resolve(result);
+ }, scope);
+
+ return q.promise;
+ },
+
+ isTriggered: function (id, scope) {
+ var q = $q.defer();
+ scope = scope || null;
+
+ $window.cordova.plugins.notification.local.isTriggered(id, function (result) {
+ q.resolve(result);
+ }, scope);
+
+ return q.promise;
+ },
+
+ hasPermission: function (scope) {
+ var q = $q.defer();
+ scope = scope || null;
+
+ $window.cordova.plugins.notification.local.hasPermission(function (result) {
+ if (result) {
+ q.resolve(result);
+ } else {
+ q.reject(result);
+ }
+ }, scope);
+
+ return q.promise;
+ },
+
+ registerPermission: function (scope) {
+ var q = $q.defer();
+ scope = scope || null;
+
+ $window.cordova.plugins.notification.local.registerPermission(function (result) {
+ if (result) {
+ q.resolve(result);
+ } else {
+ q.reject(result);
+ }
+ }, scope);
+
+ return q.promise;
+ },
+
+ promptForPermission: function (scope) {
+ console.warn('Deprecated: use "registerPermission" instead.');
+
+ var q = $q.defer();
+ scope = scope || null;
+
+ $window.cordova.plugins.notification.local.registerPermission(function (result) {
+ if (result) {
+ q.resolve(result);
+ } else {
+ q.reject(result);
+ }
+ }, scope);
+
+ return q.promise;
+ },
+
+ getAllIds: function (scope) {
+ var q = $q.defer();
+ scope = scope || null;
+
+ $window.cordova.plugins.notification.local.getAllIds(function (result) {
+ q.resolve(result);
+ }, scope);
+
+ return q.promise;
+ },
+
+ getIds: function (scope) {
+ var q = $q.defer();
+ scope = scope || null;
+
+ $window.cordova.plugins.notification.local.getIds(function (result) {
+ q.resolve(result);
+ }, scope);
+
+ return q.promise;
+ },
+
+ getScheduledIds: function (scope) {
+ var q = $q.defer();
+ scope = scope || null;
+
+ $window.cordova.plugins.notification.local.getScheduledIds(function (result) {
+ q.resolve(result);
+ }, scope);
+
+ return q.promise;
+ },
+
+ getTriggeredIds: function (scope) {
+ var q = $q.defer();
+ scope = scope || null;
+
+ $window.cordova.plugins.notification.local.getTriggeredIds(function (result) {
+ q.resolve(result);
+ }, scope);
+
+ return q.promise;
+ },
+
+ get: function (ids, scope) {
+ var q = $q.defer();
+ scope = scope || null;
+
+ $window.cordova.plugins.notification.local.get(ids, function (result) {
+ q.resolve(result);
+ }, scope);
+
+ return q.promise;
+ },
+
+ getAll: function (scope) {
+ var q = $q.defer();
+ scope = scope || null;
+
+ $window.cordova.plugins.notification.local.getAll(function (result) {
+ q.resolve(result);
+ }, scope);
+
+ return q.promise;
+ },
+
+ getScheduled: function (ids, scope) {
+ var q = $q.defer();
+ scope = scope || null;
+
+ $window.cordova.plugins.notification.local.getScheduled(ids, function (result) {
+ q.resolve(result);
+ }, scope);
+
+ return q.promise;
+ },
+
+ getAllScheduled: function (scope) {
+ var q = $q.defer();
+ scope = scope || null;
+
+ $window.cordova.plugins.notification.local.getAllScheduled(function (result) {
+ q.resolve(result);
+ }, scope);
+
+ return q.promise;
+ },
+
+ getTriggered: function (ids, scope) {
+ var q = $q.defer();
+ scope = scope || null;
+
+ $window.cordova.plugins.notification.local.getTriggered(ids, function (result) {
+ q.resolve(result);
+ }, scope);
+
+ return q.promise;
+ },
+
+ getAllTriggered: function (scope) {
+ var q = $q.defer();
+ scope = scope || null;
+
+ $window.cordova.plugins.notification.local.getAllTriggered(function (result) {
+ q.resolve(result);
+ }, scope);
+
+ return q.promise;
+ },
+
+ getDefaults: function () {
+ return $window.cordova.plugins.notification.local.getDefaults();
+ },
+
+ setDefaults: function (Object) {
+ $window.cordova.plugins.notification.local.setDefaults(Object);
+ }
+ };
+ }]);
+
+// install : cordova plugin add https://github.com/floatinghotpot/cordova-plugin-mmedia.git
+// link : https://github.com/floatinghotpot/cordova-plugin-mmedia
+
+angular.module('ngCordova.plugins.mMediaAds', [])
+
+ .factory('$cordovaMMediaAds', ['$q', '$window', function ($q, $window) {
+
+ return {
+ setOptions: function (options) {
+ var d = $q.defer();
+
+ $window.mMedia.setOptions(options, function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ createBanner: function (options) {
+ var d = $q.defer();
+
+ $window.mMedia.createBanner(options, function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ removeBanner: function () {
+ var d = $q.defer();
+
+ $window.mMedia.removeBanner(function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ showBanner: function (position) {
+ var d = $q.defer();
+
+ $window.mMedia.showBanner(position, function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ showBannerAtXY: function (x, y) {
+ var d = $q.defer();
+
+ $window.mMedia.showBannerAtXY(x, y, function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ hideBanner: function () {
+ var d = $q.defer();
+
+ $window.mMedia.hideBanner(function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ prepareInterstitial: function (options) {
+ var d = $q.defer();
+
+ $window.mMedia.prepareInterstitial(options, function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ showInterstitial: function () {
+ var d = $q.defer();
+
+ $window.mMedia.showInterstitial(function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ }
+ };
+ }]);
+
+// install : cordova plugin add cordova-plugin-media
+// link : https://github.com/apache/cordova-plugin-media
+
+/* globals Media: true */
+angular.module('ngCordova.plugins.media', [])
+
+.service('NewMedia', ['$q', '$interval', function ($q, $interval) {
+ var q, q2, q3, mediaStatus = null, mediaPosition = -1, mediaTimer, mediaDuration = -1;
+
+ function setTimer(media) {
+ if (angular.isDefined(mediaTimer)) {
+ return;
+ }
+
+ mediaTimer = $interval(function () {
+ if (mediaDuration < 0) {
+ mediaDuration = media.getDuration();
+ if (q && mediaDuration > 0) {
+ q.notify({duration: mediaDuration});
+ }
+ }
+
+ media.getCurrentPosition(
+ // success callback
+ function (position) {
+ if (position > -1) {
+ mediaPosition = position;
+ }
+ },
+ // error callback
+ function (e) {
+ console.log('Error getting pos=' + e);
+ });
+
+ if (q) {
+ q.notify({position: mediaPosition});
+ }
+
+ }, 1000);
+ }
+
+ function clearTimer() {
+ if (angular.isDefined(mediaTimer)) {
+ $interval.cancel(mediaTimer);
+ mediaTimer = undefined;
+ }
+ }
+
+ function resetValues() {
+ mediaPosition = -1;
+ mediaDuration = -1;
+ }
+
+ function NewMedia(src) {
+ this.media = new Media(src,
+ function (success) {
+ clearTimer();
+ resetValues();
+ q.resolve(success);
+ }, function (error) {
+ clearTimer();
+ resetValues();
+ q.reject(error);
+ }, function (status) {
+ mediaStatus = status;
+ q.notify({status: mediaStatus});
+ });
+ }
+
+ // iOS quirks :
+ // - myMedia.play({ numberOfLoops: 2 }) -> looping
+ // - myMedia.play({ playAudioWhenScreenIsLocked : false })
+ NewMedia.prototype.play = function (options) {
+ q = $q.defer();
+
+ if (typeof options !== 'object') {
+ options = {};
+ }
+
+ this.media.play(options);
+
+ setTimer(this.media);
+
+ return q.promise;
+ };
+
+ NewMedia.prototype.pause = function () {
+ clearTimer();
+ this.media.pause();
+ };
+
+ NewMedia.prototype.stop = function () {
+ this.media.stop();
+ };
+
+ NewMedia.prototype.release = function () {
+ this.media.release();
+ this.media = undefined;
+ };
+
+ NewMedia.prototype.seekTo = function (timing) {
+ this.media.seekTo(timing);
+ };
+
+ NewMedia.prototype.setVolume = function (volume) {
+ this.media.setVolume(volume);
+ };
+
+ NewMedia.prototype.startRecord = function () {
+ this.media.startRecord();
+ };
+
+ NewMedia.prototype.stopRecord = function () {
+ this.media.stopRecord();
+ };
+
+ NewMedia.prototype.currentTime = function () {
+ q2 = $q.defer();
+ this.media.getCurrentPosition(function (position){
+ q2.resolve(position);
+ });
+ return q2.promise;
+ };
+
+ NewMedia.prototype.getDuration = function () {
+ q3 = $q.defer();
+ this.media.getDuration(function (duration){
+ q3.resolve(duration);
+ });
+ return q3.promise;
+ };
+
+ return NewMedia;
+
+}])
+.factory('$cordovaMedia', ['NewMedia', function (NewMedia) {
+ return {
+ newMedia: function (src) {
+ return new NewMedia(src);
+ }
+ };
+}]);
+
+// install : cordova plugin add https://github.com/floatinghotpot/cordova-mobfox-pro.git
+// link : https://github.com/floatinghotpot/cordova-mobfox-pro
+
+angular.module('ngCordova.plugins.mobfoxAds', [])
+
+ .factory('$cordovaMobFoxAds', ['$q', '$window', function ($q, $window) {
+
+ return {
+ setOptions: function (options) {
+ var d = $q.defer();
+
+ $window.MobFox.setOptions(options, function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ createBanner: function (options) {
+ var d = $q.defer();
+
+ $window.MobFox.createBanner(options, function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ removeBanner: function () {
+ var d = $q.defer();
+
+ $window.MobFox.removeBanner(function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ showBanner: function (position) {
+ var d = $q.defer();
+
+ $window.MobFox.showBanner(position, function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ showBannerAtXY: function (x, y) {
+ var d = $q.defer();
+
+ $window.MobFox.showBannerAtXY(x, y, function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ hideBanner: function () {
+ var d = $q.defer();
+
+ $window.MobFox.hideBanner(function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ prepareInterstitial: function (options) {
+ var d = $q.defer();
+
+ $window.MobFox.prepareInterstitial(options, function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ showInterstitial: function () {
+ var d = $q.defer();
+
+ $window.MobFox.showInterstitial(function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ }
+ };
+ }]);
+
+angular.module('ngCordova.plugins', [
+ 'ngCordova.plugins.3dtouch',
+ 'ngCordova.plugins.actionSheet',
+ 'ngCordova.plugins.adMob',
+ 'ngCordova.plugins.appAvailability',
+ 'ngCordova.plugins.appRate',
+ 'ngCordova.plugins.appVersion',
+ 'ngCordova.plugins.backgroundGeolocation',
+ 'ngCordova.plugins.badge',
+ 'ngCordova.plugins.barcodeScanner',
+ 'ngCordova.plugins.batteryStatus',
+ 'ngCordova.plugins.beacon',
+ 'ngCordova.plugins.ble',
+ 'ngCordova.plugins.bluetoothSerial',
+ 'ngCordova.plugins.brightness',
+ 'ngCordova.plugins.calendar',
+ 'ngCordova.plugins.camera',
+ 'ngCordova.plugins.capture',
+ 'ngCordova.plugins.clipboard',
+ 'ngCordova.plugins.contacts',
+ 'ngCordova.plugins.datePicker',
+ 'ngCordova.plugins.device',
+ 'ngCordova.plugins.deviceMotion',
+ 'ngCordova.plugins.deviceOrientation',
+ 'ngCordova.plugins.dialogs',
+ 'ngCordova.plugins.emailComposer',
+ 'ngCordova.plugins.facebook',
+ 'ngCordova.plugins.facebookAds',
+ 'ngCordova.plugins.file',
+ 'ngCordova.plugins.fileTransfer',
+ 'ngCordova.plugins.fileOpener2',
+ 'ngCordova.plugins.flashlight',
+ 'ngCordova.plugins.flurryAds',
+ 'ngCordova.plugins.ga',
+ 'ngCordova.plugins.geolocation',
+ 'ngCordova.plugins.globalization',
+ 'ngCordova.plugins.googleAds',
+ 'ngCordova.plugins.googleAnalytics',
+ 'ngCordova.plugins.googleMap',
+ 'ngCordova.plugins.googlePlayGame',
+ 'ngCordova.plugins.googlePlus',
+ 'ngCordova.plugins.healthKit',
+ 'ngCordova.plugins.httpd',
+ 'ngCordova.plugins.iAd',
+ 'ngCordova.plugins.imagePicker',
+ 'ngCordova.plugins.inAppBrowser',
+ 'ngCordova.plugins.instagram',
+ 'ngCordova.plugins.keyboard',
+ 'ngCordova.plugins.keychain',
+ 'ngCordova.plugins.launchNavigator',
+ 'ngCordova.plugins.localNotification',
+ 'ngCordova.plugins.media',
+ 'ngCordova.plugins.mMediaAds',
+ 'ngCordova.plugins.mobfoxAds',
+ 'ngCordova.plugins.mopubAds',
+ 'ngCordova.plugins.nativeAudio',
+ 'ngCordova.plugins.network',
+ 'ngCordova.plugins.pinDialog',
+ 'ngCordova.plugins.preferences',
+ 'ngCordova.plugins.printer',
+ 'ngCordova.plugins.progressIndicator',
+ 'ngCordova.plugins.push',
+ 'ngCordova.plugins.push_v5',
+ 'ngCordova.plugins.sms',
+ 'ngCordova.plugins.socialSharing',
+ 'ngCordova.plugins.spinnerDialog',
+ 'ngCordova.plugins.splashscreen',
+ 'ngCordova.plugins.sqlite',
+ 'ngCordova.plugins.statusbar',
+ 'ngCordova.plugins.toast',
+ 'ngCordova.plugins.touchid',
+ 'ngCordova.plugins.vibration',
+ 'ngCordova.plugins.videoCapturePlus',
+ 'ngCordova.plugins.zip',
+ 'ngCordova.plugins.insomnia'
+]);
+
+// install : cordova plugin add https://github.com/floatinghotpot/cordova-plugin-mopub.git
+// link : https://github.com/floatinghotpot/cordova-plugin-mopub
+
+angular.module('ngCordova.plugins.mopubAds', [])
+ .factory('$cordovaMoPubAds', ['$q', '$window', function ($q, $window) {
+
+ return {
+ setOptions: function (options) {
+ var d = $q.defer();
+
+ $window.MoPub.setOptions(options, function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ createBanner: function (options) {
+ var d = $q.defer();
+
+ $window.MoPub.createBanner(options, function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ removeBanner: function () {
+ var d = $q.defer();
+
+ $window.MoPub.removeBanner(function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ showBanner: function (position) {
+ var d = $q.defer();
+
+ $window.MoPub.showBanner(position, function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ showBannerAtXY: function (x, y) {
+ var d = $q.defer();
+
+ $window.MoPub.showBannerAtXY(x, y, function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ hideBanner: function () {
+ var d = $q.defer();
+
+ $window.MoPub.hideBanner(function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ prepareInterstitial: function (options) {
+ var d = $q.defer();
+
+ $window.MoPub.prepareInterstitial(options, function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ },
+
+ showInterstitial: function () {
+ var d = $q.defer();
+
+ $window.MoPub.showInterstitial(function () {
+ d.resolve();
+ }, function () {
+ d.reject();
+ });
+
+ return d.promise;
+ }
+ };
+ }]);
+
+// install : cordova plugin add https://github.com/sidneys/cordova-plugin-nativeaudio.git
+// link : https://github.com/sidneys/cordova-plugin-nativeaudio
+
+angular.module('ngCordova.plugins.nativeAudio', [])
+
+ .factory('$cordovaNativeAudio', ['$q', '$window', function ($q, $window) {
+
+ return {
+ preloadSimple: function (id, assetPath) {
+ var q = $q.defer();
+ $window.plugins.NativeAudio.preloadSimple(id, assetPath, function (result) {
+ q.resolve(result);
+ }, function (err) {
+ q.reject(err);
+ });
+
+ return q.promise;
+ },
+
+ preloadComplex: function (id, assetPath, volume, voices, delay) {
+ var q = $q.defer();
+ $window.plugins.NativeAudio.preloadComplex(id, assetPath, volume, voices, delay, function (result) {
+ q.resolve(result);
+ }, function (err) {
+ q.reject(err);
+ });
+
+ return q.promise;
+ },
+
+ play: function (id, completeCallback) {
+ var q = $q.defer();
+ $window.plugins.NativeAudio.play(id, function (result) {
+ q.resolve(result);
+ }, function (err) {
+ q.reject(err);
+ }, completeCallback);
+
+ return q.promise;
+ },
+
+ stop: function (id) {
+ var q = $q.defer();
+ $window.plugins.NativeAudio.stop(id, function (result) {
+ q.resolve(result);
+ }, function (err) {
+ q.reject(err);
+ });
+ return q.promise;
+ },
+
+ loop: function (id) {
+ var q = $q.defer();
+ $window.plugins.NativeAudio.loop(id, function (result) {
+ q.resolve(result);
+ }, function (err) {
+ q.reject(err);
+ });
+
+ return q.promise;
+ },
+
+ unload: function (id) {
+ var q = $q.defer();
+ $window.plugins.NativeAudio.unload(id, function (result) {
+ q.resolve(result);
+ }, function (err) {
+ q.reject(err);
+ });
+
+ return q.promise;
+ },
+
+ setVolumeForComplexAsset: function (id, volume) {
+ var q = $q.defer();
+ $window.plugins.NativeAudio.setVolumeForComplexAsset(id, volume, function (result) {
+ q.resolve(result);
+ }, function (err) {
+ q.reject(err);
+ });
+
+ return q.promise;
+ }
+ };
+ }]);
+
+// install : cordova plugin add cordova-plugin-network-information
+// link : https://github.com/apache/cordova-plugin-network-information
+
+/* globals Connection: true */
+angular.module('ngCordova.plugins.network', [])
+
+ .factory('$cordovaNetwork', ['$rootScope', '$timeout', function ($rootScope, $timeout) {
+
+ /**
+ * Fires offline a event
+ */
+ var offlineEvent = function () {
+ var networkState = navigator.connection.type;
+ $timeout(function () {
+ $rootScope.$broadcast('$cordovaNetwork:offline', networkState);
+ });
+ };
+
+ /**
+ * Fires online a event
+ */
+ var onlineEvent = function () {
+ var networkState = navigator.connection.type;
+ $timeout(function () {
+ $rootScope.$broadcast('$cordovaNetwork:online', networkState);
+ });
+ };
+
+ document.addEventListener('deviceready', function () {
+ if (navigator.connection) {
+ document.addEventListener('offline', offlineEvent, false);
+ document.addEventListener('online', onlineEvent, false);
+ }
+ });
+
+ return {
+ getNetwork: function () {
+ return navigator.connection.type;
+ },
+
+ isOnline: function () {
+ var networkState = navigator.connection.type;
+ return networkState !== Connection.UNKNOWN && networkState !== Connection.NONE;
+ },
+
+ isOffline: function () {
+ var networkState = navigator.connection.type;
+ return networkState === Connection.UNKNOWN || networkState === Connection.NONE;
+ },
+
+ clearOfflineWatch: function () {
+ document.removeEventListener('offline', offlineEvent);
+ $rootScope.$$listeners['$cordovaNetwork:offline'] = [];
+ },
+
+ clearOnlineWatch: function () {
+ document.removeEventListener('online', onlineEvent);
+ $rootScope.$$listeners['$cordovaNetwork:online'] = [];
+ }
+ };
+ }])
+ .run(['$injector', function ($injector) {
+ $injector.get('$cordovaNetwork'); //ensure the factory always gets initialised
+ }]);
+
+// install : cordova plugin add https://github.com/Paldom/PinDialog.git
+// link : https://github.com/Paldom/PinDialog
+
+angular.module('ngCordova.plugins.pinDialog', [])
+
+ .factory('$cordovaPinDialog', ['$q', '$window', function ($q, $window) {
+
+ return {
+ prompt: function (message, title, buttons) {
+ var q = $q.defer();
+
+ $window.plugins.pinDialog.prompt(message, function (res) {
+ q.resolve(res);
+ }, title, buttons);
+
+ return q.promise;
+ }
+ };
+ }]);
+
+// install : cordova plugin add cordova-plugin-app-preferences
+// link : https://github.com/apla/me.apla.cordova.app-preferences
+
+angular.module('ngCordova.plugins.preferences', [])
+
+ .factory('$cordovaPreferences', ['$window', '$q', function ($window, $q) {
+
+ return {
+
+ pluginNotEnabledMessage: 'Plugin not enabled',
+
+ /**
+ * Decorate the promise object.
+ * @param promise The promise object.
+ */
+ decoratePromise: function (promise){
+ promise.success = function (fn) {
+ promise.then(fn);
+ return promise;
+ };
+
+ promise.error = function (fn) {
+ promise.then(null, fn);
+ return promise;
+ };
+ },
+
+ /**
+ * Store the value of the given dictionary and key.
+ * @param key The key of the preference.
+ * @param value The value to set.
+ * @param dict The dictionary. It's optional.
+ * @returns Returns a promise.
+ */
+ store: function (key, value, dict) {
+ var deferred = $q.defer();
+ var promise = deferred.promise;
+
+ function ok(value){
+ deferred.resolve(value);
+ }
+
+ function errorCallback(error){
+ deferred.reject(new Error(error));
+ }
+
+ if($window.plugins){
+ var storeResult;
+ if(arguments.length === 3){
+ storeResult = $window.plugins.appPreferences.store(dict, key, value);
+ } else {
+ storeResult = $window.plugins.appPreferences.store(key, value);
+ }
+
+ storeResult.then(ok, errorCallback);
+ } else {
+ deferred.reject(new Error(this.pluginNotEnabledMessage));
+ }
+
+ this.decoratePromise(promise);
+ return promise;
+ },
+
+ /**
+ * Fetch the value by the given dictionary and key.
+ * @param key The key of the preference to retrieve.
+ * @param dict The dictionary. It's optional.
+ * @returns Returns a promise.
+ */
+ fetch: function (key, dict) {
+ var deferred = $q.defer();
+ var promise = deferred.promise;
+
+ function ok(value){
+ deferred.resolve(value);
+ }
+
+ function errorCallback(error){
+ deferred.reject(new Error(error));
+ }
+
+ if($window.plugins){
+ var fetchResult;
+ if(arguments.length === 2){
+ fetchResult = $window.plugins.appPreferences.fetch(dict, key);
+ } else {
+ fetchResult = $window.plugins.appPreferences.fetch(key);
+ }
+ fetchResult.then(ok, errorCallback);
+ } else {
+ deferred.reject(new Error(this.pluginNotEnabledMessage));
+ }
+
+ this.decoratePromise(promise);
+ return promise;
+ },
+
+ /**
+ * Remove the value by the given key.
+ * @param key The key of the preference to retrieve.
+ * @param dict The dictionary. It's optional.
+ * @returns Returns a promise.
+ */
+ remove: function (key, dict) {
+ var deferred = $q.defer();
+ var promise = deferred.promise;
+
+ function ok(value){
+ deferred.resolve(value);
+ }
+
+ function errorCallback(error){
+ deferred.reject(new Error(error));
+ }
+
+ if($window.plugins){
+ var removeResult;
+ if(arguments.length === 2){
+ removeResult = $window.plugins.appPreferences.remove(dict, key);
+ } else {
+ removeResult = $window.plugins.appPreferences.remove(key);
+ }
+ removeResult.then(ok, errorCallback);
+ } else {
+ deferred.reject(new Error(this.pluginNotEnabledMessage));
+ }
+
+ this.decoratePromise(promise);
+ return promise;
+ },
+
+ /**
+ * Show the application preferences.
+ * @returns Returns a promise.
+ */
+ show: function () {
+ var deferred = $q.defer();
+ var promise = deferred.promise;
+
+ function ok(value){
+ deferred.resolve(value);
+ }
+
+ function errorCallback(error){
+ deferred.reject(new Error(error));
+ }
+
+ if($window.plugins){
+ $window.plugins.appPreferences.show()
+ .then(ok, errorCallback);
+ } else {
+ deferred.reject(new Error(this.pluginNotEnabledMessage));
+ }
+
+ this.decoratePromise(promise);
+ return promise;
+ }
+ };
+
+ }]);
+
+// install : cordova plugin add https://github.com/katzer/cordova-plugin-printer.git
+// link : https://github.com/katzer/cordova-plugin-printer
+
+angular.module('ngCordova.plugins.printer', [])
+
+ .factory('$cordovaPrinter', ['$q', '$window', function ($q, $window) {
+
+ return {
+ isAvailable: function () {
+ var q = $q.defer();
+
+ $window.plugin.printer.isAvailable(function (isAvailable) {
+ q.resolve(isAvailable);
+ });
+
+ return q.promise;
+ },
+
+ print: function (doc, options) {
+ var q = $q.defer();
+ $window.plugin.printer.print(doc, options, function () {
+ q.resolve();
+ });
+ return q.promise;
+ }
+ };
+ }]);
+
+// install : cordova plugin add https://github.com/pbernasconi/cordova-progressIndicator.git
+// link : http://pbernasconi.github.io/cordova-progressIndicator/
+
+/* globals ProgressIndicator: true */
+angular.module('ngCordova.plugins.progressIndicator', [])
+
+ .factory('$cordovaProgress', [function () {
+
+ return {
+ show: function (_message) {
+ var message = _message || 'Please wait...';
+ return ProgressIndicator.show(message);
+ },
+
+ showSimple: function (_dim) {
+ var dim = _dim || false;
+ return ProgressIndicator.showSimple(dim);
+ },
+
+ showSimpleWithLabel: function (_dim, _label) {
+ var dim = _dim || false;
+ var label = _label || 'Loading...';
+ return ProgressIndicator.showSimpleWithLabel(dim, label);
+ },
+
+ showSimpleWithLabelDetail: function (_dim, _label, _detail) {
+ var dim = _dim || false;
+ var label = _label || 'Loading...';
+ var detail = _detail || 'Please wait';
+ return ProgressIndicator.showSimpleWithLabelDetail(dim, label, detail);
+ },
+
+ showDeterminate: function (_dim, _timeout) {
+ var dim = _dim || false;
+ var timeout = _timeout || 50000;
+ return ProgressIndicator.showDeterminate(dim, timeout);
+ },
+
+ showDeterminateWithLabel: function (_dim, _timeout, _label) {
+ var dim = _dim || false;
+ var timeout = _timeout || 50000;
+ var label = _label || 'Loading...';
+
+ return ProgressIndicator.showDeterminateWithLabel(dim, timeout, label);
+ },
+
+ showAnnular: function (_dim, _timeout) {
+ var dim = _dim || false;
+ var timeout = _timeout || 50000;
+ return ProgressIndicator.showAnnular(dim, timeout);
+ },
+
+ showAnnularWithLabel: function (_dim, _timeout, _label) {
+ var dim = _dim || false;
+ var timeout = _timeout || 50000;
+ var label = _label || 'Loading...';
+ return ProgressIndicator.showAnnularWithLabel(dim, timeout, label);
+ },
+
+ showBar: function (_dim, _timeout) {
+ var dim = _dim || false;
+ var timeout = _timeout || 50000;
+ return ProgressIndicator.showBar(dim, timeout);
+ },
+
+ showBarWithLabel: function (_dim, _timeout, _label) {
+ var dim = _dim || false;
+ var timeout = _timeout || 50000;
+ var label = _label || 'Loading...';
+ return ProgressIndicator.showBarWithLabel(dim, timeout, label);
+ },
+
+ showSuccess: function (_dim, _label) {
+ var dim = _dim || false;
+ var label = _label || 'Success';
+ return ProgressIndicator.showSuccess(dim, label);
+ },
+
+ showText: function (_dim, _text, _position) {
+ var dim = _dim || false;
+ var text = _text || 'Warning';
+ var position = _position || 'center';
+ return ProgressIndicator.showText(dim, text, position);
+ },
+
+ hide: function () {
+ return ProgressIndicator.hide();
+ }
+ };
+
+ }]);
+
+// install : cordova plugin add https://github.com/phonegap-build/PushPlugin.git
+// link : https://github.com/phonegap-build/PushPlugin
+
+angular.module('ngCordova.plugins.push', [])
+
+ .factory('$cordovaPush', ['$q', '$window', '$rootScope', '$timeout', function ($q, $window, $rootScope, $timeout) {
+
+ return {
+ onNotification: function (notification) {
+ $timeout(function () {
+ $rootScope.$broadcast('$cordovaPush:notificationReceived', notification);
+ });
+ },
+
+ register: function (config) {
+ var q = $q.defer();
+ var injector;
+ if (config !== undefined && config.ecb === undefined) {
+ if (document.querySelector('[ng-app]') === null) {
+ injector = 'document.body';
+ }
+ else {
+ injector = 'document.querySelector(\'[ng-app]\')';
+ }
+ config.ecb = 'angular.element(' + injector + ').injector().get(\'$cordovaPush\').onNotification';
+ }
+
+ $window.plugins.pushNotification.register(function (token) {
+ q.resolve(token);
+ }, function (error) {
+ q.reject(error);
+ }, config);
+
+ return q.promise;
+ },
+
+ unregister: function (options) {
+ var q = $q.defer();
+ $window.plugins.pushNotification.unregister(function (result) {
+ q.resolve(result);
+ }, function (error) {
+ q.reject(error);
+ }, options);
+
+ return q.promise;
+ },
+
+ // iOS only
+ setBadgeNumber: function (number) {
+ var q = $q.defer();
+ $window.plugins.pushNotification.setApplicationIconBadgeNumber(function (result) {
+ q.resolve(result);
+ }, function (error) {
+ q.reject(error);
+ }, number);
+ return q.promise;
+ }
+ };
+ }]);
+
+
+// install : cordova plugin add phonegap-plugin-push
+// link : https://github.com/phonegap/phonegap-plugin-push
+
+angular.module('ngCordova.plugins.push_v5', [])
+ .factory('$cordovaPushV5',['$q', '$rootScope', '$timeout', function ($q, $rootScope, $timeout) {
+ /*global PushNotification*/
+
+ var push;
+ return {
+ initialize : function (options) {
+ var q = $q.defer();
+ push = PushNotification.init(options);
+ q.resolve(push);
+ return q.promise;
+ },
+ onNotification : function () {
+ $timeout(function () {
+ push.on('notification', function (notification) {
+ $rootScope.$emit('$cordovaPushV5:notificationReceived', notification);
+ });
+ });
+ },
+ onError : function () {
+ $timeout(function () {
+ push.on('error', function (error) { $rootScope.$emit('$cordovaPushV5:errorOccurred', error);});
+ });
+ },
+ register : function () {
+ var q = $q.defer();
+ if (push === undefined) {
+ q.reject(new Error('init must be called before any other operation'));
+ } else {
+ push.on('registration', function (data) {
+ q.resolve(data.registrationId);
+ });
+ }
+ return q.promise;
+ },
+ unregister : function () {
+ var q = $q.defer();
+ if (push === undefined) {
+ q.reject(new Error('init must be called before any other operation'));
+ } else {
+ push.unregister(function (success) {
+ q.resolve(success);
+ },function (error) {
+ q.reject(error);
+ });
+ }
+ return q.promise;
+ },
+ getBadgeNumber : function () {
+ var q = $q.defer();
+ if (push === undefined) {
+ q.reject(new Error('init must be called before any other operation'));
+ } else {
+ push.getApplicationIconBadgeNumber(function (success) {
+ q.resolve(success);
+ }, function (error) {
+ q.reject(error);
+ });
+ }
+ return q.promise;
+ },
+ setBadgeNumber : function (number) {
+ var q = $q.defer();
+ if (push === undefined) {
+ q.reject(new Error('init must be called before any other operation'));
+ } else {
+ push.setApplicationIconBadgeNumber(function (success) {
+ q.resolve(success);
+ }, function (error) {
+ q.reject(error);
+ }, number);
+ }
+ return q.promise;
+ },
+ finish: function (){
+ var q = $q.defer();
+ if (push === undefined) {
+ q.reject(new Error('init must be called before any other operation'));
+ } else {
+ push.finish(function (success) {
+ q.resolve(success);
+ }, function (error) {
+ q.reject(error);
+ });
+ }
+ return q.promise;
+ }
+ };
+ }]);
+
+// install : cordova plugin add cordova-plugin-recentscontrol
+// link : https://github.com/smcpjames/cordova-plugin-recentscontrol
+
+/* globals RecentsControl: true */
+angular.module('ngCordova.plugins.recentsControl', [])
+
+.factory('$cordovaRecents', function () {
+ return {
+ setColor: function (color) {
+ return RecentsControl.setColor(color);
+ },
+
+ setDescription: function (desc) {
+ return RecentsControl.setDescription(desc);
+ },
+
+ setOptions: function (colorStr, desc) {
+ return RecentsControl.setOptions(colorStr, desc);
+ }
+ };
+});
+// install : cordova plugin add https://github.com/gitawego/cordova-screenshot.git
+// link : https://github.com/gitawego/cordova-screenshot
+
+angular.module('ngCordova.plugins.screenshot', [])
+.factory('$cordovaScreenshot', ['$q', function ($q) {
+ return {
+ captureToFile: function (opts) {
+
+ var options = opts || {};
+
+ var extension = options.extension || 'jpg';
+ var quality = options.quality || '100';
+
+ var defer = $q.defer();
+
+ if (!navigator.screenshot) {
+ defer.resolve(null);
+ return defer.promise;
+ }
+
+ navigator.screenshot.save(function (error, res) {
+ if (error) {
+ defer.reject(error);
+ } else {
+ defer.resolve(res.filePath);
+ }
+ }, extension, quality, options.filename);
+
+ return defer.promise;
+ },
+ captureToUri: function (opts) {
+
+ var options = opts || {};
+
+ var extension = options.extension || 'jpg';
+ var quality = options.quality || '100';
+
+ var defer = $q.defer();
+
+ if (!navigator.screenshot) {
+ defer.resolve(null);
+ return defer.promise;
+ }
+
+ navigator.screenshot.URI(function (error, res) {
+ if (error) {
+ defer.reject(error);
+ } else {
+ defer.resolve(res.URI);
+ }
+ }, extension, quality, options.filename);
+
+ return defer.promise;
+ }
+ };
+}]);
+// install : cordova plugin add https://github.com/xseignard/cordovarduino.git
+// link : https://github.com/xseignard/cordovarduino
+
+/* globals serial: true */
+angular.module('ngCordova.plugins.serial', [])
+
+ .factory('$cordovaSerial', ['$q', function ($q) {
+
+ var serialService = {};
+
+ serialService.requestPermission = function requestPermission(options) {
+ var q = $q.defer();
+
+ serial.requestPermission(options, function success() {
+ q.resolve();
+ }, function error(err) {
+ q.reject(err);
+ });
+
+ return q.promise;
+ };
+
+ serialService.open = function(options) {
+ var q = $q.defer();
+
+ serial.open(options, function success() {
+ q.resolve();
+ }, function error(err) {
+ q.reject(err);
+ });
+
+ return q.promise;
+ };
+
+ serialService.write = function(data) {
+ var q = $q.defer();
+
+ serial.write(data, function success() {
+ q.resolve();
+ }, function error(err) {
+ q.reject(err);
+ });
+
+ return q.promise;
+ };
+
+ serialService.writeHex = function(data) {
+ var q = $q.defer();
+
+ serial.writeHex(data, function success() {
+ q.resolve();
+ }, function error(err) {
+ q.reject(err);
+ });
+
+ return q.promise;
+ };
+
+ serialService.read = function() {
+ var q = $q.defer();
+
+ serial.read(function success(buffer) {
+ var view = new Uint8Array(buffer);
+ q.resolve(view);
+ }, function error(err) {
+ q.reject(err);
+ });
+
+ return q.promise;
+ };
+
+ serialService.registerReadCallback = function(successCallback, errorCallback) {
+ serial.registerReadCallback(function success(buffer) {
+ var view = new Uint8Array(buffer);
+ successCallback(view);
+ }, errorCallback);
+ };
+
+ serialService.close = function() {
+ var q = $q.defer();
+
+ serial.close(function success() {
+ q.resolve();
+ }, function error(err) {
+ q.reject(err);
+ });
+
+ return q.promise;
+ };
+
+ return serialService;
+ }]);
+
+// install : cordova plugin add https://github.com/cordova-sms/cordova-sms-plugin.git
+// link : https://github.com/cordova-sms/cordova-sms-plugin
+
+/* globals sms: true */
+angular.module('ngCordova.plugins.sms', [])
+
+ .factory('$cordovaSms', ['$q', function ($q) {
+
+ return {
+ send: function (number, message, options) {
+ var q = $q.defer();
+ sms.send(number, message, options, function (res) {
+ q.resolve(res);
+ }, function (err) {
+ q.reject(err);
+ });
+ return q.promise;
+ }
+ };
+
+ }]);
+
+// install : cordova plugin add https://github.com/EddyVerbruggen/SocialSharing-PhoneGap-Plugin.git
+// link : https://github.com/EddyVerbruggen/SocialSharing-PhoneGap-Plugin
+
+// NOTE: shareViaEmail -> if user cancels sharing email, success is still called
+// TODO: add support for iPad
+
+angular.module('ngCordova.plugins.socialSharing', [])
+
+ .factory('$cordovaSocialSharing', ['$q', '$window', function ($q, $window) {
+
+ return {
+ share: function (message, subject, file, link) {
+ var q = $q.defer();
+ subject = subject || null;
+ file = file || null;
+ link = link || null;
+ $window.plugins.socialsharing.share(message, subject, file, link, function () {
+ q.resolve(true);
+ }, function () {
+ q.reject(false);
+ });
+ return q.promise;
+ },
+
+ shareWithOptions: function (options) {
+ var q = $q.defer();
+ $window.plugins.socialsharing.shareWithOptions(options, function () {
+ q.resolve(true);
+ }, function () {
+ q.reject(false);
+ });
+ return q.promise;
+ },
+
+ shareViaTwitter: function (message, file, link) {
+ var q = $q.defer();
+ file = file || null;
+ link = link || null;
+ $window.plugins.socialsharing.shareViaTwitter(message, file, link, function () {
+ q.resolve(true);
+ }, function () {
+ q.reject(false);
+ });
+ return q.promise;
+ },
+
+ shareViaWhatsApp: function (message, file, link) {
+ var q = $q.defer();
+ file = file || null;
+ link = link || null;
+ $window.plugins.socialsharing.shareViaWhatsApp(message, file, link, function () {
+ q.resolve(true);
+ }, function () {
+ q.reject(false);
+ });
+ return q.promise;
+ },
+
+ shareViaFacebook: function (message, file, link) {
+ var q = $q.defer();
+ message = message || null;
+ file = file || null;
+ link = link || null;
+ $window.plugins.socialsharing.shareViaFacebook(message, file, link, function () {
+ q.resolve(true);
+ }, function () {
+ q.reject(false);
+ });
+ return q.promise;
+ },
+
+ shareViaFacebookWithPasteMessageHint: function (message, file, link, pasteMessageHint) {
+ var q = $q.defer();
+ file = file || null;
+ link = link || null;
+ $window.plugins.socialsharing.shareViaFacebookWithPasteMessageHint(message, file, link, pasteMessageHint, function () {
+ q.resolve(true);
+ }, function () {
+ q.reject(false);
+ });
+ return q.promise;
+ },
+
+ shareViaSMS: function (message, commaSeparatedPhoneNumbers) {
+ var q = $q.defer();
+ $window.plugins.socialsharing.shareViaSMS(message, commaSeparatedPhoneNumbers, function () {
+ q.resolve(true);
+ }, function () {
+ q.reject(false);
+ });
+ return q.promise;
+ },
+
+ shareViaEmail: function (message, subject, toArr, ccArr, bccArr, fileArr) {
+ var q = $q.defer();
+ toArr = toArr || null;
+ ccArr = ccArr || null;
+ bccArr = bccArr || null;
+ fileArr = fileArr || null;
+ $window.plugins.socialsharing.shareViaEmail(message, subject, toArr, ccArr, bccArr, fileArr, function () {
+ q.resolve(true);
+ }, function () {
+ q.reject(false);
+ });
+ return q.promise;
+ },
+
+ shareVia: function (via, message, subject, file, link) {
+ var q = $q.defer();
+ message = message || null;
+ subject = subject || null;
+ file = file || null;
+ link = link || null;
+ $window.plugins.socialsharing.shareVia(via, message, subject, file, link, function () {
+ q.resolve(true);
+ }, function () {
+ q.reject(false);
+ });
+ return q.promise;
+ },
+
+ canShareViaEmail: function () {
+ var q = $q.defer();
+ $window.plugins.socialsharing.canShareViaEmail(function () {
+ q.resolve(true);
+ }, function () {
+ q.reject(false);
+ });
+ return q.promise;
+ },
+
+ canShareVia: function (via, message, subject, file, link) {
+ var q = $q.defer();
+ $window.plugins.socialsharing.canShareVia(via, message, subject, file, link, function (success) {
+ q.resolve(success);
+ }, function (error) {
+ q.reject(error);
+ });
+ return q.promise;
+ },
+
+ available: function () {
+ var q = $q.defer();
+ window.plugins.socialsharing.available(function (isAvailable) {
+ if (isAvailable) {
+ q.resolve();
+ }
+ else {
+ q.reject();
+ }
+ });
+
+ return q.promise;
+ }
+ };
+ }]);
+
+// install : cordova plugin add https://github.com/Paldom/SpinnerDialog.git
+// link : https://github.com/Paldom/SpinnerDialog
+
+angular.module('ngCordova.plugins.spinnerDialog', [])
+
+ .factory('$cordovaSpinnerDialog', ['$window', function ($window) {
+
+ return {
+ show: function (title, message, fixed, iosOptions) {
+ fixed = fixed || false;
+ return $window.plugins.spinnerDialog.show(title, message, fixed, iosOptions);
+ },
+ hide: function () {
+ return $window.plugins.spinnerDialog.hide();
+ }
+ };
+
+ }]);
+
+// install : cordova plugin add cordova-plugin-splashscreen
+// link : https://github.com/apache/cordova-plugin-splashscreen
+
+angular.module('ngCordova.plugins.splashscreen', [])
+
+ .factory('$cordovaSplashscreen', [function () {
+
+ return {
+ hide: function () {
+ return navigator.splashscreen.hide();
+ },
+
+ show: function () {
+ return navigator.splashscreen.show();
+ }
+ };
+
+ }]);
+
+// install : cordova plugin add https://github.com/litehelpers/Cordova-sqlite-storage.git
+// link : https://github.com/litehelpers/Cordova-sqlite-storage
+
+angular.module('ngCordova.plugins.sqlite', [])
+
+ .factory('$cordovaSQLite', ['$q', '$window', function ($q, $window) {
+
+ return {
+ openDB: function (options, background) {
+
+ if (angular.isObject(options) && !angular.isString(options)) {
+ if (typeof background !== 'undefined') {
+ options.bgType = background;
+ }
+ return $window.sqlitePlugin.openDatabase(options);
+ }
+
+ return $window.sqlitePlugin.openDatabase({
+ name: options,
+ bgType: background
+ });
+ },
+
+ execute: function (db, query, binding) {
+ var q = $q.defer();
+ db.transaction(function (tx) {
+ tx.executeSql(query, binding, function (tx, result) {
+ q.resolve(result);
+ },
+ function (transaction, error) {
+ q.reject(error);
+ });
+ });
+ return q.promise;
+ },
+
+ insertCollection: function (db, query, bindings) {
+ var q = $q.defer();
+ var coll = bindings.slice(0); // clone collection
+
+ db.transaction(function (tx) {
+ (function insertOne() {
+ var record = coll.splice(0, 1)[0]; // get the first record of coll and reduce coll by one
+ try {
+ tx.executeSql(query, record, function (tx, result) {
+ if (coll.length === 0) {
+ q.resolve(result);
+ } else {
+ insertOne();
+ }
+ }, function (transaction, error) {
+ q.reject(error);
+ return;
+ });
+ } catch (exception) {
+ q.reject(exception);
+ }
+ })();
+ });
+ return q.promise;
+ },
+
+ nestedExecute: function (db, query1, query2, binding1, binding2) {
+ var q = $q.defer();
+
+ db.transaction(function (tx) {
+ tx.executeSql(query1, binding1, function (tx, result) {
+ q.resolve(result);
+ tx.executeSql(query2, binding2, function (tx, res) {
+ q.resolve(res);
+ });
+ });
+ },
+ function (transaction, error) {
+ q.reject(error);
+ });
+
+ return q.promise;
+ },
+
+ deleteDB: function (dbName) {
+ var q = $q.defer();
+
+ $window.sqlitePlugin.deleteDatabase(dbName, function (success) {
+ q.resolve(success);
+ }, function (error) {
+ q.reject(error);
+ });
+
+ return q.promise;
+ }
+ };
+ }]);
+
+// install : cordova plugin add cordova-plugin-statusbar
+// link : https://github.com/apache/cordova-plugin-statusbar
+
+/* globals StatusBar: true */
+angular.module('ngCordova.plugins.statusbar', [])
+
+.factory('$cordovaStatusbar', [function () {
+
+ return {
+
+ /**
+ * @param {boolean} bool
+ */
+ overlaysWebView: function (bool) {
+ return StatusBar.overlaysWebView(!!bool);
+ },
+
+ STYLES: {
+ DEFAULT: 0,
+ LIGHT_CONTENT: 1,
+ BLACK_TRANSLUCENT: 2,
+ BLACK_OPAQUE: 3
+ },
+
+ /**
+ * @param {number} style
+ */
+ style: function (style) {
+ switch (style) {
+ // Default
+ case 0:
+ return StatusBar.styleDefault();
+
+ // LightContent
+ case 1:
+ return StatusBar.styleLightContent();
+
+ // BlackTranslucent
+ case 2:
+ return StatusBar.styleBlackTranslucent();
+
+ // BlackOpaque
+ case 3:
+ return StatusBar.styleBlackOpaque();
+
+ default:
+ return StatusBar.styleDefault();
+ }
+ },
+
+ // supported names:
+ // black, darkGray, lightGray, white, gray, red, green,
+ // blue, cyan, yellow, magenta, orange, purple, brown
+ styleColor: function (color) {
+ return StatusBar.backgroundColorByName(color);
+ },
+
+ styleHex: function (colorHex) {
+ return StatusBar.backgroundColorByHexString(colorHex);
+ },
+
+ hide: function () {
+ return StatusBar.hide();
+ },
+
+ show: function () {
+ return StatusBar.show();
+ },
+
+ isVisible: function () {
+ return StatusBar.isVisible;
+ }
+ };
+}]);
+
+// install : cordova plugin add https://github.com/EddyVerbruggen/Toast-PhoneGap-Plugin.git
+// link : https://github.com/EddyVerbruggen/Toast-PhoneGap-Plugin
+
+angular.module('ngCordova.plugins.toast', [])
+
+ .factory('$cordovaToast', ['$q', '$window', function ($q, $window) {
+
+ return {
+ showShortTop: function (message) {
+ var q = $q.defer();
+ $window.plugins.toast.showShortTop(message, function (response) {
+ q.resolve(response);
+ }, function (error) {
+ q.reject(error);
+ });
+ return q.promise;
+ },
+
+ showShortCenter: function (message) {
+ var q = $q.defer();
+ $window.plugins.toast.showShortCenter(message, function (response) {
+ q.resolve(response);
+ }, function (error) {
+ q.reject(error);
+ });
+ return q.promise;
+ },
+
+ showShortBottom: function (message) {
+ var q = $q.defer();
+ $window.plugins.toast.showShortBottom(message, function (response) {
+ q.resolve(response);
+ }, function (error) {
+ q.reject(error);
+ });
+ return q.promise;
+ },
+
+ showLongTop: function (message) {
+ var q = $q.defer();
+ $window.plugins.toast.showLongTop(message, function (response) {
+ q.resolve(response);
+ }, function (error) {
+ q.reject(error);
+ });
+ return q.promise;
+ },
+
+ showLongCenter: function (message) {
+ var q = $q.defer();
+ $window.plugins.toast.showLongCenter(message, function (response) {
+ q.resolve(response);
+ }, function (error) {
+ q.reject(error);
+ });
+ return q.promise;
+ },
+
+ showLongBottom: function (message) {
+ var q = $q.defer();
+ $window.plugins.toast.showLongBottom(message, function (response) {
+ q.resolve(response);
+ }, function (error) {
+ q.reject(error);
+ });
+ return q.promise;
+ },
+
+ showWithOptions: function (options) {
+ var q = $q.defer();
+ $window.plugins.toast.showWithOptions(options, function (response) {
+ q.resolve(response);
+ }, function (error) {
+ q.reject(error);
+ });
+ return q.promise;
+ },
+
+ show: function (message, duration, position) {
+ var q = $q.defer();
+ $window.plugins.toast.show(message, duration, position, function (response) {
+ q.resolve(response);
+ }, function (error) {
+ q.reject(error);
+ });
+ return q.promise;
+ },
+
+ hide: function () {
+ var q = $q.defer();
+ try {
+ $window.plugins.toast.hide();
+ q.resolve();
+ } catch (error) {
+ q.reject(error && error.message);
+ }
+ return q.promise;
+ }
+ };
+
+ }]);
+
+// install : cordova plugin add https://github.com/leecrossley/cordova-plugin-touchid.git
+// link : https://github.com/leecrossley/cordova-plugin-touchid
+
+/* globals touchid: true */
+angular.module('ngCordova.plugins.touchid', [])
+
+ .factory('$cordovaTouchID', ['$q', function ($q) {
+
+ return {
+ checkSupport: function () {
+ var defer = $q.defer();
+ if (!window.cordova) {
+ defer.reject('Not supported without cordova.js');
+ } else {
+ touchid.checkSupport(function (value) {
+ defer.resolve(value);
+ }, function (err) {
+ defer.reject(err);
+ });
+ }
+
+ return defer.promise;
+ },
+
+ authenticate: function (authReasonText) {
+ var defer = $q.defer();
+ if (!window.cordova) {
+ defer.reject('Not supported without cordova.js');
+ } else {
+ touchid.authenticate(function (value) {
+ defer.resolve(value);
+ }, function (err) {
+ defer.reject(err);
+ }, authReasonText);
+ }
+
+ return defer.promise;
+ }
+ };
+ }]);
+
+// install : cordova plugin add cordova-plugin-tts
+// link : https://github.com/smcpjames/cordova-plugin-tts
+
+/* globals TTS: true */
+angular.module('ngCordova.plugins.tts', [])
+
+.factory('$cordovaTTS', function () {
+ return {
+ speak: function (text, onfulfilled, onrejected) {
+ return TTS.speak(text, onfulfilled, onrejected);
+ }
+ };
+});
+// install : cordova plugin add https://github.com/aerogear/aerogear-cordova-push.git
+// link : https://github.com/aerogear/aerogear-cordova-push
+
+angular.module('ngCordova.plugins.upsPush', [])
+
+ .factory('$cordovaUpsPush', ['$q', '$window', '$rootScope', '$timeout', function ($q, $window, $rootScope, $timeout) {
+ return {
+ register: function (config) {
+ var q = $q.defer();
+
+ $window.push.register(function (notification) {
+ $timeout(function () {
+ $rootScope.$broadcast('$cordovaUpsPush:notificationReceived', notification);
+ });
+ }, function () {
+ q.resolve();
+ }, function (error) {
+ q.reject(error);
+ }, config);
+
+ return q.promise;
+ },
+
+ unregister: function (options) {
+ var q = $q.defer();
+ $window.push.unregister(function () {
+ q.resolve();
+ }, function (error) {
+ q.reject(error);
+ }, options);
+
+ return q.promise;
+ },
+
+ // iOS only
+ setBadgeNumber: function (number) {
+ var q = $q.defer();
+ $window.push.setApplicationIconBadgeNumber(function () {
+ q.resolve();
+ }, number);
+ return q.promise;
+ }
+ };
+ }]);
+
+// install : cordova plugin add cordova-plugin-vibration
+// link : https://github.com/apache/cordova-plugin-vibration
+
+angular.module('ngCordova.plugins.vibration', [])
+
+ .factory('$cordovaVibration', [function () {
+
+ return {
+ vibrate: function (times) {
+ return navigator.notification.vibrate(times);
+ },
+ vibrateWithPattern: function (pattern, repeat) {
+ return navigator.notification.vibrateWithPattern(pattern, repeat);
+ },
+ cancelVibration: function () {
+ return navigator.notification.cancelVibration();
+ }
+ };
+ }]);
+
+// install : cordova plugin add https://github.com/EddyVerbruggen/VideoCapturePlus-PhoneGap-Plugin.git
+// link : https://github.com/EddyVerbruggen/VideoCapturePlus-PhoneGap-Plugin
+
+angular.module('ngCordova.plugins.videoCapturePlus', [])
+
+ .provider('$cordovaVideoCapturePlus', [function () {
+
+ var defaultOptions = {};
+
+
+ /**
+ * the nr of videos to record, default 1 (on iOS always 1)
+ *
+ * @param limit
+ */
+ this.setLimit = function setLimit(limit) {
+ defaultOptions.limit = limit;
+ };
+
+
+ /**
+ * max duration in seconds, default 0, which is 'forever'
+ *
+ * @param seconds
+ */
+ this.setMaxDuration = function setMaxDuration(seconds) {
+ defaultOptions.duration = seconds;
+ };
+
+
+ /**
+ * set to true to override the default low quality setting
+ *
+ * @param {Boolean} highquality
+ */
+ this.setHighQuality = function setHighQuality(highquality) {
+ defaultOptions.highquality = highquality;
+ };
+
+ /**
+ * you'll want to sniff the user-Agent/device and pass the best overlay based on that..
+ * set to true to override the default backfacing camera setting. iOS: works fine, Android: YMMV (#18)
+ *
+ * @param {Boolean} frontcamera
+ */
+ this.useFrontCamera = function useFrontCamera(frontcamera) {
+ defaultOptions.frontcamera = frontcamera;
+ };
+
+
+ /**
+ * put the png in your www folder
+ *
+ * @param {String} imageUrl
+ */
+ this.setPortraitOverlay = function setPortraitOverlay(imageUrl) {
+ defaultOptions.portraitOverlay = imageUrl;
+ };
+
+
+ /**
+ *
+ * @param {String} imageUrl
+ */
+ this.setLandscapeOverlay = function setLandscapeOverlay(imageUrl) {
+ defaultOptions.landscapeOverlay = imageUrl;
+ };
+
+
+ /**
+ * iOS only
+ *
+ * @param text
+ */
+ this.setOverlayText = function setOverlayText(text) {
+ defaultOptions.overlayText = text;
+ };
+
+
+ this.$get = ['$q', '$window', function ($q, $window) {
+ return {
+ captureVideo: function (options) {
+ var q = $q.defer();
+
+ if (!$window.plugins.videocaptureplus) {
+ q.resolve(null);
+ return q.promise;
+ }
+
+ $window.plugins.videocaptureplus.captureVideo(q.resolve, q.reject,
+ angular.extend({}, defaultOptions, options));
+
+ return q.promise;
+ }
+ };
+ }];
+ }]);
+
+// install : cordova plugin add https://github.com/MobileChromeApps/zip.git
+// link : https://github.com/MobileChromeApps/zip
+
+angular.module('ngCordova.plugins.zip', [])
+
+ .factory('$cordovaZip', ['$q', '$window', function ($q, $window) {
+
+ return {
+ unzip: function (source, destination) {
+ var q = $q.defer();
+
+ $window.zip.unzip(source, destination, function (isError) {
+ if (isError === 0) {
+ q.resolve();
+ } else {
+ q.reject();
+ }
+ }, function (progressEvent) {
+ q.notify(progressEvent);
+ });
+
+ return q.promise;
+ }
+ };
+ }]);
+
+})(); \ No newline at end of file