commit a83ef869d5159ab5c190524fbfa1a30880a26025 Author: Kim Wittenburg Date: Tue Oct 31 14:39:33 2017 +0100 Archive Project diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..2f01e1d --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["env"] +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c628afc --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# IDEA Project Files +.idea + +# Node.js +node_modules/ + +# Media Assets +assets/ + +# Generated Files +*.ite \ No newline at end of file diff --git a/Play Button.pxm b/Play Button.pxm new file mode 100644 index 0000000..562019b Binary files /dev/null and b/Play Button.pxm differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..8676477 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# iTunes Extras Template + +## Asset File Structure and Naming \ No newline at end of file diff --git a/TuneKit/sounds/Exit.aif b/TuneKit/sounds/Exit.aif new file mode 100644 index 0000000..c2d5946 Binary files /dev/null and b/TuneKit/sounds/Exit.aif differ diff --git a/TuneKit/sounds/Limit.aif b/TuneKit/sounds/Limit.aif new file mode 100644 index 0000000..e7d5aa6 Binary files /dev/null and b/TuneKit/sounds/Limit.aif differ diff --git a/TuneKit/sounds/Selection.aif b/TuneKit/sounds/Selection.aif new file mode 100644 index 0000000..fa970f3 Binary files /dev/null and b/TuneKit/sounds/Selection.aif differ diff --git a/TuneKit/sounds/SelectionChange.aif b/TuneKit/sounds/SelectionChange.aif new file mode 100644 index 0000000..acdee27 Binary files /dev/null and b/TuneKit/sounds/SelectionChange.aif differ diff --git a/TuneKit/src/TuneKit.js b/TuneKit/src/TuneKit.js new file mode 100644 index 0000000..2feba6e --- /dev/null +++ b/TuneKit/src/TuneKit.js @@ -0,0 +1,5941 @@ +/* + * Copyright © 2009 Apple Inc. All rights reserved. + */ + +const TK_VERSION = '2009-11-19'; + +/* + * Copyright © 2009 Apple Inc. All rights reserved. + */ + +/** + * The keyboard identifier for the backspace key. + * @constant + * @type int + */ +const KEYBOARD_BACKSPACE = 8; +/** + * The keyboard identifier for the left key. + * @constant + * @type int + */ +const KEYBOARD_LEFT = 37; +/** + * The keyboard identifier for the right key. + * @constant + * @type int + */ +const KEYBOARD_RIGHT = 39; +/** + * The keyboard identifier for the up key. + * @constant + * @type int + */ +const KEYBOARD_UP = 38; +/** + * The keyboard identifier for the down key. + * @constant + * @type int + */ +const KEYBOARD_DOWN = 40; +/** + * The keyboard identifier for the return key. + * @constant + * @type int + */ +const KEYBOARD_RETURN = 13; + +/** + * Indicates whether TuneKit is running on Apple TV. + * @constant + * @type bool + */ +const IS_APPLE_TV = (window.iTunes !== undefined && window.iTunes.platform == 'AppleTV'); +/** + * The Apple TV-specific iTunes system sounds interface. + * @constant + * @type bool + * @private + */ +const ATV_SOUNDS = IS_APPLE_TV ? window.iTunes.getSystemSounds() : null; + +/** + * The move sound of the Apple TV user interface. + * @constant + * @type Object + */ +const SOUND_MOVED = IS_APPLE_TV ? ATV_SOUNDS.SystemSoundScrollStart : new Audio('TuneKit/sounds/SelectionChange.aif'); +/** + * The selection sound of the Apple TV user interface. + * @constant + * @type Object + */ +const SOUND_ACTIVATED = IS_APPLE_TV ? ATV_SOUNDS.SystemSoundSelect : new Audio('TuneKit/sounds/Selection.aif'); +/** + * The limit sound of the Apple TV user interface. + * @constant + * @type Object + */ +const SOUND_LIMIT = IS_APPLE_TV ? ATV_SOUNDS.SystemSoundScrollLimit : new Audio('TuneKit/sounds/Limit.aif'); +/** + * The exit sound of the Apple TV user interface. + * @constant + * @type Object + */ +const SOUND_EXIT = IS_APPLE_TV ? ATV_SOUNDS.SystemSoundExit : new Audio('TuneKit/sounds/Exit.aif'); + +/** + * @class + * @name TKUtils + * + * @since TuneKit 1.0 + */ +function TKUtils () { +}; + +/* ==================== SOUNDS ==================== */ + +/** + * Plays a sound. + * + * @param {Object} sound The sound to play, which is either an audio element or an iTunes system sound identifier on Apple TV. + */ +TKUtils.playSound = function (sound) { + if (IS_APPLE_TV) { + ATV_SOUNDS.playSystemSound(sound); + } + else { + sound.play(); + } +}; + +/* ==================== TRANSFORMS SHORTHANDS ==================== */ + +/** + * Prints a translate3d() command that can be used as input for a -webkit-transform property. + * + * @param {int} tx The x coordinate for the translation. + * @param {int} ty The y coordinate for the translation + * + * @returns {String} The translate3d() command + */ +TKUtils.t = function (tx, ty) { + return 'translate3d(' + tx + 'px, ' + ty + 'px, 0)'; +}; + +/** + * Creates a CSS string representation for a number in pixels. + * + * @param {number} value The value to be converted. + * + * @returns {String} A CSS string representation for value in pixels. + */ +TKUtils.px = function (value) { + return value + 'px'; +}; + +/* ==================== Array ==================== */ + +/** + * Copies all properties from one object onto another. + * + * @param {Object} sourceObject The object from which we will copy properties. + * @param {Object} targetObject The array onto which we will copy properties. + */ +TKUtils.copyPropertiesFromSourceToTarget = function (source, target) { + for (var property in source) { + target[property] = source[property]; + } +}; + +/* ==================== Delegates ==================== */ + +/** + * Indicates whether an object is a Function. + * + * @param {Object} object The object purported to be a Function. + * + * @returns {bool} Whether the object is a Function. + */ +TKUtils.objectIsFunction = function (object) { + return (typeof object == 'function'); +}; + +/** + * Indicates whether an object is undefined. + * + * @param {Object} object The object purported to be undefined. + * + * @returns {bool} Whether the object is undefined. + */ +TKUtils.objectIsUndefined = function (object) { + return (object === undefined); +}; + +/** + * Indicates whether an object is a string literal or a String instance. + * + * @param {Object} object The object purported to be a string literal or a String instance. + * + * @returns {bool} Whether the object is a string literal or a String instance. + */ +TKUtils.objectIsString = function (object) { + return (typeof object == 'string' || object instanceof String); +}; + +/** + * Indicates whether an object is an Array. + * + * @param {Object} object The object purported to be an Array. + * + * @returns {bool} Whether the object is an Array. + */ +TKUtils.objectIsArray = function (object) { + return (object instanceof Array); +}; + +/** + * Indicates whether an object implements a given method, useful to check if a delegate + * object implements a given delegate method. + * + * @param {Object} object The object purported to implement a given method. + * @param {String} methodNameAsString The method name as a String. + * + * @returns {bool} Whether the object implements the given method. + */ +TKUtils.objectHasMethod = function (object, methodNameAsString) { + return ( object !== null && + !this.objectIsUndefined(object) && + !this.objectIsUndefined(object[methodNameAsString]) && + this.objectIsFunction(object[methodNameAsString]) + ); +}; + +/* ==================== INIT ==================== */ + +/** + * Sets up the .displayNames for all functions defined on the specified class, including its prototype. + * + * @param {Object} class The class. + * @param {String} className The class name as a string, in case it can not be derived from class. Optional. + */ +TKUtils.setupDisplayNames = function (object, className) { + var class_name = className || object.name; + for (var i in object) { + // make sure we don't touch properties that were synthetized + if (object.__lookupGetter__(i)) { + continue; + } + var prop = object[i]; + if (TKUtils.objectIsFunction(prop)) { + prop.displayName = TKUtils.createDisplayName(class_name, i); + } + } + for (var i in object.prototype) { + // make sure we don't touch properties that were synthetized + if (object.prototype.__lookupGetter__(i)) { + continue; + } + var prop = object.prototype[i]; + if (TKUtils.objectIsFunction(prop)) { + prop.displayName = TKUtils.createDisplayName(class_name, i); + } + } +}; + +TKUtils.createDisplayName = function (className, methodName) { + return className + '.' + methodName + '()'; +}; + +TKUtils.buildElement = function (elementData) { + // nothing to do if we don't have useful data + if (!elementData || !elementData.type) { + return null; + } + // + var element = null; + switch (elementData.type) { + case "emptyDiv": + element = document.createElement("div"); + break; + case "container": + element = document.createElement("div"); + for (var i=0; i < elementData.children.length; i++) { + element.appendChild(TKUtils.buildElement(elementData.children[i])); + } + break; + case "image": + element = document.createElement("img"); + element.src = elementData.src; + break; + case "text": + element = document.createElement("div"); + var p = document.createElement("p"); + p.innerText = elementData.text; + element.appendChild(p); + break; + default: + element = document.createElement(elementData.type); + element.innerHTML = elementData.content; + } + // add optional id + if (elementData.id) { + element.id = elementData.id; + } + // add optional class + if (elementData.className) { + element.className = elementData.className; + } + + // wrap in optional link + if (elementData.link){ + var subElement = element; + element = document.createElement("a"); + element.href = elementData.link; + element.target = "_blank"; + element.appendChild(subElement); + } + + return element; +}; + +/** + * Creates a DOM event. + * + * @param {String} eventType The event type. + * @param {Element} relatedTarget The optional related target for this event. + * + * @returns {Event} The event. + */ +TKUtils.createEvent = function (eventType, relatedTarget) { + var event = document.createEvent('Event'); + event.initEvent(eventType, true, true); + event.relatedTarget = relatedTarget; + return event; +}; + +/** + * Indicates whether a node is in some other node's subtree. + * + * @param {Node} childNode The alleged child node. + * @param {Node} allegedParentNode The alleged parent node. + * + * @returns {bool} Whether childNode is a child of allegedParentNode. + */ +TKUtils.isNodeChildOfOtherNode = function (childNode, allegedParentNode) { + var node = childNode.parentNode; + while (node !== null) { + if (node === allegedParentNode) { + return true; + break; + } + node = node.parentNode; + } + return false; +}; + +TKUtils.setupDisplayNames(TKUtils, 'TKUtils'); + +/* ==================== TKRect ==================== */ + +/** + * The top left corner of a rectangle. + * @constant + * @type int + * @private + */ +const TKRectTopLeftCorner = 0; +/** + * The middle point on the top edge of a rectangle. + * @constant + * @type int + * @private + */ +const TKRectMiddleOfTopEdge = 1; +/** + * The top right corner of a rectangle. + * @constant + * @type int + * @private + */ +const TKRectTopRightCorner = 2; +/** + * The middle point on the right edge of a rectangle. + * @constant + * @type int + * @private + */ +const TKRectMiddleOfRightEdge = 3; +/** + * The bottom right corner of a rectangle. + * @constant + * @type int + * @private + */ +const TKRectBottomRightCorner = 4; +/** + * The middle point on the bottom edge of a rectangle. + * @constant + * @type int + * @private + */ +const TKRectMiddleOfBottomEdge = 5; +/** + * The bottom left corner of a rectangle. + * @constant + * @type int + * @private + */ +const TKRectBottomLeftCorner = 6; +/** + * The middle point on the left edge of a rectangle. + * @constant + * @type int + * @private + */ +const TKRectMiddleOfLeftEdge = 7; +/** + * The center of a rectangle. + * @constant + * @type int + * @private + */ +const TKRectCenter = 8; +/** + * The top edge of a rectangle. + * @constant + * @type int + * @private + */ +const TKRectTopEdge = 9; +/** + * The right edge of a rectangle. + * @constant + * @type int + * @private + */ +const TKRectRightEdge = 10; +/** + * The bottom edge of a rectangle. + * @constant + * @type int + * @private + */ +const TKRectBottomEdge = 11; +/** + * The left edge of a rectangle. + * @constant + * @type int + * @private + */ +const TKRectLeftEdge = 12; + +/** + * @class + * + *

The TKRect provides some utilities to deal with a rectangle data type, allowing to obtain coordinates of the rectangle's points + * of interest as {@link TKPoint} objects and its edges as {@link TKSegment} objects, or obtaining a rectangle resulting in the union of several others.

+ * + * @since TuneKit 1.0 + * + * @param {float} x The x coordinate. + * @param {float} y The y coordinate. + * @param {float} width The width. + * @param {float} height The height. + */ +function TKRect (x, y, width, height) { + /** + * The x coordinate. + * @type float + */ + this.x = x || 0; + /** + * The y coordinate. + * @type float + */ + this.y = y || 0; + /** + * The width. + * @type float + */ + this.width = width || 0; + /** + * The height. + * @type float + */ + this.height = height || 0; +}; + +/** + * @private + * Provides the coordinates of a given point of interest. + * + * @param {int} index The point of interest. + * + * @returns {TKPoint} The point at the given point of interest. + */ +TKRect.prototype.pointAtPosition = function (index) { + var point; + if (index == TKRectTopLeftCorner) { + point = new TKPoint(this.x, this.y); + } + else if (index == TKRectMiddleOfTopEdge) { + point = new TKPoint(this.x + this.width / 2, this.y); + } + else if (index == TKRectTopRightCorner) { + point = new TKPoint(this.x + this.width, this.y); + } + else if (index == TKRectMiddleOfRightEdge) { + point = new TKPoint(this.x + this.width, this.y + this.height / 2); + } + else if (index == TKRectBottomRightCorner) { + point = new TKPoint(this.x + this.width, this.y + this.height); + } + else if (index == TKRectMiddleOfBottomEdge) { + point = new TKPoint(this.x + this.width / 2, this.y + this.height); + } + else if (index == TKRectBottomLeftCorner) { + point = new TKPoint(this.x, this.y + this.height); + } + else if (index == TKRectMiddleOfLeftEdge) { + point = new TKPoint(this.x, this.y + this.height / 2); + } + else if (index == TKRectCenter) { + point = new TKPoint(this.x + this.width / 2, this.y + this.height / 2); + } + return point; +}; + +/** + * @private + * Provides the segment for a given edge. + * + * @param {int} index The edge. + * + * @returns {TKSegment} The segment for the given edge. + */ +TKRect.prototype.edge = function (index) { + var edge; + if (index == TKRectTopEdge) { + edge = new TKSegment(this.pointAtPosition(TKRectTopLeftCorner), this.pointAtPosition(TKRectTopRightCorner)); + } + else if (index == TKRectRightEdge) { + edge = new TKSegment(this.pointAtPosition(TKRectTopRightCorner), this.pointAtPosition(TKRectBottomRightCorner)); + } + else if (index == TKRectBottomEdge) { + edge = new TKSegment(this.pointAtPosition(TKRectBottomLeftCorner), this.pointAtPosition(TKRectBottomRightCorner)); + } + else if (index == TKRectLeftEdge) { + edge = new TKSegment(this.pointAtPosition(TKRectTopLeftCorner), this.pointAtPosition(TKRectBottomLeftCorner)); + } + return edge; +}; + +/** + * Returns a {@link TKRect} from a rectangle returned by the Node.getBoundingClientRect method. + * + * @param {ClientRect} rect The CSS client rectangle. + * + * @returns {TKRect} The equivalent rectangle as a TuneKit data type. + */ +TKRect.rectFromClientRect = function (rect) { + return new TKRect(rect.left, rect.top, rect.width, rect.height); +}; + +/** + * @private + * Returns a {@link TKRect} encompassing the union of a list of other rectangles. + * + * @param {Array} rects The various rectangles we'd like the union of. + * + * @returns {TKRect} The rectangle encompassing the union of a list of the provided rectangles. + */ +TKRect.rectFromUnionOfRects = function (rects) { + if (rects.length < 1) { + return new TKRect(); + } + var union = rects[0]; + var rect; + for (var i = 1; i < rects.length; i++) { + rect = rects[i]; + union.x = Math.min(union.x, rect.x); + union.y = Math.min(union.y, rect.y); + union.width = Math.max(union.width, rect.x + rect.width); + union.height = Math.max(union.height, rect.y + rect.height); + } + return union; +}; + +/* ==================== TKPoint ==================== */ + +/** + * @private + * @class + * + *

The TKPoint provides a TuneKit data type to deal with points in 2D space and some utilities to work with them, such as figuring out + * the distance between two points.

+ * + * @since TuneKit 1.0 + * + * @param {float} x The x coordinate. + * @param {float} y The y coordinate. + */ +function TKPoint (x, y) { + /** + * The x coordinate. + * @type float + */ + this.x = x || 0; + /** + * The y coordinate. + * @type float + */ + this.y = y || 0; +}; + +/** + * Provides the distance between this point and another. + * + * @param {TKPoint} aPoint The point to which we'd like to figure out the distance. + * + * @returns {float} The distance between the receiver the provided point. + */ +TKPoint.prototype.distanceToPoint = function (aPoint) { + return Math.sqrt(Math.pow(aPoint.x - this.x, 2) + Math.pow(aPoint.y - this.y, 2)); +}; + +/* ==================== TKSegment ==================== */ + +/** + * @class + * @private + * + *

The TKSegment provides a TuneKit data type to deal with segments in 2D space and some utilities to work with them, such as figuring out + * the shortest distance between a segment and a point.

+ * + * @since TuneKit 1.0 + * + * @param {TKPoint} a The first extremity of the segment. + * @param {TKPoint} b The other extremity of the segment. + */ +function TKSegment (a, b) { + /** + * The first extremity of the segment. + * @type TKPoint + */ + this.a = a; + /** + * The other extremity of the segment. + * @type TKPoint + */ + this.b = b; + this.ab = new TKPoint(b.x - a.x, b.y - a.y); + /** + * The segment's length. + * @type float + */ + this.length = b.distanceToPoint(a); +}; + +// XXX: this only deals with horizontal and vertical lines +TKSegment.prototype.crossPoint = function (c) { + return (this.a.y == this.b.y) ? new TKPoint(c.x, this.a.y) : new TKPoint(this.a.x, c.y); +}; + +/** + * Computes the shortest distance between this segment and the given point. + * + * @param {TKPoint} aPoint + * + * @returns {float} The shortest distance between this segment and the given point. + */ +TKSegment.prototype.distanceToPoint = function (aPoint) { + var d; + var cross_point = this.crossPoint(aPoint); + // is it inside the segment? + if (cross_point.distanceToPoint(this.a) + cross_point.distanceToPoint(this.b) == this.length) { + d = aPoint.distanceToPoint(cross_point); + } + else { + d = Math.min(aPoint.distanceToPoint(this.a), aPoint.distanceToPoint(this.b)); + } + return d; +}; + +/* ================= Debugging ======================== */ + +var DEBUG = false; + +function debug(msg) { + if (window.DEBUG !== undefined && window.DEBUG) { + console.log("DEBUG: " + msg); + } +}; +/* + * Copyright © 2009 Apple Inc. All rights reserved. + */ + +/** + * @class + * + *

The TKClass function will process a constructor function and set up its inheritance chain, synthetize + * a number of its instance properties and mix in additional capabilities based properties defined on + * that function. The supported properties are:

+ * + * + * + *

For instance, consider the following class declaration:

+ * + *
+// extends MySuperClass
+MyClass.inherits = MySuperClass;
+
+// mixes in the TKEventTriage and TKPropertyTriage modules
+MyClass.includes = [TKEventTriage, TKPropertyTriage]; 
+
+// synthetizes the .foo and .bar properties
+MyClass.synthetizes = ['foo', 'bar'];
+
+function MyClass () {
+  // constructor code goes here
+};
+
+// process properties set up on the MyClass constructor
+TKClass(MyClass);
+ *  
+ * + *

Synthetization is fully automated as long as the class that wishes to offer synthetized properties follows + * the following rules:

+ * + * + * + *

So our previous class declaration could be extended as follows:

+ * + *
+function MyClass () {
+  this._foo = '';
+};
+
+// custom setter for .foo, the getter is not specified
+// and TKClass() will automatically create it
+MyClass.prototype.setFoo = function (newFooValue) {
+  this._foo = newFooValue;
+};
+
+// custom getter for .foo, the setter is not specified
+// and TKClass() will automatically create it
+MyClass.prototype.getBar = function (newFooValue) {
+  return 'Hello ' + this._foo;
+};
+ *  
+ * + * @since TuneKit 1.0 + * + * @constructor + * @param theClass {Object} The class. + */ + +function TKClass (theClass) { + // check out if we have inheritance set up, otherwise, make TKObject the default superclass + if (TKUtils.objectIsUndefined(theClass.inherits) && theClass !== TKObject) { + theClass.inherits = TKObject; + } + + // check out if we have mixins + if (theClass.includes) { + TKClass.mixin(theClass.prototype, theClass.includes); + } + + // synthetizes properties defined locally + var properties = (theClass.synthetizes) ? theClass.synthetizes : []; + // now synthetize + for (var i = 0; i < properties.length; i++) { + TKClass.synthetizeProperty(theClass.prototype, properties[i], true); + } + + // synthetizes properties by compiling them up the inheritance chain + var aClass = theClass; + var properties = []; + while (aClass.inherits) { + aClass = aClass.inherits; + if (aClass.synthetizes) { + properties = aClass.synthetizes.concat(properties); + } + } + // now synthetize + for (var i = 0; i < properties.length; i++) { + TKClass.synthetizeProperty(theClass.prototype, properties[i], false); + } + + // go through each method and save its name as a custom property + // that we'll use later in TKObject.callSuper() + for (var i in theClass.prototype) { + // make sure we don't touch properties that were synthetized + if (theClass.prototype.__lookupGetter__(i)) { + continue; + } + var prop = theClass.prototype[i]; + if (TKUtils.objectIsFunction(prop)) { + prop._class = theClass; + prop._name = i; + prop.displayName = TKUtils.createDisplayName(theClass.name, i); + } + } + + // inherit from the superclass + // default to TKObject if nothing is specified + if (theClass !== TKObject) { + theClass.prototype.__proto__ = theClass.inherits.prototype; + } +}; + +TKClass.synthetizeProperty = function (object, propertyName, isPropertyInherited) { + var camel_ready = propertyName.charAt(0).toUpperCase() + propertyName.substr(1); + var getter_name = 'get' + camel_ready; + var setter_name = 'set' + camel_ready; + // check on function availability + var has_getter = TKUtils.objectHasMethod(object, getter_name); + var has_setter = TKUtils.objectHasMethod(object, setter_name); + + // if we have neither a getter or a setter, then do nothing + // unless the property is defined locally + if (!isPropertyInherited && !(has_getter || has_setter)) { + return; + } + + // assign the setter function if we have one + if (has_setter) { + var specified_setter_function = function (newValue) { + object[setter_name].call(this, newValue); + this.notifyPropertyChange(propertyName); + }; + specified_setter_function.displayName = 'Specified setter for .' + propertyName + ' on ' + object.constructor.name; + object.__defineSetter__(propertyName, specified_setter_function); + } + // otherwise just assign to _propertyName + else { + var default_setter_function = function (newValue) { + this['_' + propertyName] = newValue; + this.notifyPropertyChange(propertyName); + }; + default_setter_function.displayName = 'Default setter for .' + propertyName + ' on ' + object.constructor.name; + object.__defineSetter__(propertyName, default_setter_function); + } + + // assign the getter function if we have one + if (has_getter) { + object.__defineGetter__(propertyName, object[getter_name]); + } + // otherwise just return _propertyName + else { + var default_getter_function = function () { + return this['_' + propertyName]; + }; + default_getter_function.displayName = 'Default getter for .' + propertyName + ' on ' + object.constructor.name; + object.__defineGetter__(propertyName, default_getter_function); + } +}; + +TKClass.mixin = function (target, sources) { + for (var i = 0; i < sources.length; i++) { + TKUtils.copyPropertiesFromSourceToTarget(sources[i], target); + } +}; + +TKUtils.setupDisplayNames(TKClass, 'TKClass'); +/* + * Copyright © 2009 Apple Inc. All rights reserved. + */ + +/** + * @class + * @name Element + * @description Extensions to the DOM Core Element interface. + * @since TuneKit 1.0 + */ + +/* ==================== Element Extensions ==================== */ + +/** + * Indicates whether the element has a given class name within its class attribute. + * + * @param {String} className The CSS class name. + * @returns {bool} Whether the element has this class name within its class attribute. + * @memberOf Element.prototype + */ +Element.prototype.hasClassName = function (className) { + return new RegExp('(?:^|\\s+)' + className + '(?:\\s+|$)').test(this.className); +}; + +/** + * Adds the given class name to the element's class attribute if it's not already there. + * + * @param {String} className The CSS class name. + * @memberOf Element.prototype + */ +Element.prototype.addClassName = function (className) { + if (!this.hasClassName(className)) { + this.className = [this.className, className].join(' '); + } +}; + +/** + * Removes the given class name from the element's class attribute if it's there. + * + * @param {String} className The CSS class name. + * @memberOf Element.prototype + */ +Element.prototype.removeClassName = function (className) { + if (this.hasClassName(className)) { + var curClasses = this.className; + this.className = curClasses.replace(new RegExp('(?:^|\\s+)' + className + '(?:\\s+|$)', 'g'), ' '); + } +}; + +/** + * Adds the given class name to the element's class attribute if it's not there, or removes it if it's already set. + * + * @param {String} className The CSS class name. + * @memberOf Element.prototype + */ +Element.prototype.toggleClassName = function (className) { + this[this.hasClassName(className) ? 'removeClassName' : 'addClassName'](className); +}; + +/** + * Removes all the children from an element. + * + * @memberOf Element.prototype + */ +// XXX: should this be on Node? +Element.prototype.removeAllChildren = function () { + while (this.firstChild) { + this.removeChild(this.firstChild); + } +}; + +/** + * Returns true if the element has the given child node + * FIXME: should this be on Node? + * + * @param {Element} child The child to search for + * @memberOf Element.prototype + */ +Element.prototype.hasChild = function (child) { + for (var i=0; i < this.childNodes.length; i++) { + if (this.childNodes[i] === child) { + return true; + } + } + return false; +}; + +/** + * Applies a transition definition to the element, allowing the transition to be reversed. If this method is called + * within a {@link TKTransaction}, the transition will only be commited when the transaction is completed. + * + * @param {TKTransitionDefinition} transitionDefinition The transition applied to the element. + * @param {bool} reversed Indicates whether the transition is to be applied in reverse. + */ +Element.prototype.applyTransition = function (definition, reversed) { + // nothing to do if we have no definition + if (definition === null) { + return; + } + // create a TKTransition from the definition + var transition = new TKTransition(definition); + // this view will be the target + transition.target = this; + // revert from / to values as instructed + if (reversed) { + var from = transition.from; + transition.from = transition.to; + transition.to = from; + } + // set up base properties, if any + if (definition.base) { + for (var i = 0; i < definition.base.length; i += 2) { + this.style.setProperty(definition.base[i], definition.base[i+1], ''); + } + } + // start the transition + transition.start(); +}; + +Element.prototype.getBounds = function () { + return TKRect.rectFromClientRect(this.getBoundingClientRect()); +}; + +Element.prototype.isNavigable = function () { + var is_navigable = false; + if (this._controller !== undefined && this._controller !== null && this._controller.navigableElements.indexOf(this) !== -1) { + var style = window.getComputedStyle(this); + is_navigable = ( + style.display != 'none' && style.visibility != 'hidden' && + !this.hasClassName(TKSpatialNavigationManagerInactiveCSSClass) + ); + } + return is_navigable; +}; + +TKUtils.setupDisplayNames(Element, 'Element'); +/* + * Copyright © 2009 Apple Inc. All rights reserved. + */ + +var TKEventTriage = {}; + +TKEventTriage.handleEvent = function (event) { + // first call the super class method that we may be overriding otherwise + if (this instanceof TKObject) { + this.callSuper(event); + } + // + var type = event.type; + var methodName = 'handle' + type.charAt(0).toUpperCase() + type.substr(1); + if (TKUtils.objectHasMethod(this, methodName)) { + this[methodName](event); + } +}; + +TKUtils.setupDisplayNames(TKEventTriage, 'TKEventTriage'); +/* + * Copyright © 2009 Apple Inc. All rights reserved. + */ + +/* ==================== Constants ==================== */ + +const TKObjectPropertyChanged = 'handlePropertyChange'; + +/* ==================== TKObject ==================== */ + +/** + * @class + * + *

The TKObject class is the base class for all TuneKit objects and provides a way to receive and trigger + * notifications when a JavaScript property changes.

+ * + *

Observing a property on an object is done through the + * {@link #addPropertyObserver} method, specifying the property to observe as well as the object that will + * receive the notifications of the property change. Likewise, an object can stop observing such changes using + * the {@link #removePropertyObserver} method. When a property value changes, the author needs to call the + * {@link #notifyPropertyChange} method so that registered observers will be notified. Note that synthetized + * properties will automatically trigger notifications when changed, so in this case, setters need not + * manually call {@link #notifyPropertyChange}.

+ * + *

Finally, TKObject also provides the {@link #callSuper} method to call the superclass's implementation + * of an overload method.

+ * + * @since TuneKit 1.0 + */ +function TKObject () { + this.observedProperties = {}; +}; + +/* ==================== Method overriding ==================== */ + +/** + * This method calls the object's superclass implementation of the current method + */ +TKObject.prototype.callSuper = function () { + // figure out what function called us + var caller = TKObject.prototype.callSuper.caller; + // if that function is a constructor, we call the parent class's constructor + if (TKUtils.objectHasMethod(caller, 'inherits')) { + caller.inherits.apply(this, arguments); + } + // otherwise we look at that function name in the parent prototype + else { + var parent = caller._class.inherits.prototype; + var method_name = caller._name; + if (TKUtils.objectHasMethod(parent, method_name)) { + return parent[method_name].apply(this, arguments); + } + } +}; + +/* ==================== Observing ==================== */ + +/** + * Indicates whether this object has registered observers for this property + * + * @param {String} propertyName The property that may be observed. + * + * @returns {bool} Whether this object has registered observers for this property + * @private + */ +TKObject.prototype.isPropertyObserved = function (propertyName) { + return !TKUtils.objectIsUndefined(this.observedProperties[propertyName]); +}; + +/** + * Adds an observer for the named property on this object. In case an existing + * combination of the observer and property is already registered, this method + * is a no-op. + * + * @param {String} propertyName The property to observe. + * @param {Object} observer The object that will be notified when the property is changed. + * @param {String} methodName The optional method name that will be called on the observer when the property is changed. + * If this property is not provided, the observer must implement the {@link TKPropertyValueChange} protocol. + */ +TKObject.prototype.addPropertyObserver = function (propertyName, observer, methodName) { + // create the array for this property if it's not already there + if (!this.isPropertyObserved(propertyName)) { + this.observedProperties[propertyName] = new Array(); + } + // do nothing if we already have this observer registered + else if (this.observedProperties[propertyName].indexOf(observer) > -1) { + return; + } + // now, add the observer to the observer array for this property if valid + var methodName = methodName || TKObjectPropertyChanged; + if (TKUtils.objectHasMethod(observer, methodName)) { + this.observedProperties[propertyName].push({ + observer: observer, + methodName : methodName + }); + } +}; + +/** + * Removes the observer for the named property on this object. In case an existing + * combination of the observer and property is not already registered, this method + * is a no-op. + * + * @param {String} propertyName The observed property. + * @param {Object} observer The object that was notified when the property changed. + * + * @returns {bool} Whether an observer was removed + */ +TKObject.prototype.removePropertyObserver = function (propertyName, observer) { + // do nothing if this property is not observed + if (!this.isPropertyObserved(propertyName)) { + return false; + } + // now get the observer's index in the array + var observers = this.observedProperties[propertyName]; + var observer_index = observers.indexOf(observer); + // remove the observer if it was registered + if (observer_index > -1) { + observers.splice(observer_index, 1); + } + // let the user know whether we succeeded + return (observer_index > -1); +}; + +/** + * Triggers a notification that the given property on this object has changed. + * For synthesized properties, this is called automatically upon setting the + * property. For methods that update an instance variable that is not synthesized, + * {@link #notifyPropertyChange} has to be called manually so that observers are notified. + * + * @param {String} propertyName The observed property. + */ +TKObject.prototype.notifyPropertyChange = function (propertyName) { + // do nothing if this property is not observed + if (!this.isPropertyObserved(propertyName)) { + return; + } + // now, go through each observer for this property and notify them + var observers = this.observedProperties[propertyName]; + for (var i = 0; i < observers.length; i++) { + var observer = observers[i]; + observer.observer[observer.methodName](this, propertyName); + } +}; + +/** + * Calls a method on this object after a delay, allowing any number of optional extra arguments to be + * passed after the first two mandatory ones. + * + * @param {String} methodName The method name to be called. + * @param {int} delay The delay in miliseconds. + * + * @returns {int} The timeout that can be used to call clearTimeout. + */ +TKObject.prototype.callMethodNameAfterDelay = function (methodName, delay) { + var _this = this; + var args = Array.prototype.slice.call(arguments, 2); + var generated_function = function () { + _this[methodName].apply(_this, args); + }; + generated_function.displayName = TKUtils.createDisplayName(this.constructor.name, methodName); + return setTimeout(generated_function, delay); +}; + +/** + * @class + * @name TKPropertyValueChange + * @since TuneKit 1.0 + */ + +/** + * Invoked when a property on an observed object has been changed. + * + * @name handlePropertyChange + * @function + * + * @param {Object} observedObject The observed object + * @param {String} propertyName The observed property's name as a string + * @memberOf TKPropertyValueChange.prototype + */ + +TKClass(TKObject, 'TKObject'); +/* + * Copyright © 2009 Apple Inc. All rights reserved. + */ + +/** + * @class + * @name TKTransitionDelegate + * @since TuneKit 1.0 + */ + +/** + * Invoked when a transition has completed. + * + * @name transitionDidComplete + * @function + * + * @param {TKTransition} theTransition The transition that just completed. + * @memberOf TKTransitionDelegate.prototype + */ + +const TKTransitionDidCompleteDelegate = 'transitionDidComplete'; + +const TKTransitionDefaults = { + duration : 0.5, + delay : 0, + removesTargetUponCompletion : false, + revertsToOriginalValues : false +}; + +const TKTransitionStyles = ['-webkit-transition-property', '-webkit-transition-duration', '-webkit-transition-timing-function', '-webkit-transition-delay', '-webkit-transition']; + +/* ==================== TKTransition ==================== */ + +/** + * @class + * + *

The TKTransition class allows to create a synchronized change of one or more property values on a + * {@link Element} instance or any DOM element.

+ * + *

First, the user sets the {@link #target} for the transition, + * identifying the target element, and second the set of {@link #properties}, in the form of an Array + * of CSS properties for the element. Then the {@link #to} and optional + * {@link #from} value arrays are provided to define what values will be used for the transition. Each item in the + * {@link #from} and {@link #to} arrays matches the item at the same index in the {@link #properties} array, and the same + * applies to the {@link #duration} property, which can also be a single value shared for all transitioned properties.

+ * + *

Finally, once the properties on the transition are all set, the {@link #start} method must be called so that the + * transition is started. Note that it is possible to group transitions within a {@link TKTransaction} and set a default + * set of transition properties within a transaction using the {@link TKTransaction.defaults} static property.

+ * + *

The following example shows how to make a transition that fades an element in as it animates from the right side of the screen:

+ * +
+new TKTransition({
+  target : anElement,
+  properties : ['opacity', '-webkit-transform'];
+  from : [0, 'translate(320px, 0)'],
+  to : [1, 'translate(0, 0)']
+  duration : 0.5;
+}).start();
+
+ * + *

Note that properties of a {@link TKTransition} object can be passed as a batch to the {@link TKTransition} constructor + * as an anonymous object. This also allows reuse of a set of parameters across several transitions. Also, a set of pre-baked + * transitions exist can be easily applied to an element with the {@link Element#applyTransition} method.

+ * + * @since TuneKit 1.0 + */ +function TKTransition (params) { + // set up defaults + /** + * The transition target. + * @type Element + */ + this.target = null; + /** + * The set of properties that will be transitioned. The properties are CSS properties of the targeted Element. + * @type Array + */ + this.properties = null; + /** + * The set of durations in seconds, each duration matching a property in the {@link #properties} array. + * Note that it's possible to specify a single value instead of an array to use the same duration for + * all properties. + * @type Object + */ + this.duration = null; + /** + * The set of delays in seconds, each delay matching a property in the {@link #properties} array. + * Note that it's possible to specify a single delay instead of an array to use the same delay for + * all properties. + * @type Object + */ + this.delay = null; + /** + * Optional list of values to start the transition from. Each value in this array must match the property + * at the same index in the {@link #properties} array. + * @type Array + */ + this.from = null; + /** + * Required list of values to transition to. Each value in this array must match the property + * at the same index in the {@link #properties} array. + * @type Array + */ + this.to = null; + /** + * The set of timing functions, each timing function matching a property in the {@link #properties} + * array. Note that it's possible to specify a single timing function instead of an array to use the + * same timing function for all properties. + * @type Object + */ + this.timingFunction = null; + /** + * The delegate object that implements the {@link TKTransitionDelegate} protocol. + * @type Object + */ + this.delegate = null; + /** + * Indicates whether the target needs to be removed once the transition completes. Defaults to false. + * @type bool + */ + this.removesTargetUponCompletion = null; + /** + * Indicates whether the target needs to reset the property that are transitioned to their original values + * once the transition completes. Defaults to false. + * @type bool + */ + this.revertsToOriginalValues = null; + // + this.defaultsApplied = false; + this.archivedStyles = null; + this.archivedValues = []; + // import params + TKUtils.copyPropertiesFromSourceToTarget(params, this); +}; + +/* ==================== Dealing with defaults ==================== */ + +TKTransition.prototype.applyDefaults = function () { + if (this.defaultsApplied) { + return; + } + // + for (var i in TKTransitionDefaults) { + if (this[i] === null) { + this[i] = TKTransitionDefaults[i]; + } + } + // + this.defaultsApplied = true; +}; + +/* ==================== Archiving the transition styles ==================== */ + +TKTransition.prototype.getTargetStyle = function () { + // if (this.target.style === null) { + // this.target.setAttribute('style', 'foo: bar'); + // } + return this.target.style; +}; + +TKTransition.prototype.archiveTransitionStyles = function () { + // do nothing if we already have archived styles in this run session + if (this.archivedStyles !== null) { + return; + } + // iterate all TKTransitionStyles and archive them + this.archivedStyles = []; + var style = this.getTargetStyle(); + for (var i = 0; i < TKTransitionStyles.length; i++) { + this.archivedStyles.push(style.getPropertyValue(TKTransitionStyles[i])); + } +}; + +TKTransition.prototype.restoreTransitionStyles = function () { + var style = this.getTargetStyle(); + // iterate all TKTransitionStyles and restore them + for (var i = 0; i < TKTransitionStyles.length; i++) { + style.setProperty(TKTransitionStyles[i], this.archivedStyles[i], ''); + } + // reset archived styles + this.archivedStyles = null; +}; + +/* ==================== Archiving the base values ==================== */ + +TKTransition.prototype.archiveBaseValues = function () { + // do nothing if we don't need to archive base values + if (!this.revertsToOriginalValues) { + return; + } + var style = this.getTargetStyle(); + for (var i = 0; i < this.properties.length; i++) { + this.archivedValues.push(style.getPropertyValue(this.properties[i])); + } +}; + +TKTransition.prototype.restoreBaseValues = function () { + var style = this.getTargetStyle(); + for (var i = 0; i < this.properties.length; i++) { + style.setProperty(this.properties[i], this.archivedValues[i], null); + } +}; + +/* ==================== Starting the transition ==================== */ + +/** + * Starts the transition. + */ +TKTransition.prototype.start = function () { + // if we have an active transaction, just add to it + if (TKTransaction.openTransactions > 0) { + TKTransaction.addTransition(this); + return; + } + // otherwise, we'll just get it going + this.applyDefaults(); + if (this.from === null) { + this.applyToState(); + } + else { + this.applyFromState(); + var _this = this; + setTimeout(function () { + _this.applyToState(); + }, 0); + } +}; + +/* ==================== Applying the "from" state ==================== */ + +TKTransition.prototype.applyFromState = function () { + // do nothing if we have no "from" state specified + if (this.from === null) { + return; + } + // + this.applyDefaults(); + this.archiveTransitionStyles(); + var style = this.getTargetStyle(); + style.webkitTransitionDuration = 0; + for (var i = 0; i < this.properties.length; i++) { + style.setProperty(this.properties[i], this.from[i], ''); + } +}; + +/* ==================== Applying the "to" state ==================== */ + +TKTransition.prototype.applyToState = function () { + // first, make sure we have defaults applied if some + // properties are not explicitely set on our transition + this.applyDefaults(); + + // and that we archive the transition styles to be reverted + this.archiveTransitionStyles(); + + // and that we archive the original values + this.archiveBaseValues(); + + // now compile the styles needed for this transition + this.cssProperties = []; + var transition_styles = []; + for (var i = 0; i < this.properties.length; i++) { + var property = this.properties[i]; + // do nothing if we already have an animation defined for this + if (this.cssProperties.indexOf(property) > -1) { + continue; + } + // else, set up the CSS style for this transition + var duration = (TKUtils.objectIsArray(this.duration)) ? this.duration[i] : this.duration; + var timing = (TKUtils.objectIsArray(this.timingFunction)) ? this.timingFunction[i] : this.timingFunction; + var delay = (TKUtils.objectIsArray(this.delay)) ? this.delay[i] : this.delay; + transition_styles.push([property, duration + 's', timing, delay + 's'].join(' ')); + // and remember we are animating this property + this.cssProperties.push(property); + } + + var style = this.getTargetStyle(); + for (var i = 0; i < this.properties.length; i++) { + style.setProperty(this.properties[i], this.to[i], ''); + } + + // set up the transition styles + style.webkitTransition = transition_styles.join(', '); + + // register for events to track transition completions + this.target.addEventListener('webkitTransitionEnd', this, false); + this.completedTransitions = 0; +}; + +/* ==================== Tracking transition completion ==================== */ + +// XXX: we won't be getting an event for properties that have the same value in the to +// state, so we'll need to do a little work to track css properties that won't really transition +TKTransition.prototype.handleEvent = function (event) { + // do nothing if that event just bubbled from our target's sub-tree + if (event.currentTarget !== this.target) { + return; + } + + // update the completion counter + this.completedTransitions++; + + // do nothing if we still have running transitions + if (this.completedTransitions != this.cssProperties.length) { + return; + } + + // the counter reached our properties count, fire up the delegate + if (TKUtils.objectHasMethod(this.delegate, TKTransitionDidCompleteDelegate)) { + this.delegate[TKTransitionDidCompleteDelegate](this); + } + + // remove from the tree if instructed to do so + if (this.removesTargetUponCompletion) { + this.target.parentNode.removeChild(this.target); + } + // otherwise, restore transition styles + else { + this.restoreTransitionStyles(); + } + + // restore original values if instructed to do so + if (this.revertsToOriginalValues) { + this.restoreBaseValues(); + } +}; + +TKClass(TKTransition); +/* + * Copyright © 2009 Apple Inc. All rights reserved. + */ + +/* ==================== TKTransaction ==================== */ + +/** + * @class + * + *

A transaction allows to run a variety of transitions in sync, no matter in what function or JavaScript run loop + * the transition itself was applied. Transactions can be nested, and a transaction is only truly commited when there + * are no other open transactions. In other words, there must be as many calls to {@link TKTransaction.begin} as there + * are calls to {@link TKTransaction.commit} before any transition in one of those transactions can be applied. + * + * @since TuneKit 1.0 + */ +var TKTransaction = { + transitions : [], + openTransactions : 0, + /** + * The set of default properties that will be applied to any {@link TKTransition} until the next call to {@link TKTransaction.commit}. + * Any of the properties that can be applied to a {@link TKTransition} can be applied to this object. + * @type Object + */ + defaults : {}, + defaultsStates : [] +}; + +/** + * Begins a new transaction and makes it the current transaction. + */ +TKTransaction.begin = function () { + // reset the group if we're starting fresh + if (this.openTransactions == 0) { + this.transitions = []; + this.defaults = {}; + } + // otherwise, archive the current state of defaults + else { + this.defaultsStates.push(this.defaults); + // XXX: should we restore defaults here as well? + } + // increase the number of open transactions + this.openTransactions++; +}; + +/** + * Add Transition + * + * @private + */ +TKTransaction.addTransition = function (transition) { + // first, apply the transaction defaults to the transitions + for (var i in this.defaults) { + if (transition[i] === null) { + transition[i] = this.defaults[i]; + } + } + // and add the transition to our array + this.transitions.push(transition); +}; + +/** + * Closes the current transaction. + */ +TKTransaction.commit = function () { + // do nothing if we have no open transactions + if (this.openTransactions == 0) { + return; + } + // decrease the number of open transactions + this.openTransactions--; + // if we still have open transactions, just + // restore the previous defaults state + if (this.openTransactions != 0) { + this.defaults = this.defaultsStates.pop(); + return; + } + // otherwise, it's time to shake and bake, we'll apply the + // "from" states directly and the "to" states immediately + // after in a new run loop so that the "from" styles have + // been resolved first + var transitions = this.transitions; + for (var i = 0; i < transitions.length; i++) { + transitions[i].applyFromState(); + } + setTimeout(function () { + for (var i = 0; i < transitions.length; i++) { + transitions[i].applyToState(); + } + }, 0); +}; + +TKUtils.setupDisplayNames(TKTransaction, 'TKTransaction'); +/* + * Copyright © 2009 Apple Inc. All rights reserved. + */ + +/** + * @class + * + * A virtual class that allows the definition of pre-baked transitions that can be used as parameter + * for the {@link Element#applyTransition} method. In reality, any object that provides the type of + * properties instances of this class offers can be used with that method. + * + * @name TKTransitionDefinition + * @since TuneKit 1.0 + */ + +/** + * @name TKTransitionDefinition.prototype + * @property {Array} base A set of property / value pairs that are applied to the element as the transition starts. The properties + * listed there are not supposed to be transitioned. If you wish to set a from state for a property that gets transitioned, use + * the {@link #properties} and {@link from} properties instead. + * @property {Array} properties The set of properties that will be transitioned. The properties are CSS properties + * of the element that will be transitioned using this definition. + * @property {Array} from Optional list of values to start the transition from. Each value in this array must match the property + * at the same index in the {@link #properties} array. + * @property {Array} to Required list of values to transition to. Each value in this array must match the property + * at the same index in the {@link #properties} array. + */ + +/** + * Fades out. + * @constant + * @type TKTransitionDefinition + */ +const TKViewTransitionDissolveOut = { + properties : ['opacity'], + from : [1], + to : [0] +}; + +/** + * Fades in. + * @constant + * @type TKTransitionDefinition + */ +const TKViewTransitionDissolveIn = { + properties : ['opacity'], + from : [0], + to : [1] +}; + +/** + * Fades in while scaling up to identity scale. + * @constant + * @type TKTransitionDefinition + */ +const TKViewTransitionZoomIn = { + properties : ['opacity', '-webkit-transform'], + from : [0, 'scale(0.2)'], + to : [1, 'scale(1)'] +}; + +/** + * Fades out while scaling down to identity scale. + * @constant + * @type TKTransitionDefinition + */ +const TKViewTransitionZoomOut = { + properties : ['opacity', '-webkit-transform'], + from : [0, 'scale(1.2)'], + to : [1, 'scale(1)'] +}; + +/** + * Fades in while rotating from the right. + * @constant + * @type TKTransitionDefinition + */ +const TKViewTransitionCrossSpinRight = { + properties : ['opacity', '-webkit-transform'], + from : [0, 'rotate(20deg)'], + to : [1, 'rotate(0)'] +}; + +/** + * Fades in while rotating from the left. + * @constant + * @type TKTransitionDefinition + */ +const TKViewTransitionCrossSpinLeft = { + properties : ['opacity', '-webkit-transform'], + from : [0, 'rotate(-20deg)'], + to : [1, 'rotate(0)'] +}; + +/** + * Scale transition scaling in. + * @constant + * @type TKTransitionDefinition + */ +const TKViewTransitionScaleIn = { + base : ['z-index', 1], + properties : ['-webkit-transform'], + from : ['scale(0.01)'], + to : ['scale(1)'] +}; + +/** + * Scale transition scaling out. + * @constant + * @type TKTransitionDefinition + */ +const TKViewTransitionScaleOut = { + base : ['z-index', 1], + properties : ['-webkit-transform'], + from : ['scale(1)'], + to : ['scale(0.01)'] +}; +/* + * Copyright © 2009 Apple Inc. All rights reserved. + */ + +/* ==================== Constants ==================== */ + +const TKAnimatorLinearType = 0; +const TKAnimatorSplinesType = 1; +const TKAnimatorInvalidArgsException = 2; + +const TKAnimatorAnimationDidIterate = 'animationDidIterate'; +const TKAnimatorAnimationDidEnd = 'animationDidEnd'; + +/* ==================== TKAnimator ==================== */ + +function TKAnimator (duration, delegate, spline) { + // check validity of arguments number + // if (arguments.length != 2 && arguments.length != 3 && arguments.length != 7) { + // throw TKAnimatorInvalidArgsException; + // return false; + // } + // init some base properties + this.ready = false; + this.animating = false; + this.timer = null; + // handle arguments + this.duration = duration; // ms + this.delegate = delegate; + // check we have the required delegation method + // if (!TKUtils.objectHasMethod(this.delegate, TKAnimatorAnimationDidIterate)) { + // return; + // } + // handle splines arguments, if available + if (arguments.length >= 3) { + this.type = TKAnimatorSplinesType; + this.x1 = spline[0]; + this.y1 = spline[1]; + this.x2 = spline[2]; + this.y2 = spline[3]; + this.init(); + } + else { // linear animation + this.type = TKAnimatorLinearType; + } + this.ready = true; +}; + +TKAnimator.prototype.init = function () { + // calculate the polynomial coefficients + this.cx = 3 * this.x1; + this.bx = 3 * (this.x2 - this.x1) - this.cx; + this.ax = 1 - this.cx - this.bx; + this.cy = 3 * this.y1; + this.by = 3 * (this.y2 - this.y1) - this.cy; + this.ay = 1 - this.cy - this.by; + // compute points + var numberOfPoints = (this.duration / 1000) * 240; + this.curve = new Array(numberOfPoints); + var dt = 1.0 / ( numberOfPoints - 1 ); + for (var i = 0; i < numberOfPoints; i++) { + var t = i * dt; + this.curve[i] = { + x : (this.ax * Math.pow(t, 3)) + (this.bx * Math.pow(t, 2)) + (this.cx * t), + y : (this.ay * Math.pow(t, 3)) + (this.by * Math.pow(t, 2)) + (this.cy * t) + }; + } +}; + +TKAnimator.prototype.start = function () { + if (!this.ready) { + var _this = this; + this.timer = setTimeout(function () { _this.start() }, 0); + return; + } + this.animating = true; + this.lastIndex = 0; + this.startTime = (new Date()).getTime(); + this.iterate(); +}; + +TKAnimator.prototype.stop = function () { + this.animating = false; + clearTimeout(this.timer); +}; + +TKAnimator.prototype.iterate = function () { + var ellapsedTime = (new Date()).getTime() - this.startTime; + if (ellapsedTime < this.duration) { + var x = ellapsedTime / this.duration; + // handle splines case + if (this.type == TKAnimatorSplinesType) { + var y = 0; + for (var i = this.lastIndex; i < this.curve.length; i++) { + var point = this.curve[i]; + if (point.x >= x && i > 0) { + var previous_point = this.curve[i - 1]; + if ((x - previous_point.x) < (point.x - x)) { + this.lastIndex = i - 1; + y = previous_point.y; + } + else { + this.lastIndex = i; + y = point.y; + } + break; + } + } + } + this.delegate[TKAnimatorAnimationDidIterate]((this.type == TKAnimatorSplinesType) ? y : x); + var _this = this; + this.timer = setTimeout(function () { _this.iterate() }, 0); + } + else { + this.delegate[TKAnimatorAnimationDidIterate](1); + if (TKUtils.objectHasMethod(this.delegate, TKAnimatorAnimationDidEnd)) { + this.delegate[TKAnimatorAnimationDidEnd](); + } + this.animating = false; + } +}; + +/* ==================== CLASS CREATION ==================== */ + +TKClass(TKAnimator); +/* + * Copyright © 2009 Apple Inc. All rights reserved. + */ + +// --------------------------------------------------- +// A two dimensional sliding view +// --------------------------------------------------- + +/** + * @class + * @name TKSlidingViewData + * @since TuneKit 1.0 + */ + +/** + * @name TKSlidingViewData.prototype + * + * @property {int} sideElementsVisible The number of elements on each side of the selected page. If the {@link #incrementalLoading} property is set to + * true, this specifies the number of elements in the DOM on each side as others beyond that range are removed from the tree. Otherwise, this + * specifies the threshold after which elements in either direction from the selected page get the sliding-view-element-hidden CSS class applied. + * + * @property {int} distanceBetweenElements The distance in pixels between the center points of each page, essentially the overall width of each page. + * + * @property {int} sideOffsetBefore Any additional margin in pixels to the left of each page. + * + * @property {int} sideOffsetAfter Any additional margin in pixels to the right of each page. + * + * @property {Array} elements The elements for the sliding view. + * + * @property {bool} incrementalLoading Indicates whether elements in the sliding view's DOM are added and removed gradually as we browse through it, or all + * available in one go, which is the default. If you are populating a sliding view with a large amount of content, you should consider setting this property + * to true in order to ease memory constraints and enhance performance. + * + * @property {bool} loops Whether we loop through pages. Defaults to false. + * + */ + +// data source method names +const TKSlidingViewNumberOfElements = 'slidingViewNumberOfElements'; +const TKSlidingViewElementAtIndex = 'slidingViewElementAtIndex'; + +// delegate method names +const TKSlidingViewStyleForItemAtIndex = 'slidingViewStyleForItemAtIndex'; +const TKSlidingViewDidSelectActiveElement = 'slidingViewDidSelectActiveElement'; +const TKSlidingViewDidFocusElementAtIndex = 'slidingViewDidFocusElementAtIndex'; +const TKSlidingViewDidBlurElementAtIndex = 'slidingViewDidBlurElementAtIndex'; +const TKSlidingViewDidHideElementAtIndex = 'slidingViewDidHideElementAtIndex'; // TODO: XXX +const TKSlidingViewWillUnhideElementAtIndex = 'slidingViewWillUnhideElementAtIndex'; // TODO: XXX + +const TKSlidingViewDidHoverElementAtIndex = 'slidingViewDidHoverElementAtIndex'; +const TKSlidingViewDidUnhoverElementAtIndex = 'slidingViewDidUnhoverElementAtIndex'; + +// css protocol +const TKSlidingViewCSSContainerClass = 'sliding-view'; +const TKSlidingViewCSSElementClass = 'sliding-view-element'; +const TKSlidingViewCSSFocusedClass = 'sliding-view-element-focused'; +const TKSlidingViewCSSSideBeforeClass = 'sliding-view-element-before'; +const TKSlidingViewCSSSideAfterClass = 'sliding-view-element-after'; +const TKSlidingViewCSSStagedBeforeClass = 'sliding-view-element-staged-before'; +const TKSlidingViewCSSStagedAfterClass = 'sliding-view-element-staged-after'; +const TKSlidingViewCSSHiddenClass = 'sliding-view-element-hidden'; + +// orientations +const TKSlidingViewOrientationHorizontal = 'horizontal'; +const TKSlidingViewOrientationVertical = 'vertical'; + +TKSlidingView.synthetizes = ['dataSource', + 'delegate', + 'activeElement', + 'activeElementIndex', // the index of the middle/focused element + 'orientation', // whether the view should be horizontal or vertical + 'interactive', // whether or not this view will listen for mouse events + 'sideOffsetBefore', // gap between focused element and the elements before it + 'sideOffsetAfter', // gap between focused element and the elements after it + 'distanceBetweenElements', // general distance between elements in the layout + 'sideElementsVisible', // the number of elements that are considered visible before and after the focus + 'pageControl', // a TKPageControl object that should be linked to this slider (not needed) + 'incrementalLoading', // whether or not the elements should be added to the view as required + 'loops', // if true, the sliding view loops continuously + 'raiseHoverEvents', // if true, the sliding view will use the Hover and Unhover delegate methods + 'numberOfElements']; + +function TKSlidingView (element) { + this.callSuper(); + // these defaults look ok for elements about 180 square + this._activeElementIndex = 0; + this._orientation = TKSlidingViewOrientationHorizontal; + this._interactive = true; + this._sideOffsetBefore = 160; + this._sideOffsetAfter = 160; + this._distanceBetweenElements = 25; + this._sideElementsVisible = 4; + this._pageControl = null; + this._incrementalLoading = false; + this._loops = false; + this._raiseHoverEvents = false; + + this._elements = []; + + if (element) { + this.element = element; + } else { + // create the element we'll use as a container + this.element = document.createElement("div"); + } + this.element.addClassName(TKSlidingViewCSSContainerClass); +} + +TKSlidingView.prototype.init = function () { + + if (!this.dataSource || + !TKUtils.objectHasMethod(this.dataSource, TKSlidingViewNumberOfElements) || + !TKUtils.objectHasMethod(this.dataSource, TKSlidingViewElementAtIndex)) { + return; + } + + var numElements = this.numberOfElements; + + if (this._incrementalLoading) { + // add enough to be visible + this.bufferElements(); + } else { + // add all the elements + for (var i=0; i < numElements; i++) { + var el = this.dataSource[TKSlidingViewElementAtIndex](this, i); + el.addClassName(TKSlidingViewCSSElementClass); + el._needsAppending = true; + this._elements[i] = el; + el._slidingViewIndex = i; + if (this._interactive) { + el.addEventListener("click", this, false); + } + if (this._raiseHoverEvents) { + el.addEventListener("mouseover", this, false); + el.addEventListener("mouseout", this, false); + } + } + } + + this.layout(true); +}; + +TKSlidingView.prototype.setPageControl = function (newPageControl) { + this._pageControl = newPageControl; + this._pageControl.deferCurrentPageDisplay = true; + this._pageControl.delegate = this; + this._pageControl.currentPage = this._activeElementIndex; +}; + +TKSlidingView.prototype.getActiveElement = function () { + return this._elements[this._activeElementIndex]; +}; + +TKSlidingView.prototype.setActiveElementIndex = function (newActiveElementIndex) { + + if ((this._loops || (newActiveElementIndex >= 0 && newActiveElementIndex < this.numberOfElements)) && + newActiveElementIndex != this._activeElementIndex) { + + var needsForcedLayout = (this._activeElementIndex === undefined); + + // call delegate to inform blur of current active element + if (!needsForcedLayout && TKUtils.objectHasMethod(this.delegate, TKSlidingViewDidBlurElementAtIndex)) { + this.delegate[TKSlidingViewDidBlurElementAtIndex](this, this._activeElementIndex); + } + + if (newActiveElementIndex < 0) { + this._activeElementIndex = (this.numberOfElements + newActiveElementIndex) % this.numberOfElements; + } else { + this._activeElementIndex = newActiveElementIndex % this.numberOfElements; + } + + // if there is a page control, tell it to update + if (this._pageControl) { + this._pageControl.currentPage = newActiveElementIndex; + } + + this.bufferElements(); + this.layout(needsForcedLayout); + + // call delegate to inform focus of new active element + // this needs to be done at the very end so we're use any code depending on + // .activeElement actually works since we need elements buffered + if (TKUtils.objectHasMethod(this.delegate, TKSlidingViewDidFocusElementAtIndex)) { + this.delegate[TKSlidingViewDidFocusElementAtIndex](this, this._activeElementIndex); + } + } +}; + +TKSlidingView.prototype.getNumberOfElements = function () { + if (this.dataSource) { + return this.dataSource[TKSlidingViewNumberOfElements](this); + } else { + return 0; + } +}; + +TKSlidingView.prototype.getElementAtIndex = function (index) { + return this._elements[index]; +}; + +// this method loads the elements that are necessary for +// display, and removes the ones that are not needed +TKSlidingView.prototype.bufferElements = function () { + if (this._incrementalLoading) { + var numElements = this.numberOfElements; + for (var i=0; i < numElements; i++) { + var offset = this._activeElementIndex - i; + var absOffset = Math.abs(offset); + if (this._loops) { + // FIXME: check this! doesn't seem right + var offset2 = offset + ((offset < 0) ? numElements : -numElements); + var absOffset2 = Math.abs(offset2); + if (absOffset2 <= this._sideElementsVisible) { + offset = offset2; + absOffset = absOffset2; + } + } + if (absOffset <= this._sideElementsVisible) { + if (!this._elements[i]) { + var el = this.dataSource[TKSlidingViewElementAtIndex](this, i); + el.addClassName(TKSlidingViewCSSElementClass); + el._needsAppending = true; + this._elements[i] = el; + el._slidingViewIndex = i; + if (this._interactive) { + el.addEventListener("click", this, false); + } + if (this._raiseHoverEvents) { + el.addEventListener("mouseover", this, false); + el.addEventListener("mouseout", this, false); + } + + } + } else { + // element isn't needed + if (this._elements[i]) { + this.element.removeChild(this._elements[i]); + this._elements[i] = null; + } + } + } + } +}; + +TKSlidingView.prototype.layout = function (forceLayout) { + var numElements = this.numberOfElements; + + for (var i=0; i < numElements; i++) { + var offset = this._activeElementIndex - i; + var absOffset = Math.abs(offset); + if (this._loops) { + // FIXME: check this! doesn't seem right + var offset2 = offset + ((offset < 0) ? numElements : -numElements); + var absOffset2 = Math.abs(offset2); + if (absOffset2 <= this._sideElementsVisible) { + offset = offset2; + absOffset = absOffset2; + } + } + + var element = this._elements[i]; + + if (!element) { + // only layout elements we need to + continue; + } + + // loaded elements might not yet have been added to the document + // this makes them appear in the right place + if (element._needsAppending) { + this.element.appendChild(element); + element._needsAppending = false; + } + + // Three cases for layout: + // - element is inside (visible) + // - element is just outside (one element outside each edge - called "staged") + // - element is really outside (we call this "hidden" and inform delegate) + + var transform = null; + if (absOffset <= this._sideElementsVisible) { + if (offset > 0) { + if (this._orientation == TKSlidingViewOrientationHorizontal) { + transform = "translate3d(" + (-1 * (absOffset * this._distanceBetweenElements + this._sideOffsetBefore)) + "px, 0, 0)"; + } else { + transform = "translate3d(0, " + (-1 * (absOffset * this._distanceBetweenElements + this._sideOffsetBefore)) + "px, 0)"; + } + this.applySlidingClass(element, TKSlidingViewCSSSideBeforeClass); + } else if (offset < 0) { + if (this._orientation == TKSlidingViewOrientationHorizontal) { + transform = "translate3d(" + (absOffset * this._distanceBetweenElements + this._sideOffsetAfter) + "px, 0, 0)"; + } else { + transform = "translate3d(0, " + (absOffset * this._distanceBetweenElements + this._sideOffsetAfter) + "px, 0)"; + } + this.applySlidingClass(element, TKSlidingViewCSSSideAfterClass); + } else { + transform = "translate3d(0, 0, 0)"; + this.applySlidingClass(element, TKSlidingViewCSSFocusedClass); + } + element.style.webkitTransform = transform; + element.style.opacity = 1; + } else if (absOffset == (this._sideElementsVisible + 1)) { + // FIXME: this is wrong!! should be staged classes - worried this will break things if I fix it + if (offset > 0) { + if (this._orientation == TKSlidingViewOrientationHorizontal) { + transform = "translate3d(" + (-1 * (absOffset * this._distanceBetweenElements + this._sideOffsetBefore)) + "px, 0, 0)"; + } else { + transform = "translate3d(0, " + (-1 * (absOffset * this._distanceBetweenElements + this._sideOffsetBefore)) + "px, 0)"; + } + this.applySlidingClass(element, TKSlidingViewCSSSideBeforeClass); + } else { + if (this._orientation == TKSlidingViewOrientationHorizontal) { + transform = "translate3d(" + (absOffset * this._distanceBetweenElements + this._sideOffsetAfter) + "px, 0, 0)"; + } else { + transform = "translate3d(0, " + (absOffset * this._distanceBetweenElements + this._sideOffsetAfter) + "px, 0)"; + } + this.applySlidingClass(element, TKSlidingViewCSSSideAfterClass); + } + element.style.webkitTransform = transform; + element.style.opacity = 0; + } else if (absOffset > this._sideElementsVisible || forceLayout) { + if (offset > 0) { + if (this._orientation == TKSlidingViewOrientationHorizontal) { + transform = "translate3d(" + (-1 * (absOffset * this._distanceBetweenElements + this._sideOffsetBefore)) + "px, 0, 0)"; + } else { + transform = "translate3d(0, " + (-1 * (absOffset * this._distanceBetweenElements + this._sideOffsetBefore)) + "px, 0)"; + } + } else { + if (this._orientation == TKSlidingViewOrientationHorizontal) { + transform = "translate3d(" + (absOffset * this._distanceBetweenElements + this._sideOffsetAfter) + "px, 0, 0)"; + } else { + transform = "translate3d(0, " + (absOffset * this._distanceBetweenElements + this._sideOffsetAfter) + "px, 0)"; + } + } + this.applySlidingClass(element, TKSlidingViewCSSHiddenClass); + element.style.webkitTransform = transform; + element.style.opacity = 0; + } + // now see if we have any over-ride styles to apply from the delegate + if (TKUtils.objectHasMethod(this.delegate, TKSlidingViewStyleForItemAtIndex)) { + override_styles = this.delegate[TKSlidingViewStyleForItemAtIndex](this, i); + for (var j = 0; j < override_styles.length; j++) { + var override_style = override_styles[j]; + element.style.setProperty(override_style[0], override_style[1], ''); + } + } + } +}; + +TKSlidingView.prototype.applySlidingClass = function (element, className) { + element.removeClassName(TKSlidingViewCSSFocusedClass); + element.removeClassName(TKSlidingViewCSSSideBeforeClass); + element.removeClassName(TKSlidingViewCSSSideAfterClass); + element.removeClassName(TKSlidingViewCSSStagedBeforeClass); + element.removeClassName(TKSlidingViewCSSStagedAfterClass); + element.removeClassName(TKSlidingViewCSSHiddenClass); + + element.addClassName(className); +}; + +TKSlidingView.prototype.handleEvent = function (event) { + switch (event.type) { + case "click": + this.handleClick(event); + break; + case "mouseover": + this.handleMouseover(event); + break; + case "mouseout": + this.handleMouseout(event); + break; + default: + debug("unhandled event type in TKSlidingView: " + event.type); + } +}; + +TKSlidingView.prototype.handleClick = function (event) { + // The event.target should have an _slidingViewIndex property. If + // not, then go up to parent + var target = event.target; + while (target && TKUtils.objectIsUndefined(target._slidingViewIndex)) { + target = target.parentNode; + } + if (!target) { + return; + } + + if (target._slidingViewIndex == this.activeElementIndex) { + if (TKUtils.objectHasMethod(this.delegate, TKSlidingViewDidSelectActiveElement)) { + this.delegate[TKSlidingViewDidSelectActiveElement](this, this._activeElementIndex); + } + } else { + // Check if the click was before or after the focused element. + if (target._slidingViewIndex < this.activeElementIndex) { + if (this._loops && target._slidingViewIndex == 0) { + this.activeElementIndex = 0; + } else { + this.activeElementIndex--; + } + } else { + if (this._loops && target._slidingViewIndex == this.numberOfElements - 1) { + this.activeElementIndex = this.numberOfElements - 1; + } else { + this.activeElementIndex++; + } + } + } +}; + +TKSlidingView.prototype.handleMouseover = function (event) { + // The event.target should have an _slidingViewIndex property. If + // not, then go up to parent + var target = event.target; + while (target && TKUtils.objectIsUndefined(target._slidingViewIndex)) { + target = target.parentNode; + } + if (!target) { + return; + } + + if (TKUtils.objectHasMethod(this.delegate, TKSlidingViewDidHoverElementAtIndex)) { + this.delegate[TKSlidingViewDidHoverElementAtIndex](this, target._slidingViewIndex); + } +}; + +TKSlidingView.prototype.handleMouseout = function (event) { + // The event.target should have an _slidingViewIndex property. If + // not, then go up to parent + var target = event.target; + while (target && TKUtils.objectIsUndefined(target._slidingViewIndex)) { + target = target.parentNode; + } + if (!target) { + return; + } + + if (TKUtils.objectHasMethod(this.delegate, TKSlidingViewDidUnhoverElementAtIndex)) { + this.delegate[TKSlidingViewDidUnhoverElementAtIndex](this, target._slidingViewIndex); + } +}; + +// delegate for page control +TKSlidingView.prototype.pageControlDidUpdateCurrentPage = function (control, newPageIndex) { + if (control === this._pageControl) { + this.activeElementIndex = newPageIndex; + this._pageControl.updateCurrentPageDisplay(); + } +}; + + +TKClass(TKSlidingView); + +/* ====================== Datasource helper ====================== */ + +function TKSlidingViewDataSourceHelper(data, incrementalLoading) { + this.data = data; + this.incrementalLoading = incrementalLoading; + this.elements = []; +}; + +TKSlidingViewDataSourceHelper.prototype.slidingViewNumberOfElements = function(view) { + if (this.data) { + return this.data.length; + } else { + return 0; + } +}; + +TKSlidingViewDataSourceHelper.prototype.slidingViewElementAtIndex = function(view, index) { + if (!this.data || index >= this.data.length) { + return null; + } + var element = this.elements[index]; + if (!element) { + var source = this.data[index]; + element = TKUtils.buildElement(source); + } + if (!this.incrementalLoading) { + this.elements[index] = element; + } + return element; +}; + +/* ====================== Declarative helper ====================== */ + +TKSlidingView.buildSlidingView = function(element, data) { + if (TKUtils.objectIsUndefined(data) || !data || data.type != "TKSlidingView") { + return null; + } + + var slidingView = new TKSlidingView(element); + if (!TKUtils.objectIsUndefined(data.elements)) { + slidingView.dataSource = new TKSlidingViewDataSourceHelper(data.elements, data.incrementalLoading); + } + + TKSlidingView.synthetizes.forEach(function(prop) { + if (prop != "dataSource" && prop != "delegate") { + if (!TKUtils.objectIsUndefined(data[prop])) { + slidingView[prop] = data[prop]; + } + } + }); + + return slidingView; +}; + +/* + * Copyright © 2009 Apple Inc. All rights reserved. + */ + +// --------------------------------------------------- +// A page control implementation +// --------------------------------------------------- + +/** + * @class + * @name TKPageControlData + * @since TuneKit 1.0 + */ + +/** + * @name TKPageControlData.prototype + * + * @property {int} distanceBetweenPageIndicators The distance in pixels between the center points of each indicator, essentially the overall + * width of each indicator. + * + * @property {bool} showPageElements Indicates whether individual elements for each page should be shown, or only the pill. Defaults to false= this._numPages) { + return; + } + + this._currentPage = newCurrentPage; + + if (TKUtils.objectHasMethod(this.delegate, TKPageControlDidUpdateCurrentPage)) { + this.delegate[TKPageControlDidUpdateCurrentPage](this, this._currentPage); + } + + if (!this._deferCurrentPageDisplay) { + this.updateCurrentPageDisplay(); + } +}; + +TKPageControl.prototype.updateCurrentPageDisplay = function () { + var indicatorElement = this.dataSource[TKPageControlIndicatorElement](this); + indicatorElement.style.webkitTransform = "translate(" + (this._currentPage * this._distanceBetweenPageIndicators) + "px, 0px)"; +}; + +TKPageControl.prototype.handleEvent = function (event) { + switch (event.type) { + case "mousedown": + this.handleDragBegan(event); + break; + case "mousemove": + this.handleDragMove(event); + break; + case "mouseup": + this.handleDragEnded(event); + break; + case "click": + this.handleClick(event); + break; + default: + debug("unhandled event type in TKPageControl: " + event.type); + } +}; + +TKPageControl.prototype.setupMouseInteraction = function () { + // in case we have page elements, let's look at the position of the first element as + // the minimum x value that we'll use for interactions from then on + if (this._showPageElements) { + // get the elements + var page_elements = this.element.querySelectorAll('.' + TKPageControlCSSPageElementClass); + var page_element_bounds = page_elements[0].getBounds(); + this.minX = page_element_bounds.x + (page_element_bounds.width - this._distanceBetweenPageIndicators) / 2; + } + // otherwise, use the bounds of the container + else { + this.minX = this.element.getBounds().x; + } + // now convert this value into the coordinate system of our element + var point = window.webkitConvertPointFromPageToNode(this.element, new WebKitPoint(this.minX, 0)); + this.minX = point.x; +}; + +TKPageControl.prototype.pageIndexAtXY = function (x, y) { + var point = window.webkitConvertPointFromPageToNode(this.element, new WebKitPoint(x, y)); + var page_index = Math.floor((point.x - this.minX) / this._distanceBetweenPageIndicators); + return Math.max(Math.min(page_index, this._numPages), 0); +}; + +TKPageControl.prototype.handleClick = function (event) { + // set up the mouse interaction + this.setupMouseInteraction(); + // mark that we are interacting + // XXX: should we really be doing this? This class is not being removed it seems. + this.element.addClassName("interactive"); + // set the current page based on that right from the get go + this.currentPage = this.pageIndexAtXY(event.clientX, event.clientY); +}; + +TKPageControl.prototype.handleDragBegan = function (event) { + if (!this._allowsDragging) { + return; + } + // ensure we cancel the default web page behavior for a dragging interaction + event.preventDefault(); + // set up the mouse interaction + this.setupMouseInteraction(); + // mark that we are interacting + this.element.addClassName("interactive"); + // set the current page based on that right from the get go + this.currentPage = this.pageIndexAtXY(event.clientX, event.clientY); + // track mouse moves + window.addEventListener("mousemove", this, false); + window.addEventListener("mouseup", this, false); +}; + +TKPageControl.prototype.handleDragMove = function (event) { + // ensure we cancel the default web page behavior for a dragging interaction + event.preventDefault(); + // update the page + this.currentPage = this.pageIndexAtXY(event.clientX, event.clientY); +}; + +TKPageControl.prototype.handleDragEnded = function (event) { + // ensure we cancel the default web page behavior for a dragging interaction + event.preventDefault(); + // mark that we are not interacting anymore + this.element.removeClassName("interactive"); + // stop tracking events + window.removeEventListener("mousemove", this); + window.removeEventListener("mouseup", this); +}; + +TKClass(TKPageControl); + +/* ====================== Datasource helper ====================== */ + +function TKPageControlDataSourceHelper(data) { + this.data = data; + this.pageElement = null; + this.indicatorElement = null; +}; + +TKPageControlDataSourceHelper.prototype.pageControlPageElement = function(pageControl) { + if (!this.data) { + return null; + } + if (!this.pageElement) { + this.pageElement = TKUtils.buildElement(this.data.pageElement); + } + return this.pageElement; +}; + +TKPageControlDataSourceHelper.prototype.pageControlIndicatorElement = function(pageControl) { + if (!this.data) { + return null; + } + if (!this.indicatorElement) { + this.indicatorElement = TKUtils.buildElement(this.data.indicatorElement); + } + return this.indicatorElement; +}; + +/* ====================== Declarative helper ====================== */ + +TKPageControl.buildPageControl = function(element, data) { + if (TKUtils.objectIsUndefined(data) || !data || data.type != "TKPageControl") { + return null; + } + + var pageControl = new TKPageControl(element); + + pageControl.dataSource = new TKPageControlDataSourceHelper(data); + + TKPageControl.synthetizes.forEach(function(prop) { + if (prop != "dataSource" && prop != "delegate") { + if (!TKUtils.objectIsUndefined(data[prop])) { + pageControl[prop] = data[prop]; + } + } + }); + + return pageControl; +}; + +/* + * Copyright © 2009 Apple Inc. All rights reserved. + */ + +TKController.inherits = TKObject; +TKController.synthetizes = ['view', 'navigableElements', 'actions', 'outlets', 'scrollable', 'backButton', 'navigatesTo']; + +/** + * The hash in which we keep references to all controller instantiated throughout the lifecycle of the booklet. Use a controller's {@link #id} to + * access the controller for that id. + * @type Object + */ +TKController.controllers = {}; + +/** + * The fraction of the scrollable element's width or height that is being scrolled by in order to increment or decrement the scroll offset. + * @constant + * @type float + * @private + */ +const TKControllerScrollIncrementFraction = 0.75; +/** + * The time in miliseconds that the scrolling animation lasts. + * @constant + * @type int + * @private + */ +const TKControllerScrollDuration = 500; +/** + * The spline used for the scrolling animation. + * @constant + * @type Array + * @private + */ +const TKControllerScrollSpline = [0.211196, 0.811224, 0.641221, 0.979592]; +const TKControllerScrollDirectionUp = 0; +const TKControllerScrollDirectionDown = 1; +const TKControllerScrollDirectionLeft = 0; +const TKControllerScrollDirectionRight = 1; + +/** + * @class + * + *

The TKController class is the base class for all TuneKit controllers. Controllers are useful objects that control all the core functionalities of + * a screen or sub-screen: view-loading, interaction, navigation, etc.

+ * + * @extends TKObject + * @since TuneKit 1.0 + * + * @param {Object} data A hash of properties to use as this object is initialized. + */ +function TKController (data) { + this.callSuper(); + // + this.propertiesToRestoreOnLoad = []; + // synthetized property + this._view = null; + /** + * @name TKController.prototype + * @property {Array} navigableElements The complete list of all elements that can be navigated to within this controller and all of its sub-controllers. + * The contents of this array should not be directly manipulated, instead use the {@link #addNavigableElement} and {@link #removeNavigableElement} methods. + */ + this._navigableElements = []; + /** + * The controller directly containing this controller instance, null if the controller is not attached to any yet. + * @type TKController + */ + this.parentController = null; + // default transition styles for navigation + this.enforcesCustomTransitions = false; // whether we should use the custom transitions on ATV only + /** + * The animated transition to use for this controller's view when the controller becomes inactive. + * @type TKTransitionDefinition + */ + this.becomesInactiveTransition = TKViewTransitionDissolveOut; + /** + * The animated transition to use for this controller's view when the controller becomes active. + * @type TKTransitionDefinition + */ + this.becomesActiveTransition = TKViewTransitionDissolveIn; + // default properties + /** + * The unique id for this controller's view. This is the same string that will be used for the {@link #view}'s HTML id attribute, as well + * as a key in the {@link TKController.controllers} hash, and thus must be adequate for both uses. The controller's id is used in the view-loading mechanism, + * such that if there is an HTML file in the booklet's views/ directory that shares the same name, it is that file that is loaded to provide + * the view's content. + * @type String + */ + this.id = data.id; + /** + * A DOM element to be used as the view for this controller, which overrides the default view-loading mechanism in case it's set before the view is loaded. + * @type Element + * @private + */ + this.explicitView = null; + /** + * The name of the template to be used to create the view's content. If there is an HTML file in the templates/ directory with that name, the + * view is loaded by cloning the content of that file and replacing the ID with that provided by the {@link #id} property. + * @type String + */ + this.template = null; + /** + * A list of image URIs to preload. + * @type Array + */ + this.preloads = []; + this._navigatesTo = null; + this._actions = null; + this._outlets = null; + this._scrollable = null; + this._backButton = null; + /** + * The highlighted element within that controller. This is only unique to the view managed by this controller, and not to the entire booklet or even any + * controllers that might be contained within this controller. + * @type Element + * @private + */ + this.highlightedElement = null; + /** + * Indicates that the view has not appeared on screen yet. + * @type bool + * @private + */ + this.viewNeverAppeared = true; + /** + * Indicates that the view was fully processed. + * @type bool + * @private + */ + this.viewWasProcessed = false; + /** + * The CSS selector for the default scrollable element. If this value is non-null then the up and down keys scroll the element specified + * by the selector. + * @type String + */ + this.scrollableElement = null; + /** + * The animator managing the currently scrolling element. + * @type TKAnimator + * @private + */ + this.animator = new TKAnimator(TKControllerScrollDuration, null, TKControllerScrollSpline); + this.upScrollData = { + direction: TKControllerScrollDirectionUp, + animator: this.animator + } + this.downScrollData = { + direction: TKControllerScrollDirectionDown, + animator: this.animator + } + // copy properties + this.copyNonSynthetizedProperties(data); + // register controller + TKController.controllers[this.id] = this; +}; + +/** + * A utility method to get the controller from an Object that is either the {@link TKController#id} of a controller or a controller directly. + * + * @param {Object} stringOrControllerReference Either the {@link TKController#id} of a controller or a controller directly + * @returns {TKController} The controller. + */ +TKController.resolveController = function (stringOrControllerReference) { + return (TKUtils.objectIsString(stringOrControllerReference)) ? TKController.controllers[stringOrControllerReference] : stringOrControllerReference; +}; + +/** + * A utility method to copy all properties from another object onto the controller, ignoring any property that is synthetized. + * + * @param {Object} properties An object containing a set of properties to be copied across to the receiver. + * @private + */ +TKController.prototype.copyNonSynthetizedProperties = function (properties) { + for (var property in properties) { + // don't copy synthetized properties but track them for later + if (this.__lookupSetter__(property)) { + this.propertiesToRestoreOnLoad[property] = properties[property]; + continue; + } + this[property] = properties[property]; + } +}; + +/* ==================== Managing the View ==================== */ + +TKController.prototype.getView = function () { + // create the view if it's not set yet + if (this._view === null) { + this.loadView(); + } + return this._view; +}; + +TKController.prototype.setView = function (view) { + this.explicitView = view; +}; + +TKController.prototype.loadView = function () { + this.viewNeverAppeared = false; + // first, check if we have an element already defined + var view; + if (this.explicitView !== null) { + view = this.explicitView; + // check if node is already in the document + this.viewNeverAppeared = !TKUtils.isNodeChildOfOtherNode(view, document); + } + // check if our view already exists in the DOM + else { + view = document.getElementById(this.id); + } + // if not, load it from the views directory + if (view === null) { + this.viewNeverAppeared = true; + view = this.loadFragment('views', this.id); + // there was no such view available, try and see if we have a template available + if (view === null) { + if (this.template !== null) { + view = this.loadFragment('templates', this.template); + } + // no template, just create an empty
then + if (view === null) { + view = document.createElement('div'); + } + } + } + // make sure we know when the view is added to the document if it + // wasn't part of the DOM already + if (this.viewNeverAppeared) { + view.addEventListener('DOMNodeInsertedIntoDocument', this, false); + } + // set up the correct id on our view + view.id = this.id; + // link the view to our controller + view._controller = this; + // and remember our view + this._view = view; + // let our object perform more setup code + this.viewDidLoad(); + // do post-loading processing + this.processView(); + // + this.viewWasProcessed = true; +}; + +TKController.prototype.loadFragment = function (directory, id) { + var imported_fragment = null; + // + var request = new XMLHttpRequest(); + var failed = false; + request.open('GET', directory + '/' + id + '.html', false); + try { + request.send(); + } catch (err) { + // iTunes will throw an error if the request doesn't exist + // when using the booklet:// scheme + // Mark the error here so we can take the FAIL path below, which + // is actually what we want. + failed = true; + } + // everything went well + // XXX: we should do more work to differentitate between http:// and file:// URLs here + if (!failed && ((request.status <= 0 && request.responseText !== '') || request.status == 200)) { + // XXX: this is way dirty + var loaded_fragment = document.implementation.createHTMLDocument(); + loaded_fragment.write(request.responseText); + imported_fragment = document.importNode(loaded_fragment.getElementById(id), true); + } + return imported_fragment; +}; + +/** + * This method is called once the view has been loaded and allows the controller to post-process it. + */ +TKController.prototype.processView = function () { + var view = this._view; + // restore properties that have not been set yet since construction + this.restoreProperty('navigatesTo'); + this.restoreProperty('actions'); + this.restoreProperty('outlets'); + this.restoreProperty('backButton'); + this.restoreProperty('scrollable'); + // process highlightedElement + if (this.highlightedElement !== null && !(this.highlightedElement instanceof Element)) { + this.highlightedElement = this._view.querySelector(this.highlightedElement); + } + // process links + var links = view.querySelectorAll('a'); + for (var i = 0; i < links.length; i++) { + this.addNavigableElement(links[i]); + } + // process assets to pre-load + for (var i = 0; i < this.preloads.length; i++) { + new Image().src = this.preloads[i]; + } +}; + +TKController.prototype.restoreProperty = function (property) { + var value = this.propertiesToRestoreOnLoad[property]; + if (value !== undefined && this['_' + property] === null) { + this[property] = value; + } +}; + +TKController.prototype.getArchivedProperty = function (property) { + var archived_value; + try { + var archived_value = bookletController.archive.controllers[this.id][property]; + } + catch (e) {} + return archived_value; +}; + +/** + * Indicates whether the view the controller manages was loaded yet. + * + * @returns {bool} Whether the view was loaded yet. + */ +TKController.prototype.isViewLoaded = function () { + return (this._view !== null); +}; + +/* ==================== Dealing with the various properties ==================== */ + +/** + * @name TKController.prototype + * @property {Array} navigatesTo A list of objects defining elements to act as anchors to navigate to other controllers. Each object in the array is an + * ad-hoc object with selector and controller properties. The selector is a string describing a CSS selector used + * to match the element within the {@link #view} that will act as an anchor to navigate to the controller. The controller is either a string matching + * the {@link #id} of an existing controller or an reference to a {@link TKController}. + */ +TKController.prototype.setNavigatesTo = function (navigatesTo) { + if (navigatesTo === null) { + return; + } + // unregister the previous elements if we have any + if (this._navigatesTo !== null) { + for (var i = 0; i < this._navigatesTo.length; i++) { + var element = this._navigatesTo[i]; + element._navigationData = undefined; + this.removeNavigableElement(element); + } + } + // register the new elements + this._navigatesTo = []; + for (var i = 0; i < navigatesTo.length; i++) { + var item = navigatesTo[i]; + var element = this._view.querySelector(item.selector); + if (element) { + element._navigationData = item; + this._navigatesTo.push(element); + this.addNavigableElement(element); + } + } +}; + +/** + * @name TKController.prototype + * @property {Array} actions A list of objects defining elements to act as anchors to trigger a JavaScript callback. Each object in the array is an + * ad-hoc object with selector, action and, optionally, arguments properties. The selector is a + * string describing a CSS selector used to match the element within the {@link #view} that will act as a trigger for the action. The action + * specifies the function to call when the action is triggered, which is either a string matching the name of a method on this controller instance or + * a direct reference to a function. Optionally, the arguments property can be specified in order to provide a list of arguments to be passed + * to the callback when the action is triggered. + */ +TKController.prototype.setActions = function (actions) { + if (actions === null || this._actions === actions) { + return; + } + // unregister the previous elements if we have any + if (this._actions !== null) { + for (var i = 0; i < this._actions.length; i++) { + var element = this._actions[i]; + element._actionData = undefined; + this.removeNavigableElement(element); + } + } + // register the new elements + this._actions = []; + for (var i = 0; i < actions.length; i++) { + var item = actions[i]; + var element = this._view.querySelector(item.selector); + if (element) { + element._actionData = item; + this._actions.push(element); + this.addNavigableElement(element); + } + } +}; + +/** + * @name TKController.prototype + * @property {Array} outlets A list of objects defining elements to which we want to create an automatic reference on the controller instance. Each object in + * the array has a selector and a name property. The selector is a string describing a CSS selector used to match the + * element within the {@link #view} to which we want to create a reference. The name specifies the name of the JavaScript property that will be holding + * that reference on the controller instance. + */ +TKController.prototype.setOutlets = function (outlets) { + if (outlets === null) { + return; + } + // unregister the previous outlets if we have any + if (this._outlets !== null) { + for (var i = 0; i < this._outlets.length; i++) { + this[this._outlets[i]] = undefined; + } + } + // register the new outlets + for (var i = 0; i < outlets.length; i++) { + var item = outlets[i]; + this[item.name] = this._view.querySelector(item.selector); + } + this._outlets = outlets; +}; + +/** + * @name TKController.prototype + * @property {String} backButton A CSS selector that matches an element in the {@link #view} that acts as the back button. + */ +TKController.prototype.setBackButton = function (backButton) { + if (backButton === null) { + return; + } + // forget the old back button if we have one + if (this._backButton !== null) { + this._backButton._backButton = undefined; + // restore display type on ATV + if (IS_APPLE_TV) { + this._backButton.style.display = this._backButton._previousDisplayStyle; + } + this.removeNavigableElement(this._backButton); + } + // set up the new one + if (backButton !== null) { + var element = this._view.querySelector(backButton); + element._backButton = true; + // hide it on ATV + if (IS_APPLE_TV) { + element._previousDisplayStyle = element.style.display; + element.style.display = 'none'; + } + this.addNavigableElement(element); + this._backButton = element; + } +}; + +/* ==================== Notification Methods ==================== */ + +/** + * This method is called when the view has been fully loaded but not yet processed. Override this method in order to customize the content of the view. + */ +TKController.prototype.viewDidLoad = function () {}; +TKController.prototype.viewDidUnload = function () {}; + +TKController.prototype._viewWillAppear = function () {}; +/** + * This method is called when the view managed by this controller is about to appear on screen, probably after an animated transition. + */ +TKController.prototype.viewWillAppear = function () {}; + +TKController.prototype._viewDidAppear = function () {}; +/** + * This method is called when the view managed by this controller appeared on screen, probably after an animated transition. + */ +TKController.prototype.viewDidAppear = function () {}; + +TKController.prototype._viewWillDisappear = function () {}; +/** + * This method is called when the view managed by this controller is about to disappear from the screen, probably after an animated transition. + */ +TKController.prototype.viewWillDisappear = function () {}; + +TKController.prototype._viewDidDisappear = function () {}; +/** + * This method is called when the view managed by this controller has disappeared from the screen, probably after an animated transition. + */ +TKController.prototype.viewDidDisappear = function () {}; + +/* ==================== Event Handling ==================== */ + +/** + * Entry point for all event handling, since TKController implements the DOM EventListener protocol. This method may be subclassed + * but it is important to call the superclass's implementation of this method as essential event routing happens there. + * + * @param {Event} event The event. + */ +TKController.prototype.handleEvent = function (event) { + switch (event.type) { + case 'click' : + this.elementWasActivated(event.currentTarget); + break; + case 'highlight' : + this.elementWasHighlighted(event.currentTarget, event.relatedTarget); + break; + case 'unhighlight' : + this.elementWasUnhighlighted(event.currentTarget, event.relatedTarget); + break; + case 'mouseover' : + this.elementWasHovered(event.currentTarget); + break; + case 'mouseout' : + this.elementWasUnhovered(event.currentTarget); + break; + case 'DOMNodeInsertedIntoDocument' : + this.viewWasInsertedIntoDocument(event); + break; + } +}; + +/** + * Triggered when an element is activated by the user, no matter what the host's input methods are as TuneKit abstracts the interaction that yields + * an element's activation. This method may be subclassed but it is important to call the superclass's implementation of this method as navigation anchors, + * actions, etc. are handled in that method directly. + * + * @param {Element} element The element that was just activated. + */ +TKController.prototype.elementWasActivated = function (element) { + if (element._navigationData !== undefined) { + // pointer to the controller + var controller = TKController.resolveController(element._navigationData.controller); + // error if we have an undefined object + if (controller === undefined) { + console.error('TKController.elementWasActivated: trying to push an undefined controller'); + return; + } + // otherwise, navigate to it + TKSpatialNavigationManager.sharedManager.highlightElement(element); + TKNavigationController.sharedNavigation.pushController(controller); + } + else if (element._actionData !== undefined) { + TKSpatialNavigationManager.sharedManager.highlightElement(element); + // get the callback for this action + var callback = element._actionData.action; + // see if it's a string in which case we need to get the function dynamically + if (TKUtils.objectIsString(callback) && TKUtils.objectHasMethod(this, callback)) { + callback = this[element._actionData.action]; + } + // see if we have custom arguments + if (TKUtils.objectIsArray(element._actionData.arguments)) { + callback.apply(this, element._actionData.arguments); + } + // otherwise just call the callback + else { + callback.apply(this); + } + } + else if (element._backButton !== undefined) { + TKSpatialNavigationManager.soundToPlay = SOUND_EXIT; + TKSpatialNavigationManager.sharedManager.highlightElement(element); + TKNavigationController.sharedNavigation.popController(); + } + else if (element.localName == 'a' && IS_APPLE_TV) { + element.dispatchEvent(TKUtils.createEvent('click', null)); + } + else if (element._scrollableData !== undefined) { + this.scrollWithData(element._scrollableData); + } +}; + +/** + * Triggered when an element receives highlight. + * + * @param {Element} element The element that is newly being highlighted. + * @param {Element} previouslyHighlightedElement The element that previously was highlighted, or null if there was none. + */ +TKController.prototype.elementWasHighlighted = function (element, previouslyHighlightedElement) { +}; + +/** + * Triggered when an element loses highlight. + * + * @param {Element} element The element that is newly being highlighted. + * @param {Element} nextHighlightedElement The element that is going to be highlighted next, or null if there is none. + */ +TKController.prototype.elementWasUnhighlighted = function (element, nextHighlightedElement) { +}; + +/** + * Triggered when an element is hovered, which only happens when a mouse is present. + * + * @param {Element} element The element that is being hovered. + */ +TKController.prototype.elementWasHovered = function (element) { +}; + +/** + * Triggered when an element is not hovered any longer, which only happens when a mouse is present. + * + * @param {Element} element The element that is not hovered any longer. + */ +TKController.prototype.elementWasUnhovered = function (element) { +}; + +/** + * Triggered when the view is first inserted into the document. + */ +TKController.prototype.viewWasInsertedIntoDocument = function () { + this.viewNeverAppeared = false; +}; + +/** + * Indicates whether the receiver is a descendent of the controller passed as an argument. + * + * @param {TKController} purportedParentController The controller that we assume is a parent controller to the receiver. + * @returns {bool} Whether the controller is a descendent of the other controller passed as a parameter. + */ +TKController.prototype.isDescendentOfController = function (purportedParentController) { + var is_descendent = false; + var parent = this.parentController; + while (parent !== null) { + if (parent === purportedParentController) { + is_descendent = true; + break; + } + parent = parent.parentController; + } + return is_descendent; +}; + +/* ==================== Keyboard Navigation ==================== */ + +/** + * Adds an element within the controller's view to the list of navigable elements. Any element that is interactive should be registered as navigable, even + * when a mouse is available. + * + * @param {Element} element The element we wish to make navigable. + */ +TKController.prototype.addNavigableElement = function (element) { + // nothing to do if we already know about this element + if (this._navigableElements.indexOf(element) > -1) { + return; + } + // + if (!IS_APPLE_TV) { + element.addEventListener('click', this, false); + } + element.addEventListener('highlight', this, false); + element.addEventListener('unhighlight', this, false); + element._controller = this; + // + this._navigableElements.push(element); + // + TKSpatialNavigationManager.sharedManager.addNavigableElement(element); +}; + +/** + * Removes an element within the controller's view from the list of navigable elements. + * + * @param {Element} element The element we wish to remove from the navigable elements list. + */ +TKController.prototype.removeNavigableElement = function (element) { + // find the index for this element + var index = this._navigableElements.indexOf(element); + if (index < 0) { + return; + } + // + element.removeEventListener('click', this, false); + element.removeEventListener('highlight', this, false); + element.removeEventListener('unhighlight', this, false); + element._controller = undefined; + // remove elements from the tracking arrays + this._navigableElements.splice(index, 1); + // + TKSpatialNavigationManager.sharedManager.removeNavigableElement(element); +}; + +/** + * Allows to specify custom metrics for an element displayed on screen. This method is called by the spatial navigation manager when determining what the + * element to highlight is after the uses presses a directional key on the Apple remote. By default, the CSS metrics for the elements are used, but in + * certain cases the author may wish to use different metrics that are more logical for the navigation. Return null in order to specify that + * the element has no custom metrics. + * + * @param {Element} element The element which the spatial navigation manager is inspecting. + * @returns {TKRect} The custom metrics for the given element. + */ +TKController.prototype.customMetricsForElement = function (element) { + return null; +}; + +/** + * Allows the controller to provide the spatial navigation manager with a prefered element to highlight, overriding the default behavior of using CSS metrics. + * The default implementation returns undefined, which indicates that the automatic behavior should be used, while returning null + * means that there should be no element highlighted in the provided direction. + * + * @param {Element} currentElement The element that is currently highlighted. + * @param {int} direction The direction the user is navigation towards. + * @returns {Element} The preferred element to highlight in the provided direction. + */ +TKController.prototype.preferredElementToHighlightInDirection = function (currentElement, direction) { + return undefined; +}; + +// private method meant to be over-ridden by a controller sub-classes to provide a custom element +// to highlight, returning null means there's nothing custom to report +// XXX: we really need a better mechanism to do this stuff, having a private method for subclasses and one for instances is way dirty +TKController.prototype._preferredElementToHighlightInDirection = function (currentElement, direction) { + return undefined; +}; + +/* ==================== Scrolling ==================== */ + +TKController.prototype.setScrollable = function (scrollable) { + // remove all scrollable regions if we already had some set up + if (this._scrollable !== null) { + for (var i = 0; i < this._scrollable.length; i++) { + var element = this._scrollable[i]; + element._scrollableData = undefined; + this.removeNavigableElement(element); + } + } + // process scrollable regions + this._scrollable = []; + for (var i = 0; i < scrollable.length; i++) { + var scrollable_data = scrollable[i]; + // create an animator for this scrollable element + scrollable_data.animator = new TKAnimator(TKControllerScrollDuration, null, TKControllerScrollSpline); + // check if we have an up element + if (scrollable_data.up !== undefined) { + var up_button = this._view.querySelector(scrollable_data.up); + up_button._scrollableData = { + direction: TKControllerScrollDirectionUp, + target: scrollable_data.target, + animator: scrollable_data.animator + }; + this._scrollable.push(up_button); + this.addNavigableElement(up_button); + } + // check if we have a down element + if (scrollable_data.down !== undefined) { + var down_button = this._view.querySelector(scrollable_data.down); + down_button._scrollableData = { + direction: TKControllerScrollDirectionDown, + target: scrollable_data.target, + animator: scrollable_data.animator + }; + this._scrollable.push(down_button); + this.addNavigableElement(down_button); + } + // check if we have a left element + if (scrollable_data.left !== undefined) { + var left_button = this._view.querySelector(scrollable_data.left); + left_button._scrollableData = { + direction: TKControllerScrollDirectionLeft, + target: scrollable_data.target, + animator: scrollable_data.animator + }; + this._scrollable.push(left_button); + this.addNavigableElement(left_button); + } + // check if we have a right element + if (scrollable_data.right !== undefined) { + var right_button = this._view.querySelector(scrollable_data.right); + right_button._scrollableData = { + direction: TKControllerScrollDirectionRight, + target: scrollable_data.target, + animator: scrollable_data.animator + }; + this._scrollable.push(right_button); + this.addNavigableElement(right_button); + } + } +}; + +TKController.prototype.scrollWithData = function (scrollData) { + // stop any running animation for this scrollable region + scrollData.animator.stop(); + // get a pointer to the target element + var element = this._view.querySelector(scrollData.target); + // stop right there if there's no such element + if (!(element instanceof Element)) { + TKSpatialNavigationManager.soundToPlay = SOUND_LIMIT; + return; + } + // figure out which direction we're scrolling + var vertical_scrolling = (scrollData.direction == TKControllerScrollDirectionUp || scrollData.direction == TKControllerScrollDirectionDown); + // get the increment for this scroll + var increment = element[vertical_scrolling ? 'offsetHeight' : 'offsetWidth'] * TKControllerScrollIncrementFraction; + // get the start value + var start_value = element[vertical_scrolling ? 'scrollTop' : 'scrollLeft']; + // get the target value + var target_value; + if (scrollData.direction == TKControllerScrollDirectionUp) { + target_value = Math.max(element.scrollTop - increment, 0); + } + else if (scrollData.direction == TKControllerScrollDirectionDown) { + target_value = Math.min(element.scrollTop + increment, element.scrollHeight - element.offsetHeight); + } + else if (scrollData.direction == TKControllerScrollDirectionLeft) { + target_value = Math.max(element.scrollLeft - increment, 0); + } + else if (scrollData.direction == TKControllerScrollDirectionRight) { + target_value = Math.min(element.scrollLeft + increment, element.scrollWidth - element.offsetWidth); + } + // only run if we have different values + if (start_value == target_value) { + TKSpatialNavigationManager.soundToPlay = SOUND_LIMIT; + return; + } + // set the delegate + scrollData.animator.delegate = { + animationDidIterate : function (fraction) { + element[vertical_scrolling ? 'scrollTop' : 'scrollLeft'] = start_value + fraction * (target_value - start_value); + } + } + // start the animation + scrollData.animator.start(); + // play the move sound since we're scrolling + TKSpatialNavigationManager.soundToPlay = SOUND_MOVED; +}; + +TKController.prototype.scrollUp = function () { + this.upScrollData.target = this.scrollableElement; + this.scrollWithData(this.upScrollData); +}; + +TKController.prototype.scrollDown = function () { + this.downScrollData.target = this.scrollableElement; + this.scrollWithData(this.downScrollData); +}; + +/* ==================== Keyboard Navigation ==================== */ + +/** + * Indicates whether the controller is interested in providing event handling for the key with the given identifier. By default, this method returns + * false, letting the spatial navigation manager take care of the event in order to perform navigation. In case the method returns + * true, the {@link #keyWasPressed} method is called to let the controller provide its own custom key event handling. + * + * @param {int} key The identifier for the key that was pressed. + * @returns {bool} Whether the controller wants to provide its own custom key event handling. + */ +TKController.prototype.wantsToHandleKey = function (key) { + return (this.scrollableElement !== null && (key == KEYBOARD_UP || key == KEYBOARD_DOWN)); +}; + +/** + * Triggered when a key was pressed and the receiver has expressed explicit interest in providing custom key event handling by returning true in + * the {@link #wantsToHandleKey} method. + * + * @param {int} key The identifier for the key that was pressed. + */ +TKController.prototype.keyWasPressed = function (key) { + // up should scroll + if (key == KEYBOARD_UP) { + this.scrollUp(); + } + // down should scroll + else if (key == KEYBOARD_DOWN) { + this.scrollDown(); + } + // done, play the sound + TKUtils.playSound(TKSpatialNavigationManager.soundToPlay); +}; + +/* ==================== Archival ==================== */ + +/** + * Called when the booklet's state is being archived. This method needs to return a list of properties reflecting the current state for the receiver. + * + * @returns {Object} The list of properties to archive as a hash-like object. + * @private + */ +TKController.prototype.archive = function () { + var archive = { + id: this.id + }; + // see if we can add a highlighted element index + if (this.highlightedElement !== null) { + archive.highlightedElementIndex = this.navigableElements.indexOf(this.highlightedElement); + } + // + return archive; +}; + +TKClass(TKController); +/* + * Copyright © 2009 Apple Inc. All rights reserved. + */ + +/* ==================== TKSpatialNavigationManager ==================== */ + +/** + * Indicates whether the spatial navigation manager is enabled, which currently is only the case on Apple TV, or within Safari for development purposes. + * @constant + * @type bool + */ +const TKSpatialNavigationManagerEnabled = (window.iTunes === undefined || window.iTunes.platform == 'AppleTV') ? true : (parseInt(window.iTunes.version, 0) < 9); + +/** + * The CSS class name applied to an element when it is highlighted by the spatial navigation manager. + * @constant + * @type String + */ +const TKSpatialNavigationManagerHighlightCSSClass = 'tk-highlighted'; +/** + * The CSS class name an element should have in order to be ignored by the spatial navigation manager. + * @constant + * @type String + */ +const TKSpatialNavigationManagerInactiveCSSClass = 'tk-inactive'; + +/** + * The up direction. + * @constant + * @type int + */ +const TKSpatialNavigationManagerDirectionUp = KEYBOARD_UP; +/** + * The right direction. + * @constant + * @type int + */ +const TKSpatialNavigationManagerDirectionRight = KEYBOARD_RIGHT; +/** + * The down direction. + * @constant + * @type int + */ +const TKSpatialNavigationManagerDirectionDown = KEYBOARD_DOWN; +/** + * The left direction. + * @constant + * @type int + */ +const TKSpatialNavigationManagerDirectionLeft = KEYBOARD_LEFT; +/** + * The list of keys the spatial navigation manager knows how to handle. + * @constant + * @type int + * @private + */ +const TKSpatialNavigationManagerKnownKeys = [KEYBOARD_UP, KEYBOARD_RIGHT, KEYBOARD_DOWN, KEYBOARD_LEFT, KEYBOARD_BACKSPACE, KEYBOARD_RETURN]; + +/** + * The number of controllers that are currently busy, when for instance performing a transition that should not be interrupted. When this variable is more than + * 0, key handling by the spatial navigation manager is suspended. + * @type int + */ +TKSpatialNavigationManager.busyControllers = 0; +/** + * The identifier for the sound to play for the current event loop. + * @type int + */ +TKSpatialNavigationManager.soundToPlay = null; + +/* ==================== Creating the shared instance lazily ==================== */ + +/** + * @name TKSpatialNavigationManager + * @property {TKSpatialNavigationManager} sharedManager The shared instance of the spatial navigation manager. TuneKit automatically creates a single instance + * of the {@link TKSpatialNavigationManager} class as needed, and developers should never have to create an instance themselves, instead using this property + * to retrieve the shared instance. + */ +TKSpatialNavigationManager._sharedManager = null; +TKSpatialNavigationManager.__defineGetter__('sharedManager', function () { + if (this._sharedManager === null) { + this._sharedManager = new TKSpatialNavigationManager(); + } + return this._sharedManager; +}); + +/* ==================== Constructor ==================== */ + +TKSpatialNavigationManager.inherits = TKObject; +TKSpatialNavigationManager.includes = [TKEventTriage]; +TKSpatialNavigationManager.synthetizes = ['managedController']; + +/** + * @class + * + *

The spatial navigation manager is a special controller type that sits behind the scenes and handles much of the keyboard interaction in order + * to provide navigation between navigable elements of the {@link #managedController}. By default, navigation between navigable elements is automatic and + * performed based on the location and metrics of each elements. The elements' metrics are those set by CSS and a controller is free to provide custom + * metrics for elements as it sees fit by implementing the {@link TKController#customMetricsForElement} method. Additionally, the automatic navigation + * can be completely bypassed should the managed controller provide a custom element to navigate to with the + * {@link TKController#preferredElementToHighlightInDirection} method.

+ * + * @extends TKObject + * @since TuneKit 1.0 + */ +function TKSpatialNavigationManager () { + this.callSuper(); + // + this._managedController = null; + /** + * The complete list of all elements that can be navigated to within this controller and all of its sub-controllers. + * @type Array + */ + this.navigableElements = []; + this.highlightedElement = null; + this.previousNavigation = null; + // register for keyboard events if we're running outside of the iTunes app + if (TKSpatialNavigationManagerEnabled) { + window.addEventListener('keydown', this, true); + } +}; + +/** + * @name TKSpatialNavigationManager.prototype + * @property managedController The managed controller is the controller that the spatial navigation manager queries for navigable elements + * and any customization of the otherwise automated navigation. Developers should not assign this property directly as the navigation controller takes care of + * that as the user navigates through controllers. + * @type TKController + */ +TKSpatialNavigationManager.prototype.setManagedController = function (controller) { + this._managedController = controller; + this.navigableElements = controller.navigableElements; + this.previousNavigation = null; + // is this the first time we're managing this controller? + if (controller._wasAlreadyManagedBySpatialNavigationManager === undefined) { + // see if it had an archived highlighted element + var archived_index = controller.getArchivedProperty('highlightedElementIndex'); + if (archived_index !== undefined) { + var archived_element = controller.navigableElements[archived_index]; + if (archived_element instanceof Element) { + controller.highlightedElement = archived_element; + } + } + // track that we've managed it before + controller._wasAlreadyManagedBySpatialNavigationManager = true; + } + // reset the highlighted element to be nothing + this.highlightedElement = null; + // if we have a preferred or recorded highlighted element, highlight that + if (controller.highlightedElement !== null) { + this.highlightElement(controller.highlightedElement); + } + // otherwise default to the top-most element + else { + this.highlightTopElement(); + } +}; + +TKSpatialNavigationManager.prototype.registerController = function (controller) { + var elements = controller.navigableElements; + for (var i = 0; i < elements.length; i++) { + this.addNavigableElement(elements[i]); + } +}; + +TKSpatialNavigationManager.prototype.unregisterController = function (controller) { + var elements = controller.navigableElements; + for (var i = 0; i < elements.length; i++) { + this.removeNavigableElement(elements[i]); + } +}; + +TKSpatialNavigationManager.prototype.addNavigableElement = function (element) { + // nothing to do if the new element is not rooted in the managed hierarchy or we already know it + if (!element._controller.isDescendentOfController(this._managedController) || + this.navigableElements.indexOf(element) > -1) { + return; + } + // and keep track of it + this.navigableElements.push(element); +}; + +TKSpatialNavigationManager.prototype.removeNavigableElement = function (element) { + // find the index for this element + var index = this.navigableElements.indexOf(element); + if (index < 0) { + return; + } + // remove elements from the tracking arrays + this.navigableElements.splice(index, 1); +}; + +/* ==================== Keyboard Navigation ==================== */ + +TKSpatialNavigationManager.prototype.handleKeydown = function (event) { + + var key = event.keyCode; + + // check if our controller knows what it's doing and let it take over in case it does + if (this._managedController.wantsToHandleKey(key)) { + // prevent default actions + event.stopPropagation(); + event.preventDefault(); + // have the controller do what it think is best in this case + this._managedController.keyWasPressed(key); + return; + } + + // reset the sound + TKSpatialNavigationManager.soundToPlay = null; + + // check we know about this key, otherwise, do nothing + if (TKSpatialNavigationManagerKnownKeys.indexOf(key) == -1) { + return; + } + + var navigation = TKNavigationController.sharedNavigation; + // first, check if we're hitting the back button on the home screen, in which case + // we don't want to do anything and let the User Agent do what's right to exit + if (event.keyCode == KEYBOARD_BACKSPACE && navigation.topController === homeController) { + return; + } + + // before we go any further, prevent the default action from happening + event.stopPropagation(); + event.preventDefault(); + + // check if we're busy doing other things + if (TKSpatialNavigationManager.busyControllers > 0) { + return; + } + // see if we pressed esc. so we can pop to previous controller + if (event.keyCode == KEYBOARD_BACKSPACE) { + var top_controller = navigation.topController; + if (top_controller !== homeController) { + // at any rate, play the exit sound + TKUtils.playSound(SOUND_EXIT); + // see if the top controller has a custom place to navigate to with the back button + if (top_controller.backButton instanceof Element && top_controller.backButton._navigationData !== undefined) { + navigation.pushController(TKController.resolveController(top_controller.backButton._navigationData.controller)); + } + // otherwise, just pop the controller + else { + navigation.popController(); + } + } + } + // check if we want to activate an element + else if (key == KEYBOARD_RETURN) { + if (this.highlightedElement !== null) { + var success = this.highlightedElement._controller.elementWasActivated(this.highlightedElement); + TKUtils.playSound(TKSpatialNavigationManager.soundToPlay === null ? SOUND_ACTIVATED : TKSpatialNavigationManager.soundToPlay); + } + else { + TKUtils.playSound(SOUND_LIMIT); + } + } + // keyboard nav + else { + var key_index = TKSpatialNavigationManagerKnownKeys.indexOf(key); + // do nothing if we don't have any highlightable elements or don't know about this navigation direction + if (this.navigableElements.length == 0 || key_index == -1) { + TKUtils.playSound(SOUND_LIMIT); + return; + } + // figure the index of the element to highlight + var index = this.nearestElementIndexInDirection(key); + + // get a pointer to the controller of the previous item if we have one + if (this.highlightedElement !== null) { + var previous_controller = this.highlightedElement._controller; + + // see if we're being provided custom navigation by the controller instance + var provided_preferred_element = false; + var preferred_highlighted_element = previous_controller.preferredElementToHighlightInDirection(this.highlightedElement, key); + // try again with the private method meant to be implemented by the sub-class + if (preferred_highlighted_element === undefined) { + preferred_highlighted_element = previous_controller._preferredElementToHighlightInDirection(this.highlightedElement, key); + } + // we explicitly do not want to highlight anything + if (preferred_highlighted_element === null) { + index = -1; + } + else if (preferred_highlighted_element !== undefined) { + var preferred_highlight_index = this.navigableElements.indexOf(preferred_highlighted_element); + // if this element is in our navigation list and is ready to be navigated to + if (preferred_highlight_index >= 0 && this.isElementAtIndexNavigable(preferred_highlight_index)) { + index = preferred_highlight_index; + provided_preferred_element = true; + } + } + + // stop right there if we have no useful index + if (index == -1) { + TKUtils.playSound(SOUND_LIMIT); + return; + } + } + + // get a pointer to the controller of the item we consider highlighting now + var next_controller = this.navigableElements[index]._controller; + + // we're moving out of a tab controller into one controller managed by that tab controller + // in which case we want to highlight the first item in that controller based on its orientation + if (previous_controller instanceof TKTabController && + this.highlightedElement._tabIndex !== undefined && + next_controller.parentController === previous_controller) { + index = this.navigableElements.indexOf(next_controller.highlightedElement || next_controller.navigableElements[0]); + } + // we're moving back to a tab element from an element managed by a controller + // that is itself managed by the very tab controller we're focusing, so let's highlight + // the element that is selected in that tab controller + else if (next_controller instanceof TKTabController && + this.navigableElements[index]._tabIndex !== undefined && + previous_controller.parentController === next_controller) { + index = this.navigableElements.indexOf(next_controller.tabs[next_controller.selectedIndex]); + } + // check if we were doing the reverse operation to the last one + else if (!provided_preferred_element && this.previousNavigation !== null && key_index == (this.previousNavigation.keyIndex + 2) % 4) { + var previous_element_index = this.navigableElements.indexOf(this.previousNavigation.element); + if (previous_element_index > -1 && this.isElementAtIndexNavigable(previous_element_index)) { + index = previous_element_index; + } + } + + // get a pointer to the next element to highlight + var next_highlighted_element = (index >= 0 && index < this.navigableElements.length) ? this.navigableElements[index] : null; + + // only highlight if we know what element to highlight + if (next_highlighted_element !== null && next_highlighted_element.isNavigable()) { + // track the interaction so we can go back to it + if (this.highlightedElement !== null) { + this.previousNavigation = { + element: this.highlightedElement, + keyIndex : key_index + }; + } + this.highlightElement(next_highlighted_element); + TKUtils.playSound(SOUND_MOVED); + } + else { + TKUtils.playSound(SOUND_LIMIT); + } + } +}; + +TKSpatialNavigationManager.prototype.nearestElementIndexInDirection = function (direction) { + // nothing to do if we don't have a next element + if (this.highlightedElement === null) { + if (direction == TKSpatialNavigationManagerDirectionUp) { + return this.bottomMostIndex(); + } + else if (direction == TKSpatialNavigationManagerDirectionRight) { + return this.leftMostIndex(); + } + else if (direction == TKSpatialNavigationManagerDirectionDown) { + return this.topMostIndex(); + } + else if (direction == TKSpatialNavigationManagerDirectionLeft) { + return this.rightMostIndex(); + } + } + // figure out parameters + var ref_position, target_edge; + if (direction == TKSpatialNavigationManagerDirectionUp) { + ref_position = TKRectMiddleOfTopEdge; + target_edge = TKRectBottomEdge; + } + else if (direction == TKSpatialNavigationManagerDirectionRight) { + ref_position = TKRectMiddleOfRightEdge; + target_edge = TKRectLeftEdge; + } + else if (direction == TKSpatialNavigationManagerDirectionDown) { + ref_position = TKRectMiddleOfBottomEdge; + target_edge = TKRectTopEdge; + } + else if (direction == TKSpatialNavigationManagerDirectionLeft) { + ref_position = TKRectMiddleOfLeftEdge; + target_edge = TKRectRightEdge; + } + // look for the closest element now + var index = -1; + var min_d = 10000000; + var highlight_index = this.navigableElements.indexOf(this.highlightedElement); + var ref_metrics = this.metricsForElement(this.highlightedElement); + var ref_point = ref_metrics.pointAtPosition(ref_position); + var ref_center = ref_metrics.pointAtPosition(TKRectCenter); + for (var i = 0; i < this.navigableElements.length; i++) { + // see if we should skip this element + if (!this.isElementAtIndexNavigable(i)) { + continue; + } + var metrics = this.metricsForElement(this.navigableElements[i]); + // go to next item if it's not in the right direction or already has highlight + if ((direction == TKSpatialNavigationManagerDirectionUp && metrics.pointAtPosition(TKRectBottomLeftCorner).y > ref_center.y) || + (direction == TKSpatialNavigationManagerDirectionRight && metrics.pointAtPosition(TKRectTopLeftCorner).x < ref_center.x) || + (direction == TKSpatialNavigationManagerDirectionDown && metrics.pointAtPosition(TKRectTopLeftCorner).y < ref_center.y) || + (direction == TKSpatialNavigationManagerDirectionLeft && metrics.pointAtPosition(TKRectTopRightCorner).x > ref_center.x) || + i == highlight_index) { + continue; + } + var d = metrics.edge(target_edge).distanceToPoint(ref_point); + if (d < min_d) { + min_d = d; + index = i; + } + } + // return the index, if any + return index; +}; + +TKSpatialNavigationManager.prototype.topMostIndex = function () { + var index = 0; + var min_y = 10000; + for (var i = 0; i < this.navigableElements.length; i++) { + if (!this.isElementAtIndexNavigable(i)) { + continue; + } + var y = this.metricsForElementAtIndex(i).y; + if (y < min_y) { + min_y = y; + index = i; + } + } + return index; +}; + +TKSpatialNavigationManager.prototype.rightMostIndex = function () { + var index = 0; + var max_x = 0; + for (var i = 0; i < this.navigableElements.length; i++) { + if (!this.isElementAtIndexNavigable(i)) { + continue; + } + var x = this.metricsForElementAtIndex(i).pointAtPosition(TKRectTopRightCorner).x; + if (x > max_x) { + max_x = x; + index = i; + } + } + return index; +}; + +TKSpatialNavigationManager.prototype.bottomMostIndex = function () { + var index = 0; + var max_y = 0; + for (var i = 0; i < this.navigableElements.length; i++) { + if (!this.isElementAtIndexNavigable(i)) { + continue; + } + var y = this.metricsForElementAtIndex(i).pointAtPosition(TKRectBottomRightCorner).y; + if (y > max_y) { + max_y = y; + index = i; + } + } + return index; +}; + +TKSpatialNavigationManager.prototype.leftMostIndex = function () { + var index = 0; + var min_x = 10000; + for (var i = 0; i < this.navigableElements.length; i++) { + if (!this.isElementAtIndexNavigable(i)) { + continue; + } + var y = this.metricsForElementAtIndex(i).x; + if (y < min_x) { + min_x = y; + index = i; + } + } + return index; +}; + +TKSpatialNavigationManager.prototype.metricsForElement = function (element) { + return element._controller.customMetricsForElement(element) || element.getBounds(); +}; + +TKSpatialNavigationManager.prototype.metricsForElementAtIndex = function (index) { + return this.metricsForElement(this.navigableElements[index]); +}; + +/** + * Highlight the top-most element in the list of navigable elements. + */ +TKSpatialNavigationManager.prototype.highlightTopElement = function () { + // now see if we need to enforce some default element + if (this.navigableElements.length > 0) { + this.highlightElement(this.navigableElements[this.topMostIndex()]); + } +}; + +/** + * Indicates whether a given element is navigable at the provided index in the {@link #navigableElements} array. + * + * @param {Element} element The index for the element in the {@link #navigableElements} array. + * @returns {bool} Whether the element can be navigated to. + */ +TKSpatialNavigationManager.prototype.isElementAtIndexNavigable = function (index) { + return this.navigableElements[index].isNavigable(); +}; + +/* ==================== Highlight Management ==================== */ + +/** + * Highlights a given element if it's part of the {@link #navigableElements} array. When an element receives highlight, a highlight event is + * dispatched to that element, while an unhighlight event is dispatched to the element that previously had highlight. + * + * @param {Element} element The element to highlight. + */ +TKSpatialNavigationManager.prototype.highlightElement = function (element) { + // nothing to do if we don't really have an element to highlight + if (!(element instanceof Element)) { + return; + } + // check that this element is navigable, and do nothing if it's not + var navigation_index = this.navigableElements.indexOf(element); + if (navigation_index == -1 || !this.isElementAtIndexNavigable(navigation_index)) { + return; + } + // + if (this.highlightedElement !== null) { + this.highlightedElement.dispatchEvent(TKUtils.createEvent('unhighlight', element)); + if (TKSpatialNavigationManagerEnabled) { + this.highlightedElement.removeClassName(TKSpatialNavigationManagerHighlightCSSClass); + } + } + // + element.dispatchEvent(TKUtils.createEvent('highlight', this.highlightedElement)); + if (TKSpatialNavigationManagerEnabled) { + element.addClassName(TKSpatialNavigationManagerHighlightCSSClass); + } + this.highlightedElement = element; + // track on its controller that it was the last with highlight + element._controller.highlightedElement = element; +}; + +TKClass(TKSpatialNavigationManager); +/* + * Copyright © 2009 Apple Inc. All rights reserved. + */ + +/** + * @class + * @name TKNavigationControllerDelegate + * @since TuneKit 1.0 + */ + +/** + * Indicates that a new controller is becoming the top controller and is about to become visible on screen. + * + * @name navigationControllerWillShowController + * @function + * + * @param {TKNavigationController} navigationController The navigation controller. + * @param {TKController} controller The controller that is about to be shown by the navigation controller. + * @memberOf TKNavigationControllerDelegate.prototype + */ +const TKNavigationControllerWillShowController = 'navigationControllerWillShowController'; +/** + * Indicates that a new controller has become the top controller and is fully visible on screen. + * + * @name navigationControllerDidShowController + * @function + * + * @param {TKNavigationController} navigationController The navigation controller. + * @param {TKController} controller The controller that is has been shown by the navigation controller. + * @memberOf TKNavigationControllerDelegate.prototype + */ +const TKNavigationControllerDidShowController = 'navigationControllerDidShowController'; + +TKNavigationController.inherits = TKController; +TKNavigationController.synthetizes = ['topController']; + +/** + * @property {TKNavigationController} sharedNavigation The shared instance of the navigation controller. TuneKit automatically creates a single instance + * of the {@link TKNavigationController} class as needed, and developers should never have to create an instance themselves, instead using this property + * to retrieve the shared instance. + */ +TKNavigationController.sharedNavigation = null; + +/** + * @class + * + *

The spatial navigation manager is a pre-insantiated singleton controller that handles the logic for all navigation between controllers. The currently + * showing controller is the {@link #topController}, and is the last item of the {@link #controllers} array, which traces the complete navigation history + * from the home controller onwards. By implementing the {@link TKNavigationControllerDelegate} protocol, the navigation controller's {@link #delegate} can + * track as the user navigates between controllers. While the {@link TKController#navigatesTo} property should be sufficient to alloe developers to specify + * what action triggers a navigation to a given controller, the {@link #pushController} and {@link #popController} methods also allow a programmatic + * interaction with the navigation controller.

+ * + * @extends TKController + * @since TuneKit 1.0 + * + * @param {Object} data A hash of properties to use as this object is initialized. + */ +function TKNavigationController (data) { + /** + * The list of controllers in the navigation stack. The controller at the first index is the root-most controller, while the controller at the last index is + * the top controller. + * @type Array + */ + this.controllers = []; + /** + * The delegate for this navigation controller, an object implementing the {@link TKNavigationControllerDelegate} protocol. + * @type Object + */ + this.delegate = data.delegate || null; + this.rootController = data.rootController || null; + this.previousController = null; + // + this.busy = false; + // + this.callSuper(data); + // see if we have a stack to restore + if (bookletController.archive !== undefined) { + var stack = bookletController.archive.navigationStack; + var restored_controllers = []; + for (var i = 0; i < stack.length; i++) { + var controller = TKController.controllers[stack[i]]; + restored_controllers.push(controller); + controller.loadView(); + } + // push the last controller + this.pushController(restored_controllers[restored_controllers.length - 1]); + // and fake the controllers stack + this.controllers = restored_controllers; + } + // see if we have a root set up + else if (this.rootController !== null) { + this.pushController(this.rootController); + } + // + TKNavigationController.sharedNavigation = this; +}; + +/* ==================== Controllers ==================== */ + +/** + * @name TKNavigationController.prototype + * @property {TKController} topController The controller at the top of the navigation stack of {@link #controllers}. + */ +TKNavigationController.prototype.getTopController = function () { + return (this.controllers.length > 0) ? this.controllers[this.controllers.length - 1] : null; +}; + +/** + * Pushes a new controller to the top of the navigation stack, triggering an animated transition of the new top controller using its + * {@link TKController.becomesActiveTransition} property and the {@link TKController.becomesInactiveTransition} property of the previous {@link #topController} + * @param {TKController} controller The controller to push onto the navigation stack. + */ +TKNavigationController.prototype.pushController = function (controller) { + // do nothing if we're busy + if (this.busy) { + return; + } + // + TKTransaction.begin(); + // get pointers to object we'll manipulate + var previous_controller = this.topController; + var next_view = controller.view; + // fire delegate saying we're moving to a new controller + if (TKUtils.objectHasMethod(this.delegate, TKNavigationControllerWillShowController)) { + this.delegate[TKNavigationControllerWillShowController](this, controller); + } + // put the controller in our array + this.controllers.push(controller); + // notify of upcoming change + if (previous_controller !== null) { + previous_controller._viewWillDisappear(); + previous_controller.viewWillDisappear(); + } + controller._viewWillAppear(); + controller.viewWillAppear(); + // add it to the tree + this.view.appendChild(controller.view); + // + if (previous_controller !== null) { + this.transitionToController(previous_controller, controller); + } + else { + this.busy = true; + this.transitionDidComplete(); + TKSpatialNavigationManager.sharedManager.managedController = controller; + } + // + TKTransaction.commit(); +}; + +/** + * Pops the top {@link #topController} off the navigation stack, triggering an animated transition of the new top controller using its + * {@link TKController.becomesActiveTransition} property and the {@link TKController.becomesInactiveTransition} property of the previous {@link #topController} + */ +TKNavigationController.prototype.popController = function () { + // do nothing if we're busy or if there's nothing to pop + if (this.busy || this.controllers.length < 2) { + return; + } + TKTransaction.begin(); + // fire delegate saying we're moving to a new controller + if (TKUtils.objectHasMethod(this.delegate, TKNavigationControllerWillShowController)) { + this.delegate[TKNavigationControllerWillShowController](this, this.controllers[this.controllers.length - 2]); + } + // update stack + var previous_controller = this.controllers.pop(); + var top_controller = this.topController; + // notify of upcoming change + previous_controller._viewWillDisappear(); + previous_controller.viewWillDisappear(); + top_controller._viewWillAppear(); + top_controller.viewWillAppear(); + // add it to the tree + this.view.appendChild(top_controller.view); + // transition + this.transitionToController(previous_controller, top_controller); + // + TKTransaction.commit(); +}; + +/* ==================== Transition ==================== */ + +TKNavigationController.prototype.transitionToController = function (previous_controller, top_controller) { + // mark that a transition is now in progress + this.busy = true; + // record some parameters that we will need at the end of the transition + this.previousController = previous_controller; + // figure out transitions + if (previous_controller !== null) { + if (IS_APPLE_TV && !previous_controller.enforcesCustomTransitions) { + previous_controller.becomesInactiveTransition = TKViewTransitionDissolveOut; + } + previous_controller.view.applyTransition(previous_controller.becomesInactiveTransition, false); + } + if (IS_APPLE_TV && !top_controller.enforcesCustomTransitions) { + top_controller.becomesActiveTransition = TKViewTransitionDissolveIn; + } + var top_controller_transition = top_controller.becomesActiveTransition; + top_controller_transition.delegate = this; + top_controller.view.applyTransition(top_controller_transition, false); + // + TKSpatialNavigationManager.sharedManager.managedController = top_controller; + // track that we're moving from screen to screen + window.dispatchEvent(TKUtils.createEvent('cursorwait', null)); +}; + +TKNavigationController.prototype.transitionDidComplete = function (transition) { + if (!this.busy) { + return; + } + var top_controller = this.topController; + // remove the old screen + if (this.previousController !== null) { + this.view.removeChild(this.previousController.view); + this.previousController._viewDidDisappear(); + this.previousController.viewDidDisappear(); + } + // notify of completed change + top_controller._viewDidAppear(); + top_controller.viewDidAppear(); + // fire delegate saying we've moved to a new controller + if (TKUtils.objectHasMethod(this.delegate, TKNavigationControllerDidShowController)) { + this.delegate[TKNavigationControllerDidShowController](this, top_controller); + } + // + this.busy = false; + // pre-load screens that we can navigate to from here + // for (var i = 0; i < top_controller.navigatesTo.length; i++) { + // var navigation_data = top_controller.navigatesTo[i]; + // // pointer to the controller + // var controller = navigation_data.controller; + // // if it's a string, try to find it in the controllers hash + // if (TKUtils.objectIsString(controller)) { + // controller = TKController.controllers[controller]; + // } + // // skip if we have an undefined object + // if (controller === undefined) { + // continue; + // } + // // otherwise, load it if it's not been loaded before + // if (controller._view === null) { + // controller.loadView(); + // } + // } + // done moving from screen to screen + window.dispatchEvent(TKUtils.createEvent('cursornormal', null)); +}; + +TKClass(TKNavigationController); +/* + * Copyright © 2009 Apple Inc. All rights reserved. + */ + +const TKPageSliderControllerContainerCSSClass = 'tk-page-slider-controller-view'; + +TKPageSliderController.inherits = TKController; +TKPageSliderController.synthetizes = ['slidingViewData', 'pageControlData', 'previousPageButton', 'nextPageButton', 'highlightedPageIndex']; + +/** + * @class + * + *

A page slider controller adds the ability to browse through a collection of elements, often images, with nice and smooth transitions + * set up in CSS. Using this controller type, you can easily track when a new page is highlighted or activated. Optionally, you can also + * set up a series of indicators giving the user an overview of the number of images and arrows to navigate through elements, the connection + * between the various components being automatically handled behind the scenes for you.

+ * + * @extends TKController + * @since TuneKit 1.0 + * + * @param {Object} data A hash of properties to use as this object is initialized. + */ +function TKPageSliderController (data) { + this._previousPageButton = null; + this._nextPageButton = null; + /** + * Indicates whether the pages managed by the controller are navigable with keys. Defaults to true. + * @type bool + */ + this.highlightsFocusedPage = true; + /** + * Indicates whether navigation of pages is done strictly with paging buttons. Defaults to false, allowing the Apple remote to + * be used to navigate between pages. + * @type bool + */ + this.navigatesWithPagingButtonsOnly = false; + /** + * Indicates whether the focused page can get activated. Defaults to true, setting this to false plays the limit sound when + * the user attempts to activate the focused page. + * @type bool + */ + this.activatesFocusedPage = true; + /** + * Provides the list of directions that the user can navigate out of the list of pages. By default, this array is empty, meaning that if the + * {@link #navigatesWithPagingButtonsOnly} property is set to false and pages can be navigated with arrow keys, then the user will not + * be able to move focus out of the pages from either ends. The directions allowed are the TKSpatialNavigationManagerDirection family + * of constants. + * @type Array + */ + this.allowedOutwardNavigationDirections = []; + // set up the sliding view + /** + * The backing sliding view hosting the pages. + * @type TKSlidingView + * @private + */ + this.slidingView = new TKSlidingView(); + this.slidingView.ready = false; + this.slidingView.delegate = this; + // set up the page control + /** + * The backing page control hosting the page indicators. + * @type TKPageControl + * @private + */ + this.pageControl = new TKPageControl(); + this.pageControl.delegate = this; + // + this._slidingViewData = null; + this._pageControlData = null; + // + this.callSuper(data); +}; + +TKPageSliderController.prototype.processView = function () { + this.callSuper(); + // restore properties that have not been set yet since construction + this.restoreProperty('previousPageButton'); + this.restoreProperty('nextPageButton'); + this.restoreProperty('slidingViewData'); + this.restoreProperty('pageControlData'); + this.restoreProperty('highlightedPageIndex'); + // wire up actions if we have a previous and next button wired + // add the sliding view and page control containers + this.container = this._view.appendChild(document.createElement('div')); + this.container.addClassName(TKPageSliderControllerContainerCSSClass); + this.container.appendChild(this.slidingView.element); + this.container.appendChild(this.pageControl.element); + // ensure our first page gets told about its being highlighted + this.pageWasHighlighted(this.highlightedPageIndex); + this.syncPageButtons(); + // highlight the first page in case we have no explicit highlighted element + if ((this.highlightedElement === null || !this.highlightedElement.isNavigable()) && this.slidingView.ready && this.highlightsFocusedPage) { + this.highlightedElement = this.slidingView.activeElement; + } +}; + +/** + * @name TKPageSliderController.prototype + * @property {int} highlightedPageIndex The index of the page currently selected within the collection of pages. + */ +TKPageSliderController.prototype.getHighlightedPageIndex = function () { + return this.slidingView.activeElementIndex; +}; + +TKPageSliderController.prototype.setHighlightedPageIndex = function (index) { + if (index === null) { + return; + } + // apply to the sliding view if it's ready + if (this.slidingView.ready) { + this.slidingView.activeElementIndex = index; + if (this.highlightedElement.hasClassName(TKSlidingViewCSSElementClass)) { + this.registerNavigablePages(); + this.highlightedElement = this.slidingView.activeElement; + } + } +}; + +/** + * @name TKPageSliderController.prototype + * @property {String} previousPageButton A CSS selector matching a button to be used as the button to decrement the {@link #highlightedPageIndex}. + */ +TKPageSliderController.prototype.setPreviousPageButton = function (previousPageButton) { + if (previousPageButton === null) { + return; + } + // forget old button + if (this._previousPageButton) { + this.removeNavigableElement(this._previousPageButton); + } + // process new one + if (previousPageButton !== null) { + this._previousPageButton = this.view.querySelector(previousPageButton); + if (this._previousPageButton !== null) { + if (IS_APPLE_TV && !this.navigatesWithPagingButtonsOnly) { + this._previousPageButton.style.display = 'none'; + } + else { + this.addNavigableElement(this._previousPageButton); + } + } + } +}; + +/** + * @name TKPageSliderController.prototype + * @property {String} nextPageButton A CSS selector matching a button to be used as the button to increment the {@link #highlightedPageIndex}. + */ +TKPageSliderController.prototype.setNextPageButton = function (nextPageButton) { + if (nextPageButton === null) { + return; + } + // forget old button + if (this._nextPageButton) { + this.removeNavigableElement(this._nextPageButton); + } + // process new one + if (nextPageButton !== null) { + this._nextPageButton = this.view.querySelector(nextPageButton); + if (this._nextPageButton !== null) { + if (IS_APPLE_TV && !this.navigatesWithPagingButtonsOnly) { + this._nextPageButton.style.display = 'none'; + } + else { + this.addNavigableElement(this._nextPageButton); + } + } + } +}; + +/** + * @name TKPageSliderController.prototype + * @property {TKSlidingViewData} slidingViewData The set of properties used to set up the contents of the page slider. + */ +TKPageSliderController.prototype.setSlidingViewData = function (data) { + if (data === null) { + return; + } + // set up the data source if we have .elements on the data object + if (!TKUtils.objectIsUndefined(data.elements)) { + this.slidingView.dataSource = new TKSlidingViewDataSourceHelper(data.elements); + delete data.element; + } + // see if we have some intersting bits to pass through + var archived_page_index = this.getArchivedProperty('highlightedPageIndex'); + if (archived_page_index !== undefined) { + data.activeElementIndex = archived_page_index; + } + // copy properties + TKUtils.copyPropertiesFromSourceToTarget(data, this.slidingView); + // init our view + this.slidingView.init(); + // + this.slidingView.ready = true; + // + this.syncPageButtons(); + // add the currently focused element to the list of keyboard elements and highlight it + if (this.highlightsFocusedPage) { + this.registerNavigablePages(); + this.highlightedElement = this.slidingView.activeElement; + if (this.viewWasProcessed) { + TKSpatialNavigationManager.sharedManager.highlightElement(this.slidingView.activeElement); + } + } +}; + +/** + * @name TKPageSliderController.prototype + * @property {TKPageControlData} pageControlData The set of properties used to set up the contents of the optional page indicators. + */ +TKPageSliderController.prototype.setPageControlData = function (data) { + if (data === null) { + return; + } + // set up the data source + this.pageControl.dataSource = new TKPageControlDataSourceHelper(data); + // copy properties + TKUtils.copyPropertiesFromSourceToTarget(data, this.pageControl); + // init our control + this.pageControl.init(); + // get the current page from the sliding view if we have it set only after + if (this.slidingView.ready) { + this.pageControl.currentPage = this.highlightedPageIndex; + } +}; + +/* ==================== Event Handling ==================== */ + +// private method meant to be over-ridden by a controller sub-classes to provide a custom element +// to highlight, returning null means there's nothing custom to report +TKPageSliderController.prototype._preferredElementToHighlightInDirection = function (currentElement, direction) { + var can_exit_in_direction = (this.allowedOutwardNavigationDirections.indexOf(direction) != -1); + var element = this.callSuper(currentElement, direction); + if (!this.navigatesWithPagingButtonsOnly && currentElement.hasClassName(TKSlidingViewCSSElementClass)) { + if (direction == KEYBOARD_LEFT) { + if (this.slidingView.activeElementIndex <= 0 && !this.slidingView.loops) { + if (!can_exit_in_direction) { + element = null; + } + } + else { + element = this.slidingView.getElementAtIndex((this.slidingView.activeElementIndex + this.slidingView.numberOfElements - 1) % this.slidingView.numberOfElements); + } + } + else if (direction == KEYBOARD_RIGHT) { + if (this.slidingView.activeElementIndex >= this.slidingView.numberOfElements - 1 && !this.slidingView.loops) { + if (!can_exit_in_direction) { + element = null; + } + } + else { + element = this.slidingView.getElementAtIndex((this.slidingView.activeElementIndex + 1) % this.slidingView.numberOfElements); + } + } + } + return element; +}; + +TKPageSliderController.prototype.elementWasActivated = function (element) { + // previous page button pressed + if (element === this._previousPageButton) { + var can_navigate = (this.slidingView.activeElementIndex > 0 || this.slidingView.loops); + TKSpatialNavigationManager.soundToPlay = (can_navigate) ? SOUND_MOVED : SOUND_LIMIT; + if (can_navigate) { + this.unregisterNavigablePages(); + } + this.slidingView.activeElementIndex--; + } + // next page button pressed + else if (element === this._nextPageButton) { + var can_navigate = (this.slidingView.activeElementIndex < this.slidingView.numberOfElements - 1 || this.slidingView.loops); + TKSpatialNavigationManager.soundToPlay = (can_navigate) ? SOUND_MOVED : SOUND_LIMIT; + if (can_navigate) { + this.unregisterNavigablePages(); + } + this.slidingView.activeElementIndex++; + } + // focused element in the sliding view + else if (element.hasClassName(TKSlidingViewCSSFocusedClass)) { + if (this.activatesFocusedPage) { + this.pageWasSelected(this.slidingView.activeElementIndex); + } + else { + TKSpatialNavigationManager.soundToPlay = SOUND_LIMIT; + } + } + // fall back to default behavior + else { + this.callSuper(element); + } +}; + +TKPageSliderController.prototype.elementWasHighlighted = function (element, previouslyHighlightedElement) { + if (element.hasClassName(TKSlidingViewCSSElementClass)) { + this.slidingView.activeElementIndex = element._slidingViewIndex; + // track navigation on all elements if we're getting highlight for the first time + if (previouslyHighlightedElement !== null && + (!previouslyHighlightedElement.hasClassName(TKSlidingViewCSSElementClass) || + previouslyHighlightedElement._controller !== this)) { + this.registerNavigablePages(); + } + } +}; + +TKPageSliderController.prototype.elementWasUnhighlighted = function (element, nextHighlightedElement) { + // are we focusing an element outside of our sliding view? + if (element.hasClassName(TKSlidingViewCSSElementClass) && + (!nextHighlightedElement.hasClassName(TKSlidingViewCSSElementClass) || + nextHighlightedElement._controller !== this)) { + // make all the other elements non-navigable + this.unregisterNavigablePages(); + // save for the active one + this.addNavigableElement(this.slidingView.activeElement); + } +}; + +TKPageSliderController.prototype.syncPageButtons = function () { + // nothing to do if the sliding view is looping + if (this.slidingView.loops || !this.isViewLoaded()) { + return; + } + // check if the previous page button needs hiding + if (this._previousPageButton instanceof Element) { + this._previousPageButton[(this.slidingView.activeElementIndex <= 0 ? 'add' : 'remove') + 'ClassName']('inactive'); + } + // check if the next page button needs hiding + if (this._nextPageButton instanceof Element) { + this._nextPageButton[(this.slidingView.activeElementIndex >= this.slidingView.numberOfElements - 1 ? 'add' : 'remove') + 'ClassName']('inactive'); + } +}; + +TKPageSliderController.prototype.registerNavigablePages = function () { + if (this.navigatesWithPagingButtonsOnly) { + this.addNavigableElement(this.slidingView.activeElement); + } + else { + var elements = this.slidingView.element.querySelectorAll('.' + TKSlidingViewCSSElementClass); + for (var i = 0; i < elements.length; i++) { + this.addNavigableElement(elements[i]); + } + } +}; + +TKPageSliderController.prototype.unregisterNavigablePages = function () { + var elements = this.slidingView.element.querySelectorAll('.' + TKSlidingViewCSSElementClass); + for (var i = 0; i < elements.length; i++) { + this.removeNavigableElement(elements[i]); + } +}; + +/* ==================== TKSlidingView Protocol ==================== */ + +TKPageSliderController.prototype.slidingViewDidFocusElementAtIndex = function (view, index) { + if (this.highlightsFocusedPage && this.slidingView.ready) { + if (this.highlightedElement.hasClassName(TKSlidingViewCSSElementClass)) { + this.registerNavigablePages(); + } + else { + this.unregisterNavigablePages(); + this.addNavigableElement(this.slidingView.activeElement); + } + // make sure the element has focus + if (this.viewWasProcessed) { + TKSpatialNavigationManager.sharedManager.highlightElement(this.slidingView.activeElement); + } + } + // + this.pageControl.currentPage = index; + this.pageWasHighlighted(index); + // update the states of previous and next buttons + this.syncPageButtons(); +}; + +TKPageSliderController.prototype.slidingViewDidBlurElementAtIndex = function (view, index) { + if (this.highlightsFocusedPage) { + this.unregisterNavigablePages(); + } +}; + +TKPageSliderController.prototype.slidingViewDidSelectActiveElement = function (view, index) { + this.pageWasSelected(index); +}; + +TKPageSliderController.prototype.slidingViewStyleForItemAtIndex = function (view, index) { + return this.styleForPageAtIndex(index); +}; + +TKPageSliderController.prototype.slidingViewDidHoverElementAtIndex = function (view, index) { + this.pageWasHovered(index); +}; + +TKPageSliderController.prototype.slidingViewDidUnhoverElementAtIndex = function (view, index) { + this.pageWasUnhovered(index); +}; + +/* ==================== Placeholder Methods ==================== */ + +/** + * Triggered as the {@link #highlightedPageIndex} property has changed when a new page became focused. + * + * @param {int} index The index of the newly focused page + */ +TKPageSliderController.prototype.pageWasHighlighted = function (index) {}; + +/** + * Triggered as the focused page was selected by the user, either from clicking on the page or using the play/pause remote key. + * + * @param {int} index The index of the activated page + */ +TKPageSliderController.prototype.pageWasSelected = function (index) {}; + +TKPageSliderController.prototype.pageWasHovered = function (index) {}; + +TKPageSliderController.prototype.pageWasUnhovered = function (index) {}; + +/** + * This method allows to provide custom style rules for a page programatically any time the {@link #highlightedPageIndex} property changes. The values in this + * array are expected to be individual two-value arrays, where the first index holds the CSS property name, and the second index its value. + * + * @param {Array} index The index of the page for which we are trying to obtain custom styles. + */ +TKPageSliderController.prototype.styleForPageAtIndex = function (index) { + return []; +}; + +/* ==================== TKPageControl Protocol ==================== */ + +TKPageSliderController.prototype.pageControlDidUpdateCurrentPage = function (control, newPageIndex) { + this.slidingView.activeElementIndex = newPageIndex; + this.pageControl.updateCurrentPageDisplay(); +}; + +/* ==================== Archival ==================== */ + +TKPageSliderController.prototype.archive = function () { + var archive = this.callSuper(); + archive.highlightedPageIndex = this.highlightedPageIndex; + return archive; +}; + +TKClass(TKPageSliderController); +/* + * Copyright © 2009 Apple Inc. All rights reserved. + */ + +TKLyricsController.inherits = TKController; +TKLyricsController.synthetizes = ['currentSong', 'previousSongButton', 'nextSongButton']; + +/** + * @class + * + *

A lyrics controller is designed to make it easy to flip through a collection of lyrics. This controller type automatically wires the left and + * right keys to navigate between songs, and the author should leverage the {@link TKController#scrollableElement} property to identify the container + * for the scrolling lyrics region. Loading of new song content should be done using the {@link #songDidChange} method.

+ * + * @extends TKController + * @since TuneKit 1.0 + * + * @param {Object} data A hash of properties to use as this object is initialized. + */ +function TKLyricsController (data) { + /** + * The number of songs that the controller will be flipping through. + * @type int + */ + this.numberOfSongs = 0; + this.defaultToFirstSong = false; + this._currentSong = null; + this._previousSongButton = null; + this._nextSongButton = null; + // + this.callSuper(data); +}; + +/* ==================== View Processing ==================== */ + +TKLyricsController.prototype.processView = function () { + this.callSuper(); + // restore properties that have not been set yet since construction + this.restoreProperty('previousSongButton'); + this.restoreProperty('nextSongButton'); + this.restoreProperty('currentSong'); + // default to first song + if (this.defaultToFirstSong && this._currentSong === null) { + this.currentSong = 0; + } +}; + +/* ==================== Keyboard Navigation ==================== */ + +TKLyricsController.prototype.wantsToHandleKey = function (key) { + return (key == KEYBOARD_LEFT || key == KEYBOARD_RIGHT) ? true : this.callSuper(key); +}; + +TKLyricsController.prototype.keyWasPressed = function (key) { + // default action is move, so wire that up + TKSpatialNavigationManager.soundToPlay = SOUND_MOVED; + // left should go to the previous song + if (key == KEYBOARD_LEFT) { + this.goToPreviousSong(); + } + // right should go to the next song + else if (key == KEYBOARD_RIGHT) { + this.goToNextSong(); + } + // let the default behavior happen too + this.callSuper(key); +}; + +/* ==================== Previous / Next Buttons ==================== */ + +/** + * @name TKLyricsController.prototype + * @property {String} previousSongButton The CSS selector for the element acting as the trigger to navigate to the previous song. + */ +TKLyricsController.prototype.setPreviousSongButton = function (previousSongButton) { + if (previousSongButton === null || IS_APPLE_TV) { + return; + } + // forget old button + if (this._previousSongButton) { + this.removeNavigableElement(this._previousSongButton); + } + // process new one + if (previousSongButton !== null) { + this._previousSongButton = this.view.querySelector(previousSongButton); + if (this._previousSongButton !== null) { + this.addNavigableElement(this._previousSongButton); + } + } +}; + +/** + * @name TKLyricsController.prototype + * @property {String} nextSongButton The CSS selector for the element acting as the trigger to navigate to the next song. + */ +TKLyricsController.prototype.setNextSongButton = function (nextSongButton) { + if (nextSongButton === null || IS_APPLE_TV) { + return; + } + // forget old button + if (this._nextSongButton) { + this.removeNavigableElement(this._nextSongButton); + } + // process new one + if (nextSongButton !== null) { + this._nextSongButton = this.view.querySelector(nextSongButton); + if (this._nextSongButton !== null) { + this.addNavigableElement(this._nextSongButton); + } + } +}; + +TKLyricsController.prototype.elementWasActivated = function (element) { + // previous page button pressed + if (element === this._previousSongButton) { + this.goToPreviousSong(); + } + // next page button pressed + else if (element === this._nextSongButton) { + this.goToNextSong(); + } + // fall back to default behavior + else { + this.callSuper(element); + } +}; + +/* ==================== Page Navigation ==================== */ + +/** + * Shows the previous song. + */ +TKLyricsController.prototype.goToPreviousSong = function () { + this.currentSong = ((this._currentSong + this.numberOfSongs) - 1) % this.numberOfSongs; +}; + +/** + * Shows the next song. + */ +TKLyricsController.prototype.goToNextSong = function () { + this.currentSong = (this._currentSong + 1) % this.numberOfSongs; +}; + +/** + * @name TKLyricsController.prototype + * @property {int} currentSong The index of the current song. Whenever this value changes, the {@link #songDidChange} method is called. + */ +TKLyricsController.prototype.setCurrentSong = function (song) { + if (song === null || song < 0 || song >= this.numberOfSongs) { + return; + } + // track what song we're on + this._currentSong = song; + // let our instance know that we've moved to a new song + this.songDidChange(song); +}; + +/** + * Triggered when a new song is displayed by the controller. + */ +TKLyricsController.prototype.songDidChange = function (song) {}; + +/* ==================== Archival ==================== */ + +TKLyricsController.prototype.archive = function () { + var archive = this.callSuper(); + archive.currentSong = this.currentSong; + return archive; +}; + +TKClass(TKLyricsController); +/* + * Copyright © 2009 Apple Inc. All rights reserved. + */ + +/** + * @class + * @name TKTabControllerDelegate + * @since TuneKit 1.0 + */ + +/** + * Indicates that a new controller is becoming the tab controller's selected controller and is about to become visible on screen. + * + * @name tabControllerWillShowController + * @function + * + * @param {TKTabController} tabController The tab controller. + * @param {TKController} controller The controller that is about to be shown by the tab controller. + * @memberOf TKTabControllerDelegate.prototype + */ +const TKTabControllerWillShowController = 'tabControllerWillShowController'; +/** + * Indicates that a new controller has become the tab controller's selected controller and is fully visible on screen. + * + * @name tabControllerDidShowController + * @function + * + * @param {TKTabController} tabController The tab controller. + * @param {TKController} controller The controller that is being shown by the tab controller. + * @memberOf TKTabControllerDelegate.prototype + */ +const TKTabControllerDidShowController = 'tabControllerDidShowController'; + +/** + * The CSS class name applied to an element acting as a tab once it is the selected tab. + * @constant + * @type String + */ +const TKTabControllerSelectedCSSClass = 'tk-tab-selected'; + +TKTabController.inherits = TKController; +TKTabController.synthetizes = ['selectedController', 'selectedIndex', 'tabsSelector']; + +/** + * @class + * + *

A tab controller allows to bind a series of elements within the view, the tabs, to display each a given controller, the tab controller only + * allowing a single tab to be selected at once. Tabs are specified using the {@link #tabsSelector} CSS selector, and a controller for each tab needs to be + * stored in the {@link #controllers} array. By implementing the {@link TKTabControllerDelegate} protocol, a tab controller's {@link #delegate} can track as + * the user navigates between tabs. At any given time, the {@link #selectedController} property allows to find out which of the controllers is currently + * selected.

+ * + * @extends TKController + * @since TuneKit 1.0 + * + * @param {Object} data A hash of properties to use as this object is initialized. + */ +function TKTabController (data) { + this._tabsSelector = null; + this._selectedController = null; + /** + * The controllers managed by this tab controller, ordered in the same way as the elements matched by {@link #tabsSelector} are. + * @type Array + */ + this.controllers = []; + this.tabs = []; + /** + * The delegate for this tab controller, an object implementing the {@link TKTabControllerDelegate} protocol. + * @type Object + */ + this.delegate = null; + this.previousController = null; + // + this.busy = false; + // + this.callSuper(data); +}; + +/* ==================== Additional View Processing ==================== */ + +TKTabController.prototype.processView = function () { + this.callSuper(); + // + this.host = this._view.appendChild(document.createElement('div')); + this.host.addClassName('tk-tab-contents'); + // restore properties that have not been set yet since construction + this.restoreProperty('tabsSelector'); + this.restoreProperty('selectedIndex'); +}; + +/* ==================== Synthetized Properties ==================== */ + +/** + * @name TKTabController.prototype + * @property {String} tabsSelector A CSS selector matching the elements within the controller's view that should act as triggers to display the matching + * controller in the {@link #controllers} array. + */ +TKTabController.prototype.setTabsSelector = function (tabsSelector) { + if (tabsSelector === null) { + return; + } + // forget the old tabs + for (var i = 0; i < this.tabs.length; i++) { + var element = this.tabs[i]; + element._tabIndex = undefined; + element._controller = undefined; + this.removeNavigableElement(element); + } + // get the new tabs + this._tabsSelector = tabsSelector; + this.tabs = this._view.querySelectorAll(this._tabsSelector); + // nothing to do if we don't have tabs + if (this.tabs.length < 1) { + return; + } + for (var i = 0; i < this.tabs.length; i++) { + var tab = this.tabs[i]; + tab._tabIndex = i; + tab._controller = this; + this.addNavigableElement(tab); + } + // reset to the first tab, unless we have an archived one + var archived_index = this.getArchivedProperty('selectedIndex'); + this.selectedIndex = (archived_index === undefined || archived_index < 0) ? 0 : archived_index; +}; + +/* ==================== Keyboard Handling ==================== */ + +TKTabController.prototype.getNavigableElements = function () { + return this._navigableElements.concat(this.selectedController.navigableElements); +}; + +TKTabController.prototype.handleEvent = function (event) { + this.callSuper(event); + // + if (event.currentTarget._tabIndex !== undefined) { + if (event.type == 'highlight') { + this.selectedIndex = event.currentTarget._tabIndex; + } + } +}; + +TKTabController.prototype.elementWasActivated = function (element) { + this.callSuper(element); + // tab was activated + if (element._tabIndex !== undefined) { + // highlight in case we weren't already + if (this.highlightedElement !== element) { + TKSpatialNavigationManager.sharedManager.highlightElement(element); + } + // notify we were activated + else { + this.tabAtIndexWasActivated(element._tabIndex); + } + } +}; + +/** + * Indicates that a new tab has been activated at the given index. + * + * @param {int} index The index for the tab that was just activated. + */ +TKTabController.prototype.tabAtIndexWasActivated = function (index) {}; + +/* ==================== Controllers ==================== */ + +/** + * @name TKTabController.prototype + * @property {int} selectedIndex The index of the selected tab. + */ +TKTabController.prototype.getSelectedIndex = function () { + return (this._selectedController === null) ? -1 : this.controllers.indexOf(this._selectedController); +}; + +TKTabController.prototype.setSelectedIndex = function (index) { + var selected_index = this.selectedIndex; + if (index !== selected_index && index >= 0 && index < this.controllers.length) { + // move to the new controller + this.selectedController = this.controllers[index]; + } +}; + +/** + * @name TKTabController.prototype + * @property {TKController} selectedController The selected controller. + */ +TKTabController.prototype.setSelectedController = function (controller) { + var selected_index = this.controllers.indexOf(controller); + // do nothing if we don't know about such a controller, or we're already on this controller + if (controller === this._selectedController || selected_index == -1) { + return; + } + // clean up before starting a new transition + if (this.busy) { + this.transitionDidComplete(null); + } + // + TKTransaction.begin(); + // mark that a transition is now in progress + this.busy = true; + // get pointers to object we'll manipulate + var previous_controller = this._selectedController; + var next_view = controller.view; + // fire delegate saying we're moving to a new controller + if (TKUtils.objectHasMethod(this.delegate, TKTabControllerWillShowController)) { + this.delegate[TKTabControllerWillShowController](this, controller); + } + // track this is our newly selected controller + this._selectedController = controller; + // notify of upcoming change and update tabs styling + if (previous_controller !== null) { + previous_controller._viewWillDisappear(); + previous_controller.viewWillDisappear(); + } + controller._viewWillAppear(); + controller.viewWillAppear(); + // add it to the tree + this.host.appendChild(controller.view); + // transition + var manager = TKSpatialNavigationManager.sharedManager; + if (previous_controller !== null) { + // unregister the old controller + previous_controller.parentController = null; + manager.unregisterController(previous_controller); + // remove the selected CSS class from the previous tab, if any + this.tabs[this.controllers.indexOf(previous_controller)].removeClassName(TKTabControllerSelectedCSSClass); + this.transitionToController(previous_controller, controller); + } + else { + this.busy = true; + this.transitionDidComplete(); + // highlight the element + if (this.highlightedElement === null) { + this.highlightedElement = this.tabs[this.selectedIndex] + } + } + // add the selected CSS class to the new controller's tab + this.tabs[selected_index].addClassName(TKTabControllerSelectedCSSClass) + // also ensure that element has highlight + manager.highlightElement(this.tabs[selected_index]); + // and that the new controller is registered for navigation + controller.parentController = this; + manager.registerController(controller); + // + TKTransaction.commit(); +}; + +/* ==================== Transition ==================== */ + +TKTabController.prototype.transitionToController = function (previous_controller, top_controller) { + // record some parameters that we will need at the end of the transition + this.previousController = previous_controller; + // figure out transitions + if (previous_controller !== null) { + previous_controller.view.applyTransition(previous_controller.becomesInactiveTransition, false); + } + var top_controller_transition = top_controller.becomesActiveTransition; + top_controller_transition.delegate = this; + top_controller.view.applyTransition(top_controller_transition, false); +}; + +TKTabController.prototype.transitionDidComplete = function (transition) { + // update the highlightable items and notify of completed change + if (this.previousController !== null) { + if (this.previousController.view.parentNode === this.host) { + this.host.removeChild(this.previousController.view); + } + this.previousController._viewDidDisappear(); + this.previousController.viewDidDisappear(); + } + this._selectedController._viewDidAppear(); + this._selectedController.viewDidAppear(); + // fire delegate saying we've moved to a new controller + if (TKUtils.objectHasMethod(this.delegate, TKTabControllerDidShowController)) { + this.delegate[TKTabControllerDidShowController](this, this._selectedController); + } + // not busy anymore + this.busy = false; +}; + +/* ==================== Archival ==================== */ + +TKTabController.prototype.archive = function () { + var archive = this.callSuper(); + archive.selectedIndex = this.selectedIndex; + return archive; +}; + +TKClass(TKTabController); +/* + * Copyright © 2009 Apple Inc. All rights reserved. + */ + +const TKSlideshowControllerContainerCSSClass = 'tk-slideshow-controller-view'; + +TKSlideshowController.inherits = TKController; +TKSlideshowController.synthetizes = ['slidingViewData', 'togglePlaybackButton', 'playing', 'currentSlideIndex', 'numberOfSlides', 'previousSlideButton', 'nextSlideButton']; + +/** + * @class + * + *

A slideshow controller plays through a collection of slides, also allowing to control the playback state and position of the slideshow. Control + * buttons are easily wired and remote-based navigation is completely automated.

+ * + * @extends TKController + * @since TuneKit 1.0 + * + * @param {Object} data A hash of properties to use as this object is initialized. + */ +function TKSlideshowController (data) { + // public properties + this._slidingViewData = null; + this._togglePlaybackButton = null; + this._previousSlideButton = null; + this._nextSlideButton = null; + this._playing = null; + /** + * Indicates whether the slideshow loops through for constant playback. Defaults to true. + * @type bool + */ + this.loops = true; + /** + * Indicates the duration in milliseconds each slide remains on screen. + * @type int + */ + this.interval = 3000; + this.timer = null; + this.willNeedToResume = true; + // set up the sliding view + this.slidingView = new TKSlidingView(); + this.slidingView.ready = false; + this.slidingView.delegate = this; + // + this.callSuper(data); +}; + +/* ==================== View Processing ==================== */ + +TKSlideshowController.prototype.processView = function () { + this.callSuper(); + // restore properties that have not been set yet since construction + this.restoreProperty('slidingViewData'); + this.restoreProperty('previousSlideButton'); + this.restoreProperty('nextSlideButton'); + this.restoreProperty('togglePlaybackButton'); + this.restoreProperty('currentSlideIndex'); + this.restoreProperty('playing'); + // add the sliding view and slide control containers + this.container = this._view.appendChild(document.createElement('div')); + this.container.addClassName(TKSlideshowControllerContainerCSSClass); + this.container.appendChild(this.slidingView.element); + // ensure our first slide gets told about its being highlighted + this.slideDidChange(this.currentSlideIndex); + // start playing by default + if (this._playing === null) { + this.willNeedToResume = true; + } +}; + +TKSlideshowController.prototype._viewDidAppear = function () { + if (this.willNeedToResume) { + this.playing = true; + } +}; + +TKSlideshowController.prototype._viewWillDisappear = function () { + this.willNeedToResume = this.playing; + this.playing = false; +}; + +/* ==================== Synthetized Properties ==================== */ + +/** + * @name TKSlideshowController.prototype + * @property {bool} playing Indicates the current playback state of the slideshow, defaults to true. + */ +TKSlideshowController.prototype.setPlaying = function (playing) { + if (this._playing == playing) { + return; + } + // pause + if (!playing) { + window.clearTimeout(this.timer); + } + // resume + else { + this.rewindTimer(); + } + // remember ivar + this._playing = playing; + // inform the playback state changed + this.playbackStateDidChange(); +}; + +/** + * @name TKSlideshowController.prototype + * @property {int} numberOfSlides Indicates how many slides total are in the slideshow. + */ +TKSlideshowController.prototype.getNumberOfSlides = function () { + return (!this.slidingView.ready) ? 0 : this.slidingView.numberOfElements; +}; + +/** + * @name TKSlideshowController.prototype + * @property {int} currentSlideIndex Indicates the index of the current slide. + */ +TKSlideshowController.prototype.getCurrentSlideIndex = function () { + return this.slidingView.activeElementIndex; +}; + +TKSlideshowController.prototype.setCurrentSlideIndex = function (index) { + if (index === null || !this.slidingView.ready) { + return; + } + // out of range + if (index < 0 || index >= this.numberOfSlides) { + return; + } + // update the slideshow index + this.slidingView.activeElementIndex = index; + // update timers if we're running + if (this.playing) { + // cancel the previous timer in case we still had one running + window.clearTimeout(this.timer); + // rewind it, and set playback to false in case we can't rewind any further + if (!this.rewindTimer()) { + this.playing = false; + } + } +}; + +/** + * @name TKSlideshowController.prototype + * @property {String} togglePlaybackButton A CSS selector matching a button to be used as the button to control the playback state. + */ +TKSlideshowController.prototype.setTogglePlaybackButton = function (togglePlaybackButton) { + if (togglePlaybackButton === null) { + return; + } + // forget old button + if (this._togglePlaybackButton) { + this._togglePlaybackButton.removeEventListener('click', this, false); + } + // process new one + this._togglePlaybackButton = this.view.querySelector(togglePlaybackButton); + if (this._togglePlaybackButton !== null) { + if (IS_APPLE_TV) { + this._togglePlaybackButton.style.display = 'none'; + } + else { + this._togglePlaybackButton.addEventListener('click', this, false); + } + } +}; + +/** + * @name TKSlideshowController.prototype + * @property {String} previousSlideButton A CSS selector matching a button to be used as the button to decrement the {@link #currentSlideIndex}. + */ +TKSlideshowController.prototype.setPreviousSlideButton = function (previousSlideButton) { + if (previousSlideButton === null) { + return; + } + // forget old button + if (this._previousSlideButton) { + this._previousSlideButton.removeEventListener('click', this, false); + } + // process new one + this._previousSlideButton = this.view.querySelector(previousSlideButton); + if (this._previousSlideButton !== null) { + if (IS_APPLE_TV) { + this._previousSlideButton.style.display = 'none'; + } + else { + this._previousSlideButton.addEventListener('click', this, false); + } + } +}; + +/** + * @name TKSlideshowController.prototype + * @property {String} nextSlideButton A CSS selector matching a button to be used as the button to increment the {@link #currentSlideIndex}. + */ +TKSlideshowController.prototype.setNextSlideButton = function (nextSlideButton) { + if (nextSlideButton === null) { + return; + } + // forget old button + if (this._nextSlideButton) { + this._nextSlideButton.removeEventListener('click', this, false); + } + // process new one + this._nextSlideButton = this.view.querySelector(nextSlideButton); + if (this._nextSlideButton !== null) { + if (IS_APPLE_TV) { + this._nextSlideButton.style.display = 'none'; + } + else { + this._nextSlideButton.addEventListener('click', this, false); + } + } +}; + +/** + * @name TKSlideshowController.prototype + * @property {TKSlidingViewData} slidingViewData The set of properties used to set up the contents of the page slider. + */ +TKSlideshowController.prototype.setSlidingViewData = function (data) { + if (data === null) { + return; + } + // set up the data source if we have .elements on the data object + if (!TKUtils.objectIsUndefined(data.elements)) { + this.slidingView.dataSource = new TKSlidingViewDataSourceHelper(data.elements); + delete data.element; + } + // see if we have some intersting bits to pass through + var archived_slide_index = this.getArchivedProperty('currentSlideIndex'); + if (archived_slide_index !== undefined) { + data.activeElementIndex = archived_slide_index; + } + // copy properties + TKUtils.copyPropertiesFromSourceToTarget(data, this.slidingView); + // init our view + this.slidingView.init(); + // + this.slidingView.ready = true; +}; + +/* ==================== Previous / Next Slide ==================== */ + +TKSlideshowController.prototype.attemptToGoToPreviousSlide = function () { + if (!this.loops && this.currentSlideIndex <= 0) { + TKSpatialNavigationManager.soundToPlay = SOUND_LIMIT; + } + else { + this.goToPreviousSlide(); + } +}; + +TKSlideshowController.prototype.attemptToGoToNextSlide = function () { + if (!this.loops && this.currentSlideIndex >= this.numberOfSlides - 1) { + TKSpatialNavigationManager.soundToPlay = SOUND_LIMIT; + } + else { + this.goToNextSlide(); + } +}; + +TKSlideshowController.prototype.goToPreviousSlide = function () { + this.currentSlideIndex = ((this.currentSlideIndex + this.numberOfSlides) - 1) % this.numberOfSlides; +}; + +TKSlideshowController.prototype.goToNextSlide = function () { + this.currentSlideIndex = (this.currentSlideIndex + 1) % this.numberOfSlides; +}; + +TKSlideshowController.prototype.rewindTimer = function () { + if (this.loops || this.currentSlideIndex < this.numberOfSlides - 1) { + var _this = this; + this.timer = window.setTimeout(function () { + _this.goToNextSlide(); + }, this.interval); + return true; + } + else { + return false; + } +}; + +/* ==================== Keyboard Navigation ==================== */ + +TKSlideshowController.prototype.wantsToHandleKey = function (key) { + return (key == KEYBOARD_LEFT || key == KEYBOARD_RIGHT || key == KEYBOARD_RETURN) ? true : this.callSuper(key); +}; + +TKSlideshowController.prototype.keyWasPressed = function (key) { + // default action is move, so wire that up + TKSpatialNavigationManager.soundToPlay = SOUND_MOVED; + // left should go to the previous slide + if (key == KEYBOARD_LEFT) { + this.attemptToGoToPreviousSlide(); + } + // right should go to the next slide + else if (key == KEYBOARD_RIGHT) { + this.attemptToGoToNextSlide(); + } + // return key should toggle playback + else if (key == KEYBOARD_RETURN) { + TKSpatialNavigationManager.soundToPlay = SOUND_ACTIVATED; + this.playing = !this.playing; + } + // let the default behavior happen too + this.callSuper(key); +}; + +TKSlideshowController.prototype.elementWasActivated = function (element) { + // toggle playback button pressed + if (element === this._togglePlaybackButton) { + this.playing = !this.playing; + } + // previous slide button pressed + else if (element === this._previousSlideButton) { + this.attemptToGoToPreviousSlide(); + } + // next slide button pressed + else if (element === this._nextSlideButton) { + this.attemptToGoToNextSlide(); + } + // fall back to default behavior + else { + this.callSuper(element); + } +}; + +/* ==================== TKSlidingView Protocol ==================== */ + +TKSlideshowController.prototype.slidingViewDidFocusElementAtIndex = function (view, index) { + this.slideDidChange(index); +}; + +TKSlideshowController.prototype.slidingViewStyleForItemAtIndex = function (view, index) { + return this.styleForSlideAtIndex(index); +}; + +/* ==================== Placeholder Methods ==================== */ + +/** + * Triggered when the playback state has changed. + */ +TKSlideshowController.prototype.playbackStateDidChange = function () {}; + +/** + * Triggered when the {@link #currentSlideIndex} property has changed. + * + * @param {int} index The index of the current slide. + */ +TKSlideshowController.prototype.slideDidChange = function (index) {}; + +/** + * This method allows to provide custom style rules for a slide programatically any time the {@link #currentSlideIndex} property changes. The values in this + * array are expected to be individual two-value arrays, where the first index holds the CSS property name, and the second index its value. + * + * @param {Array} index The index of the slide for which we are trying to obtain custom styles. + */ +TKSlideshowController.prototype.styleForSlideAtIndex = function (index) { + return []; +}; + +/* ==================== Archival ==================== */ + +TKSlideshowController.prototype.archive = function () { + var archive = this.callSuper(); + archive.currentSlideIndex = this.currentSlideIndex; + return archive; +}; + +TKClass(TKSlideshowController); +/* + * Copyright © 2009 Apple Inc. All rights reserved. + */ + +// XXX: this should be set dynamically as needed +const ARCHIVES_AND_RESTORES = false; + +var bookletController = new TKObject(); + +bookletController.init = function () { + // stop current playback + window.iTunes.stop(); + + // set up audio loop + this.startAudioLoop(); + + // determine play position if this a movie + if (appData.feature.XID){ + appData.feature.trackObj = this.getTrack(appData.feature); + if (appData.feature.trackObj && appData.feature.trackObj.bookmark != 0){ + this.dispatchDisplayUpdates(); + } + } + + // add event listeners + window.addEventListener("play", this, false); + window.addEventListener("pause", this, false); + window.addEventListener("videoclosed", this, false); + window.addEventListener("unload", this, false); + + // check if we have an archive to restore from + this.restoreFromJSON(); + + // create our navigation controller and add the home controller as the root controller + this.navigation = new TKNavigationController({ + id : 'navigation', + rootController : homeController, + delegate : this + }); +}; + +bookletController.archiveToJSON = function () { + if (!ARCHIVES_AND_RESTORES) { + return; + } + // + var archive = { + controllers: {} + }; + // first, get the navigation stack of controllers + var controllers_stack_IDs = []; + for (var i = 0; i < this.navigation.controllers.length; i++) { + controllers_stack_IDs.push(this.navigation.controllers[i].id); + } + archive.navigationStack = controllers_stack_IDs; + // then archive all known controllers that aren't the navigation stack + for (var i in TKController.controllers) { + if (i == 'navigation') { + continue; + } + archive.controllers[i] = TKController.controllers[i].archive(); + } + // archive is complete, now save it to disk + window.localStorage.archive = JSON.stringify(archive); +}; + +bookletController.restoreFromJSON = function () { + if (!ARCHIVES_AND_RESTORES) { + return; + } + // check if we have an archive to restore from + var json_archive = window.localStorage.archive; + if (json_archive === undefined) { + return; + } + // parse the JSON archive into a JS object that we track at all times + this.archive = JSON.parse(json_archive); +}; + +bookletController.handleEvent = function(event) { + switch (event.type) { + case "play": + // video has started to play - stop the audio loop + this.stopAudioLoop(); + //this.dispatchDisplayUpdates(); + break; + case "pause": + // video has paused + //this.dispatchDisplayUpdates(); + break; + case "videoclosed": + // video has stopped - restart audio loop + this.startAudioLoop(); + this.dispatchDisplayUpdates(); + + // stub to check what chapter we are on for sticky chapters + debug(iTunes.currentChapter); + break; + case "unload": + // archive our state + this.archiveToJSON(); + break; + default: + debug("Unknown event type in bookletController : " + event.type); + } +}; + +bookletController.startAudioLoop = function() { + + // if the loop exists already, we're being asked to resume + // check if we're set to do so, and if not, exit without starting playback + if (this.audioLoop && appData.audioLoop && !appData.audioLoop.loop) { + this.audioLoop.pause(); + this.audioLoop.volume = 0; + return; + } + + // create the loop if it doesn't exist yet + if (appData.audioLoop && !this.audioLoop) { + this.audioLoop = new Audio(); + this.audioLoop.src = appData.audioLoop.src; + // make sure this background audio is never displayed + this.audioLoop.style.display = "none"; + // add it to the document so that iTunes will notice it is there + document.body.appendChild(this.audioLoop); + this.audioLoop.volume = 0; + } + + if (this.audioLoop) { + this.audioLoop.loop = appData.audioLoop.loop; + this.audioLoop.volume = Math.min(1, window.iTunes.volume); + this.audioLoop.play(); + } +}; + +bookletController.stopAudioLoop = function() { + if (this.audioLoop) { + this.audioLoop.pause(); + this.audioLoop.loop = false; + this.audioLoop.currentTime = 0; // reset to beginning + } +}; + +bookletController.playbackHasStarted = function (){ + return (this.trackBookmark(appData.feature) == 0) ? false : true; +} + +bookletController.play = function (trackObj) { + var track = this.getTrack(trackObj); + + if (track != null){ + iTunes.stop(); + track.play(); + } +}; + +bookletController.playFeature = function (){ + bookletController.play(appData.feature); +}; + +bookletController.resumeFeature = function(){ + bookletController.playFeature(); +}; + +bookletController.playChapter = function (index){ + iTunes.stop(); + if (appData.feature.trackObj != null){ + appData.feature.trackObj.play({startChapterIndex : index}); + } +}; + +bookletController.getChapter = function (){ + if (appData.feature.trackObj && appData.feature.trackObj.chapters){ + if ((iTunes.currentChapter == 0 && appData.feature.trackObj.bookmark != 0) || IS_APPLE_TV){ + var estimatedChapter = Math.floor((appData.feature.trackObj.bookmark / appData.feature.trackObj.duration) * appData.feature.trackObj.chapters.length); + var actualChapter = -1; + + if ((appData.feature.trackObj.chapters[estimatedChapter].startOffsetTime / 1000) == appData.feature.trackObj.bookmark){ + } else if ((appData.feature.trackObj.chapters[estimatedChapter].startOffsetTime / 1000) < appData.feature.trackObj.bookmark){ + while (estimatedChapter < appData.feature.trackObj.chapters.length && (appData.feature.trackObj.chapters[estimatedChapter].startOffsetTime / 1000) < appData.feature.trackObj.bookmark){ + estimatedChapter++; + } + } else if ((appData.feature.trackObj.chapters[estimatedChapter].startOffsetTime / 1000) > appData.feature.trackObj.bookmark){ + while (estimatedChapter >= 0 && (appData.feature.trackObj.chapters[estimatedChapter].startOffsetTime / 1000) > appData.feature.trackObj.bookmark){ + estimatedChapter--; + } + } + actualChapter = estimatedChapter; + debug("GET CHAPTER: estimating: " + actualChapter); + return actualChapter; + } else { + debug("GET CHAPTER: itunes query: " + iTunes.currentChapter); + return iTunes.currentChapter; + } + } else { + return -1; + } +} + +bookletController.buildPlaylist = function (tracks){ + var tracklistObj = iTunes.createTempPlaylist(); + var tracklist = []; + + for (var i = 0; i < tracks.length; i++){ + var track = this.getTrack(tracks[i]); + if (track != null){ + tracklist.push(track); + } + } + + tracklistObj.addTracks(tracklist); + debug("added " + tracklist.length + " of " + tracks.length + " tracks successfully."); + return tracklistObj; +}; + +bookletController.buildNonLibraryPlaylist = function (tracks){ + var tracklistObj = iTunes.createTempPlaylist(); + var tracklist = []; + + for (var i = 0; i < tracks.length; i++){ + var track = {}; + track.url = "videos/" + tracks[i].src; + track.title = tracks[i].string; + track.artist = appData.feature.artist; + track.album = appData.feature.title; + debug("adding: " + track.title + " (" + track.url + ")"); + tracklist.push(track); + } + + debug("pushing to tracklistOb"); + tracklistObj.addURLs(tracklist); + return tracklistObj; +}; + +bookletController.trackDuration = function (trackObj){ + var track = this.getTrack(trackObj); + if (track != null){ + debug("querying duration"); + return track.durationAsString; + } else { + return "0:00"; + } +}; + +bookletController.trackNumber = function (trackObj){ + var track = this.getTrack(trackObj); + if (track != null){ + debug("querying track number"); + return track.trackNumber; + } else { + return "0"; + } +}; + +bookletController.trackBookmark = function (trackObj){ + var track = this.getTrack(trackObj); + if (track != null){ + debug("querying bookmark"); + return track.bookmark; + } else { + return "0"; + } +}; + +bookletController.getTrack = function (trackObj){ + if (trackObj.XID){ + debug("searching by XID: " + trackObj.XID); + var iTunesTrack = window.iTunes.findTracksByXID(trackObj.XID); + if (iTunesTrack.length > 0){ + debug("found by XID"); + return iTunesTrack[0]; + } else { + debug("XID not found in library"); + return null; + } + } else { + debug("no XID"); + return null; + } +}; + +bookletController.playNonLibraryContent = function (trackObj){ + debug("videos/" + trackObj.src); + debug({title : trackObj.string, artist : appData.feature.artist, album : appData.feature.title}); + window.iTunes.play("videos/" + trackObj.src, {title : trackObj.string, artist : appData.feature.artist, album : appData.feature.title}); +}; + +bookletController.childControllers = []; + +bookletController.registerForDisplayUpdates = function (childController) { + if (this.childControllers.indexOf(childController) == -1) { + this.childControllers.push(childController); + } +}; + +bookletController.dispatchDisplayUpdates = function () { + for (var i=0; i < this.childControllers.length; i++) { + if (TKUtils.objectIsFunction(this.childControllers[i].updateDisplay)) { + this.childControllers[i].updateDisplay(); + } + } +}; + +/* ================= iTunes Emulation ======================== */ + +var iTunesEmulator = { + volume : 1, + platform : 'Emulator', + version : '-1' +}; + +iTunesEmulator.play = function (mediaLocation) { + debug("iTunesEmulator - play: " + mediaLocation); +}; + +iTunesEmulator.stop = function () { + debug("iTunesEmulator - stop"); +}; + +iTunesEmulator.findTracksByStoreID = function (storeID) { + debug("iTunesEmulator - findTracksByStoreID: " + storeID); + return [new ITunesTrackEmulator()]; +}; + +iTunesEmulator.findTracksByXID = function (xID) { + debug("iTunesEmulator - findTracksByXID: " + xID); + return [new ITunesTrackEmulator()]; +}; + +iTunesEmulator.createTempPlaylist = function () { + return { + tracks: [], + addTracks: function () {} + }; +}; + +function ITunesTrackEmulator() { +} + +ITunesTrackEmulator.prototype.play = function (params) { + debug("iTunesTrackEmulator - play: " + params); + // fake the play event to the window + var event = document.createEvent("HTMLEvents"); + event.initEvent("play", false, false); + window.dispatchEvent(event); + setTimeout(function() { + debug("iTunesTrackEmulator - coming back from playback"); + event = document.createEvent("HTMLEvents"); + event.initEvent("videoclosed", false, false); + window.dispatchEvent(event); + }, 5000); +}; + +/* ================= Initialisation ======================== */ + +window.addEventListener('load', function () { + // override select start + document.onselectstart = function() { return false; }; + // check for iTunes object, create a dummy if it doesn't exist + if (!window.iTunes) { + window.iTunes = iTunesEmulator; + } + // init the booklet controller + bookletController.init(); +}, false); diff --git a/compile.sh b/compile.sh new file mode 100644 index 0000000..f79ab19 --- /dev/null +++ b/compile.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash + +BABEL=./node_modules/babel-cli/bin/babel.js +SASS=./node_modules/node-sass/bin/node-sass +MUSTACHE=./node_modules/mustache/bin/mustache + +DATA_FILE=data.json + +SOURCE_ROOT=src +TARGET_DIR="iTunes Extras.ite" + +rm -r "${TARGET_DIR}" +mkdir -p "${TARGET_DIR}" + +cp -R TuneKit/ "${TARGET_DIR}/TuneKit/" + +for file in $(find ${SOURCE_ROOT} -type f) +do + echo -n "Compiling $file..." + dir=$(dirname "$file") + filename=$(basename "$file") + extension="${filename##*.}" + filename="${filename%.*}" + filename="${filename%.*}" # For double extensions + + if [[ $filename == _* ]]; then + echo -en "\033[0;33m" + echo " Skipping" + echo -en "\033[0;00m" + continue + fi + + targetDir="${TARGET_DIR}${dir#${SOURCE_ROOT}}" + partialTarget="${targetDir}/${filename}" + + mkdir -p "$targetDir" + + case "$file" in + *.xml.mustache ) + ${MUSTACHE} "${DATA_FILE}" "${file}" "${partialTarget}.xml" + ;; + *.plist.mustache ) + ${MUSTACHE} "${DATA_FILE}" "${file}" "${partialTarget}.plist" + ;; + *.js.mustache ) + ${MUSTACHE} "${DATA_FILE}" "${file}" | ${BABEL} --out-file "${partialTarget}.js" --presets env + ;; + *.js ) + ${BABEL} "${file}" --out-file "${partialTarget}.js" --presets env + ;; + *.scss ) + ${SASS} --no-cache --output-style compressed "$file" > "${partialTarget}.css" + ;; + *) + cp "$file" "${partialTarget}.${extension}" + ;; + esac + echo -en "\033[0;92m" + echo " Done" + echo -en "\033[0;00m" +done + +echo -n "Copying Assets... " +cp -R assets/ "${TARGET_DIR}/" +echo -en "\033[0;92m" +echo " Done" +echo -en "\033[0;00m" \ No newline at end of file diff --git a/data.json b/data.json new file mode 100644 index 0000000..65f0d2f --- /dev/null +++ b/data.json @@ -0,0 +1,76 @@ +{ + "XID": "TEST:uuid:5F861BFF-C205-4F6C-94F0-2FEEF662404D", + "movieID": 982749837, + "extrasID": 530706165, + "extrasVersion": "1.0", + "extrasBuildNumber": 42, + "meta": { + "title": "König Ödipus", + "artist": "Sven Schütze", + "description": "Mit nur neun Requisiten und rasanten Rollenwechseln erzählt Bodo Wartke die Geschichte des Ödipus, Sohn des Laios, König von Theben, der unwissend seinen eigenen Vater tötet. Und später, als Belohnung dafür, dass er Theben von der Sphinx befreit, Iokaste, die Witwe des Königs und damit seine eigene Mutter, zur Ehefrau erhält. Ein Solo-Theater mit dem Klavierkabarettisten in allen 14 Rollen! Bodo Wartkes exzellente Darbietung seiner Bühnenfassung der klassischen Tragödie „König Ödipus“ bietet einen barrierefreien Einstieg in einen zu Recht berühmten Sagenstoff und vereint Komödie und Tragödie zu einem fantastischen und unvergesslichen Theaterabend – so macht Bildung Spaß! (aus dem Pressetext).", + "longDescription": "Mit nur neun Requisiten und rasanten Rollenwechseln erzählt Bodo Wartke die Geschichte des Ödipus, Sohn des Laios, König von Theben, der unwissend seinen eigenen Vater tötet. Und später, als Belohnung dafür, dass er Theben von der Sphinx befreit, Iokaste, die Witwe des Königs und damit seine eigene Mutter, zur Ehefrau erhält. Ein Solo-Theater mit dem Klavierkabarettisten in allen 14 Rollen! Bodo Wartkes exzellente Darbietung seiner Bühnenfassung der klassischen Tragödie „König Ödipus“ bietet einen barrierefreien Einstieg in einen zu Recht berühmten Sagenstoff und vereint Komödie und Tragödie zu einem fantastischen und unvergesslichen Theaterabend – so macht Bildung Spaß! (aus dem Pressetext).", + "genre": "Comedy", + "releaseDate": "2010-04-11T00:00:00Z", + "year": "2010", + "studio": "ReimKultur", + "sort-name": "König Ödipus" + }, + "features": [ + { + "title": "Gespräch I", + "description": "Katrin Jäger spricht mit Bodo Wartke, Sven Schütze und Carmen Kalisch über die Entsehung von „König Ödipus“, die Adaptionsarbeit, die Proben, über dramaturgische und schauspielerische Herausforderungen.", + "src": "Gespraech_I.m4v", + "imageName": "Gespraech_I.png", + "duration": "43:03" + }, + { + "title": "Interview mit Polybos", + "description": "Katrin Jäger spricht mit Bodo Wartke über die Figur des Polybos.", + "src": "Interview_mit_Polybos.m4v", + "imageName": "Interview_mit_Polybos.png", + "duration": "0:50" + }, + { + "title": "Gespräch II", + "description": "Katrin Jäger spricht mit Bodo Wartke, Sven Schütze und Carmen Kalisch tiefergehend über die Problematik des Schicksalsbegriffs und den Umgang damit in Bodo Wartkes „König Ödipus“.", + "src": "Gespraech_II.m4v", + "imageName": "Gespraech_II.png", + "duration": "28:02" + }, + { + "title": "Gespräche III", + "description": "Carl der Löwe plaudert mit Katrin Jäger über seine langjährige Karriere im Showgeschäft und seine Rolle als Sphinx im „König Ödipus“.", + "src": "Gespraech_III.m4v", + "imageName": "Gespraech_III.png", + "duration": "07:02" + }, + { + "title": "Das Making-of", + "description": "Wie „König Ödipus“ auf die DVD kam.", + "src": "Making-of.m4v", + "imageName": "Making-of.png", + "duration": "43:10" + }, + { + "title": "Episode IV: Ödipus und Teiresias", + "description": "Die erste von Bodo verfasste Szene des Dramas, hier in der Fassung der DVD zu „Ich denke, also sing’ ich“, aufgezeichnet 2004 im Werk9 in Berlin.", + "src": "Episode_IV.m4v", + "imageName": "Episode_IV.png", + "duration": "9:41" + }, + { + "title": "Finale Mortale", + "description": "Anlässlich der gemeinsamen „United Slapstick Show“ erweiterte Bodo im Jahr 2000 das bis dahin als alleinstehende Zugabe existierende Ende des Stückes um etliche Rollen und Verse.", + "src": "Finale_Mortale.m4v", + "imageName": "Finale_Mortale.png", + "duration": "10:12" + }, + { + "title": "Antigone", + "description": "Das Sequel zu „König Ödipus“.", + "src": "Antigone.m4v", + "imageName": "Antigone.png", + "duration": "01:32" + } + ] +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..0291835 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3246 @@ +{ + "name": "iTunes Extras Template", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "abbrev": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.0.tgz", + "integrity": "sha1-0FVMIlZjbi9W58LlrRg/hZQo2B8=" + }, + "ajv": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.2.3.tgz", + "integrity": "sha1-wG9Zh3jETGsWGrr+NGa4GtGBTtI=", + "requires": { + "co": "4.6.0", + "fast-deep-equal": "1.0.0", + "json-schema-traverse": "0.3.1", + "json-stable-stringify": "1.0.1" + } + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=" + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "anymatch": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", + "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", + "requires": { + "micromatch": "2.3.11", + "normalize-path": "2.1.1" + } + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + }, + "are-we-there-yet": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", + "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", + "requires": { + "delegates": "1.0.0", + "readable-stream": "2.3.3" + } + }, + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "requires": { + "arr-flatten": "1.1.0" + } + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" + }, + "array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=" + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=" + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "async-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", + "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=" + }, + "async-foreach": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", + "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", + "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" + }, + "babel-cli": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-cli/-/babel-cli-6.26.0.tgz", + "integrity": "sha1-UCq1SHTX24itALiHoGODzgPQAvE=", + "dev": true, + "requires": { + "babel-core": "6.26.0", + "babel-polyfill": "6.26.0", + "babel-register": "6.26.0", + "babel-runtime": "6.26.0", + "chokidar": "1.7.0", + "commander": "2.11.0", + "convert-source-map": "1.5.0", + "fs-readdir-recursive": "1.0.0", + "glob": "7.1.2", + "lodash": "4.17.4", + "output-file-sync": "1.1.2", + "path-is-absolute": "1.0.1", + "slash": "1.0.0", + "source-map": "0.5.7", + "v8flags": "2.1.1" + } + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "requires": { + "chalk": "1.1.3", + "esutils": "2.0.2", + "js-tokens": "3.0.2" + } + }, + "babel-core": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.0.tgz", + "integrity": "sha1-rzL3izGm/O8RnIew/Y2XU/A6C7g=", + "requires": { + "babel-code-frame": "6.26.0", + "babel-generator": "6.26.0", + "babel-helpers": "6.24.1", + "babel-messages": "6.23.0", + "babel-register": "6.26.0", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "convert-source-map": "1.5.0", + "debug": "2.6.9", + "json5": "0.5.1", + "lodash": "4.17.4", + "minimatch": "3.0.4", + "path-is-absolute": "1.0.1", + "private": "0.1.7", + "slash": "1.0.0", + "source-map": "0.5.7" + } + }, + "babel-generator": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.0.tgz", + "integrity": "sha1-rBriAHC3n248odMmlhMFN3TyDcU=", + "requires": { + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "detect-indent": "4.0.0", + "jsesc": "1.3.0", + "lodash": "4.17.4", + "source-map": "0.5.7", + "trim-right": "1.0.1" + } + }, + "babel-helper-builder-binary-assignment-operator-visitor": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz", + "integrity": "sha1-zORReto1b0IgvK6KAsKzRvmlZmQ=", + "requires": { + "babel-helper-explode-assignable-expression": "6.24.1", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-call-delegate": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz", + "integrity": "sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340=", + "requires": { + "babel-helper-hoist-variables": "6.24.1", + "babel-runtime": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-define-map": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz", + "integrity": "sha1-pfVtq0GiX5fstJjH66ypgZ+Vvl8=", + "requires": { + "babel-helper-function-name": "6.24.1", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "lodash": "4.17.4" + } + }, + "babel-helper-explode-assignable-expression": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz", + "integrity": "sha1-8luCz33BBDPFX3BZLVdGQArCLKo=", + "requires": { + "babel-runtime": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-function-name": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", + "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=", + "requires": { + "babel-helper-get-function-arity": "6.24.1", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-get-function-arity": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", + "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=", + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-hoist-variables": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz", + "integrity": "sha1-HssnaJydJVE+rbyZFKc/VAi+enY=", + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-optimise-call-expression": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz", + "integrity": "sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc=", + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-regex": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz", + "integrity": "sha1-MlxZ+QL4LyS3T6zu0DY5VPZJXnI=", + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "lodash": "4.17.4" + } + }, + "babel-helper-remap-async-to-generator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz", + "integrity": "sha1-XsWBgnrXI/7N04HxySg5BnbkVRs=", + "requires": { + "babel-helper-function-name": "6.24.1", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-replace-supers": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz", + "integrity": "sha1-v22/5Dk40XNpohPKiov3S2qQqxo=", + "requires": { + "babel-helper-optimise-call-expression": "6.24.1", + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helpers": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", + "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=", + "requires": { + "babel-runtime": "6.26.0", + "babel-template": "6.26.0" + } + }, + "babel-messages": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-check-es2015-constants": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz", + "integrity": "sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=", + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-syntax-async-functions": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz", + "integrity": "sha1-ytnK0RkbWtY0vzCuCHI5HgZHvpU=" + }, + "babel-plugin-syntax-exponentiation-operator": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz", + "integrity": "sha1-nufoM3KQ2pUoggGmpX9BcDF4MN4=" + }, + "babel-plugin-syntax-trailing-function-commas": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz", + "integrity": "sha1-ugNgk3+NBuQBgKQ/4NVhb/9TLPM=" + }, + "babel-plugin-transform-async-to-generator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz", + "integrity": "sha1-ZTbjeK/2yx1VF6wOQOs+n8jQh2E=", + "requires": { + "babel-helper-remap-async-to-generator": "6.24.1", + "babel-plugin-syntax-async-functions": "6.13.0", + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-arrow-functions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz", + "integrity": "sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE=", + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-block-scoped-functions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz", + "integrity": "sha1-u8UbSflk1wy42OC5ToICRs46YUE=", + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-block-scoping": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz", + "integrity": "sha1-1w9SmcEwjQXBL0Y4E7CgnnOxiV8=", + "requires": { + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "lodash": "4.17.4" + } + }, + "babel-plugin-transform-es2015-classes": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz", + "integrity": "sha1-WkxYpQyclGHlZLSyo7+ryXolhNs=", + "requires": { + "babel-helper-define-map": "6.26.0", + "babel-helper-function-name": "6.24.1", + "babel-helper-optimise-call-expression": "6.24.1", + "babel-helper-replace-supers": "6.24.1", + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-plugin-transform-es2015-computed-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz", + "integrity": "sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM=", + "requires": { + "babel-runtime": "6.26.0", + "babel-template": "6.26.0" + } + }, + "babel-plugin-transform-es2015-destructuring": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz", + "integrity": "sha1-mXux8auWf2gtKwh2/jWNYOdlxW0=", + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-duplicate-keys": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz", + "integrity": "sha1-c+s9MQypaePvnskcU3QabxV2Qj4=", + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-plugin-transform-es2015-for-of": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz", + "integrity": "sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE=", + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-function-name": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz", + "integrity": "sha1-g0yJhTvDaxrw86TF26qU/Y6sqos=", + "requires": { + "babel-helper-function-name": "6.24.1", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-plugin-transform-es2015-literals": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz", + "integrity": "sha1-T1SgLWzWbPkVKAAZox0xklN3yi4=", + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-modules-amd": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz", + "integrity": "sha1-Oz5UAXI5hC1tGcMBHEvS8AoA0VQ=", + "requires": { + "babel-plugin-transform-es2015-modules-commonjs": "6.26.0", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0" + } + }, + "babel-plugin-transform-es2015-modules-commonjs": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.0.tgz", + "integrity": "sha1-DYOUApt9xqvhqX7xgeAHWN0uXYo=", + "requires": { + "babel-plugin-transform-strict-mode": "6.24.1", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-plugin-transform-es2015-modules-systemjs": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz", + "integrity": "sha1-/4mhQrkRmpBhlfXxBuzzBdlAfSM=", + "requires": { + "babel-helper-hoist-variables": "6.24.1", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0" + } + }, + "babel-plugin-transform-es2015-modules-umd": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz", + "integrity": "sha1-rJl+YoXNGO1hdq22B9YCNErThGg=", + "requires": { + "babel-plugin-transform-es2015-modules-amd": "6.24.1", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0" + } + }, + "babel-plugin-transform-es2015-object-super": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz", + "integrity": "sha1-JM72muIcuDp/hgPa0CH1cusnj40=", + "requires": { + "babel-helper-replace-supers": "6.24.1", + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-parameters": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz", + "integrity": "sha1-V6w1GrScrxSpfNE7CfZv3wpiXys=", + "requires": { + "babel-helper-call-delegate": "6.24.1", + "babel-helper-get-function-arity": "6.24.1", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-plugin-transform-es2015-shorthand-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz", + "integrity": "sha1-JPh11nIch2YbvZmkYi5R8U3jiqA=", + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-plugin-transform-es2015-spread": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz", + "integrity": "sha1-1taKmfia7cRTbIGlQujdnxdG+NE=", + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-sticky-regex": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz", + "integrity": "sha1-AMHNsaynERLN8M9hJsLta0V8zbw=", + "requires": { + "babel-helper-regex": "6.26.0", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-plugin-transform-es2015-template-literals": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz", + "integrity": "sha1-qEs0UPfp+PH2g51taH2oS7EjbY0=", + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-typeof-symbol": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz", + "integrity": "sha1-3sCfHN3/lLUqxz1QXITfWdzOs3I=", + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-unicode-regex": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz", + "integrity": "sha1-04sS9C6nMj9yk4fxinxa4frrNek=", + "requires": { + "babel-helper-regex": "6.26.0", + "babel-runtime": "6.26.0", + "regexpu-core": "2.0.0" + } + }, + "babel-plugin-transform-exponentiation-operator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz", + "integrity": "sha1-KrDJx/MJj6SJB3cruBP+QejeOg4=", + "requires": { + "babel-helper-builder-binary-assignment-operator-visitor": "6.24.1", + "babel-plugin-syntax-exponentiation-operator": "6.13.0", + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-regenerator": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz", + "integrity": "sha1-4HA2lvveJ/Cj78rPi03KL3s6jy8=", + "requires": { + "regenerator-transform": "0.10.1" + } + }, + "babel-plugin-transform-strict-mode": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz", + "integrity": "sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=", + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-polyfill": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz", + "integrity": "sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM=", + "requires": { + "babel-runtime": "6.26.0", + "core-js": "2.5.1", + "regenerator-runtime": "0.10.5" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", + "integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg=" + } + } + }, + "babel-preset-env": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/babel-preset-env/-/babel-preset-env-1.6.0.tgz", + "integrity": "sha512-OVgtQRuOZKckrILgMA5rvctvFZPv72Gua9Rt006AiPoB0DJKGN07UmaQA+qRrYgK71MVct8fFhT0EyNWYorVew==", + "dev": true, + "requires": { + "babel-plugin-check-es2015-constants": "6.22.0", + "babel-plugin-syntax-trailing-function-commas": "6.22.0", + "babel-plugin-transform-async-to-generator": "6.24.1", + "babel-plugin-transform-es2015-arrow-functions": "6.22.0", + "babel-plugin-transform-es2015-block-scoped-functions": "6.22.0", + "babel-plugin-transform-es2015-block-scoping": "6.26.0", + "babel-plugin-transform-es2015-classes": "6.24.1", + "babel-plugin-transform-es2015-computed-properties": "6.24.1", + "babel-plugin-transform-es2015-destructuring": "6.23.0", + "babel-plugin-transform-es2015-duplicate-keys": "6.24.1", + "babel-plugin-transform-es2015-for-of": "6.23.0", + "babel-plugin-transform-es2015-function-name": "6.24.1", + "babel-plugin-transform-es2015-literals": "6.22.0", + "babel-plugin-transform-es2015-modules-amd": "6.24.1", + "babel-plugin-transform-es2015-modules-commonjs": "6.26.0", + "babel-plugin-transform-es2015-modules-systemjs": "6.24.1", + "babel-plugin-transform-es2015-modules-umd": "6.24.1", + "babel-plugin-transform-es2015-object-super": "6.24.1", + "babel-plugin-transform-es2015-parameters": "6.24.1", + "babel-plugin-transform-es2015-shorthand-properties": "6.24.1", + "babel-plugin-transform-es2015-spread": "6.22.0", + "babel-plugin-transform-es2015-sticky-regex": "6.24.1", + "babel-plugin-transform-es2015-template-literals": "6.22.0", + "babel-plugin-transform-es2015-typeof-symbol": "6.23.0", + "babel-plugin-transform-es2015-unicode-regex": "6.24.1", + "babel-plugin-transform-exponentiation-operator": "6.24.1", + "babel-plugin-transform-regenerator": "6.26.0", + "browserslist": "2.4.0", + "invariant": "2.2.2", + "semver": "5.4.1" + } + }, + "babel-preset-es2015": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz", + "integrity": "sha1-1EBQ1rwsn+6nAqrzjXJ6AhBTiTk=", + "dev": true, + "requires": { + "babel-plugin-check-es2015-constants": "6.22.0", + "babel-plugin-transform-es2015-arrow-functions": "6.22.0", + "babel-plugin-transform-es2015-block-scoped-functions": "6.22.0", + "babel-plugin-transform-es2015-block-scoping": "6.26.0", + "babel-plugin-transform-es2015-classes": "6.24.1", + "babel-plugin-transform-es2015-computed-properties": "6.24.1", + "babel-plugin-transform-es2015-destructuring": "6.23.0", + "babel-plugin-transform-es2015-duplicate-keys": "6.24.1", + "babel-plugin-transform-es2015-for-of": "6.23.0", + "babel-plugin-transform-es2015-function-name": "6.24.1", + "babel-plugin-transform-es2015-literals": "6.22.0", + "babel-plugin-transform-es2015-modules-amd": "6.24.1", + "babel-plugin-transform-es2015-modules-commonjs": "6.26.0", + "babel-plugin-transform-es2015-modules-systemjs": "6.24.1", + "babel-plugin-transform-es2015-modules-umd": "6.24.1", + "babel-plugin-transform-es2015-object-super": "6.24.1", + "babel-plugin-transform-es2015-parameters": "6.24.1", + "babel-plugin-transform-es2015-shorthand-properties": "6.24.1", + "babel-plugin-transform-es2015-spread": "6.22.0", + "babel-plugin-transform-es2015-sticky-regex": "6.24.1", + "babel-plugin-transform-es2015-template-literals": "6.22.0", + "babel-plugin-transform-es2015-typeof-symbol": "6.23.0", + "babel-plugin-transform-es2015-unicode-regex": "6.24.1", + "babel-plugin-transform-regenerator": "6.26.0" + } + }, + "babel-register": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", + "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=", + "requires": { + "babel-core": "6.26.0", + "babel-runtime": "6.26.0", + "core-js": "2.5.1", + "home-or-tmp": "2.0.0", + "lodash": "4.17.4", + "mkdirp": "0.5.1", + "source-map-support": "0.4.18" + } + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "requires": { + "core-js": "2.5.1", + "regenerator-runtime": "0.11.0" + } + }, + "babel-template": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", + "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", + "requires": { + "babel-runtime": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "lodash": "4.17.4" + } + }, + "babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", + "requires": { + "babel-code-frame": "6.26.0", + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "debug": "2.6.9", + "globals": "9.18.0", + "invariant": "2.2.2", + "lodash": "4.17.4" + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "requires": { + "babel-runtime": "6.26.0", + "esutils": "2.0.2", + "lodash": "4.17.4", + "to-fast-properties": "1.0.3" + } + }, + "babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==" + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "requires": { + "tweetnacl": "0.14.5" + } + }, + "binary-extensions": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.10.0.tgz", + "integrity": "sha1-muuabF6IY4qtFx4Wf1kAq+JINdA=" + }, + "block-stream": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", + "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", + "requires": { + "inherits": "2.0.3" + } + }, + "boom": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", + "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", + "requires": { + "hoek": "4.2.0" + } + }, + "brace-expansion": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", + "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "requires": { + "expand-range": "1.8.2", + "preserve": "0.2.0", + "repeat-element": "1.1.2" + } + }, + "browserslist": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-2.4.0.tgz", + "integrity": "sha512-aM2Gt4x9bVlCUteADBS6JP0F+2tMWKM1jQzUulVROtdFWFIcIVvY76AJbr7GDqy0eDhn+PcnpzzivGxY4qiaKQ==", + "requires": { + "caniuse-lite": "1.0.30000738", + "electron-to-chromium": "1.3.22" + } + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=" + }, + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=" + }, + "camelcase-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "requires": { + "camelcase": "2.1.1", + "map-obj": "1.0.1" + } + }, + "caniuse-lite": { + "version": "1.0.30000738", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000738.tgz", + "integrity": "sha1-GCDDya25oRfjEaW9yh0lvDQojro=" + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "chokidar": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", + "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", + "requires": { + "anymatch": "1.3.2", + "async-each": "1.0.1", + "fsevents": "1.1.2", + "glob-parent": "2.0.0", + "inherits": "2.0.3", + "is-binary-path": "1.0.1", + "is-glob": "2.0.1", + "path-is-absolute": "1.0.1", + "readdirp": "2.1.0" + } + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wrap-ansi": "2.1.0" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, + "combined-stream": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", + "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", + "requires": { + "delayed-stream": "1.0.0" + } + }, + "commander": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", + "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + }, + "convert-source-map": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.0.tgz", + "integrity": "sha1-ms1whRxtXf3ZPZKC5e35SgP/RrU=" + }, + "core-js": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.1.tgz", + "integrity": "sha1-rmh03GaTd4m4B1T/VCjfZoGcpQs=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cross-spawn": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", + "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=", + "requires": { + "lru-cache": "4.1.1", + "which": "1.3.0" + } + }, + "cryptiles": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", + "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", + "requires": { + "boom": "5.2.0" + }, + "dependencies": { + "boom": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", + "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", + "requires": { + "hoek": "4.2.0" + } + } + } + }, + "currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "requires": { + "array-find-index": "1.0.2" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "1.0.0" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + }, + "detect-indent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", + "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", + "requires": { + "repeating": "2.0.1" + } + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "requires": { + "jsbn": "0.1.1" + } + }, + "electron-to-chromium": { + "version": "1.3.22", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.22.tgz", + "integrity": "sha1-QyLVLBUUBuPq73StAmdog+hBZBg=" + }, + "error-ex": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", + "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", + "requires": { + "is-arrayish": "0.2.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "requires": { + "is-posix-bracket": "0.1.1" + } + }, + "expand-range": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", + "requires": { + "fill-range": "2.2.3" + } + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "requires": { + "is-extglob": "1.0.0" + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fast-deep-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz", + "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=" + }, + "filename-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", + "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=" + }, + "fill-range": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz", + "integrity": "sha1-ULd9/X5Gm8dJJHCWNpn+eoSFpyM=", + "requires": { + "is-number": "2.1.0", + "isobject": "2.1.0", + "randomatic": "1.1.7", + "repeat-element": "1.1.2", + "repeat-string": "1.6.1" + } + }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "requires": { + "path-exists": "2.1.0", + "pinkie-promise": "2.0.1" + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" + }, + "for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "requires": { + "for-in": "1.0.2" + } + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz", + "integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=", + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.5", + "mime-types": "2.1.17" + } + }, + "fs-readdir-recursive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.0.0.tgz", + "integrity": "sha1-jNF0XItPiinIyuw5JHaSG6GV9WA=" + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fsevents": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.2.tgz", + "integrity": "sha512-Sn44E5wQW4bTHXvQmvSHwqbuiXtduD6Rrjm2ZtUEGbyrig+nUH3t/QD4M4/ZXViY556TBpRgZkHLDx3JxPwxiw==", + "requires": { + "nan": "2.7.0", + "node-pre-gyp": "0.6.36" + }, + "dependencies": { + "abbrev": { + "version": "1.1.0", + "bundled": true + }, + "ajv": { + "version": "4.11.8", + "bundled": true, + "requires": { + "co": "4.6.0", + "json-stable-stringify": "1.0.1" + } + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true + }, + "aproba": { + "version": "1.1.1", + "bundled": true + }, + "are-we-there-yet": { + "version": "1.1.4", + "bundled": true, + "requires": { + "delegates": "1.0.0", + "readable-stream": "2.2.9" + } + }, + "asn1": { + "version": "0.2.3", + "bundled": true + }, + "assert-plus": { + "version": "0.2.0", + "bundled": true + }, + "asynckit": { + "version": "0.4.0", + "bundled": true + }, + "aws-sign2": { + "version": "0.6.0", + "bundled": true + }, + "aws4": { + "version": "1.6.0", + "bundled": true + }, + "balanced-match": { + "version": "0.4.2", + "bundled": true + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "bundled": true, + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, + "block-stream": { + "version": "0.0.9", + "bundled": true, + "requires": { + "inherits": "2.0.3" + } + }, + "boom": { + "version": "2.10.1", + "bundled": true, + "requires": { + "hoek": "2.16.3" + } + }, + "brace-expansion": { + "version": "1.1.7", + "bundled": true, + "requires": { + "balanced-match": "0.4.2", + "concat-map": "0.0.1" + } + }, + "buffer-shims": { + "version": "1.0.0", + "bundled": true + }, + "caseless": { + "version": "0.12.0", + "bundled": true + }, + "co": { + "version": "4.6.0", + "bundled": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true + }, + "combined-stream": { + "version": "1.0.5", + "bundled": true, + "requires": { + "delayed-stream": "1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "bundled": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true + }, + "cryptiles": { + "version": "2.0.5", + "bundled": true, + "requires": { + "boom": "2.10.1" + } + }, + "dashdash": { + "version": "1.14.1", + "bundled": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true + } + } + }, + "debug": { + "version": "2.6.8", + "bundled": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.4.2", + "bundled": true + }, + "delayed-stream": { + "version": "1.0.0", + "bundled": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true + }, + "ecc-jsbn": { + "version": "0.1.1", + "bundled": true, + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "extend": { + "version": "3.0.1", + "bundled": true + }, + "extsprintf": { + "version": "1.0.2", + "bundled": true + }, + "forever-agent": { + "version": "0.6.1", + "bundled": true + }, + "form-data": { + "version": "2.1.4", + "bundled": true, + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.5", + "mime-types": "2.1.15" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true + }, + "fstream": { + "version": "1.0.11", + "bundled": true, + "requires": { + "graceful-fs": "4.1.11", + "inherits": "2.0.3", + "mkdirp": "0.5.1", + "rimraf": "2.6.1" + } + }, + "fstream-ignore": { + "version": "1.0.5", + "bundled": true, + "requires": { + "fstream": "1.0.11", + "inherits": "2.0.3", + "minimatch": "3.0.4" + } + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "requires": { + "aproba": "1.1.1", + "console-control-strings": "1.1.0", + "has-unicode": "2.0.1", + "object-assign": "4.1.1", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.2" + } + }, + "getpass": { + "version": "0.1.7", + "bundled": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true + } + } + }, + "glob": { + "version": "7.1.2", + "bundled": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "graceful-fs": { + "version": "4.1.11", + "bundled": true + }, + "har-schema": { + "version": "1.0.5", + "bundled": true + }, + "har-validator": { + "version": "4.2.1", + "bundled": true, + "requires": { + "ajv": "4.11.8", + "har-schema": "1.0.5" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true + }, + "hawk": { + "version": "3.1.3", + "bundled": true, + "requires": { + "boom": "2.10.1", + "cryptiles": "2.0.5", + "hoek": "2.16.3", + "sntp": "1.0.9" + } + }, + "hoek": { + "version": "2.16.3", + "bundled": true + }, + "http-signature": { + "version": "1.1.1", + "bundled": true, + "requires": { + "assert-plus": "0.2.0", + "jsprim": "1.4.0", + "sshpk": "1.13.0" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true + }, + "ini": { + "version": "1.3.4", + "bundled": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-typedarray": { + "version": "1.0.0", + "bundled": true + }, + "isarray": { + "version": "1.0.0", + "bundled": true + }, + "isstream": { + "version": "0.1.2", + "bundled": true + }, + "jodid25519": { + "version": "1.0.2", + "bundled": true, + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "jsbn": { + "version": "0.1.1", + "bundled": true, + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "bundled": true + }, + "json-stable-stringify": { + "version": "1.0.1", + "bundled": true, + "requires": { + "jsonify": "0.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "bundled": true + }, + "jsonify": { + "version": "0.0.0", + "bundled": true + }, + "jsprim": { + "version": "1.4.0", + "bundled": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.0.2", + "json-schema": "0.2.3", + "verror": "1.3.6" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true + } + } + }, + "mime-db": { + "version": "1.27.0", + "bundled": true + }, + "mime-types": { + "version": "2.1.15", + "bundled": true, + "requires": { + "mime-db": "1.27.0" + } + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "requires": { + "brace-expansion": "1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true + }, + "node-pre-gyp": { + "version": "0.6.36", + "bundled": true, + "requires": { + "mkdirp": "0.5.1", + "nopt": "4.0.1", + "npmlog": "4.1.0", + "rc": "1.2.1", + "request": "2.81.0", + "rimraf": "2.6.1", + "semver": "5.3.0", + "tar": "2.2.1", + "tar-pack": "3.4.0" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "requires": { + "abbrev": "1.1.0", + "osenv": "0.1.4" + } + }, + "npmlog": { + "version": "4.1.0", + "bundled": true, + "requires": { + "are-we-there-yet": "1.1.4", + "console-control-strings": "1.1.0", + "gauge": "2.7.4", + "set-blocking": "2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true + }, + "oauth-sign": { + "version": "0.8.2", + "bundled": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true + }, + "osenv": { + "version": "0.1.4", + "bundled": true, + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true + }, + "performance-now": { + "version": "0.2.0", + "bundled": true + }, + "process-nextick-args": { + "version": "1.0.7", + "bundled": true + }, + "punycode": { + "version": "1.4.1", + "bundled": true + }, + "qs": { + "version": "6.4.0", + "bundled": true + }, + "rc": { + "version": "1.2.1", + "bundled": true, + "requires": { + "deep-extend": "0.4.2", + "ini": "1.3.4", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true + } + } + }, + "readable-stream": { + "version": "2.2.9", + "bundled": true, + "requires": { + "buffer-shims": "1.0.0", + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "string_decoder": "1.0.1", + "util-deprecate": "1.0.2" + } + }, + "request": { + "version": "2.81.0", + "bundled": true, + "requires": { + "aws-sign2": "0.6.0", + "aws4": "1.6.0", + "caseless": "0.12.0", + "combined-stream": "1.0.5", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.1.4", + "har-validator": "4.2.1", + "hawk": "3.1.3", + "http-signature": "1.1.1", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.15", + "oauth-sign": "0.8.2", + "performance-now": "0.2.0", + "qs": "6.4.0", + "safe-buffer": "5.0.1", + "stringstream": "0.0.5", + "tough-cookie": "2.3.2", + "tunnel-agent": "0.6.0", + "uuid": "3.0.1" + } + }, + "rimraf": { + "version": "2.6.1", + "bundled": true, + "requires": { + "glob": "7.1.2" + } + }, + "safe-buffer": { + "version": "5.0.1", + "bundled": true + }, + "semver": { + "version": "5.3.0", + "bundled": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true + }, + "sntp": { + "version": "1.0.9", + "bundled": true, + "requires": { + "hoek": "2.16.3" + } + }, + "sshpk": { + "version": "1.13.0", + "bundled": true, + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jodid25519": "1.0.2", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true + } + } + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "string_decoder": { + "version": "1.0.1", + "bundled": true, + "requires": { + "safe-buffer": "5.0.1" + } + }, + "stringstream": { + "version": "0.0.5", + "bundled": true + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true + }, + "tar": { + "version": "2.2.1", + "bundled": true, + "requires": { + "block-stream": "0.0.9", + "fstream": "1.0.11", + "inherits": "2.0.3" + } + }, + "tar-pack": { + "version": "3.4.0", + "bundled": true, + "requires": { + "debug": "2.6.8", + "fstream": "1.0.11", + "fstream-ignore": "1.0.5", + "once": "1.4.0", + "readable-stream": "2.2.9", + "rimraf": "2.6.1", + "tar": "2.2.1", + "uid-number": "0.0.6" + } + }, + "tough-cookie": { + "version": "2.3.2", + "bundled": true, + "requires": { + "punycode": "1.4.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "bundled": true, + "requires": { + "safe-buffer": "5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "bundled": true, + "optional": true + }, + "uid-number": { + "version": "0.0.6", + "bundled": true + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true + }, + "uuid": { + "version": "3.0.1", + "bundled": true + }, + "verror": { + "version": "1.3.6", + "bundled": true, + "requires": { + "extsprintf": "1.0.2" + } + }, + "wide-align": { + "version": "1.1.2", + "bundled": true, + "requires": { + "string-width": "1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true + } + } + }, + "fstream": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", + "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", + "requires": { + "graceful-fs": "4.1.11", + "inherits": "2.0.3", + "mkdirp": "0.5.1", + "rimraf": "2.6.2" + } + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "requires": { + "aproba": "1.2.0", + "console-control-strings": "1.1.0", + "has-unicode": "2.0.1", + "object-assign": "4.1.1", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.2" + } + }, + "gaze": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.2.tgz", + "integrity": "sha1-hHIkZ3rbiHDWeSV+0ziP22HkAQU=", + "requires": { + "globule": "1.2.0" + } + }, + "get-caller-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", + "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=" + }, + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=" + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "1.0.0" + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "requires": { + "glob-parent": "2.0.0", + "is-glob": "2.0.1" + } + }, + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "requires": { + "is-glob": "2.0.1" + } + }, + "globals": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", + "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==" + }, + "globule": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.0.tgz", + "integrity": "sha1-HcScaCLdnoovoAuiopUAboZkvQk=", + "requires": { + "glob": "7.1.2", + "lodash": "4.17.4", + "minimatch": "3.0.4" + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", + "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", + "requires": { + "ajv": "5.2.3", + "har-schema": "2.0.0" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "requires": { + "ansi-regex": "2.1.1" + } + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + }, + "hawk": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", + "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", + "requires": { + "boom": "4.3.1", + "cryptiles": "3.1.2", + "hoek": "4.2.0", + "sntp": "2.0.2" + } + }, + "hoek": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz", + "integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ==" + }, + "home-or-tmp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", + "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=", + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "hosted-git-info": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz", + "integrity": "sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg==" + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "1.0.0", + "jsprim": "1.4.1", + "sshpk": "1.13.1" + } + }, + "in-publish": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz", + "integrity": "sha1-4g/146KvwmkDILbcVSaCqcf631E=" + }, + "indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "requires": { + "repeating": "2.0.1" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "invariant": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.2.tgz", + "integrity": "sha1-nh9WrArNtr8wMwbzOL47IErmA2A=", + "requires": { + "loose-envify": "1.3.1" + } + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "requires": { + "binary-extensions": "1.10.0" + } + }, + "is-buffer": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.5.tgz", + "integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=" + }, + "is-builtin-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "requires": { + "builtin-modules": "1.1.1" + } + }, + "is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=" + }, + "is-equal-shallow": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", + "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "requires": { + "is-primitive": "2.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=" + }, + "is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "requires": { + "is-extglob": "1.0.0" + } + }, + "is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "requires": { + "kind-of": "3.2.2" + } + }, + "is-posix-bracket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", + "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=" + }, + "is-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", + "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=" + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "requires": { + "isarray": "1.0.0" + } + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "js-base64": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.3.2.tgz", + "integrity": "sha512-Y2/+DnfJJXT1/FCwUebUhLWb3QihxiSC42+ctHLGogmW2jPY6LCapMdFZXRvVP2z6qyKW7s6qncE/9gSqZiArw==" + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=" + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "jsesc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", + "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=" + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "requires": { + "jsonify": "0.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=" + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.5" + } + }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "requires": { + "invert-kv": "1.0.0" + } + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "2.2.0", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "strip-bom": "2.0.0" + } + }, + "lodash": { + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" + }, + "lodash.assign": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", + "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=" + }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" + }, + "lodash.mergewith": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.0.tgz", + "integrity": "sha1-FQzwoWeR9ZA7iJHqsVRgknS96lU=" + }, + "loose-envify": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", + "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", + "requires": { + "js-tokens": "3.0.2" + } + }, + "loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "requires": { + "currently-unhandled": "0.4.1", + "signal-exit": "3.0.2" + } + }, + "lru-cache": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz", + "integrity": "sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==", + "requires": { + "pseudomap": "1.0.2", + "yallist": "2.1.2" + } + }, + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=" + }, + "meow": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "requires": { + "camelcase-keys": "2.1.0", + "decamelize": "1.2.0", + "loud-rejection": "1.6.0", + "map-obj": "1.0.1", + "minimist": "1.2.0", + "normalize-package-data": "2.4.0", + "object-assign": "4.1.1", + "read-pkg-up": "1.0.1", + "redent": "1.0.0", + "trim-newlines": "1.0.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + } + } + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "requires": { + "arr-diff": "2.0.0", + "array-unique": "0.2.1", + "braces": "1.8.5", + "expand-brackets": "0.1.5", + "extglob": "0.3.2", + "filename-regex": "2.0.1", + "is-extglob": "1.0.0", + "is-glob": "2.0.1", + "kind-of": "3.2.2", + "normalize-path": "2.1.1", + "object.omit": "2.0.1", + "parse-glob": "3.0.4", + "regex-cache": "0.4.4" + } + }, + "mime-db": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", + "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=" + }, + "mime-types": { + "version": "2.1.17", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", + "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", + "requires": { + "mime-db": "1.30.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "1.1.8" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "mustache": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-2.3.0.tgz", + "integrity": "sha1-QCj3d4sXcIpImTCm5SrDvKDaQdA=", + "dev": true + }, + "nan": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.7.0.tgz", + "integrity": "sha1-2Vv3IeyHfgjbJ27T/G63j5CDrUY=" + }, + "node-gyp": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.6.2.tgz", + "integrity": "sha1-m/vlRWIoYoSDjnUOrAUpWFP6HGA=", + "requires": { + "fstream": "1.0.11", + "glob": "7.1.2", + "graceful-fs": "4.1.11", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "nopt": "3.0.6", + "npmlog": "4.1.2", + "osenv": "0.1.4", + "request": "2.83.0", + "rimraf": "2.6.2", + "semver": "5.3.0", + "tar": "2.2.1", + "which": "1.3.0" + }, + "dependencies": { + "semver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=" + } + } + }, + "node-sass": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.5.3.tgz", + "integrity": "sha1-0JydEXlkEjnRuX/8YjH9zsU+FWg=", + "dev": true, + "requires": { + "async-foreach": "0.1.3", + "chalk": "1.1.3", + "cross-spawn": "3.0.1", + "gaze": "1.1.2", + "get-stdin": "4.0.1", + "glob": "7.1.2", + "in-publish": "2.0.0", + "lodash.assign": "4.2.0", + "lodash.clonedeep": "4.5.0", + "lodash.mergewith": "4.6.0", + "meow": "3.7.0", + "mkdirp": "0.5.1", + "nan": "2.7.0", + "node-gyp": "3.6.2", + "npmlog": "4.1.2", + "request": "2.83.0", + "sass-graph": "2.2.4", + "stdout-stream": "1.4.0" + } + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "requires": { + "abbrev": "1.1.0" + } + }, + "normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "requires": { + "hosted-git-info": "2.5.0", + "is-builtin-module": "1.0.0", + "semver": "5.4.1", + "validate-npm-package-license": "3.0.1" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "requires": { + "remove-trailing-separator": "1.1.0" + } + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "requires": { + "are-we-there-yet": "1.1.4", + "console-control-strings": "1.1.0", + "gauge": "2.7.4", + "set-blocking": "2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object.omit": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", + "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", + "requires": { + "for-own": "0.1.5", + "is-extendable": "0.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1.0.2" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" + }, + "os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "requires": { + "lcid": "1.0.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + }, + "osenv": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.4.tgz", + "integrity": "sha1-Qv5tWVPfBsgGS+bxdsPQWqqjRkQ=", + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "output-file-sync": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/output-file-sync/-/output-file-sync-1.1.2.tgz", + "integrity": "sha1-0KM+7+YaIF+suQCS6CZZjVJFznY=", + "requires": { + "graceful-fs": "4.1.11", + "mkdirp": "0.5.1", + "object-assign": "4.1.1" + } + }, + "parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "requires": { + "glob-base": "0.3.0", + "is-dotfile": "1.0.3", + "is-extglob": "1.0.0", + "is-glob": "2.0.1" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "requires": { + "error-ex": "1.3.1" + } + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "requires": { + "pinkie-promise": "2.0.1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "requires": { + "graceful-fs": "4.1.11", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "requires": { + "pinkie": "2.0.4" + } + }, + "preserve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", + "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=" + }, + "private": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/private/-/private-0.1.7.tgz", + "integrity": "sha1-aM5eih7woju1cMwoU3tTMqumPvE=" + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "qs": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" + }, + "randomatic": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", + "integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==", + "requires": { + "is-number": "3.0.0", + "kind-of": "4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.5" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "requires": { + "is-buffer": "1.1.5" + } + } + } + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "requires": { + "load-json-file": "1.1.0", + "normalize-package-data": "2.4.0", + "path-type": "1.1.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "requires": { + "find-up": "1.1.2", + "read-pkg": "1.1.0" + } + }, + "readable-stream": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "safe-buffer": "5.1.1", + "string_decoder": "1.0.3", + "util-deprecate": "1.0.2" + } + }, + "readdirp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", + "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", + "requires": { + "graceful-fs": "4.1.11", + "minimatch": "3.0.4", + "readable-stream": "2.3.3", + "set-immediate-shim": "1.0.1" + } + }, + "redent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", + "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "requires": { + "indent-string": "2.1.0", + "strip-indent": "1.0.1" + } + }, + "regenerate": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.3.3.tgz", + "integrity": "sha512-jVpo1GadrDAK59t/0jRx5VxYWQEDkkEKi6+HjE3joFVLfDOh9Xrdh0dF1eSq+BI/SwvTQ44gSscJ8N5zYL61sg==" + }, + "regenerator-runtime": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.0.tgz", + "integrity": "sha512-/aA0kLeRb5N9K0d4fw7ooEbI+xDe+DKD499EQqygGqeS8N3xto15p09uY2xj7ixP81sNPXvRLnAQIqdVStgb1A==" + }, + "regenerator-transform": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.10.1.tgz", + "integrity": "sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q==", + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "private": "0.1.7" + } + }, + "regex-cache": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", + "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", + "requires": { + "is-equal-shallow": "0.1.3" + } + }, + "regexpu-core": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-2.0.0.tgz", + "integrity": "sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA=", + "requires": { + "regenerate": "1.3.3", + "regjsgen": "0.2.0", + "regjsparser": "0.1.5" + } + }, + "regjsgen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", + "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=" + }, + "regjsparser": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", + "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", + "requires": { + "jsesc": "0.5.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=" + } + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" + }, + "repeat-element": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", + "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=" + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "requires": { + "is-finite": "1.0.2" + } + }, + "request": { + "version": "2.83.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz", + "integrity": "sha512-lR3gD69osqm6EYLk9wB/G1W/laGWjzH90t1vEa2xuxHD5KUrSzp9pUSfTm+YC5Nxt2T8nMPEvKlhbQayU7bgFw==", + "requires": { + "aws-sign2": "0.7.0", + "aws4": "1.6.0", + "caseless": "0.12.0", + "combined-stream": "1.0.5", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.3.1", + "har-validator": "5.0.3", + "hawk": "6.0.2", + "http-signature": "1.2.0", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.17", + "oauth-sign": "0.8.2", + "performance-now": "2.1.0", + "qs": "6.5.1", + "safe-buffer": "5.1.1", + "stringstream": "0.0.5", + "tough-cookie": "2.3.3", + "tunnel-agent": "0.6.0", + "uuid": "3.1.0" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "requires": { + "glob": "7.1.2" + } + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + }, + "sass-graph": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz", + "integrity": "sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=", + "requires": { + "glob": "7.1.2", + "lodash": "4.17.4", + "scss-tokenizer": "0.2.3", + "yargs": "7.1.0" + } + }, + "scss-tokenizer": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", + "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=", + "requires": { + "js-base64": "2.3.2", + "source-map": "0.4.4" + }, + "dependencies": { + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "requires": { + "amdefine": "1.0.1" + } + } + } + }, + "semver": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", + "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==" + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=" + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=" + }, + "sntp": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.0.2.tgz", + "integrity": "sha1-UGQRDwr4X3z9t9a2ekACjOUrSys=", + "requires": { + "hoek": "4.2.0" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "source-map-support": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", + "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", + "requires": { + "source-map": "0.5.7" + } + }, + "spdx-correct": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz", + "integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=", + "requires": { + "spdx-license-ids": "1.2.2" + } + }, + "spdx-expression-parse": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz", + "integrity": "sha1-m98vIOH0DtRH++JzJmGR/O1RYmw=" + }, + "spdx-license-ids": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz", + "integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=" + }, + "sshpk": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", + "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + } + }, + "stdout-stream": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.0.tgz", + "integrity": "sha1-osfIWH5U2UJ+qe2zrD8s1SLfN4s=", + "requires": { + "readable-stream": "2.3.3" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "requires": { + "safe-buffer": "5.1.1" + } + }, + "stringstream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "requires": { + "is-utf8": "0.2.1" + } + }, + "strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "requires": { + "get-stdin": "4.0.1" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + }, + "tar": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", + "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", + "requires": { + "block-stream": "0.0.9", + "fstream": "1.0.11", + "inherits": "2.0.3" + } + }, + "to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=" + }, + "tough-cookie": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz", + "integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=", + "requires": { + "punycode": "1.4.1" + } + }, + "trim-newlines": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=" + }, + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=" + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "5.1.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, + "user-home": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/user-home/-/user-home-1.1.1.tgz", + "integrity": "sha1-K1viOjK2Onyd640PKNSFcko98ZA=" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "uuid": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", + "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" + }, + "v8flags": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-2.1.1.tgz", + "integrity": "sha1-qrGh+jDUX4jdMhFIh1rALAtV5bQ=", + "requires": { + "user-home": "1.1.1" + } + }, + "validate-npm-package-license": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz", + "integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=", + "requires": { + "spdx-correct": "1.0.2", + "spdx-expression-parse": "1.0.4" + } + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "1.3.0" + } + }, + "which": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", + "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", + "requires": { + "isexe": "2.0.0" + } + }, + "which-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", + "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=" + }, + "wide-align": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", + "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", + "requires": { + "string-width": "1.0.2" + } + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + }, + "yargs": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz", + "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=", + "requires": { + "camelcase": "3.0.0", + "cliui": "3.2.0", + "decamelize": "1.2.0", + "get-caller-file": "1.0.2", + "os-locale": "1.4.0", + "read-pkg-up": "1.0.1", + "require-directory": "2.1.1", + "require-main-filename": "1.0.1", + "set-blocking": "2.0.0", + "string-width": "1.0.2", + "which-module": "1.0.0", + "y18n": "3.2.1", + "yargs-parser": "5.0.0" + }, + "dependencies": { + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=" + } + } + }, + "yargs-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz", + "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=", + "requires": { + "camelcase": "3.0.0" + }, + "dependencies": { + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=" + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..57b9f87 --- /dev/null +++ b/package.json @@ -0,0 +1,10 @@ +{ + "name": "iTunes Extras Template", + "version": "1.0.0", + "devDependencies": { + "babel-cli": "^6.26.0", + "babel-preset-env": "^1.6.0", + "mustache": "^2.3.0", + "node-sass": "^4.5.3" + } +} diff --git a/src/controllers/chapters.js b/src/controllers/chapters.js new file mode 100644 index 0000000..b7da43a --- /dev/null +++ b/src/controllers/chapters.js @@ -0,0 +1,74 @@ +const chaptersController = new TKPageSliderController({ + id: 'chapters', + previousPageButton: '.carousel-previous', + nextPageButton: '.carousel-next', + outlets: [{name: 'label', selector: '.carousel-label'}], + title: _('Chapters') +}); + +chaptersController.viewDidLoad = function () { + this.view.appendChild(TKUtils.buildElement({ + type: 'emptyDiv', + className: 'carousel-previous button' + })); + this.view.appendChild(TKUtils.buildElement({ + type: 'emptyDiv', + className: 'carousel-next button' + })); + this.view.appendChild(TKUtils.buildElement({ + type: 'emptyDiv', + className: 'carousel-label' + })); + this.slidingViewData = { + sideElementsVisible: 4, + distanceBetweenElements: 800, + sideOffsetBefore: 0, + sideOffsetAfter: 0, + elements: this.createThumbnails(), + incrementalLoading: true + }; + + bookletController.registerForDisplayUpdates(this); +}; + +chaptersController.viewDidAppear = function () { + this.updateDisplay(); +}; + +chaptersController.updateDisplay = function () { + if (this.highlightedPageIndex !== (bookletController.getChapter() - 1)) { + this.highlightedPageIndex = bookletController.getChapter() - 1; + } +}; + +/* ==================== Creating Pages ==================== */ + +chaptersController.createThumbnails = function () { + let elements = []; + for (let i = 1; i <= appData.chapters.length; i++) { + let padded_index = (i < 10) ? '0' + i : i; + let url = 'images/chapters/chapter' + padded_index + '.jpg'; + elements.push({ + type: 'container', + children: [{ + type: 'container', children: [ + {type: 'image', src: url}, + {type: 'image', src: 'images/interface/buttonPlayCircle.png'} + ] + }] + }); + } + return elements; +}; + +/* ==================== Pages Navigation ==================== */ + +// called when the user focuses another page +chaptersController.pageWasHighlighted = function (index) { + this.label.textContent = appData.chapters[index]; +}; + +// called when the user activates the focused pages +chaptersController.pageWasSelected = function (index) { + bookletController.playChapter(index + 1); +}; \ No newline at end of file diff --git a/src/controllers/data.js.mustache b/src/controllers/data.js.mustache new file mode 100644 index 0000000..59023b9 --- /dev/null +++ b/src/controllers/data.js.mustache @@ -0,0 +1,25 @@ +let appData = { + feature: { + XID: "{{{ XID }}}", + title: "{{{ meta.title }}}", + artist: "{{{ meta.artist }}}" + }, + audioLoop: { + src: "audio/background.m4a", + loop: true + }, + chapters: [ + {{#chapters}}"{{{.}}}",{{/chapters}} + ], + features: [ + {{#features}} + { + title: "{{{title}}}", + description: "{{{description}}}", + src: "{{{src}}}", + imageName: "{{{imageName}}}", + duration: "{{{duration}}}" + }, + {{/features}} + ] +}; \ No newline at end of file diff --git a/src/controllers/features.js b/src/controllers/features.js new file mode 100644 index 0000000..540a448 --- /dev/null +++ b/src/controllers/features.js @@ -0,0 +1,87 @@ +let featuresHelper = {}; + +featuresHelper.createFeatureController = function (feature, index) { + let controller = new TKController({ + id: 'selection-' + index + }); + controller.viewDidLoad = function () { + this.view.classList.add('selection'); + this.view.appendChild(TKUtils.buildElement({ + type: 'container', + children: [ + { + type: 'image', + src: 'images/' + feature.imageName + }, { + type: 'image', + src: 'images/interface/buttonPlayCircle.png', + className: 'play-overlay' + }] + })); + this.view.appendChild(TKUtils.buildElement({ + type: 'div', + className: 'title', + content: feature.title + })); + this.view.appendChild(TKUtils.buildElement({ + type: 'div', + className: 'description', + content: feature.description + })); + this.view.querySelector('.play-overlay').addEventListener('click', () => { + console.log(feature); + bookletController.playNonLibraryContent({src: feature.src, string: feature.title}); + }) + }; + return controller; +} +; + +let featuresController = new TKTabController({ + id: 'features', + tabsSelector: '.list .list-item', + controllers: appData.features.map(featuresHelper.createFeatureController), + title: _('Features') +}); + +featuresController.viewDidLoad = function () { + const vignetteElement = TKUtils.buildElement({ + type: 'emptyDiv', + className: 'vignette' + }); + this.view.appendChild(vignetteElement); + this.view.addClassName('list-on-right'); + this.buildFeaturesList(appData.features); +}; + +featuresController.buildFeaturesList = function (features) { + let featureElements = features.map(feature => { + return { + type: 'container', + className: 'list-item', + children: [ + {type: 'image', src: 'images/' + feature.imageName}, + { + type: 'container', + className: 'list-item-body', + children: [ + {type: 'div', className: 'title', content: feature.title}, + {type: 'div', className: 'duration', content: formatTime(feature.duration)} + ] + } + ] + } + }); + this.view.appendChild(TKUtils.buildElement({ + type: 'container', + className: 'list', + children: [ + {type: 'div', className: 'list-title', content: _('Features')}, + { + type: 'container', + className: 'list-content', + children: featureElements + } + ] + })); +}; \ No newline at end of file diff --git a/src/controllers/home.js b/src/controllers/home.js new file mode 100644 index 0000000..c24aa71 --- /dev/null +++ b/src/controllers/home.js @@ -0,0 +1,32 @@ +const homeController = new TKController({ + id: 'home', + actions: [ + {selector: '.play', action: bookletController.playFeature} + ], + navigatesTo: [ + {selector: '.features', controller: 'features'} + ], + highlightedElement: '.play' +}); + +homeController.viewDidLoad = function () { + let mainMenuItems = []; + mainMenuItems.push({ + type: 'div', + className: 'play button', + content: _('Play') + }); + if (typeof appData.features !== 'undefined' && appData.features.length > 0) { + mainMenuItems.push({ + type: 'div', + className: 'features button', + content: _('Features') + }) + } + + this.view.appendChild(TKUtils.buildElement({ + type: 'container', + className: 'main-menu', + children: mainMenuItems + })); +}; diff --git a/src/controllers/navigation.js b/src/controllers/navigation.js new file mode 100644 index 0000000..239eac8 --- /dev/null +++ b/src/controllers/navigation.js @@ -0,0 +1,31 @@ +window.addEventListener('load', () => { + let backButtonElement = document.querySelector('#header .back'); + backButtonElement.innerHTML = _('Back'); + TKNavigationController.sharedNavigation.backButton = '.back'; +}); + +bookletController.navigationControllerWillShowController = function (navigationController, controller) { + function showBackgroundForController() { + let imagePath = 'images/'; + if (navigationController.rootController !== controller) { + imagePath += controller.id + '/' + } + imagePath += 'background.png'; + controller.view.style.backgroundImage = "url('" + imagePath + "')"; + } + + function toggleHeader() { + let headerElement = document.getElementById('header'); + let titleElement = headerElement.querySelector('.title'); + if (controller === navigationController.rootController) { + headerElement.addClassName('hidden'); + } else { + titleElement.innerHTML = controller.title; + headerElement.removeClassName('hidden'); + } + } + + toggleHeader(); + showBackgroundForController(); +}; + diff --git a/src/controllers/strings.js b/src/controllers/strings.js new file mode 100644 index 0000000..c4d1089 --- /dev/null +++ b/src/controllers/strings.js @@ -0,0 +1,37 @@ +let STRINGS_DE = { + "Back": "Zurück", + "Play": "Wiedergabe", + "Chapters": "Kapitel", + "Features": "Bonusmaterial", + "Hr.": "Std.", + "Min.": "Min.", + "Sec.": "Sek." +}; + +function _(key) { + if (key in STRINGS_DE) { + return STRINGS_DE[key] + } else { + return key + } +} + +function formatTime(timeString) { + const components = timeString.split(':').map(Number); + let formattedComponents = []; + if (components[0] > 0) { + formattedComponents.push(components[0]); + const name = components.length === 3 ? _('Hr.') : _('Min.'); + formattedComponents.push(name); + } + if (components[1] > 0) { + formattedComponents.push(components[1]); + const name = components.length === 3 ? _('Min.') : _('Sec.'); + formattedComponents.push(name); + } + if (formattedComponents.length < 4 && components.length >= 3 && components[2] > 0) { + formattedComponents.push(components[2]); + formattedComponents.push(_('Sec.')); + } + return formattedComponents.join(' '); +} \ No newline at end of file diff --git a/src/css/_functions.scss b/src/css/_functions.scss new file mode 100644 index 0000000..38eac40 --- /dev/null +++ b/src/css/_functions.scss @@ -0,0 +1,23 @@ +$directions: ( + left: 135deg, + right: -45deg, +); + +@mixin arrow($direction, $size, $thickness) { + $angle: 0; + @if map-has_key($directions, $direction) { + $angle: #{map_get($directions, $direction)} + } @else { + $angle: $direction + } + + content: ' '; + border: 0 solid; + border-right-width: $thickness; + border-bottom-width: $thickness; + display: inline-block; + padding: $size; + transform: rotate($angle); + -webkit-transform: rotate($angle); + margin-bottom: $size / 2; +} \ No newline at end of file diff --git a/src/css/_variables.scss b/src/css/_variables.scss new file mode 100644 index 0000000..b5f1af9 --- /dev/null +++ b/src/css/_variables.scss @@ -0,0 +1,124 @@ +// Global (default) Values +$page-min-width: 1280px; +$page-min-height: 720px; + +$content-margin-left: 75px; +$content-margin-right: 75px; +$content-margin-top: 140px; // Distance from top of page to top of content (ignoring the header) +$content-margin-bottom: 60px; + +$default-text-color: rgba(235, 235, 235, 1); +$default-border-color: rgba(255, 255, 255, 0.3); +$default-transition-duration: 500ms; + +$controller-change-transition-duration: $default-transition-duration; + +$default-font-family: "Helvetica Neue", "Helvetica", sans-serif; +$default-font-size: 1rem; +$default-font-weight: 300; + +$play-overlay-size: 100px; + +// Z-Indexes +$header-z-index: 10; +$carousel-controls-z-index: 2; + +// Header + +$header-left: $content-margin-left; +$header-right: $content-margin-right; +$header-top: 0; + +$header-vertical-padding: 0.8rem; +$header-horizontal-padding: 0; +$header-border-width: 1px; +$header-border-color: $default-border-color; + +$header-font-size: 1.7rem; + +$header-transition-duration: $controller-change-transition-duration; + +// Back Button + +$back-button-arrow-thickness: 1px; +$back-button-arrow-size: 4px; // TODO: Use rem instead +$back-button-arrow-text-spacer: 5px; + +$back-button-font-size: 1.1rem; +$back-button-corner-radius: 5px; +$back-button-background-color: $header-border-color; +$back-button-horizontal-padding: 10px; +$back-button-vertical-padding: 7px; + +// Main Menu +$main-menu-bottom-distance: 62px; +$main-menu-padding: 25px; +$main-menu-item-distance: 100px; + +$main-menu-font-size: 1.7rem; + +$main-menu-blur-radius: 10px; +$main-menu-background-color: rgba(0, 0, 0, 0.3); +$main-menu-border-width: 1px; +$main-menu-border-color: $default-border-color; + +// Carousel + +$carousel-top-space: $content-margin-top; +$carousel-left-margin: 0; +$carousel-right-margin: 0; + +$carousel-item-width: 800px; +$carousel-item-height: 450px; +$carousel-secondary-scale-factor: 0.75; + +$carousel-transition-duration: $default-transition-duration; + +$carousel-label-top-margin: 20px; +$carousel-label-font-size: 2.25rem; + +$carousel-control-buttons-top-margin: $carousel-top-space + 0.5 * $carousel-item-height; +$carousel-previous-left: $content-margin-left; +$carousel-next-right: $content-margin-right; +$carousel-control-buttons-size: 15px; +$carousel-control-buttons-thickness: 5px; + +// List on Right + +$lor-margin-left: $content-margin-left; +$lor-margin-right: $content-margin-right; +$lor-margin-top: $content-margin-top; +$lor-margin-bottom: $content-margin-bottom; + +$lor-list-margin-top: $lor-margin-top; +$lor-list-margin-right: $lor-margin-right; +$lor-list-margin-bottom: $lor-margin-bottom; +$lor-list-width: 320px; +$lor-list-background-color: rgba(0, 0, 0, 0.75); + +$lor-list-title-color: white; +$lor-list-title-padding: 10px; + +$lor-item-border: 1px solid $default-border-color; +$lor-item-background-color: transparent; +$lor-item-selected-background: linear-gradient(to bottom, rgba(41, 137, 216, 0.75) 0%, rgba(30, 87, 153, 0.75) 100%); +$lor-item-padding: 12px; + +$lor-item-image-width: 64px; +$lor-item-image-height: 36px; +$lor-item-title-color: white; +$lor-item-duration-sep: 5px; +$lor-item-duration-color: rgb(140, 140, 140); + +$lor-selection-width: 800px; +$lor-selection-image-max-height: 450px; +$lor-selection-title-sep: 10px; +$lor-selection-title-font-size: 1.6rem; +$lor-selection-title-color: white; +$lor-selection-description-sep: 10px; +$lor-selection-description-font-size: 1.6rem; +$lor-selection-description-font-weight: 200; +$lor-selection-description-color: rgba(255, 255, 255, 0.75); + +$lor-vignette-color: black; +$lor-vignette-size: 750px; \ No newline at end of file diff --git a/src/css/carousel.scss b/src/css/carousel.scss new file mode 100644 index 0000000..cf92789 --- /dev/null +++ b/src/css/carousel.scss @@ -0,0 +1,92 @@ +@import "variables"; +@import "functions"; + +/* Sliding View */ + +.tk-page-slider-controller-view { + .sliding-view { + position: absolute; + top: $carousel-top-space; + left: $carousel-left-margin; + right: $carousel-right-margin; + overflow: hidden; + + height: $carousel-item-height; + } + + .sliding-view-element { + position: absolute; + left: calc(50% - #{$carousel-item-width / 2}); + -webkit-transition: -webkit-transform $carousel-transition-duration; + cursor: pointer; + + div { + -webkit-transition: -webkit-transform $carousel-transition-duration; + -webkit-transform: scale($carousel-secondary-scale-factor); + } + + &.sliding-view-element-focused { + div { + -webkit-transform: scale(1); + } + } + + // The actual image of the item + div img:nth-child(1) { + width: $carousel-item-width; + height: $carousel-item-height; + } + + // The play button + div img:nth-child(2) { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + opacity: 0; + -webkit-transition: opacity $carousel-transition-duration; + } + .sliding-view-element-focused div img:nth-child(2) { + opacity: 1; + } + } + + .sliding-view-element-hidden { + display: none; + } +} + +/* Label */ + +.carousel-label { + position: absolute; + left: calc(50% - #{0.5 * $carousel-item-width}); + right: calc(50% - #{0.5 * $carousel-item-width}); + top: $carousel-top-space + $carousel-item-height + $carousel-label-top-margin; + + font-size: $carousel-label-font-size; + text-align: center; + overflow: visible; + overflow-wrap: break-word; + word-wrap: break-word; + hyphens: auto; +} + +/* Arrows */ + +.carousel-previous { + @include arrow(left, $carousel-control-buttons-size, $carousel-control-buttons-thickness); + + position: absolute; + top: $carousel-control-buttons-top-margin; + left: $carousel-previous-left; + z-index: $carousel-controls-z-index; +} + +.carousel-next { + @include arrow(right, $carousel-control-buttons-size, $carousel-control-buttons-thickness); + position: absolute; + margin-top: $carousel-control-buttons-top-margin; + right: $carousel-next-right; + z-index: $carousel-controls-z-index; +} diff --git a/src/css/home.scss b/src/css/home.scss new file mode 100644 index 0000000..621b1bf --- /dev/null +++ b/src/css/home.scss @@ -0,0 +1,26 @@ +@import "variables"; + +.main-menu { + position: absolute; + left: 0; + right: 0; + bottom: $main-menu-bottom-distance; + padding: $main-menu-padding; + + -webkit-backdrop-filter: blur($main-menu-blur-radius); + background-color: $main-menu-background-color; + border-top: $main-menu-border-width solid $main-menu-border-color; + border-bottom: $main-menu-border-width solid $main-menu-border-color; + + display: flex; + justify-content: center; + + div { + text-align: center; + font-size: $main-menu-font-size; + } + + div + div { + margin-left: $main-menu-item-distance; + } +} \ No newline at end of file diff --git a/src/css/list-on-right.scss b/src/css/list-on-right.scss new file mode 100644 index 0000000..b6277b9 --- /dev/null +++ b/src/css/list-on-right.scss @@ -0,0 +1,95 @@ +@import "variables"; + +.list-on-right .list { + position: absolute; + top: $lor-list-margin-top; + right: $lor-list-margin-right; + bottom: $lor-list-margin-bottom; + width: $lor-list-width; + background-color: $lor-list-background-color; + + display: flex; + flex-direction: column; + + .list-title { + color: $lor-list-title-color; + padding: $lor-list-title-padding; + border-bottom: $lor-item-border; + } + + .list-content { + overflow: scroll; + } + + .list-item { + background-color: $lor-item-background-color; + display: flex; + + & + .list-item { + border-top: $lor-item-border; + } + + &.tk-tab-selected { + background: $lor-item-selected-background; + } + + img { + width: $lor-item-image-width; + height: $lor-item-image-height; + margin: $lor-item-padding; + } + + .list-item-body { + flex: 1; + margin: $lor-item-padding $lor-item-padding $lor-item-padding 0; + } + + .title { + color: $lor-item-title-color; + } + + .duration { + margin-top: $lor-item-duration-sep; + color: $lor-item-duration-color; + } + } +} + +.list-on-right .selection { + position: absolute; + left: $lor-margin-left; + top: $lor-margin-top; + width: $lor-selection-width; + + div { + position: relative; + text-align: center; + } + + img:not(.play-overlay) { + max-width: 100%; + max-height: $lor-selection-image-max-height; + } + + .title { + margin-top: $lor-selection-title-sep; + font-size: $lor-selection-title-font-size; + color: $lor-selection-title-color; + } + + .description { + margin-top: $lor-selection-description-sep; + font-size: $lor-selection-description-font-size; + font-weight: $lor-selection-description-font-weight; + color: $lor-selection-description-color; + } +} + +.vignette { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + box-shadow: 0 0 $lor-vignette-size $lor-vignette-color inset; +} \ No newline at end of file diff --git a/src/css/shared.scss b/src/css/shared.scss new file mode 100644 index 0000000..4f7497d --- /dev/null +++ b/src/css/shared.scss @@ -0,0 +1,103 @@ +@import "variables"; +@import "functions"; + +* { + -webkit-user-select: none; + -webkit-user-drag: none; +} + +html { + min-width: $page-min-width; + min-height: $page-min-height; + height: 100%; +} + +body { + margin: 0; + width: 100%; + height: 100%; + + font-family: $default-font-family; + font-weight: $default-font-weight; + font-size: $default-font-size; + color: $default-text-color; +} + +#header { + position: absolute; + left: $header-left; + right: $header-right; + top: $header-top; + padding: $header-vertical-padding $header-horizontal-padding; + z-index: $header-z-index; + border-bottom: $header-border-width solid $header-border-color; + + -webkit-transition: opacity $header-transition-duration; + + text-align: center; + font-size: $header-font-size; + + .back { + position: absolute; + font-size: $back-button-font-size; + border-radius: $back-button-corner-radius; + background-color: $back-button-background-color; + padding: $back-button-vertical-padding $back-button-horizontal-padding; + + &::before { + @include arrow(left, $back-button-arrow-size, $back-button-arrow-thickness); + margin-right: $back-button-arrow-text-spacer; + } + } +} + +#navigation { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + + background-image: url("/images/background.png"); + background-size: cover; + + > div { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + + background-size: cover; + } +} + +// Elements +.button { + cursor: pointer; + + &:active { + color: white; + } +} + +.hidden { + opacity: 0; +} + +.inactive { + opacity: 0; + pointer-events: none; +} + +.play-overlay { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + + width: $play-overlay-size; + height: $play-overlay-size; + + cursor: pointer; +} \ No newline at end of file diff --git a/src/iTunesMetadata.plist.mustache b/src/iTunesMetadata.plist.mustache new file mode 100644 index 0000000..f9bdb2c --- /dev/null +++ b/src/iTunesMetadata.plist.mustache @@ -0,0 +1,58 @@ + + + + + Metadata + + artistName + {{{ meta.artist }}} + description + {{{ meta.description }}} + longDescription + {{{ meta.longDescription }}} + genre + {{{ meta.genre }}} + itemId + {{{ extrasID }}} + itemName + {{{ meta.title }}} - iTunes Extras + kind + feature-movie + releaseDate + {{{ meta.releaseDate }}} + sort-artist-status + 128 + sort-name + {{{ meta.sort-name}}} + sort-name-status + 1 + year + {{{ meta.year }}} + studio + {{{ meta.studio }}} + playlistId + {{{ movieID }}} + playlistName + {{{ meta.title }}} + xid + {{{ XID }}}_ITUNES_EXTRAS + + Name File + {{{ meta.title }}} - iTunes Extras.ite + associated-adam-ids + + {{{ movieID }}} + + xid-asset-mapping + + {{{ XID }}} + + {{{ movieID }}} + + {{{ XID }}}_ITUNES_EXTRAS + + {{{ extrasID }}} + + + + diff --git a/src/images/interface/buttonPlayCircle.png b/src/images/interface/buttonPlayCircle.png new file mode 100644 index 0000000..418e314 Binary files /dev/null and b/src/images/interface/buttonPlayCircle.png differ diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..6d7ff31 --- /dev/null +++ b/src/index.html @@ -0,0 +1,33 @@ + + + + + + + iTunes Extra for Movie + + + + + + + + + + + + + + + + + + + + + diff --git a/src/manifest.xml.mustache b/src/manifest.xml.mustache new file mode 100644 index 0000000..1aec220 --- /dev/null +++ b/src/manifest.xml.mustache @@ -0,0 +1,14 @@ + + + + + + + + + + + + +